Merge branch 'main' into multi-server-completions-tailwind

Julia created

Change summary

.github/workflows/publish_collab_image.yml                             |    7 
Cargo.lock                                                             |  508 
Cargo.toml                                                             |   14 
Dockerfile                                                             |    2 
assets/icons/ai.svg                                                    |   23 
assets/icons/arrow_left.svg                                            |    3 
assets/icons/arrow_right.svg                                           |    3 
assets/icons/case_insensitive_12.svg                                   |    5 
assets/icons/channel_hash.svg                                          |    6 
assets/icons/check.svg                                                 |    3 
assets/icons/check_circle.svg                                          |    4 
assets/icons/chevron_down.svg                                          |    3 
assets/icons/chevron_left.svg                                          |    3 
assets/icons/chevron_right.svg                                         |    3 
assets/icons/chevron_up.svg                                            |    3 
assets/icons/conversations.svg                                         |    2 
assets/icons/copilot.svg                                               |    9 
assets/icons/copy.svg                                                  |    5 
assets/icons/ellipsis.svg                                              |    5 
assets/icons/error.svg                                                 |    4 
assets/icons/exit.svg                                                  |    4 
assets/icons/feedback.svg                                              |    6 
assets/icons/file_icons/elixir.svg                                     |    3 
assets/icons/file_icons/file_types.json                                |   33 
assets/icons/file_icons/phoenix.svg                                    |    4 
assets/icons/file_icons/python.svg                                     |    6 
assets/icons/filter.svg                                                |    3 
assets/icons/filter_12.svg                                             |    3 
assets/icons/filter_14.svg                                             |    6 
assets/icons/hash.svg                                                  |    6 
assets/icons/html.svg                                                  |    5 
assets/icons/inlay_hint.svg                                            |    5 
assets/icons/kebab.svg                                                 |    5 
assets/icons/lock.svg                                                  |    6 
assets/icons/magnifying_glass.svg                                      |    3 
assets/icons/match_case.svg                                            |    3 
assets/icons/match_word.svg                                            |    2 
assets/icons/maximize.svg                                              |    4 
assets/icons/microphone.svg                                            |    5 
assets/icons/minimize.svg                                              |    4 
assets/icons/plus.svg                                                  |    3 
assets/icons/project.svg                                               |    5 
assets/icons/replace.svg                                               |    9 
assets/icons/replace_all.svg                                           |    2 
assets/icons/replace_next.svg                                          |    5 
assets/icons/screen.svg                                                |    4 
assets/icons/split.svg                                                 |    5 
assets/icons/success.svg                                               |    4 
assets/icons/terminal.svg                                              |    5 
assets/icons/warning.svg                                               |    6 
assets/icons/word_search_12.svg                                        |    4 
assets/icons/word_search_14.svg                                        |    2 
assets/icons/x.svg                                                     |    3 
assets/keymaps/default.json                                            |   44 
assets/keymaps/textmate.json                                           |   19 
assets/keymaps/vim.json                                                |  116 
assets/settings/default.json                                           |   46 
crates/ai/Cargo.toml                                                   |    7 
crates/ai/src/ai.rs                                                    |  118 
crates/ai/src/assistant.rs                                             |  858 
crates/ai/src/assistant_settings.rs                                    |   35 
crates/ai/src/streaming_diff.rs                                        |  293 
crates/audio/Cargo.toml                                                |    2 
crates/audio/src/audio.rs                                              |   40 
crates/breadcrumbs/src/breadcrumbs.rs                                  |    8 
crates/call/Cargo.toml                                                 |    1 
crates/call/src/call.rs                                                |   51 
crates/call/src/room.rs                                                |  118 
crates/channel/Cargo.toml                                              |   51 
crates/channel/src/channel.rs                                          |   14 
crates/channel/src/channel_buffer.rs                                   |  197 
crates/channel/src/channel_store.rs                                    |  656 
crates/channel/src/channel_store_tests.rs                              |  168 
crates/client/Cargo.toml                                               |    3 
crates/client/src/client.rs                                            |    9 
crates/client/src/telemetry.rs                                         |    3 
crates/client/src/test.rs                                              |    1 
crates/client/src/user.rs                                              |   55 
crates/collab/Cargo.toml                                               |    7 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql         |   86 
crates/collab/migrations/20230727150500_add_channels.sql               |   30 
crates/collab/migrations/20230819154600_add_channel_buffers.sql        |   40 
crates/collab/migrations/20230825190322_add_server_feature_flags.sql   |   16 
crates/collab/src/bin/seed.rs                                          |   18 
crates/collab/src/db.rs                                                | 3095 
crates/collab/src/db/ids.rs                                            |  128 
crates/collab/src/db/queries.rs                                        |   11 
crates/collab/src/db/queries/access_tokens.rs                          |   53 
crates/collab/src/db/queries/buffers.rs                                |  588 
crates/collab/src/db/queries/channels.rs                               |  697 
crates/collab/src/db/queries/contacts.rs                               |  298 
crates/collab/src/db/queries/projects.rs                               |  926 
crates/collab/src/db/queries/rooms.rs                                  | 1093 
crates/collab/src/db/queries/servers.rs                                |   81 
crates/collab/src/db/queries/signups.rs                                |  349 
crates/collab/src/db/queries/users.rs                                  |  297 
crates/collab/src/db/signup.rs                                         |   57 
crates/collab/src/db/tables.rs                                         |   26 
crates/collab/src/db/tables/access_token.rs                            |    2 
crates/collab/src/db/tables/buffer.rs                                  |   45 
crates/collab/src/db/tables/buffer_operation.rs                        |   34 
crates/collab/src/db/tables/buffer_snapshot.rs                         |   31 
crates/collab/src/db/tables/channel.rs                                 |   48 
crates/collab/src/db/tables/channel_buffer_collaborator.rs             |   43 
crates/collab/src/db/tables/channel_member.rs                          |   59 
crates/collab/src/db/tables/channel_path.rs                            |   15 
crates/collab/src/db/tables/contact.rs                                 |   28 
crates/collab/src/db/tables/feature_flag.rs                            |   40 
crates/collab/src/db/tables/follower.rs                                |    5 
crates/collab/src/db/tables/language_server.rs                         |    2 
crates/collab/src/db/tables/project.rs                                 |    2 
crates/collab/src/db/tables/project_collaborator.rs                    |    2 
crates/collab/src/db/tables/room.rs                                    |   17 
crates/collab/src/db/tables/room_participant.rs                        |    2 
crates/collab/src/db/tables/server.rs                                  |    2 
crates/collab/src/db/tables/signup.rs                                  |   28 
crates/collab/src/db/tables/user.rs                                    |   33 
crates/collab/src/db/tables/user_feature.rs                            |   42 
crates/collab/src/db/tables/worktree.rs                                |    2 
crates/collab/src/db/tables/worktree_diagnostic_summary.rs             |    2 
crates/collab/src/db/tables/worktree_entry.rs                          |    2 
crates/collab/src/db/tables/worktree_repository.rs                     |    2 
crates/collab/src/db/tables/worktree_repository_statuses.rs            |    2 
crates/collab/src/db/tables/worktree_settings_file.rs                  |    2 
crates/collab/src/db/tests.rs                                          | 1205 
crates/collab/src/db/tests/buffer_tests.rs                             |  165 
crates/collab/src/db/tests/db_tests.rs                                 | 1573 
crates/collab/src/db/tests/feature_flag_tests.rs                       |   60 
crates/collab/src/rpc.rs                                               |  730 
crates/collab/src/tests.rs                                             |  181 
crates/collab/src/tests/channel_buffer_tests.rs                        |  426 
crates/collab/src/tests/channel_tests.rs                               |  924 
crates/collab/src/tests/integration_tests.rs                           |  326 
crates/collab/src/tests/randomized_integration_tests.rs                |  121 
crates/collab_ui/Cargo.toml                                            |    6 
crates/collab_ui/src/channel_view.rs                                   |  355 
crates/collab_ui/src/collab_panel.rs                                   | 2715 
crates/collab_ui/src/collab_panel/channel_modal.rs                     |  613 
crates/collab_ui/src/collab_panel/contact_finder.rs                    |  151 
crates/collab_ui/src/collab_panel/panel_settings.rs                    |   39 
crates/collab_ui/src/collab_titlebar_item.rs                           |  221 
crates/collab_ui/src/collab_ui.rs                                      |   62 
crates/collab_ui/src/contact_list.rs                                   | 1385 
crates/collab_ui/src/contacts_popover.rs                               |  137 
crates/collab_ui/src/face_pile.rs                                      |   47 
crates/collab_ui/src/incoming_call_notification.rs                     |   31 
crates/collab_ui/src/notifications.rs                                  |    5 
crates/collab_ui/src/project_shared_notification.rs                    |   27 
crates/component_test/Cargo.toml                                       |   18 
crates/component_test/src/component_test.rs                            |  121 
crates/copilot/src/copilot.rs                                          |    6 
crates/diagnostics/src/diagnostics.rs                                  |    4 
crates/diagnostics/src/items.rs                                        |    6 
crates/drag_and_drop/src/drag_and_drop.rs                              |   14 
crates/editor/src/blink_manager.rs                                     |   13 
crates/editor/src/display_map.rs                                       |   97 
crates/editor/src/display_map/block_map.rs                             |    8 
crates/editor/src/display_map/fold_map.rs                              |    8 
crates/editor/src/display_map/inlay_map.rs                             |  204 
crates/editor/src/display_map/tab_map.rs                               |    8 
crates/editor/src/display_map/wrap_map.rs                              |    8 
crates/editor/src/editor.rs                                            |  573 
crates/editor/src/editor_settings.rs                                   |    2 
crates/editor/src/editor_tests.rs                                      |  115 
crates/editor/src/element.rs                                           |  353 
crates/editor/src/hover_popover.rs                                     |  467 
crates/editor/src/inlay_hint_cache.rs                                  |  667 
crates/editor/src/items.rs                                             |   29 
crates/editor/src/link_go_to_definition.rs                             |  810 
crates/editor/src/movement.rs                                          |   19 
crates/editor/src/multi_buffer.rs                                      |  287 
crates/editor/src/scroll.rs                                            |    9 
crates/editor/src/scroll/autoscroll.rs                                 |   73 
crates/editor/src/selections_collection.rs                             |   20 
crates/editor/src/test/editor_lsp_test_context.rs                      |   31 
crates/editor/src/test/editor_test_context.rs                          |    2 
crates/feature_flags/Cargo.toml                                        |    4 
crates/feature_flags/src/feature_flags.rs                              |   79 
crates/feedback/src/deploy_feedback_button.rs                          |    2 
crates/feedback/src/feedback_editor.rs                                 |    2 
crates/fs/Cargo.toml                                                   |    1 
crates/fs/src/fs.rs                                                    |   67 
crates/gpui/Cargo.toml                                                 |    2 
crates/gpui/examples/components.rs                                     |  122 
crates/gpui/examples/text.rs                                           |    1 
crates/gpui/playground/Cargo.lock                                      | 2919 
crates/gpui/playground/Cargo.toml                                      |   26 
crates/gpui/playground/docs/thoughts.md                                |   72 
crates/gpui/playground/src/adapter.rs                                  |   78 
crates/gpui/playground/src/color.rs                                    |  276 
crates/gpui/playground/src/components.rs                               |  100 
crates/gpui/playground/src/div.rs                                      |  108 
crates/gpui/playground/src/element.rs                                  |  158 
crates/gpui/playground/src/hoverable.rs                                |   76 
crates/gpui/playground/src/interactive.rs                              |   34 
crates/gpui/playground/src/layout_context.rs                           |   54 
crates/gpui/playground/src/paint_context.rs                            |   71 
crates/gpui/playground/src/playground.rs                               |   83 
crates/gpui/playground/src/style.rs                                    |  286 
crates/gpui/playground/src/text.rs                                     |  151 
crates/gpui/playground/src/themes.rs                                   |   84 
crates/gpui/playground/src/themes/rose_pine.rs                         |  133 
crates/gpui/playground/src/view.rs                                     |   26 
crates/gpui/playground_macros/Cargo.toml                               |   14 
crates/gpui/playground_macros/src/derive_element.rs                    |   91 
crates/gpui/playground_macros/src/derive_into_element.rs               |   69 
crates/gpui/playground_macros/src/playground_macros.rs                 |   26 
crates/gpui/playground_macros/src/styleable_helpers.rs                 |  147 
crates/gpui/playground_macros/src/tailwind_lengths.rs                  |   99 
crates/gpui/src/app.rs                                                 |  427 
crates/gpui/src/app/action.rs                                          |    7 
crates/gpui/src/app/window.rs                                          |  233 
crates/gpui/src/color.rs                                               |   62 
crates/gpui/src/elements.rs                                            |  133 
crates/gpui/src/elements/align.rs                                      |    8 
crates/gpui/src/elements/canvas.rs                                     |    5 
crates/gpui/src/elements/clipped.rs                                    |    8 
crates/gpui/src/elements/component.rs                                  |  345 
crates/gpui/src/elements/constrained_box.rs                            |   12 
crates/gpui/src/elements/container.rs                                  |   19 
crates/gpui/src/elements/empty.rs                                      |    4 
crates/gpui/src/elements/expanded.rs                                   |    8 
crates/gpui/src/elements/flex.rs                                       |   35 
crates/gpui/src/elements/hook.rs                                       |    9 
crates/gpui/src/elements/image.rs                                      |    4 
crates/gpui/src/elements/keystroke_label.rs                            |    2 
crates/gpui/src/elements/label.rs                                      |    4 
crates/gpui/src/elements/list.rs                                       |   32 
crates/gpui/src/elements/mouse_event_handler.rs                        |    8 
crates/gpui/src/elements/overlay.rs                                    |    8 
crates/gpui/src/elements/resizable.rs                                  |  153 
crates/gpui/src/elements/stack.rs                                      |   13 
crates/gpui/src/elements/svg.rs                                        |    6 
crates/gpui/src/elements/text.rs                                       |    6 
crates/gpui/src/elements/tooltip.rs                                    |   28 
crates/gpui/src/elements/uniform_list.rs                               |   11 
crates/gpui/src/fonts.rs                                               |   56 
crates/gpui/src/geometry.rs                                            |  256 
crates/gpui/src/gpui.rs                                                |    5 
crates/gpui/src/keymap_matcher/keymap_context.rs                       |    4 
crates/gpui/src/platform.rs                                            |    3 
crates/gpui/src/platform/event.rs                                      |   20 
crates/gpui/src/platform/mac/event.rs                                  |    2 
crates/gpui/src/platform/mac/geometry.rs                               |   15 
crates/gpui/src/platform/mac/screen.rs                                 |   82 
crates/gpui/src/platform/mac/window.rs                                 |   43 
crates/gpui/src/platform/test.rs                                       |    4 
crates/gpui/src/scene.rs                                               |   28 
crates/gpui/src/scene/mouse_region.rs                                  |   48 
crates/gpui/src/scene/region.rs                                        |    7 
crates/gpui/src/text_layout.rs                                         |    2 
crates/gpui/tests/test.rs                                              |   14 
crates/gpui_macros/Cargo.toml                                          |    6 
crates/gpui_macros/src/gpui_macros.rs                                  |   54 
crates/gpui_macros/tests/test.rs                                       |   14 
crates/install_cli/src/install_cli.rs                                  |    2 
crates/language/src/buffer.rs                                          |   90 
crates/language/src/buffer_tests.rs                                    |  159 
crates/language/src/language.rs                                        |    3 
crates/language/src/proto.rs                                           |   13 
crates/language/src/syntax_map.rs                                      |   22 
crates/language/src/syntax_map/syntax_map_tests.rs                     |    8 
crates/language_tools/src/lsp_log.rs                                   |   14 
crates/language_tools/src/syntax_tree_view.rs                          |    3 
crates/lsp/src/lsp.rs                                                  |   10 
crates/menu/src/menu.rs                                                |    3 
crates/picker/src/picker.rs                                            |    1 
crates/project/src/lsp_command.rs                                      |  618 
crates/project/src/project.rs                                          |  746 
crates/project/src/project_tests.rs                                    |   17 
crates/project/src/search.rs                                           |   73 
crates/project/src/terminals.rs                                        |   67 
crates/project/src/worktree.rs                                         |   20 
crates/project/src/worktree_tests.rs                                   |    2 
crates/project_panel/src/project_panel.rs                              |   34 
crates/quick_action_bar/Cargo.toml                                     |   23 
crates/quick_action_bar/src/quick_action_bar.rs                        |  185 
crates/recent_projects/src/highlighted_workspace_location.rs           |    4 
crates/refineable/Cargo.toml                                           |   15 
crates/refineable/derive_refineable/Cargo.toml                         |   15 
crates/refineable/derive_refineable/src/derive_refineable.rs           |  188 
crates/refineable/src/refineable.rs                                    |   14 
crates/rope/src/rope.rs                                                |   10 
crates/rpc/Cargo.toml                                                  |    2 
crates/rpc/proto/zed.proto                                             |  222 
crates/rpc/src/peer.rs                                                 |   54 
crates/rpc/src/proto.rs                                                |   56 
crates/rpc/src/rpc.rs                                                  |    2 
crates/search/Cargo.toml                                               |    4 
crates/search/src/buffer_search.rs                                     |  397 
crates/search/src/history.rs                                           |  184 
crates/search/src/mode.rs                                              |   65 
crates/search/src/project_search.rs                                    |  732 
crates/search/src/search.rs                                            |  224 
crates/search/src/search_bar.rs                                        |  174 
crates/semantic_index/Cargo.toml                                       |    1 
crates/semantic_index/src/db.rs                                        |   59 
crates/semantic_index/src/embedding.rs                                 |    4 
crates/semantic_index/src/parsing.rs                                   |   14 
crates/semantic_index/src/semantic_index.rs                            |  470 
crates/semantic_index/src/semantic_index_tests.rs                      |    7 
crates/settings/Cargo.toml                                             |    2 
crates/settings/src/keymap_file.rs                                     |   19 
crates/settings/src/settings_store.rs                                  |    3 
crates/staff_mode/src/staff_mode.rs                                    |   36 
crates/sum_tree/src/tree_map.rs                                        |   12 
crates/terminal/Cargo.toml                                             |    2 
crates/terminal/src/terminal.rs                                        |  179 
crates/terminal/src/terminal_settings.rs                               |  163 
crates/terminal_view/src/terminal_element.rs                           |    7 
crates/terminal_view/src/terminal_panel.rs                             |   12 
crates/terminal_view/src/terminal_view.rs                              |   21 
crates/text/Cargo.toml                                                 |    1 
crates/text/src/text.rs                                                |  126 
crates/theme/src/components.rs                                         |  481 
crates/theme/src/theme.rs                                              |  175 
crates/theme/src/ui.rs                                                 |   30 
crates/theme_selector/Cargo.toml                                       |    2 
crates/theme_selector/src/theme_selector.rs                            |    4 
crates/vcs_menu/src/lib.rs                                             |   34 
crates/vim/src/editor_events.rs                                        |    8 
crates/vim/src/mode_indicator.rs                                       |   12 
crates/vim/src/motion.rs                                               |  285 
crates/vim/src/normal.rs                                               |  239 
crates/vim/src/normal/case.rs                                          |    6 
crates/vim/src/normal/change.rs                                        |    6 
crates/vim/src/normal/paste.rs                                         |  468 
crates/vim/src/normal/scroll.rs                                        |   40 
crates/vim/src/normal/search.rs                                        |   19 
crates/vim/src/normal/substitute.rs                                    |   12 
crates/vim/src/object.rs                                               |   73 
crates/vim/src/state.rs                                                |   29 
crates/vim/src/test.rs                                                 |  245 
crates/vim/src/test/neovim_backed_test_context.rs                      |  124 
crates/vim/src/test/neovim_connection.rs                               |  230 
crates/vim/src/test/vim_binding_test_context.rs                        |   64 
crates/vim/src/test/vim_test_context.rs                                |   27 
crates/vim/src/utils.rs                                                |   27 
crates/vim/src/vim.rs                                                  |  124 
crates/vim/src/visual.rs                                               |  868 
crates/vim/test_data/test_enter_visual_line_mode.json                  |    6 
crates/vim/test_data/test_enter_visual_mode.json                       |   10 
crates/vim/test_data/test_folds.json                                   |   23 
crates/vim/test_data/test_multiline_surrounding_character_objects.json |    4 
crates/vim/test_data/test_next_line_start.json                         |    3 
crates/vim/test_data/test_p.json                                       |   13 
crates/vim/test_data/test_paste.json                                   |   31 
crates/vim/test_data/test_paste_visual.json                            |   42 
crates/vim/test_data/test_paste_visual_block.json                      |   31 
crates/vim/test_data/test_visual_block_insert.json                     |   18 
crates/vim/test_data/test_visual_block_mode.json                       |   38 
crates/vim/test_data/test_visual_delete.json                           |    2 
crates/vim/test_data/test_visual_line_delete.json                      |   15 
crates/vim/test_data/test_visual_object.json                           |   19 
crates/vim/test_data/test_visual_paste.json                            |   26 
crates/vim/test_data/test_visual_word_object.json                      |   96 
crates/vim/test_data/test_visual_yank.json                             |   29 
crates/vim/test_data/test_wrapped_lines.json                           |   50 
crates/welcome/src/welcome.rs                                          |    2 
crates/workspace/Cargo.toml                                            |    1 
crates/workspace/src/dock.rs                                           |  224 
crates/workspace/src/item.rs                                           |   14 
crates/workspace/src/pane.rs                                           |  122 
crates/workspace/src/pane/dragged_item_receiver.rs                     |   10 
crates/workspace/src/pane_group.rs                                     |    4 
crates/workspace/src/shared_screen.rs                                  |    2 
crates/workspace/src/toolbar.rs                                        |  202 
crates/workspace/src/workspace.rs                                      |   75 
crates/zed/Cargo.toml                                                  |    9 
crates/zed/resources/zed.entitlements                                  |    8 
crates/zed/src/languages/c.rs                                          |    2 
crates/zed/src/languages/cpp/config.toml                               |    2 
crates/zed/src/languages/javascript/config.toml                        |    3 
crates/zed/src/languages/json.rs                                       |    4 
crates/zed/src/languages/python.rs                                     |    6 
crates/zed/src/languages/python/config.toml                            |    2 
crates/zed/src/languages/rust.rs                                       |    2 
crates/zed/src/languages/tsx/config.toml                               |    1 
crates/zed/src/languages/typescript.rs                                 |    5 
crates/zed/src/languages/typescript/config.toml                        |    3 
crates/zed/src/main.rs                                                 |   18 
crates/zed/src/zed.rs                                                  |   45 
rust-toolchain.toml                                                    |    2 
script/bundle                                                          |  163 
script/lib/bump-version.sh                                             |    2 
script/start-local-collaboration                                       |    2 
script/zed-with-local-servers                                          |    5 
styles/.eslintrc.js                                                    |    1 
styles/src/build_themes.ts                                             |    9 
styles/src/build_tokens.ts                                             |    4 
styles/src/common.ts                                                   |    1 
styles/src/component/button.ts                                         |  127 
styles/src/component/icon_button.ts                                    |   72 
styles/src/component/index.ts                                          |    6 
styles/src/component/indicator.ts                                      |   15 
styles/src/component/input.ts                                          |   23 
styles/src/component/tab.ts                                            |   73 
styles/src/component/tab_bar_button.ts                                 |   67 
styles/src/component/text_button.ts                                    |   64 
styles/src/element/index.ts                                            |    6 
styles/src/element/margin.ts                                           |   41 
styles/src/element/padding.ts                                          |   41 
styles/src/element/toggle.ts                                           |    2 
styles/src/style_tree/app.ts                                           |   12 
styles/src/style_tree/assistant.ts                                     |  150 
styles/src/style_tree/collab_modals.ts                                 |  155 
styles/src/style_tree/collab_panel.ts                                  |  410 
styles/src/style_tree/component_test.ts                                |   26 
styles/src/style_tree/contact_finder.ts                                |   72 
styles/src/style_tree/contact_list.ts                                  |  247 
styles/src/style_tree/contacts_popover.ts                              |   10 
styles/src/style_tree/context_menu.ts                                  |   10 
styles/src/style_tree/editor.ts                                        |    3 
styles/src/style_tree/feedback.ts                                      |    2 
styles/src/style_tree/picker.ts                                        |    2 
styles/src/style_tree/project_panel.ts                                 |   16 
styles/src/style_tree/search.ts                                        |  372 
styles/src/style_tree/status_bar.ts                                    |   69 
styles/src/style_tree/tab_bar.ts                                       |   22 
styles/src/style_tree/titlebar.ts                                      |   28 
styles/src/style_tree/toolbar.ts                                       |   38 
styles/src/style_tree/workspace.ts                                     |   43 
styles/src/theme/create_theme.ts                                       |   19 
styles/src/theme/index.ts                                              |    1 
styles/src/theme/tokens/theme.ts                                       |    6 
styles/tsconfig.json                                                   |    7 
test.rs                                                                | 5670 
427 files changed, 41,919 insertions(+), 11,944 deletions(-)

Detailed changes

.github/workflows/publish_collab_image.yml 🔗

@@ -11,7 +11,7 @@ env:
 
 jobs:
   publish:
-    name: Publish collab server image 
+    name: Publish collab server image
     runs-on:
       - self-hosted
       - deploy
@@ -22,6 +22,9 @@ jobs:
       - name: Sign into DigitalOcean docker registry
         run: doctl registry login
 
+      - name: Prune Docker system
+        run: docker system prune
+
       - name: Checkout repo
         uses: actions/checkout@v3
         with:
@@ -41,6 +44,6 @@ jobs:
 
       - name: Build docker image
         run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
-    
+
       - name: Publish docker image
         run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}

Cargo.lock 🔗

@@ -102,14 +102,20 @@ dependencies = [
  "anyhow",
  "chrono",
  "collections",
+ "ctor",
  "editor",
+ "env_logger 0.9.3",
  "fs",
  "futures 0.3.28",
  "gpui",
+ "indoc",
  "isahc",
  "language",
+ "log",
  "menu",
+ "ordered-float",
  "project",
+ "rand 0.8.5",
  "regex",
  "schemars",
  "search",
@@ -126,18 +132,17 @@ dependencies = [
 [[package]]
 name = "alacritty_config"
 version = "0.1.2-dev"
-source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5"
+source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca"
 dependencies = [
  "log",
  "serde",
  "toml 0.7.6",
- "winit",
 ]
 
 [[package]]
 name = "alacritty_config_derive"
 version = "0.2.2-dev"
-source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5"
+source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -147,7 +152,7 @@ dependencies = [
 [[package]]
 name = "alacritty_terminal"
 version = "0.20.0-dev"
-source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5"
+source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca"
 dependencies = [
  "alacritty_config",
  "alacritty_config_derive",
@@ -213,30 +218,6 @@ version = "0.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049"
 
-[[package]]
-name = "android-activity"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0"
-dependencies = [
- "android-properties",
- "bitflags 1.3.2",
- "cc",
- "jni-sys",
- "libc",
- "log",
- "ndk",
- "ndk-context",
- "ndk-sys",
- "num_enum 0.6.1",
-]
-
-[[package]]
-name = "android-properties"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
-
 [[package]]
 name = "android-tzdata"
 version = "0.1.1"
@@ -926,25 +907,6 @@ dependencies = [
  "generic-array",
 ]
 
-[[package]]
-name = "block-sys"
-version = "0.1.0-beta.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146"
-dependencies = [
- "objc-sys",
-]
-
-[[package]]
-name = "block2"
-version = "0.2.0-alpha.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42"
-dependencies = [
- "block-sys",
- "objc2-encode",
-]
-
 [[package]]
 name = "blocking"
 version = "1.3.1"
@@ -1107,6 +1069,7 @@ dependencies = [
  "anyhow",
  "async-broadcast",
  "audio",
+ "channel",
  "client",
  "collections",
  "fs",
@@ -1126,20 +1089,6 @@ dependencies = [
  "util",
 ]
 
-[[package]]
-name = "calloop"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8"
-dependencies = [
- "bitflags 1.3.2",
- "log",
- "nix 0.25.1",
- "slotmap",
- "thiserror",
- "vec_map",
-]
-
 [[package]]
 name = "cap-fs-ext"
 version = "0.24.4"
@@ -1249,10 +1198,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
-name = "cfg_aliases"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+name = "channel"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "collections",
+ "db",
+ "feature_flags",
+ "futures 0.3.28",
+ "gpui",
+ "image",
+ "language",
+ "lazy_static",
+ "log",
+ "parking_lot 0.11.2",
+ "postage",
+ "rand 0.8.5",
+ "rpc",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "settings",
+ "smol",
+ "sum_tree",
+ "tempfile",
+ "text",
+ "thiserror",
+ "time 0.3.24",
+ "tiny_http",
+ "url",
+ "util",
+ "uuid 1.4.1",
+]
 
 [[package]]
 name = "chrono"
@@ -1376,12 +1354,6 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
 
-[[package]]
-name = "claxon"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
-
 [[package]]
 name = "cli"
 version = "0.1.0"
@@ -1407,6 +1379,7 @@ dependencies = [
  "async-tungstenite",
  "collections",
  "db",
+ "feature_flags",
  "futures 0.3.28",
  "gpui",
  "image",
@@ -1421,9 +1394,9 @@ dependencies = [
  "serde_derive",
  "settings",
  "smol",
- "staff_mode",
  "sum_tree",
  "tempfile",
+ "text",
  "thiserror",
  "time 0.3.24",
  "tiny_http",
@@ -1479,7 +1452,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.16.0"
+version = "0.19.0"
 dependencies = [
  "anyhow",
  "async-tungstenite",
@@ -1488,8 +1461,11 @@ dependencies = [
  "axum-extra",
  "base64 0.13.1",
  "call",
+ "channel",
  "clap 3.2.25",
  "client",
+ "clock",
+ "collab_ui",
  "collections",
  "ctor",
  "dashmap",
@@ -1514,6 +1490,7 @@ dependencies = [
  "pretty_assertions",
  "project",
  "prometheus",
+ "prost 0.8.0",
  "rand 0.8.5",
  "reqwest",
  "rpc",
@@ -1526,6 +1503,7 @@ dependencies = [
  "settings",
  "sha-1 0.9.8",
  "sqlx",
+ "text",
  "theme",
  "time 0.3.24",
  "tokio",
@@ -1548,21 +1526,26 @@ dependencies = [
  "anyhow",
  "auto_update",
  "call",
+ "channel",
  "client",
  "clock",
  "collections",
  "context_menu",
+ "db",
  "editor",
+ "feature_flags",
  "feedback",
  "futures 0.3.28",
  "fuzzy",
  "gpui",
+ "language",
  "log",
  "menu",
  "picker",
  "postage",
  "project",
  "recent_projects",
+ "schemars",
  "serde",
  "serde_derive",
  "settings",
@@ -1623,6 +1606,19 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "component_test"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "project",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "concurrent-queue"
 version = "2.2.0"
@@ -2070,15 +2066,6 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
-[[package]]
-name = "cursor-icon"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "740bb192a8e2d1350119916954f4409ee7f62f149b536911eeb78ba5a20526bf"
-dependencies = [
- "serde",
-]
-
 [[package]]
 name = "dashmap"
 version = "5.5.0"
@@ -2161,6 +2148,15 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "derive_refineable"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "dhat"
 version = "0.3.2"
@@ -2285,12 +2281,6 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
-[[package]]
-name = "dispatch"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
-
 [[package]]
 name = "dlib"
 version = "0.5.2"
@@ -2544,6 +2534,14 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
 
+[[package]]
+name = "feature_flags"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+]
+
 [[package]]
 name = "feedback"
 version = "0.1.0"
@@ -2769,6 +2767,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempfile",
+ "text",
  "time 0.3.24",
  "util",
 ]
@@ -3149,6 +3148,7 @@ dependencies = [
  "png",
  "postage",
  "rand 0.8.5",
+ "refineable",
  "resvg",
  "schemars",
  "seahash",
@@ -3160,6 +3160,7 @@ dependencies = [
  "smol",
  "sqlez",
  "sum_tree",
+ "taffy",
  "time 0.3.24",
  "tiny-skia",
  "usvg",
@@ -3172,12 +3173,18 @@ dependencies = [
 name = "gpui_macros"
 version = "0.1.0"
 dependencies = [
- "gpui",
+ "lazy_static",
  "proc-macro2",
  "quote",
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "grid"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c"
+
 [[package]]
 name = "h2"
 version = "0.3.20"
@@ -3967,17 +3974,6 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
 
-[[package]]
-name = "lewton"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
-dependencies = [
- "byteorder",
- "ogg",
- "tinyvec",
-]
-
 [[package]]
 name = "libc"
 version = "0.2.147"
@@ -4530,7 +4526,7 @@ dependencies = [
  "bitflags 1.3.2",
  "jni-sys",
  "ndk-sys",
- "num_enum 0.5.11",
+ "num_enum",
  "raw-window-handle",
  "thiserror",
 ]
@@ -4572,19 +4568,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "nix"
-version = "0.25.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
-dependencies = [
- "autocfg",
- "bitflags 1.3.2",
- "cfg-if 1.0.0",
- "libc",
- "memoffset 0.6.5",
-]
-
 [[package]]
 name = "nix"
 version = "0.26.2"
@@ -4750,16 +4733,7 @@ version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
 dependencies = [
- "num_enum_derive 0.5.11",
-]
-
-[[package]]
-name = "num_enum"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1"
-dependencies = [
- "num_enum_derive 0.6.1",
+ "num_enum_derive",
 ]
 
 [[package]]
@@ -4774,18 +4748,6 @@ dependencies = [
  "syn 1.0.109",
 ]
 
-[[package]]
-name = "num_enum_derive"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6"
-dependencies = [
- "proc-macro-crate 1.3.1",
- "proc-macro2",
- "quote",
- "syn 2.0.28",
-]
-
 [[package]]
 name = "nvim-rs"
 version = "0.5.0"
@@ -4811,32 +4773,6 @@ dependencies = [
  "objc_exception",
 ]
 
-[[package]]
-name = "objc-sys"
-version = "0.2.0-beta.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7"
-
-[[package]]
-name = "objc2"
-version = "0.3.0-beta.3.patch-leaks.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468"
-dependencies = [
- "block2",
- "objc-sys",
- "objc2-encode",
-]
-
-[[package]]
-name = "objc2-encode"
-version = "2.0.0-pre.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512"
-dependencies = [
- "objc-sys",
-]
-
 [[package]]
 name = "objc_exception"
 version = "0.1.2"
@@ -4890,15 +4826,6 @@ dependencies = [
  "cc",
 ]
 
-[[package]]
-name = "ogg"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
-dependencies = [
- "byteorder",
-]
-
 [[package]]
 name = "once_cell"
 version = "1.18.0"
@@ -4955,15 +4882,6 @@ dependencies = [
  "vcpkg",
 ]
 
-[[package]]
-name = "orbclient"
-version = "0.3.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "221d488cd70617f1bd599ed8ceb659df2147d9393717954d82a0f5e8032a6ab1"
-dependencies = [
- "redox_syscall 0.3.5",
-]
-
 [[package]]
 name = "ordered-float"
 version = "2.10.0"
@@ -5258,6 +5176,33 @@ version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
+[[package]]
+name = "playground"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "derive_more",
+ "gpui",
+ "log",
+ "parking_lot 0.11.2",
+ "playground_macros",
+ "refineable",
+ "serde",
+ "simplelog",
+ "smallvec",
+ "taffy",
+ "util",
+]
+
+[[package]]
+name = "playground_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "plist"
 version = "1.5.0"
@@ -5711,6 +5656,18 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "quick_action_bar"
+version = "0.1.0"
+dependencies = [
+ "ai",
+ "editor",
+ "gpui",
+ "search",
+ "theme",
+ "workspace",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.32"
@@ -5924,6 +5881,16 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "refineable"
+version = "0.1.0"
+dependencies = [
+ "derive_refineable",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "regalloc2"
 version = "0.2.3"
@@ -6142,11 +6109,8 @@ version = "0.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa"
 dependencies = [
- "claxon",
  "cpal",
  "hound",
- "lewton",
- "symphonia",
 ]
 
 [[package]]
@@ -6238,9 +6202,9 @@ dependencies = [
 
 [[package]]
 name = "rust-embed"
-version = "6.8.1"
+version = "8.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
+checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
 dependencies = [
  "rust-embed-impl",
  "rust-embed-utils",
@@ -6249,9 +6213,9 @@ dependencies = [
 
 [[package]]
 name = "rust-embed-impl"
-version = "6.8.1"
+version = "8.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
+checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6262,9 +6226,9 @@ dependencies = [
 
 [[package]]
 name = "rust-embed-utils"
-version = "7.8.1"
+version = "8.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
+checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada"
 dependencies = [
  "globset",
  "sha2 0.10.7",
@@ -6727,6 +6691,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
+ "sha1",
  "smol",
  "tempdir",
  "theme",
@@ -6890,6 +6855,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "collections",
+ "feature_flags",
  "fs",
  "futures 0.3.28",
  "gpui",
@@ -6905,7 +6871,6 @@ dependencies = [
  "serde_json_lenient",
  "smallvec",
  "sqlez",
- "staff_mode",
  "toml 0.5.11",
  "tree-sitter",
  "tree-sitter-json 0.19.0",
@@ -7145,15 +7110,6 @@ dependencies = [
  "pin-project-lite 0.1.12",
 ]
 
-[[package]]
-name = "smol_str"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c"
-dependencies = [
- "serde",
-]
-
 [[package]]
 name = "snippet"
 version = "0.1.0"
@@ -7339,14 +7295,6 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
-[[package]]
-name = "staff_mode"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "gpui",
-]
-
 [[package]]
 name = "static_assertions"
 version = "1.1.0"
@@ -7480,56 +7428,6 @@ dependencies = [
  "siphasher",
 ]
 
-[[package]]
-name = "symphonia"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941"
-dependencies = [
- "lazy_static",
- "symphonia-bundle-mp3",
- "symphonia-core",
- "symphonia-metadata",
-]
-
-[[package]]
-name = "symphonia-bundle-mp3"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a"
-dependencies = [
- "bitflags 1.3.2",
- "lazy_static",
- "log",
- "symphonia-core",
- "symphonia-metadata",
-]
-
-[[package]]
-name = "symphonia-core"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142"
-dependencies = [
- "arrayvec 0.7.4",
- "bitflags 1.3.2",
- "bytemuck",
- "lazy_static",
- "log",
-]
-
-[[package]]
-name = "symphonia-metadata"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0"
-dependencies = [
- "encoding_rs",
- "lazy_static",
- "log",
- "symphonia-core",
-]
-
 [[package]]
 name = "syn"
 version = "1.0.109"
@@ -7599,6 +7497,17 @@ dependencies = [
  "winx",
 ]
 
+[[package]]
+name = "taffy"
+version = "0.3.11"
+source = "git+https://github.com/DioxusLabs/taffy?rev=dab541d6104d58e2e10ce90c4a1dad0b703160cd#dab541d6104d58e2e10ce90c4a1dad0b703160cd"
+dependencies = [
+ "arrayvec 0.7.4",
+ "grid",
+ "num-traits",
+ "slotmap",
+]
+
 [[package]]
 name = "take-until"
 version = "0.2.0"
@@ -7723,7 +7632,6 @@ dependencies = [
  "ctor",
  "digest 0.9.0",
  "env_logger 0.9.3",
- "fs",
  "gpui",
  "lazy_static",
  "log",
@@ -7766,6 +7674,7 @@ name = "theme_selector"
 version = "0.1.0"
 dependencies = [
  "editor",
+ "feature_flags",
  "fs",
  "fuzzy",
  "gpui",
@@ -7775,7 +7684,6 @@ dependencies = [
  "postage",
  "settings",
  "smol",
- "staff_mode",
  "theme",
  "util",
  "workspace",
@@ -8278,7 +8186,7 @@ dependencies = [
 [[package]]
 name = "tree-sitter"
 version = "0.20.10"
-source = "git+https://github.com/tree-sitter/tree-sitter?rev=1c65ca24bc9a734ab70115188f465e12eecf224e#1c65ca24bc9a734ab70115188f465e12eecf224e"
+source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222"
 dependencies = [
  "cc",
  "regex",
@@ -8840,12 +8748,6 @@ dependencies = [
  "workspace",
 ]
 
-[[package]]
-name = "vec_map"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
-
 [[package]]
 name = "version_check"
 version = "0.9.4"
@@ -9310,17 +9212,6 @@ dependencies = [
  "wasm-bindgen",
 ]
 
-[[package]]
-name = "web-time"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19353897b48e2c4d849a2d73cb0aeb16dc2be4e00c565abfc11eb65a806e47de"
-dependencies = [
- "js-sys",
- "once_cell",
- "wasm-bindgen",
-]
-
 [[package]]
 name = "webpki"
 version = "0.21.4"
@@ -9636,42 +9527,6 @@ version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
 
-[[package]]
-name = "winit"
-version = "0.29.0-beta.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f1afaf8490cc3f1309520ebb53a4cd3fc3642c7df8064a4b074bb9867998d44"
-dependencies = [
- "android-activity",
- "atomic-waker",
- "bitflags 2.3.3",
- "calloop",
- "cfg_aliases",
- "core-foundation",
- "core-graphics",
- "cursor-icon",
- "dispatch",
- "js-sys",
- "libc",
- "log",
- "ndk",
- "ndk-sys",
- "objc2",
- "once_cell",
- "orbclient",
- "raw-window-handle",
- "redox_syscall 0.3.5",
- "serde",
- "smol_str",
- "unicode-segmentation",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "web-time",
- "windows-sys",
- "xkbcommon-dl",
-]
-
 [[package]]
 name = "winnow"
 version = "0.5.2"
@@ -9730,6 +9585,7 @@ dependencies = [
  "async-recursion 1.0.4",
  "bincode",
  "call",
+ "channel",
  "client",
  "collections",
  "context_menu",
@@ -9789,25 +9645,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "xkbcommon-dl"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6924668544c48c0133152e7eec86d644a056ca3d09275eb8d5cdb9855f9d8699"
-dependencies = [
- "bitflags 2.3.3",
- "dlib",
- "log",
- "once_cell",
- "xkeysym",
-]
-
-[[package]]
-name = "xkeysym"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621"
-
 [[package]]
 name = "xmlparser"
 version = "0.13.5"
@@ -9860,7 +9697,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.100.0"
+version = "0.103.0"
 dependencies = [
  "activity_indicator",
  "ai",
@@ -9874,6 +9711,7 @@ dependencies = [
  "backtrace",
  "breadcrumbs",
  "call",
+ "channel",
  "chrono",
  "cli",
  "client",
@@ -9881,6 +9719,7 @@ dependencies = [
  "collab_ui",
  "collections",
  "command_palette",
+ "component_test",
  "context_menu",
  "copilot",
  "copilot_button",
@@ -9889,6 +9728,7 @@ dependencies = [
  "diagnostics",
  "editor",
  "env_logger 0.9.3",
+ "feature_flags",
  "feedback",
  "file_finder",
  "fs",
@@ -9919,6 +9759,7 @@ dependencies = [
  "project",
  "project_panel",
  "project_symbols",
+ "quick_action_bar",
  "rand 0.8.5",
  "recent_projects",
  "regex",
@@ -9934,7 +9775,6 @@ dependencies = [
  "simplelog",
  "smallvec",
  "smol",
- "staff_mode",
  "sum_tree",
  "tempdir",
  "terminal_view",

Cargo.toml 🔗

@@ -6,6 +6,7 @@ members = [
     "crates/auto_update",
     "crates/breadcrumbs",
     "crates/call",
+    "crates/channel",
     "crates/cli",
     "crates/client",
     "crates/clock",
@@ -13,10 +14,13 @@ members = [
     "crates/collab_ui",
     "crates/collections",
     "crates/command_palette",
+    "crates/component_test",
     "crates/context_menu",
     "crates/copilot",
     "crates/copilot_button",
     "crates/db",
+    "crates/refineable",
+    "crates/refineable/derive_refineable",
     "crates/diagnostics",
     "crates/drag_and_drop",
     "crates/editor",
@@ -28,6 +32,8 @@ members = [
     "crates/git",
     "crates/go_to_line",
     "crates/gpui",
+    "crates/gpui/playground",
+    "crates/gpui/playground_macros",
     "crates/gpui_macros",
     "crates/install_cli",
     "crates/journal",
@@ -56,7 +62,7 @@ members = [
     "crates/snippet",
     "crates/sqlez",
     "crates/sqlez_macros",
-    "crates/staff_mode",
+    "crates/feature_flags",
     "crates/sum_tree",
     "crates/terminal",
     "crates/text",
@@ -91,9 +97,11 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 ordered-float = { version = "2.1.1" }
 parking_lot = { version = "0.11.1" }
 postage = { version = "0.5", features = ["futures-traits"] }
+prost = { version = "0.8" }
 rand = { version = "0.8.5" }
+refineable = { path = "./crates/refineable" }
 regex = { version = "1.5" }
-rust-embed = { version = "6.3", features = ["include-exclude"] }
+rust-embed = { version = "8.0", features = ["include-exclude"] }
 schemars = { version = "0.8" }
 serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -135,7 +143,7 @@ tree-sitter-lua = "0.0.14"
 tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
 
 [patch.crates-io]
-tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "1c65ca24bc9a734ab70115188f465e12eecf224e" }
+tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
 async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
 
 # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457

Dockerfile 🔗

@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust:1.71-bullseye as builder
+FROM rust:1.72-bullseye as builder
 WORKDIR app
 COPY . .
 

assets/icons/ai.svg 🔗

@@ -0,0 +1,23 @@
+<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 8.94203V11C7.38649 11 6.61351 11 4 11V10.6812L10 5.31884V5H4V7.08696" stroke="black" stroke-width="1.25"/>
+<circle cx="0.5" cy="8" r="0.5" fill="black"/>
+<circle cx="1.49976" cy="5.82825" r="0.5" fill="black" fill-opacity="0.75"/>
+<circle cx="1.49976" cy="10.1719" r="0.5" fill="black" fill-opacity="0.75"/>
+<circle cx="13.5" cy="8.01581" r="0.5" fill="black"/>
+<circle cx="12.5" cy="5.84387" r="0.5" fill="black" fill-opacity="0.75"/>
+<circle cx="12.5" cy="10.1877" r="0.5" fill="black" fill-opacity="0.75"/>
+<circle cx="6.99219" cy="1.48438" r="0.5" fill="black"/>
+<circle cx="4.5" cy="2.5" r="0.5" fill="black" fill-opacity="0.75"/>
+<circle cx="0.5" cy="12.016" r="0.5" fill="black"/>
+<circle cx="0.5" cy="3.98438" r="0.5" fill="black"/>
+<circle cx="13.5" cy="12.016" r="0.5" fill="black"/>
+<circle cx="13.5" cy="3.98438" r="0.5" fill="black"/>
+<circle cx="2.49976" cy="14.516" r="0.5" fill="black"/>
+<circle cx="2.48413" cy="1.48438" r="0.5" fill="black"/>
+<circle cx="11.5" cy="14.516" r="0.5" fill="black"/>
+<circle cx="11.5" cy="1.48438" r="0.5" fill="black"/>
+<circle cx="9.49609" cy="2.48438" r="0.5" fill="black" fill-opacity="0.75"/>
+<circle cx="6.99219" cy="14.5" r="0.5" fill="black"/>
+<circle cx="4.50391" cy="13.516" r="0.5" fill="black" fill-opacity="0.75"/>
+<circle cx="9.49609" cy="13.5" r="0.5" fill="black" fill-opacity="0.75"/>
+</svg>

assets/icons/arrow_left.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.125 6.99344L6.35938 3.63281M3.125 6.99344L6.35938 10.3672M3.125 6.99344H11" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/arrow_right.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.8906 7.00125L7.64062 3.64062M10.8906 7.00125L7.64062 10.375M10.8906 7.00125H3" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/case_insensitive_12.svg 🔗

@@ -0,0 +1,8 @@
+<?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/channel_hash.svg 🔗

@@ -0,0 +1,6 @@
+<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.74785" y2="11.0142" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<line x1="6.26624" y1="2.99597" x2="3.7346" y2="11.3717" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<line x1="3.15982" y1="5.3799" x2="11.9098" y2="5.3799" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<line x1="2.0983" y1="8.62407" x2="10.8483" y2="8.62407" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/check.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.98438 7.85115L6.13569 9.44983L9.98438 4.08141" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/check_circle.svg 🔗

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

assets/icons/chevron_down.svg 🔗

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

assets/icons/chevron_left.svg 🔗

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

assets/icons/chevron_right.svg 🔗

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

assets/icons/chevron_up.svg 🔗

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

assets/icons/conversations.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.46115 8.43419C7.30678 8.43419 8.92229 7.43411 8.92229 5.21171C8.92229 2.98933 7.30678 1.98926 5.46115 1.98926C3.61553 1.98926 2 2.98933 2 5.21171C2 6.028 2.21794 6.67935 2.58519 7.17685C2.7184 7.35732 2.69033 7.77795 2.58387 7.97539C2.32908 8.44793 2.81048 8.9657 3.33372 8.84571C3.72539 8.75597 4.13621 8.63447 4.49574 8.4715C4.62736 8.41181 4.7727 8.38777 4.91631 8.40402C5.09471 8.42416 5.27678 8.43419 5.46115 8.43419Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>

assets/icons/copilot.svg 🔗

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

assets/icons/copy.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="2" y="5.64062" width="6.35938" height="6.35938" rx="0.5" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M8.01562 3.75H5.625V2.03125H11.9375V8.39062H10.2656V6C10.2656 4.75736 9.25827 3.75 8.01562 3.75Z" fill="black" fill-opacity="0.5"/>
+<path d="M5.625 3.125V2.5C5.625 2.22386 5.84886 2 6.125 2H11.5C11.7761 2 12 2.22386 12 2.5V7.875C12 8.15114 11.7761 8.375 11.5 8.375H10.8906" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/ellipsis.svg 🔗

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

assets/icons/error.svg 🔗

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

assets/icons/exit.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.3594 7.00127L9.86062 4.5025M12.3594 7.00127L9.86062 9.50002M12.3594 7.00127L5 7.00127" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H6" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/feedback.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 3.5C2 3.22386 2.22386 3 2.5 3H11.5C11.7761 3 12 3.22386 12 3.5V10.5C12 10.7761 11.7761 11 11.5 11H2.5C2.22386 11 2 10.7761 2 10.5V3.5Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M3 4L6.95312 7L11 4" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 9L5 8" stroke="black" stroke-opacity="0.5" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 9L9 8" stroke="black" stroke-opacity="0.5" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/elixir.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 2L8.6165 2.10275C8.65805 1.8534 8.54532 1.60357 8.33085 1.46975C8.11639 1.33594 7.84243 1.34449 7.63673 1.49142L8 2ZM9.88714 8.62257C10.1098 9.73604 9.86526 10.3554 9.4569 10.7229C9.00367 11.1308 8.19498 11.375 7 11.375V12.625C8.30502 12.625 9.49633 12.3692 10.2931 11.6521C11.1347 10.8946 11.3902 9.76396 11.1129 8.37743L9.88714 8.62257ZM7 11.375C5.87824 11.375 5.17563 11.0417 4.75444 10.6206C4.32847 10.1946 4.125 9.61372 4.125 9H2.875C2.875 9.88628 3.17153 10.8054 3.87056 11.5044C4.57437 12.2083 5.62176 12.625 7 12.625V11.375ZM4.125 9C4.125 7.72699 5.00594 4.90668 8.36327 2.50858L7.63673 1.49142C3.99406 4.09332 2.875 7.27301 2.875 9H4.125ZM7.3835 1.89725C7.09577 3.62363 7.69108 4.78835 8.35497 5.78419C9.03189 6.79957 9.66859 7.52983 9.88714 8.62257L11.1129 8.37743C10.8314 6.97017 9.96811 5.95043 9.39503 5.09081C8.80892 4.21165 8.40423 3.37637 8.6165 2.10275L7.3835 1.89725Z" fill="black"/>
+</svg>

assets/icons/file_icons/file_types.json 🔗

@@ -21,23 +21,27 @@
         "dll": "storage",
         "doc": "document",
         "docx": "document",
+        "eex": "elixir",
         "eslintrc": "eslint",
         "eslintrc.js": "eslint",
         "eslintrc.json": "eslint",
+        "ex": "elixir",
+        "exs": "elixir",
+        "fish": "terminal",
+        "flac": "audio",
         "fmp": "storage",
         "fp7": "storage",
-        "flac": "audio",
-        "fish": "terminal",
         "frm": "storage",
         "gdb": "storage",
+        "gif": "image",
         "gitattributes": "vcs",
         "gitignore": "vcs",
         "gitmodules": "vcs",
-        "gif": "image",
         "go": "code",
         "h": "code",
         "handlebars": "code",
         "hbs": "template",
+        "heex": "elixir",
         "htm": "template",
         "html": "template",
         "ib": "storage",
@@ -51,16 +55,16 @@
         "ldf": "storage",
         "lock": "lock",
         "log": "log",
-        "mdb": "storage",
         "md": "document",
+        "mdb": "storage",
         "mdf": "storage",
         "mdx": "document",
         "mp3": "audio",
         "mp4": "video",
         "myd": "storage",
         "myi": "storage",
-        "ods": "document",
         "odp": "document",
+        "ods": "document",
         "odt": "document",
         "ogg": "video",
         "pdb": "storage",
@@ -74,24 +78,24 @@
         "profile": "terminal",
         "ps1": "terminal",
         "psd": "image",
-        "py": "code",
+        "py": "python",
         "rb": "code",
         "rkt": "code",
         "rs": "rust",
         "rtf": "document",
         "sav": "storage",
         "scm": "code",
+        "sdf": "storage",
         "sh": "terminal",
         "sqlite": "storage",
-        "sdf": "storage",
         "svelte": "template",
         "svg": "image",
         "swift": "code",
-        "ts": "typescript",
-        "tsx": "code",
         "tiff": "image",
         "toml": "toml",
+        "ts": "typescript",
         "tsv": "storage",
+        "tsx": "code",
         "txt": "document",
         "wav": "audio",
         "webm": "video",
@@ -103,9 +107,9 @@
         "zlogin": "terminal",
         "zsh": "terminal",
         "zsh_aliases": "terminal",
-        "zshenv": "terminal",
         "zsh_histfile": "terminal",
         "zsh_profile": "terminal",
+        "zshenv": "terminal",
         "zshrc": "terminal"
     },
     "types": {
@@ -127,6 +131,9 @@
         "document": {
             "icon": "icons/file_icons/book.svg"
         },
+        "elixir": {
+            "icon": "icons/file_icons/elixir.svg"
+        },
         "eslint": {
             "icon": "icons/file_icons/eslint.svg"
         },
@@ -145,9 +152,15 @@
         "log": {
             "icon": "icons/file_icons/info.svg"
         },
+        "phoenix": {
+            "icon": "icons/file_icons/phoenix.svg"
+        },
         "prettier": {
             "icon": "icons/file_icons/prettier.svg"
         },
+        "python": {
+            "icon": "icons/file_icons/python.svg"
+        },
         "rust": {
             "icon": "icons/file_icons/rust.svg"
         },

assets/icons/file_icons/phoenix.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 8C12 7.32138 11.9375 6.5 11.7188 5.75C11.0625 6.53125 9.875 7.1875 9 7.5C9.75 4.90625 8.5625 2.1875 7 2C7 3.96875 6.625 4.90625 5.5 6.5C4 4 2.5 5.5 2 6C2.5 6.5 3.21832 7.24064 3.34375 8.3125C3.6875 11.25 5.75 12 7.5 12C9.25 12 9.5 10 11.5 11" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<circle cx="4.03125" cy="6.625" r="1.53125" fill="black"/>
+</svg>

assets/icons/file_icons/python.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.18452 1.9164C5.01625 1.9164 3.98489 2.77625 3.91991 3.9468H3.72024C2.81569 3.9468 2 4.63733 2 5.587V7.1098C2 8.05947 2.81569 8.75 3.72024 8.75H4.33631C4.67376 8.75 5.02976 8.48561 5.02976 8.06155C5.02976 7.46058 5.51694 6.9734 6.11791 6.9734H7.27976C8.18431 6.9734 9 6.28288 9 5.3332V4.0642C9 2.83419 7.93913 1.9164 6.73214 1.9164H6.18452Z" stroke="black" stroke-width="1.25"/>
+<path d="M7.79613 12.0836C8.97889 12.0836 10.0103 11.2025 10.0702 10.0191H10.2738C11.1885 10.0191 12 9.31459 12 8.36187V6.8135C12 5.86077 11.1885 5.15625 10.2738 5.15625H9.65439C9.30991 5.15625 8.96057 5.42749 8.96057 5.84577C8.96057 6.46262 8.46051 6.96268 7.84365 6.96268H6.69494C5.78027 6.96268 4.96875 7.6672 4.96875 8.61993V9.91023C4.96875 11.148 6.02678 12.0836 7.24554 12.0836H7.79613Z" stroke="black" stroke-width="1.25"/>
+<circle cx="6.03975" cy="3.9167" r="0.633501" fill="black"/>
+<circle cx="7.92285" cy="10.0793" r="0.670898" fill="black"/>
+</svg>

assets/icons/filter.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.6749 2.40608C11.8058 2.24239 11.6893 1.99991 11.4796 1.99991H2.51996C2.31033 1.99991 2.19379 2.24239 2.32474 2.40608L5.14583 5.93246C5.34148 6.17701 5.44808 6.48087 5.44808 6.79412C5.44808 7.46881 5.44808 10.334 5.44808 11.5016C5.44808 11.7778 5.67194 11.9999 5.94808 11.9999H8.05153C8.32767 11.9999 8.55153 11.7778 8.55153 11.5016C8.55153 10.334 8.55153 7.46881 8.55153 6.79412C8.55153 6.48087 8.65815 6.17701 8.8538 5.93246L11.6749 2.40608Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/filter_12.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.6748 1.40617C10.8058 1.24248 10.6892 1 10.4796 1H1.51991C1.31028 1 1.19374 1.24248 1.32469 1.40617L4.14578 4.93255C4.34144 5.1771 4.44803 5.48097 4.44803 5.79421C4.44803 6.4689 4.44803 9.33412 4.44803 10.5017C4.44803 10.7779 4.67189 11 4.94803 11H7.05148C7.32762 11 7.55148 10.7779 7.55148 10.5017C7.55148 9.33412 7.55148 6.4689 7.55148 5.79421C7.55148 5.48097 7.6581 5.1771 7.85376 4.93255L10.6748 1.40617Z" stroke="#787D87" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/filter_14.svg 🔗

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
+<g id="surface1">
+<path style="fill:none;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(47.058824%,49.019608%,52.941176%);stroke-opacity:1;stroke-miterlimit:4;" d="M 10.674107 1.40625 C 10.804688 1.242188 10.690848 1.001116 10.479911 1.001116 L 1.520089 1.001116 C 1.309152 1.001116 1.195312 1.242188 1.325893 1.40625 L 4.145089 4.93192 C 4.342634 5.176339 4.446429 5.481027 4.446429 5.795759 C 4.446429 6.46875 4.446429 9.334821 4.446429 10.503348 C 4.446429 10.777902 4.670759 10.998884 4.948661 10.998884 L 7.051339 10.998884 C 7.329241 10.998884 7.550223 10.777902 7.550223 10.503348 C 7.550223 9.334821 7.550223 6.46875 7.550223 5.795759 C 7.550223 5.481027 7.657366 5.176339 7.854911 4.93192 Z M 10.674107 1.40625 " transform="matrix(1.166667,0,0,1.166667,0,0)"/>
+</g>
+</svg>

assets/icons/hash.svg 🔗

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

assets/icons/html.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.15735 3.17108L5.84271 10.8289" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M4 5L2 7L4 9" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 9L12 7L10 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/inlay_hint.svg 🔗

@@ -0,0 +1,5 @@
+<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/kebab.svg 🔗

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

assets/icons/lock.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="3" y="5" width="8" height="7" rx="0.5" stroke="black" stroke-width="1.25"/>
+<path d="M4 4C4 2.89543 4.89543 2 6 2H8C9.10457 2 10 2.89543 10 4V5H4V4Z" stroke="black" stroke-opacity="0.75" stroke-width="1.25"/>
+<circle cx="7" cy="8" r="1" fill="black"/>
+<path d="M7 8V9.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/magnifying_glass.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 12L9.41379 9.41379M2 6.31034C2 3.92981 3.92981 2 6.31034 2C8.6909 2 10.6207 3.92981 10.6207 6.31034C10.6207 8.6909 8.6909 10.6207 6.31034 10.6207C3.92981 10.6207 2 8.6909 2 6.31034Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/match_case.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.47087 3.20502H4.93146L7.12233 10.845H6.16733L5.60557 8.91252H2.78552L2.235 10.845H1.28L3.47087 3.20502ZM5.3921 8.06988L4.24611 4.02519H4.15622L3.01023 8.06988H5.3921Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.35784 3.05502H5.04449L7.32139 10.995H6.05473L5.49297 9.06253H2.89876L2.34823 10.995H1.08094L3.35784 3.05502ZM4.20117 4.41683L3.20863 7.91989H5.1937L4.20117 4.41683Z" fill="black"/>

assets/icons/match_word.svg 🔗

@@ -0,0 +1,5 @@
+<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.74677 9.48683L4.07035 6.03229L3.38589 9.48683H2.17618L1.00285 4.00778H2.27563L2.81571 7.41751L3.48443 4.01749H4.65869L5.31824 7.41173L5.8574 4.00778H7.13018L5.95684 9.48683H4.74677Z" fill="black"/>

assets/icons/maximize.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 8.5V12M2 12H5.5M2 12L6.01562 7.98437" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 5.5V2M12 2L8.5 2M12 2L8.01562 5.98437" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/microphone.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.5 8.5C10.5 8.5 9.375 10 7 10C4.625 10 3.5 8.5 3.5 8.5" stroke="black" stroke-width="1.25"/>
+<rect x="5" y="2" width="4" height="5.40625" rx="2" fill="black" fill-opacity="0.25" stroke="black" stroke-width="1.25"/>
+<path d="M7 10V12M7 12H9M7 12H5" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/minimize.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.01563 11.4844L6.01563 7.98438M6.01563 7.98438L2.51563 7.98437M6.01563 7.98438L2 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.01562 2.48438V5.98438M8.01562 5.98438H11.5156M8.01562 5.98438L12 2" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/plus.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 3V11M11 7H3" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/project.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.03125 2V2.03125M2.03125 8C2.03125 10 5 10 5 10M2.03125 8V2.03125M2.03125 8L2.03125 11M2.03125 2.03125C2.03125 4 5 4 5 4" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<rect x="7.375" y="2.375" width="4.25" height="3.25" rx="1.125" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
+<rect x="7.375" y="8.375" width="4.25" height="3.25" rx="1.125" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
+</svg>

assets/icons/replace.svg 🔗

@@ -0,0 +1,11 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 12C4.97279 12 3.22735 10.7936 2.4425 9.0595M7 2C9.11228 2 10.9186 3.30981 11.6512 5.16152" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<circle cx="1.65625" cy="1.67188" r="0.625" fill="black" fill-opacity="0.75"/>
+<circle cx="3.71094" cy="1.67188" r="0.625" fill="black" fill-opacity="0.75"/>
+<circle cx="4.96094" cy="3.36719" r="0.625" fill="black" fill-opacity="0.75"/>
+<circle cx="3.71094" cy="4.79688" r="0.625" fill="black" fill-opacity="0.75"/>
+<circle cx="4.60156" cy="6.67188" r="0.625" fill="black" fill-opacity="0.75"/>
+<circle cx="1.65625" cy="4.17188" r="0.625" fill="black" fill-opacity="0.75"/>
+<circle cx="1.65625" cy="6.67188" r="0.625" fill="black" fill-opacity="0.75"/>

assets/icons/replace_all.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.10517 5.8012C4.07193 5.73172 4.00176 5.6875 3.92475 5.6875H3.44609C3.33564 5.6875 3.24609 5.77704 3.24609 5.8875V7.26172C3.24609 7.53786 3.02224 7.76172 2.74609 7.76172H2.64062C2.36448 7.76172 2.14062 7.53786 2.14062 7.26172V2.625C2.14062 2.34886 2.36448 2.125 2.64062 2.125H4.1875C5.41406 2.125 6.16406 2.80469 6.16406 3.92188C6.16406 4.57081 5.85885 5.12418 5.36073 5.40943C5.25888 5.46775 5.20921 5.59421 5.2617 5.69918L5.93117 7.03811C6.09739 7.37056 5.85564 7.76172 5.48395 7.76172H5.35806C5.16552 7.76172 4.99009 7.65117 4.907 7.47748L4.10517 5.8012ZM3.44609 3.03125C3.33564 3.03125 3.24609 3.12079 3.24609 3.23125V4.63594C3.24609 4.74639 3.33564 4.83594 3.44609 4.83594H4.03125C4.66016 4.83594 5.03516 4.49609 5.03516 3.92578C5.03516 3.36719 4.66797 3.03125 4.04297 3.03125H3.44609Z" fill="black" fill-opacity="0.75"/>

assets/icons/replace_next.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.96454 5.6762C3.93131 5.60672 3.86114 5.5625 3.78412 5.5625H3.30547C3.19501 5.5625 3.10547 5.65204 3.10547 5.7625V7.13672C3.10547 7.41286 2.88161 7.63672 2.60547 7.63672H2.5C2.22386 7.63672 2 7.41286 2 7.13672V2.5C2 2.22386 2.22386 2 2.5 2H4.04688C5.27344 2 6.02344 2.67969 6.02344 3.79688C6.02344 4.44581 5.71823 4.99918 5.2201 5.28443C5.11826 5.34275 5.06859 5.46921 5.12107 5.57418L5.79054 6.91311C5.95677 7.24556 5.71502 7.63672 5.34333 7.63672H5.21743C5.02489 7.63672 4.84946 7.52617 4.76638 7.35248L3.96454 5.6762ZM3.30547 2.90625C3.19501 2.90625 3.10547 2.99579 3.10547 3.10625V4.51094C3.10547 4.62139 3.19501 4.71094 3.30547 4.71094H3.89062C4.51953 4.71094 4.89453 4.37109 4.89453 3.80078C4.89453 3.24219 4.52734 2.90625 3.90234 2.90625H3.30547Z" fill="black" fill-opacity="0.75"/>
+<path d="M3.78412 5.6125C3.84188 5.6125 3.89451 5.64567 3.91944 5.69777L4.72127 7.37405C4.81266 7.56511 5.00564 7.68672 5.21743 7.68672H5.34333C5.75219 7.68672 6.01811 7.25645 5.83526 6.89075L5.1658 5.55182C5.12715 5.47453 5.16207 5.37528 5.24495 5.32782C5.76044 5.03262 6.07344 4.46155 6.07344 3.79688C6.07344 3.22658 5.88164 2.76303 5.52873 2.44248C5.17642 2.12247 4.6691 1.95 4.04688 1.95H2.5C2.19624 1.95 1.95 2.19624 1.95 2.5V7.13672C1.95 7.44048 2.19624 7.68672 2.5 7.68672H2.60547C2.90923 7.68672 3.15547 7.44048 3.15547 7.13672V5.7625C3.15547 5.67966 3.22263 5.6125 3.30547 5.6125H3.78412ZM3.15547 3.10625C3.15547 3.02341 3.22263 2.95625 3.30547 2.95625H3.90234C4.20626 2.95625 4.44101 3.03787 4.59926 3.18111C4.75686 3.32376 4.84453 3.5329 4.84453 3.80078C4.84453 4.07452 4.75491 4.28758 4.59484 4.43268C4.43413 4.57837 4.19643 4.66094 3.89062 4.66094H3.30547C3.22263 4.66094 3.15547 4.59378 3.15547 4.51094V3.10625Z" stroke="black" stroke-opacity="0.75" stroke-width="0.1"/>
+<path d="M7.5 5.88672C9.433 5.88672 11 7.45372 11 9.38672V12M11 12L13 10M11 12L9 10" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/screen.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="2" y="2" width="10" height="7" rx="0.5" fill="black" fill-opacity="0.25" stroke="black" stroke-width="1.25"/>
+<path d="M7 9V12M7 12H9M7 12H5" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/split.svg 🔗

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

assets/icons/success.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 2.5C2 2.22386 2.22386 2 2.5 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H2.5C2.22386 12 2 11.7761 2 11.5V2.5Z" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M4.60938 7.625L6.3125 8.89062L9.35938 4.64062" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/terminal.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1.65625 2.5C1.65625 2.22386 1.88011 2 2.15625 2H11.8437C12.1199 2 12.3438 2.22386 12.3438 2.5V11.5C12.3438 11.7761 12.1199 12 11.8437 12H2.15625C1.88011 12 1.65625 11.7761 1.65625 11.5V2.5Z" stroke="black" stroke-width="1.25"/>
+<path d="M4.375 9L6.375 7L4.375 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.625 9L9.90625 9" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/warning.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.45563 12.3438H11.5444C11.9137 12.3438 12.1556 11.9571 11.994 11.625L10.2346 8.00952C9.77174 7.05841 8.89104 6.37821 7.85383 6.17077C7.29019 6.05804 6.70981 6.05804 6.14617 6.17077C5.10896 6.37821 4.22826 7.05841 3.76542 8.00952L2.00603 11.625C1.84442 11.9571 2.08628 12.3438 2.45563 12.3438Z" fill="#001A33" fill-opacity="0.157"/>
+<path d="M9.5 6.5L11.994 11.625C12.1556 11.9571 11.9137 12.3438 11.5444 12.3438H2.45563C2.08628 12.3438 1.84442 11.9571 2.00603 11.625L4.5 6.5" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 7L7 2" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<circle cx="7" cy="9.24219" r="0.75" fill="#11181C"/>
+</svg>

assets/icons/word_search_12.svg 🔗

@@ -0,0 +1,8 @@
+<?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:evenodd;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 4.070312 8.132812 L 3.488281 5.171875 L 2.902344 8.132812 L 1.867188 8.132812 L 0.859375 3.433594 L 1.949219 3.433594 L 2.414062 6.359375 L 2.988281 3.445312 L 3.992188 3.445312 L 4.558594 6.351562 L 5.019531 3.433594 L 6.113281 3.433594 L 5.105469 8.132812 Z M 4.070312 8.132812 "/>

assets/icons/word_search_14.svg 🔗

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

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

assets/keymaps/default.json 🔗

@@ -13,6 +13,7 @@
       "cmd-up": "menu::SelectFirst",
       "cmd-down": "menu::SelectLast",
       "enter": "menu::Confirm",
+      "ctrl-enter": "menu::ShowContextMenu",
       "cmd-enter": "menu::SecondaryConfirm",
       "escape": "menu::Cancel",
       "ctrl-c": "menu::Cancel",
@@ -172,6 +173,7 @@
     "context": "Editor && mode == full",
     "bindings": {
       "enter": "editor::Newline",
+      "shift-enter": "editor::Newline",
       "cmd-shift-enter": "editor::NewlineAbove",
       "cmd-enter": "editor::NewlineBelow",
       "alt-z": "editor::ToggleSoftWrap",
@@ -224,7 +226,8 @@
       "tab": "buffer_search::FocusEditor",
       "enter": "search::SelectNextMatch",
       "shift-enter": "search::SelectPrevMatch",
-      "alt-enter": "search::SelectAllMatches"
+      "alt-enter": "search::SelectAllMatches",
+      "alt-tab": "search::CycleMode"
     }
   },
   {
@@ -237,7 +240,8 @@
   {
     "context": "ProjectSearchBar",
     "bindings": {
-      "escape": "project_search::ToggleFocus"
+      "escape": "project_search::ToggleFocus",
+      "alt-tab": "search::CycleMode"
     }
   },
   {
@@ -250,7 +254,8 @@
   {
     "context": "ProjectSearchView",
     "bindings": {
-      "escape": "project_search::ToggleFocus"
+      "escape": "project_search::ToggleFocus",
+      "alt-tab": "search::CycleMode"
     }
   },
   {
@@ -262,7 +267,8 @@
       "alt-enter": "search::SelectAllMatches",
       "alt-cmd-c": "search::ToggleCaseSensitive",
       "alt-cmd-w": "search::ToggleWholeWord",
-      "alt-cmd-r": "search::ToggleRegex"
+      "alt-tab": "search::CycleMode",
+      "alt-cmd-f": "project_search::ToggleFilters"
     }
   },
   // Bindings from VS Code
@@ -513,8 +519,10 @@
   {
     "bindings": {
       "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
-      "cmd-shift-c": "collab::ToggleContactsMenu",
-      "cmd-alt-i": "zed::DebugElements"
+      // TODO: Move this to a dock open action
+      "cmd-shift-c": "collab_panel::ToggleFocus",
+      "cmd-alt-i": "zed::DebugElements",
+      "ctrl-shift-:": "editor::ToggleInlayHints",
     }
   },
   {
@@ -522,7 +530,8 @@
     "bindings": {
       "alt-enter": "editor::OpenExcerpts",
       "cmd-f8": "editor::GoToHunk",
-      "cmd-shift-f8": "editor::GoToPrevHunk"
+      "cmd-shift-f8": "editor::GoToPrevHunk",
+      "ctrl-enter": "assistant::InlineAssist"
     }
   },
   {
@@ -536,6 +545,8 @@
     "bindings": {
       "left": "project_panel::CollapseSelectedEntry",
       "right": "project_panel::ExpandSelectedEntry",
+      "cmd-n": "project_panel::NewFile",
+      "alt-cmd-n": "project_panel::NewDirectory",
       "cmd-x": "project_panel::Cut",
       "cmd-c": "project_panel::Copy",
       "cmd-v": "project_panel::Paste",
@@ -549,6 +560,25 @@
       "alt-shift-f": "project_panel::NewSearchInDirectory"
     }
   },
+  {
+    "context": "CollabPanel",
+    "bindings": {
+      "ctrl-backspace": "collab_panel::Remove",
+      "space": "menu::Confirm"
+    }
+  },
+  {
+    "context": "ChannelModal",
+    "bindings": {
+      "tab": "channel_modal::ToggleMode"
+    }
+  },
+  {
+    "context": "ChannelModal > Picker > Editor",
+    "bindings": {
+      "tab": "channel_modal::ToggleMode"
+    }
+  },
   {
     "context": "Terminal",
     "bindings": {

assets/keymaps/textmate.json 🔗

@@ -2,7 +2,6 @@
   {
     "bindings": {
       "cmd-shift-o": "projects::OpenRecent",
-      "cmd-shift-b": "branches::OpenRecent",
       "cmd-alt-tab": "project_panel::ToggleFocus"
     }
   },
@@ -12,8 +11,9 @@
       "cmd-l": "go_to_line::Toggle",
       "ctrl-shift-d": "editor::DuplicateLine",
       "cmd-b": "editor::GoToDefinition",
-      "alt-cmd-b": "editor::GoToDefinition",
       "cmd-j": "editor::ScrollCursorCenter",
+      "cmd-enter": "editor::NewlineBelow",
+      "cmd-alt-enter": "editor::NewLineAbove",
       "cmd-shift-l": "editor::SelectLine",
       "cmd-shift-t": "outline::Toggle",
       "alt-backspace": "editor::DeleteToPreviousWordStart",
@@ -51,14 +51,17 @@
         }
       ],
       "ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
-      "ctrl-shift-right": "editor::SelectToNextSubwordEnd"
+      "ctrl-shift-right": "editor::SelectToNextSubwordEnd",
+      "ctrl-w": "editor::SelectNext",
+      "ctrl-u": "editor::ConvertToUpperCase",
+      "ctrl-shift-u": "editor::ConvertToLowerCase",
+      "ctrl-alt-u": "editor::ConvertToUpperCamelCase",
+      "ctrl-_": "editor::ConvertToSnakeCase"
     }
   },
   {
     "context": "Editor && mode == full",
-    "bindings": {
-      "cmd-alt-enter": "editor::NewlineAbove"
-    }
+    "bindings": {}
   },
   {
     "context": "BufferSearchBar",
@@ -85,5 +88,9 @@
   {
     "context": "ProjectPanel",
     "bindings": {}
+  },
+  {
+    "context": "Dock",
+    "bindings": {}
   }
 ]

assets/keymaps/vim.json 🔗

@@ -103,9 +103,19 @@
       ],
       "v": "vim::ToggleVisual",
       "shift-v": "vim::ToggleVisualLine",
+      "ctrl-v": "vim::ToggleVisualBlock",
+      "ctrl-q": "vim::ToggleVisualBlock",
       "*": "vim::MoveToNext",
       "#": "vim::MoveToPrev",
       "0": "vim::StartOfLine", // When no number operator present, use start of line motion
+      "ctrl-f": "vim::PageDown",
+      "pagedown": "vim::PageDown",
+      "ctrl-b": "vim::PageUp",
+      "pageup": "vim::PageUp",
+      "ctrl-d": "vim::ScrollDown",
+      "ctrl-u": "vim::ScrollUp",
+      "ctrl-e": "vim::LineDown",
+      "ctrl-y": "vim::LineUp",
       // "g" commands
       "g g": "vim::StartOfDocument",
       "g h": "editor::Hover",
@@ -127,10 +137,67 @@
           "partialWord": true
         }
       ],
+      "g j": [
+        "vim::Down",
+        {
+          "displayLines": true
+        }
+      ],
+      "g down": [
+        "vim::Down",
+        {
+          "displayLines": true
+        }
+      ],
+      "g k": [
+        "vim::Up",
+        {
+          "displayLines": true
+        }
+      ],
+      "g up": [
+        "vim::Up",
+        {
+          "displayLines": true
+        }
+      ],
+      "g $": [
+        "vim::EndOfLine",
+        {
+          "displayLines": true
+        }
+      ],
+      "g end": [
+        "vim::EndOfLine",
+        {
+          "displayLines": true
+        }
+      ],
+      "g 0": [
+        "vim::StartOfLine",
+        {
+          "displayLines": true
+        }
+      ],
+      "g home": [
+        "vim::StartOfLine",
+        {
+          "displayLines": true
+        }
+      ],
+      "g ^": [
+        "vim::FirstNonWhitespace",
+        {
+          "displayLines": true
+        }
+      ],
       // z commands
       "z t": "editor::ScrollCursorTop",
       "z z": "editor::ScrollCursorCenter",
       "z b": "editor::ScrollCursorBottom",
+      "z c": "editor::Fold",
+      "z o": "editor::UnfoldLines",
+      "z f": "editor::FoldSelectedRanges",
       // Count support
       "1": [
         "vim::Number",
@@ -277,6 +344,12 @@
       "shift-o": "vim::InsertLineAbove",
       "~": "vim::ChangeCase",
       "p": "vim::Paste",
+      "shift-p": [
+        "vim::Paste",
+        {
+          "before": true
+        }
+      ],
       "u": "editor::Undo",
       "ctrl-r": "editor::Redo",
       "/": "vim::Search",
@@ -293,14 +366,6 @@
           "backwards": true
         }
       ],
-      "ctrl-f": "vim::PageDown",
-      "pagedown": "vim::PageDown",
-      "ctrl-b": "vim::PageUp",
-      "pageup": "vim::PageUp",
-      "ctrl-d": "vim::ScrollDown",
-      "ctrl-u": "vim::ScrollUp",
-      "ctrl-e": "vim::LineDown",
-      "ctrl-y": "vim::LineUp",
       "r": [
         "vim::PushOperator",
         "Replace"
@@ -365,7 +430,7 @@
     }
   },
   {
-    "context": "Editor && vim_mode == visual && !VimWaiting",
+    "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject",
     "bindings": {
       "u": "editor::Undo",
       "o": "vim::OtherEnd",
@@ -373,10 +438,21 @@
       "d": "vim::VisualDelete",
       "x": "vim::VisualDelete",
       "y": "vim::VisualYank",
-      "p": "vim::VisualPaste",
+      "p": "vim::Paste",
+      "shift-p": [
+        "vim::Paste",
+        {
+          "preserveClipboard": true
+        }
+      ],
       "s": "vim::Substitute",
       "c": "vim::Substitute",
       "~": "vim::ChangeCase",
+      "shift-i": [
+        "vim::SwitchMode",
+        "Insert"
+      ],
+      "shift-a": "vim::InsertAfter",
       "r": [
         "vim::PushOperator",
         "Replace"
@@ -394,11 +470,27 @@
         "Normal"
       ],
       ">": "editor::Indent",
-      "<": "editor::Outdent"
+      "<": "editor::Outdent",
+      "i": [
+        "vim::PushOperator",
+        {
+          "Object": {
+            "around": false
+          }
+        }
+      ],
+      "a": [
+        "vim::PushOperator",
+        {
+          "Object": {
+            "around": true
+          }
+        }
+      ],
     }
   },
   {
-    "context": "Editor && vim_mode == insert",
+    "context": "Editor && vim_mode == insert && !menu",
     "bindings": {
       "escape": "vim::NormalBefore",
       "ctrl-c": "vim::NormalBefore",

assets/settings/default.json 🔗

@@ -98,6 +98,7 @@
     // Whether to show selections in the scrollbar.
     "selections": true
   },
+  "relative_line_numbers": false,
   // Inlay hint related settings
   "inlay_hints": {
     // Global switch to toggle hints on and off, switched off by default.
@@ -122,13 +123,29 @@
     // Amount of indentation for nested items.
     "indent_size": 20
   },
+  "collaboration_panel": {
+    // Whether to show the collaboration panel button in the status bar.
+    "button": true,
+    // Where to dock channels panel. Can be 'left' or 'right'.
+    "dock": "left",
+    // Default width of the channels panel.
+    "default_width": 240
+  },
   "assistant": {
+    // Whether to show the assistant panel button in the status bar.
+    "button": true,
     // Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
     "dock": "right",
     // Default width when the assistant is docked to the left or right.
     "default_width": 640,
     // Default height when the assistant is docked to the bottom.
-    "default_height": 320
+    "default_height": 320,
+    // The default OpenAI model to use when starting new conversations. This
+    // setting can take two values:
+    //
+    // 1. "gpt-3.5-turbo-0613""
+    // 2. "gpt-4-0613""
+    "default_open_ai_model": "gpt-4-0613"
   },
   // Whether the screen sharing icon is shown in the os status bar.
   "show_call_status_icon": true,
@@ -268,8 +285,6 @@
     //          "directory": "~/zed/projects/"
     //        }
     //      }
-    //
-    //
     "working_directory": "current_project_directory",
     // Set the cursor blinking behavior in the terminal.
     // May take 4 values:
@@ -318,13 +333,32 @@
     //         "line_height": {
     //           "custom": 2
     //         },
-    "line_height": "comfortable"
+    "line_height": "comfortable",
+    // Activate the python virtual environment, if one is found, in the
+    // terminal's working directory (as resolved by the working_directory
+    // setting). Set this to "off" to disable this behavior.
+    "detect_venv": {
+      "on": {
+        // Default directories to search for virtual environments, relative
+        // to the current working directory. We recommend overriding this
+        // in your project's settings, rather than globally.
+        "directories": [
+          ".env",
+          "env",
+          ".venv",
+          "venv"
+        ],
+        // Can also be 'csh' and 'fish'
+        "activate_script": "default"
+      }
+    }
     // Set the terminal's font size. If this option is not included,
     // the terminal will default to matching the buffer's font size.
-    // "font_size": "15"
+    // "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 Mono"
+    // "font_family": "Zed Mono",
+    // ---
   },
   // Difference settings for semantic_index
   "semantic_index": {

crates/ai/Cargo.toml 🔗

@@ -24,7 +24,9 @@ workspace = { path = "../workspace" }
 anyhow.workspace = true
 chrono = { version = "0.4", features = ["serde"] }
 futures.workspace = true
+indoc.workspace = true
 isahc.workspace = true
+ordered-float.workspace = true
 regex.workspace = true
 schemars.workspace = true
 serde.workspace = true
@@ -35,3 +37,8 @@ tiktoken-rs = "0.4"
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+
+ctor.workspace = true
+env_logger.workspace = true
+log.workspace = true
+rand.workspace = true

crates/ai/src/ai.rs 🔗

@@ -1,27 +1,33 @@
 pub mod assistant;
 mod assistant_settings;
+mod streaming_diff;
 
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 pub use assistant::AssistantPanel;
+use assistant_settings::OpenAIModel;
 use chrono::{DateTime, Local};
 use collections::HashMap;
 use fs::Fs;
-use futures::StreamExt;
-use gpui::AppContext;
+use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
+use gpui::{executor::Background, AppContext};
+use isahc::{http::StatusCode, Request, RequestExt};
 use regex::Regex;
 use serde::{Deserialize, Serialize};
 use std::{
     cmp::Reverse,
     ffi::OsStr,
     fmt::{self, Display},
+    io,
     path::PathBuf,
     sync::Arc,
 };
 use util::paths::CONVERSATIONS_DIR;
 
+const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
+
 // Data types for chat completion requests
 #[derive(Debug, Serialize)]
-struct OpenAIRequest {
+pub struct OpenAIRequest {
     model: String,
     messages: Vec<RequestMessage>,
     stream: bool,
@@ -60,7 +66,7 @@ struct SavedConversation {
     messages: Vec<SavedMessage>,
     message_metadata: HashMap<MessageId, MessageMetadata>,
     summary: String,
-    model: String,
+    model: OpenAIModel,
 }
 
 impl SavedConversation {
@@ -115,7 +121,7 @@ struct RequestMessage {
 }
 
 #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
-struct ResponseMessage {
+pub struct ResponseMessage {
     role: Option<Role>,
     content: Option<String>,
 }
@@ -149,7 +155,7 @@ impl Display for Role {
 }
 
 #[derive(Deserialize, Debug)]
-struct OpenAIResponseStreamEvent {
+pub struct OpenAIResponseStreamEvent {
     pub id: Option<String>,
     pub object: String,
     pub created: u32,
@@ -159,14 +165,14 @@ struct OpenAIResponseStreamEvent {
 }
 
 #[derive(Deserialize, Debug)]
-struct Usage {
+pub struct Usage {
     pub prompt_tokens: u32,
     pub completion_tokens: u32,
     pub total_tokens: u32,
 }
 
 #[derive(Deserialize, Debug)]
-struct ChatChoiceDelta {
+pub struct ChatChoiceDelta {
     pub index: u32,
     pub delta: ResponseMessage,
     pub finish_reason: Option<String>,
@@ -190,3 +196,97 @@ struct OpenAIChoice {
 pub fn init(cx: &mut AppContext) {
     assistant::init(cx);
 }
+
+pub async fn stream_completion(
+    api_key: String,
+    executor: Arc<Background>,
+    mut request: OpenAIRequest,
+) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
+    request.stream = true;
+
+    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
+
+    let json_data = serde_json::to_string(&request)?;
+    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
+        .header("Content-Type", "application/json")
+        .header("Authorization", format!("Bearer {}", api_key))
+        .body(json_data)?
+        .send_async()
+        .await?;
+
+    let status = response.status();
+    if status == StatusCode::OK {
+        executor
+            .spawn(async move {
+                let mut lines = BufReader::new(response.body_mut()).lines();
+
+                fn parse_line(
+                    line: Result<String, io::Error>,
+                ) -> Result<Option<OpenAIResponseStreamEvent>> {
+                    if let Some(data) = line?.strip_prefix("data: ") {
+                        let event = serde_json::from_str(&data)?;
+                        Ok(Some(event))
+                    } else {
+                        Ok(None)
+                    }
+                }
+
+                while let Some(line) = lines.next().await {
+                    if let Some(event) = parse_line(line).transpose() {
+                        let done = event.as_ref().map_or(false, |event| {
+                            event
+                                .choices
+                                .last()
+                                .map_or(false, |choice| choice.finish_reason.is_some())
+                        });
+                        if tx.unbounded_send(event).is_err() {
+                            break;
+                        }
+
+                        if done {
+                            break;
+                        }
+                    }
+                }
+
+                anyhow::Ok(())
+            })
+            .detach();
+
+        Ok(rx)
+    } else {
+        let mut body = String::new();
+        response.body_mut().read_to_string(&mut body).await?;
+
+        #[derive(Deserialize)]
+        struct OpenAIResponse {
+            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!(
+                "Failed to connect to OpenAI API: {}",
+                response.error.message,
+            )),
+
+            _ => Err(anyhow!(
+                "Failed to connect to OpenAI API: {} {}",
+                response.status(),
+                body,
+            )),
+        }
+    }
+}
+
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}

crates/ai/src/assistant.rs 🔗

@@ -1,53 +1,63 @@
 use crate::{
-    assistant_settings::{AssistantDockPosition, AssistantSettings},
-    MessageId, MessageMetadata, MessageStatus, OpenAIRequest, OpenAIResponseStreamEvent,
-    RequestMessage, Role, SavedConversation, SavedConversationMetadata, SavedMessage,
+    assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
+    stream_completion,
+    streaming_diff::{Hunk, StreamingDiff},
+    MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role,
+    SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL,
 };
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
-use collections::{HashMap, HashSet};
+use collections::{hash_map, HashMap, HashSet, VecDeque};
 use editor::{
-    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint},
+    display_map::{
+        BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
+    },
     scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
-    Anchor, Editor, ToOffset,
+    Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
 };
 use fs::Fs;
-use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
+use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
 use gpui::{
     actions,
-    elements::*,
-    executor::Background,
+    elements::{
+        ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable,
+        Stack, Svg, Text, UniformList, UniformListState,
+    },
+    fonts::HighlightStyle,
     geometry::vector::{vec2f, Vector2F},
     platform::{CursorStyle, MouseButton},
-    Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle,
-    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext,
+    ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    WindowContext,
+};
+use language::{
+    language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _,
+    TransactionId,
 };
-use isahc::{http::StatusCode, Request, RequestExt};
-use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
 use search::BufferSearchBar;
-use serde::Deserialize;
 use settings::SettingsStore;
 use std::{
-    cell::RefCell,
+    cell::{Cell, RefCell},
     cmp, env,
     fmt::Write,
-    io, iter,
+    future, iter,
     ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
     time::Duration,
 };
-use theme::AssistantStyle;
+use theme::{
+    components::{action_button::Button, ComponentExt},
+    AssistantStyle,
+};
 use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
     searchable::Direction,
-    Save, ToggleZoom, Toolbar, Workspace,
+    Save, Toast, ToggleZoom, Toolbar, Workspace,
 };
 
-const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
-
 actions!(
     assistant,
     [
@@ -58,6 +68,8 @@ actions!(
         QuoteSelection,
         ToggleFocus,
         ResetKey,
+        InlineAssist,
+        ToggleIncludeConversation,
     ]
 );
 
@@ -89,6 +101,13 @@ pub fn init(cx: &mut AppContext) {
             workspace.toggle_panel_focus::<AssistantPanel>(cx);
         },
     );
+    cx.add_action(AssistantPanel::inline_assist);
+    cx.add_action(AssistantPanel::cancel_last_inline_assist);
+    cx.add_action(InlineAssistant::confirm);
+    cx.add_action(InlineAssistant::cancel);
+    cx.add_action(InlineAssistant::toggle_include_conversation);
+    cx.add_action(InlineAssistant::move_up);
+    cx.add_action(InlineAssistant::move_down);
 }
 
 #[derive(Debug)]
@@ -118,10 +137,17 @@ pub struct AssistantPanel {
     languages: Arc<LanguageRegistry>,
     fs: Arc<dyn Fs>,
     subscriptions: Vec<Subscription>,
+    next_inline_assist_id: usize,
+    pending_inline_assists: HashMap<usize, PendingInlineAssist>,
+    pending_inline_assist_ids_by_editor: HashMap<WeakViewHandle<Editor>, Vec<usize>>,
+    include_conversation_in_next_inline_assist: bool,
+    inline_prompt_history: VecDeque<String>,
     _watch_saved_conversations: Task<Result<()>>,
 }
 
 impl AssistantPanel {
+    const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
+
     pub fn load(
         workspace: WeakViewHandle<Workspace>,
         cx: AsyncAppContext,
@@ -158,7 +184,7 @@ impl AssistantPanel {
                     });
 
                     let toolbar = cx.add_view(|cx| {
-                        let mut toolbar = Toolbar::new(None);
+                        let mut toolbar = Toolbar::new();
                         toolbar.set_can_navigate(false, cx);
                         toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
                         toolbar
@@ -181,6 +207,11 @@ impl AssistantPanel {
                         width: None,
                         height: None,
                         subscriptions: Default::default(),
+                        next_inline_assist_id: 0,
+                        pending_inline_assists: Default::default(),
+                        pending_inline_assist_ids_by_editor: Default::default(),
+                        include_conversation_in_next_inline_assist: false,
+                        inline_prompt_history: Default::default(),
                         _watch_saved_conversations,
                     };
 
@@ -192,6 +223,7 @@ impl AssistantPanel {
                                 old_dock_position = new_dock_position;
                                 cx.emit(AssistantPanelEvent::DockPositionChanged);
                             }
+                            cx.notify();
                         })];
 
                     this
@@ -200,6 +232,720 @@ impl AssistantPanel {
         })
     }
 
+    pub fn inline_assist(
+        workspace: &mut Workspace,
+        _: &InlineAssist,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let this = if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
+            if this
+                .update(cx, |assistant, cx| assistant.load_api_key(cx))
+                .is_some()
+            {
+                this
+            } else {
+                workspace.focus_panel::<AssistantPanel>(cx);
+                return;
+            }
+        } else {
+            return;
+        };
+
+        let active_editor = if let Some(active_editor) = workspace
+            .active_item(cx)
+            .and_then(|item| item.act_as::<Editor>(cx))
+        {
+            active_editor
+        } else {
+            return;
+        };
+
+        this.update(cx, |assistant, cx| {
+            assistant.new_inline_assist(&active_editor, cx)
+        });
+    }
+
+    fn new_inline_assist(&mut self, editor: &ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+        let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
+        let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
+        let selection = editor.read(cx).selections.newest_anchor().clone();
+        let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot);
+        let assist_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
+            InlineAssistKind::Generate
+        } else {
+            InlineAssistKind::Transform
+        };
+        let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
+        let inline_assistant = cx.add_view(|cx| {
+            let assistant = InlineAssistant::new(
+                inline_assist_id,
+                assist_kind,
+                measurements.clone(),
+                self.include_conversation_in_next_inline_assist,
+                self.inline_prompt_history.clone(),
+                cx,
+            );
+            cx.focus_self();
+            assistant
+        });
+        let block_id = editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |selections| {
+                selections.select_anchor_ranges([selection.head()..selection.head()])
+            });
+            editor.insert_blocks(
+                [BlockProperties {
+                    style: BlockStyle::Flex,
+                    position: selection.head().bias_left(&snapshot),
+                    height: 2,
+                    render: Arc::new({
+                        let inline_assistant = inline_assistant.clone();
+                        move |cx: &mut BlockContext| {
+                            measurements.set(BlockMeasurements {
+                                anchor_x: cx.anchor_x,
+                                gutter_width: cx.gutter_width,
+                            });
+                            ChildView::new(&inline_assistant, cx).into_any()
+                        }
+                    }),
+                    disposition: if selection.reversed {
+                        BlockDisposition::Above
+                    } else {
+                        BlockDisposition::Below
+                    },
+                }],
+                Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
+                cx,
+            )[0]
+        });
+
+        self.pending_inline_assists.insert(
+            inline_assist_id,
+            PendingInlineAssist {
+                kind: assist_kind,
+                editor: editor.downgrade(),
+                range,
+                highlighted_ranges: Default::default(),
+                inline_assistant: Some((block_id, inline_assistant.clone())),
+                code_generation: Task::ready(None),
+                transaction_id: None,
+                _subscriptions: vec![
+                    cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
+                    cx.subscribe(editor, {
+                        let inline_assistant = inline_assistant.downgrade();
+                        move |this, editor, event, cx| {
+                            if let Some(inline_assistant) = inline_assistant.upgrade(cx) {
+                                match event {
+                                    editor::Event::SelectionsChanged { local } => {
+                                        if *local && inline_assistant.read(cx).has_focus {
+                                            cx.focus(&editor);
+                                        }
+                                    }
+                                    editor::Event::TransactionUndone {
+                                        transaction_id: tx_id,
+                                    } => {
+                                        if let Some(pending_assist) =
+                                            this.pending_inline_assists.get(&inline_assist_id)
+                                        {
+                                            if pending_assist.transaction_id == Some(*tx_id) {
+                                                // Notice we are supplying `undo: false` here. This
+                                                // is because there's no need to undo the transaction
+                                                // because the user just did so.
+                                                this.close_inline_assist(
+                                                    inline_assist_id,
+                                                    false,
+                                                    cx,
+                                                );
+                                            }
+                                        }
+                                    }
+                                    _ => {}
+                                }
+                            }
+                        }
+                    }),
+                ],
+            },
+        );
+        self.pending_inline_assist_ids_by_editor
+            .entry(editor.downgrade())
+            .or_default()
+            .push(inline_assist_id);
+        self.update_highlights_for_editor(&editor, cx);
+    }
+
+    fn handle_inline_assistant_event(
+        &mut self,
+        inline_assistant: ViewHandle<InlineAssistant>,
+        event: &InlineAssistantEvent,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let assist_id = inline_assistant.read(cx).id;
+        match event {
+            InlineAssistantEvent::Confirmed {
+                prompt,
+                include_conversation,
+            } => {
+                self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
+            }
+            InlineAssistantEvent::Canceled => {
+                self.close_inline_assist(assist_id, true, cx);
+            }
+            InlineAssistantEvent::Dismissed => {
+                self.hide_inline_assist(assist_id, cx);
+            }
+            InlineAssistantEvent::IncludeConversationToggled {
+                include_conversation,
+            } => {
+                self.include_conversation_in_next_inline_assist = *include_conversation;
+            }
+        }
+    }
+
+    fn cancel_last_inline_assist(
+        workspace: &mut Workspace,
+        _: &editor::Cancel,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let panel = if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
+            panel
+        } else {
+            return;
+        };
+        let editor = if let Some(editor) = workspace
+            .active_item(cx)
+            .and_then(|item| item.downcast::<Editor>())
+        {
+            editor
+        } else {
+            return;
+        };
+
+        let handled = panel.update(cx, |panel, cx| {
+            if let Some(assist_id) = panel
+                .pending_inline_assist_ids_by_editor
+                .get(&editor.downgrade())
+                .and_then(|assist_ids| assist_ids.last().copied())
+            {
+                panel.close_inline_assist(assist_id, true, cx);
+                true
+            } else {
+                false
+            }
+        });
+
+        if !handled {
+            cx.propagate_action();
+        }
+    }
+
+    fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
+        self.hide_inline_assist(assist_id, cx);
+
+        if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) {
+            if let hash_map::Entry::Occupied(mut entry) = self
+                .pending_inline_assist_ids_by_editor
+                .entry(pending_assist.editor)
+            {
+                entry.get_mut().retain(|id| *id != assist_id);
+                if entry.get().is_empty() {
+                    entry.remove();
+                }
+            }
+
+            if let Some(editor) = pending_assist.editor.upgrade(cx) {
+                self.update_highlights_for_editor(&editor, cx);
+
+                if undo {
+                    if let Some(transaction_id) = pending_assist.transaction_id {
+                        editor.update(cx, |editor, cx| {
+                            editor.buffer().update(cx, |buffer, cx| {
+                                buffer.undo_transaction(transaction_id, cx)
+                            });
+                        });
+                    }
+                }
+            }
+        }
+    }
+
+    fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext<Self>) {
+        if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) {
+            if let Some(editor) = pending_assist.editor.upgrade(cx) {
+                if let Some((block_id, _)) = pending_assist.inline_assistant.take() {
+                    editor.update(cx, |editor, cx| {
+                        editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
+                    });
+                }
+            }
+        }
+    }
+
+    fn confirm_inline_assist(
+        &mut self,
+        inline_assist_id: usize,
+        user_prompt: &str,
+        include_conversation: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
+            api_key
+        } else {
+            return;
+        };
+
+        let conversation = if include_conversation {
+            self.active_editor()
+                .map(|editor| editor.read(cx).conversation.clone())
+        } else {
+            None
+        };
+
+        let pending_assist =
+            if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) {
+                pending_assist
+            } else {
+                return;
+            };
+
+        let editor = if let Some(editor) = pending_assist.editor.upgrade(cx) {
+            editor
+        } else {
+            return;
+        };
+
+        self.inline_prompt_history.push_back(user_prompt.into());
+        if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN {
+            self.inline_prompt_history.pop_front();
+        }
+        let range = pending_assist.range.clone();
+        let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
+        let selected_text = snapshot
+            .text_for_range(range.start..range.end)
+            .collect::<Rope>();
+
+        let selection_start = range.start.to_point(&snapshot);
+        let selection_end = range.end.to_point(&snapshot);
+
+        let mut base_indent: Option<language::IndentSize> = None;
+        let mut start_row = selection_start.row;
+        if snapshot.is_line_blank(start_row) {
+            if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) {
+                start_row = prev_non_blank_row;
+            }
+        }
+        for row in start_row..=selection_end.row {
+            if snapshot.is_line_blank(row) {
+                continue;
+            }
+
+            let line_indent = snapshot.indent_size_for_line(row);
+            if let Some(base_indent) = base_indent.as_mut() {
+                if line_indent.len < base_indent.len {
+                    *base_indent = line_indent;
+                }
+            } else {
+                base_indent = Some(line_indent);
+            }
+        }
+
+        let mut normalized_selected_text = selected_text.clone();
+        if let Some(base_indent) = base_indent {
+            for row in selection_start.row..=selection_end.row {
+                let selection_row = row - selection_start.row;
+                let line_start =
+                    normalized_selected_text.point_to_offset(Point::new(selection_row, 0));
+                let indent_len = if row == selection_start.row {
+                    base_indent.len.saturating_sub(selection_start.column)
+                } else {
+                    let line_len = normalized_selected_text.line_len(selection_row);
+                    cmp::min(line_len, base_indent.len)
+                };
+                let indent_end = cmp::min(
+                    line_start + indent_len as usize,
+                    normalized_selected_text.len(),
+                );
+                normalized_selected_text.replace(line_start..indent_end, "");
+            }
+        }
+
+        let language = snapshot.language_at(range.start);
+        let language_name = if let Some(language) = language.as_ref() {
+            if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
+                None
+            } else {
+                Some(language.name())
+            }
+        } else {
+            None
+        };
+        let language_name = language_name.as_deref();
+
+        let mut prompt = String::new();
+        if let Some(language_name) = language_name {
+            writeln!(prompt, "You're an expert {language_name} engineer.").unwrap();
+        }
+        match pending_assist.kind {
+            InlineAssistKind::Transform => {
+                writeln!(
+                    prompt,
+                    "You're currently working inside an editor on this file:"
+                )
+                .unwrap();
+                if let Some(language_name) = language_name {
+                    writeln!(prompt, "```{language_name}").unwrap();
+                } else {
+                    writeln!(prompt, "```").unwrap();
+                }
+                for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) {
+                    write!(prompt, "{chunk}").unwrap();
+                }
+                writeln!(prompt, "```").unwrap();
+
+                writeln!(
+                    prompt,
+                    "In particular, the user has selected the following text:"
+                )
+                .unwrap();
+                if let Some(language_name) = language_name {
+                    writeln!(prompt, "```{language_name}").unwrap();
+                } else {
+                    writeln!(prompt, "```").unwrap();
+                }
+                writeln!(prompt, "{normalized_selected_text}").unwrap();
+                writeln!(prompt, "```").unwrap();
+                writeln!(prompt).unwrap();
+                writeln!(
+                    prompt,
+                    "Modify the selected text given the user prompt: {user_prompt}"
+                )
+                .unwrap();
+                writeln!(
+                    prompt,
+                    "You MUST reply only with the edited selected text, not the entire file."
+                )
+                .unwrap();
+            }
+            InlineAssistKind::Generate => {
+                writeln!(
+                    prompt,
+                    "You're currently working inside an editor on this file:"
+                )
+                .unwrap();
+                if let Some(language_name) = language_name {
+                    writeln!(prompt, "```{language_name}").unwrap();
+                } else {
+                    writeln!(prompt, "```").unwrap();
+                }
+                for chunk in snapshot.text_for_range(Anchor::min()..range.start) {
+                    write!(prompt, "{chunk}").unwrap();
+                }
+                write!(prompt, "<|>").unwrap();
+                for chunk in snapshot.text_for_range(range.start..Anchor::max()) {
+                    write!(prompt, "{chunk}").unwrap();
+                }
+                writeln!(prompt).unwrap();
+                writeln!(prompt, "```").unwrap();
+                writeln!(
+                    prompt,
+                    "Assume the cursor is located where the `<|>` marker is."
+                )
+                .unwrap();
+                writeln!(
+                    prompt,
+                    "Text can't be replaced, so assume your answer will be inserted at the cursor."
+                )
+                .unwrap();
+                writeln!(
+                    prompt,
+                    "Complete the text given the user prompt: {user_prompt}"
+                )
+                .unwrap();
+            }
+        }
+        if let Some(language_name) = language_name {
+            writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap();
+        }
+        writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap();
+        writeln!(prompt, "Never make remarks about the output.").unwrap();
+
+        let mut messages = Vec::new();
+        let mut model = settings::get::<AssistantSettings>(cx)
+            .default_open_ai_model
+            .clone();
+        if let Some(conversation) = conversation {
+            let conversation = conversation.read(cx);
+            let buffer = conversation.buffer.read(cx);
+            messages.extend(
+                conversation
+                    .messages(cx)
+                    .map(|message| message.to_open_ai_message(buffer)),
+            );
+            model = conversation.model.clone();
+        }
+
+        messages.push(RequestMessage {
+            role: Role::User,
+            content: prompt,
+        });
+        let request = OpenAIRequest {
+            model: model.full_name().into(),
+            messages,
+            stream: true,
+        };
+        let response = stream_completion(api_key, cx.background().clone(), request);
+        let editor = editor.downgrade();
+
+        pending_assist.code_generation = cx.spawn(|this, mut cx| {
+            async move {
+                let mut edit_start = range.start.to_offset(&snapshot);
+
+                let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
+                let diff = cx.background().spawn(async move {
+                    let chunks = strip_markdown_codeblock(response.await?.filter_map(
+                        |message| async move {
+                            match message {
+                                Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)),
+                                Err(error) => Some(Err(error)),
+                            }
+                        },
+                    ));
+                    futures::pin_mut!(chunks);
+                    let mut diff = StreamingDiff::new(selected_text.to_string());
+
+                    let mut indent_len;
+                    let indent_text;
+                    if let Some(base_indent) = base_indent {
+                        indent_len = base_indent.len;
+                        indent_text = match base_indent.kind {
+                            language::IndentKind::Space => " ",
+                            language::IndentKind::Tab => "\t",
+                        };
+                    } else {
+                        indent_len = 0;
+                        indent_text = "";
+                    };
+
+                    let mut first_line_len = 0;
+                    let mut first_line_non_whitespace_char_ix = None;
+                    let mut first_line = true;
+                    let mut new_text = String::new();
+
+                    while let Some(chunk) = chunks.next().await {
+                        let chunk = chunk?;
+
+                        let mut lines = chunk.split('\n');
+                        if let Some(mut line) = lines.next() {
+                            if first_line {
+                                if first_line_non_whitespace_char_ix.is_none() {
+                                    if let Some(mut char_ix) =
+                                        line.find(|ch: char| !ch.is_whitespace())
+                                    {
+                                        line = &line[char_ix..];
+                                        char_ix += first_line_len;
+                                        first_line_non_whitespace_char_ix = Some(char_ix);
+                                        let first_line_indent = char_ix
+                                            .saturating_sub(selection_start.column as usize)
+                                            as usize;
+                                        new_text.push_str(&indent_text.repeat(first_line_indent));
+                                        indent_len = indent_len.saturating_sub(char_ix as u32);
+                                    }
+                                }
+                                first_line_len += line.len();
+                            }
+
+                            if first_line_non_whitespace_char_ix.is_some() {
+                                new_text.push_str(line);
+                            }
+                        }
+
+                        for line in lines {
+                            first_line = false;
+                            new_text.push('\n');
+                            if !line.is_empty() {
+                                new_text.push_str(&indent_text.repeat(indent_len as usize));
+                            }
+                            new_text.push_str(line);
+                        }
+
+                        let hunks = diff.push_new(&new_text);
+                        hunks_tx.send(hunks).await?;
+                        new_text.clear();
+                    }
+                    hunks_tx.send(diff.finish()).await?;
+
+                    anyhow::Ok(())
+                });
+
+                while let Some(hunks) = hunks_rx.next().await {
+                    let editor = if let Some(editor) = editor.upgrade(&cx) {
+                        editor
+                    } else {
+                        break;
+                    };
+
+                    let this = if let Some(this) = this.upgrade(&cx) {
+                        this
+                    } else {
+                        break;
+                    };
+
+                    this.update(&mut cx, |this, cx| {
+                        let pending_assist = if let Some(pending_assist) =
+                            this.pending_inline_assists.get_mut(&inline_assist_id)
+                        {
+                            pending_assist
+                        } else {
+                            return;
+                        };
+
+                        pending_assist.highlighted_ranges.clear();
+                        editor.update(cx, |editor, cx| {
+                            let transaction = editor.buffer().update(cx, |buffer, cx| {
+                                // Avoid grouping assistant edits with user edits.
+                                buffer.finalize_last_transaction(cx);
+
+                                buffer.start_transaction(cx);
+                                buffer.edit(
+                                    hunks.into_iter().filter_map(|hunk| match hunk {
+                                        Hunk::Insert { text } => {
+                                            let edit_start = snapshot.anchor_after(edit_start);
+                                            Some((edit_start..edit_start, text))
+                                        }
+                                        Hunk::Remove { len } => {
+                                            let edit_end = edit_start + len;
+                                            let edit_range = snapshot.anchor_after(edit_start)
+                                                ..snapshot.anchor_before(edit_end);
+                                            edit_start = edit_end;
+                                            Some((edit_range, String::new()))
+                                        }
+                                        Hunk::Keep { len } => {
+                                            let edit_end = edit_start + len;
+                                            let edit_range = snapshot.anchor_after(edit_start)
+                                                ..snapshot.anchor_before(edit_end);
+                                            edit_start += len;
+                                            pending_assist.highlighted_ranges.push(edit_range);
+                                            None
+                                        }
+                                    }),
+                                    None,
+                                    cx,
+                                );
+
+                                buffer.end_transaction(cx)
+                            });
+
+                            if let Some(transaction) = transaction {
+                                if let Some(first_transaction) = pending_assist.transaction_id {
+                                    // Group all assistant edits into the first transaction.
+                                    editor.buffer().update(cx, |buffer, cx| {
+                                        buffer.merge_transactions(
+                                            transaction,
+                                            first_transaction,
+                                            cx,
+                                        )
+                                    });
+                                } else {
+                                    pending_assist.transaction_id = Some(transaction);
+                                    editor.buffer().update(cx, |buffer, cx| {
+                                        buffer.finalize_last_transaction(cx)
+                                    });
+                                }
+                            }
+                        });
+
+                        this.update_highlights_for_editor(&editor, cx);
+                    });
+                }
+
+                if let Err(error) = diff.await {
+                    this.update(&mut cx, |this, cx| {
+                        let pending_assist = if let Some(pending_assist) =
+                            this.pending_inline_assists.get_mut(&inline_assist_id)
+                        {
+                            pending_assist
+                        } else {
+                            return;
+                        };
+
+                        if let Some((_, inline_assistant)) =
+                            pending_assist.inline_assistant.as_ref()
+                        {
+                            inline_assistant.update(cx, |inline_assistant, cx| {
+                                inline_assistant.set_error(error, cx);
+                            });
+                        } else if let Some(workspace) = this.workspace.upgrade(cx) {
+                            workspace.update(cx, |workspace, cx| {
+                                workspace.show_toast(
+                                    Toast::new(
+                                        inline_assist_id,
+                                        format!("Inline assistant error: {}", error),
+                                    ),
+                                    cx,
+                                );
+                            })
+                        }
+                    })?;
+                } else {
+                    let _ = this.update(&mut cx, |this, cx| {
+                        this.close_inline_assist(inline_assist_id, false, cx)
+                    });
+                }
+
+                anyhow::Ok(())
+            }
+            .log_err()
+        });
+    }
+
+    fn update_highlights_for_editor(
+        &self,
+        editor: &ViewHandle<Editor>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let mut background_ranges = Vec::new();
+        let mut foreground_ranges = Vec::new();
+        let empty_inline_assist_ids = Vec::new();
+        let inline_assist_ids = self
+            .pending_inline_assist_ids_by_editor
+            .get(&editor.downgrade())
+            .unwrap_or(&empty_inline_assist_ids);
+
+        for inline_assist_id in inline_assist_ids {
+            if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) {
+                background_ranges.push(pending_assist.range.clone());
+                foreground_ranges.extend(pending_assist.highlighted_ranges.iter().cloned());
+            }
+        }
+
+        let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
+        merge_ranges(&mut background_ranges, &snapshot);
+        merge_ranges(&mut foreground_ranges, &snapshot);
+        editor.update(cx, |editor, cx| {
+            if background_ranges.is_empty() {
+                editor.clear_background_highlights::<PendingInlineAssist>(cx);
+            } else {
+                editor.highlight_background::<PendingInlineAssist>(
+                    background_ranges,
+                    |theme| theme.assistant.inline.pending_edit_background,
+                    cx,
+                );
+            }
+
+            if foreground_ranges.is_empty() {
+                editor.clear_text_highlights::<PendingInlineAssist>(cx);
+            } else {
+                editor.highlight_text::<PendingInlineAssist>(
+                    foreground_ranges,
+                    HighlightStyle {
+                        fade_out: Some(0.6),
+                        ..Default::default()
+                    },
+                    cx,
+                );
+            }
+        });
+    }
+
     fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<ConversationEditor> {
         let editor = cx.add_view(|cx| {
             ConversationEditor::new(
@@ -569,6 +1315,32 @@ impl AssistantPanel {
             .iter()
             .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path))
     }
+
+    fn load_api_key(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
+        if self.api_key.borrow().is_none() && !self.has_read_credentials {
+            self.has_read_credentials = true;
+            let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
+                Some(api_key)
+            } else if let Some((_, api_key)) = cx
+                .platform()
+                .read_credentials(OPENAI_API_URL)
+                .log_err()
+                .flatten()
+            {
+                String::from_utf8(api_key).log_err()
+            } else {
+                None
+            };
+            if let Some(api_key) = api_key {
+                *self.api_key.borrow_mut() = Some(api_key);
+            } else if self.api_key_editor.is_none() {
+                self.api_key_editor = Some(build_api_key_editor(cx));
+                cx.notify();
+            }
+        }
+
+        self.api_key.borrow().clone()
+    }
 }
 
 fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> ViewHandle<Editor> {
@@ -725,10 +1497,10 @@ impl Panel for AssistantPanel {
         }
     }
 
-    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
         match self.position(cx) {
-            DockPosition::Left | DockPosition::Right => self.width = Some(size),
-            DockPosition::Bottom => self.height = Some(size),
+            DockPosition::Left | DockPosition::Right => self.width = size,
+            DockPosition::Bottom => self.height = size,
         }
         cx.notify();
     }
@@ -752,27 +1524,7 @@ impl Panel for AssistantPanel {
 
     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         if active {
-            if self.api_key.borrow().is_none() && !self.has_read_credentials {
-                self.has_read_credentials = true;
-                let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
-                    Some(api_key)
-                } else if let Some((_, api_key)) = cx
-                    .platform()
-                    .read_credentials(OPENAI_API_URL)
-                    .log_err()
-                    .flatten()
-                {
-                    String::from_utf8(api_key).log_err()
-                } else {
-                    None
-                };
-                if let Some(api_key) = api_key {
-                    *self.api_key.borrow_mut() = Some(api_key);
-                } else if self.api_key_editor.is_none() {
-                    self.api_key_editor = Some(build_api_key_editor(cx));
-                    cx.notify();
-                }
-            }
+            self.load_api_key(cx);
 
             if self.editors.is_empty() {
                 self.new_conversation(cx);
@@ -780,8 +1532,10 @@ impl Panel for AssistantPanel {
         }
     }
 
-    fn icon_path(&self) -> &'static str {
-        "icons/robot_14.svg"
+    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
+        settings::get::<AssistantSettings>(cx)
+            .button
+            .then(|| "icons/ai.svg")
     }
 
     fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
@@ -830,7 +1584,7 @@ struct Conversation {
     pending_summary: Task<Option<()>>,
     completion_count: usize,
     pending_completions: Vec<PendingCompletion>,
-    model: String,
+    model: OpenAIModel,
     token_count: Option<usize>,
     max_token_count: usize,
     pending_token_count: Task<Option<()>>,
@@ -850,17 +1604,16 @@ impl Conversation {
         language_registry: Arc<LanguageRegistry>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        let model = "gpt-3.5-turbo-0613";
         let markdown = language_registry.language_for_name("Markdown");
         let buffer = cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, "", cx);
+            let mut buffer = Buffer::new(0, cx.model_id() as u64, "");
             buffer.set_language_registry(language_registry);
             cx.spawn_weak(|buffer, mut cx| async move {
                 let markdown = markdown.await?;
                 let buffer = buffer
                     .upgrade(&cx)
                     .ok_or_else(|| anyhow!("buffer was dropped"))?;
-                buffer.update(&mut cx, |buffer, cx| {
+                buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
                     buffer.set_language(Some(markdown), cx)
                 });
                 anyhow::Ok(())
@@ -869,6 +1622,9 @@ impl Conversation {
             buffer
         });
 
+        let settings = settings::get::<AssistantSettings>(cx);
+        let model = settings.default_open_ai_model.clone();
+
         let mut this = Self {
             message_anchors: Default::default(),
             messages_metadata: Default::default(),

crates/ai/src/assistant_settings.rs 🔗

@@ -3,6 +3,37 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::Setting;
 
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub enum OpenAIModel {
+    #[serde(rename = "gpt-3.5-turbo-0613")]
+    ThreePointFiveTurbo,
+    #[serde(rename = "gpt-4-0613")]
+    Four,
+}
+
+impl OpenAIModel {
+    pub fn full_name(&self) -> &'static str {
+        match self {
+            OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
+            OpenAIModel::Four => "gpt-4-0613",
+        }
+    }
+
+    pub fn short_name(&self) -> &'static str {
+        match self {
+            OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo",
+            OpenAIModel::Four => "gpt-4",
+        }
+    }
+
+    pub fn cycle(&self) -> Self {
+        match self {
+            OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four,
+            OpenAIModel::Four => OpenAIModel::ThreePointFiveTurbo,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum AssistantDockPosition {
@@ -13,16 +44,20 @@ pub enum AssistantDockPosition {
 
 #[derive(Deserialize, Debug)]
 pub struct AssistantSettings {
+    pub button: bool,
     pub dock: AssistantDockPosition,
     pub default_width: f32,
     pub default_height: f32,
+    pub default_open_ai_model: OpenAIModel,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct AssistantSettingsContent {
+    pub button: Option<bool>,
     pub dock: Option<AssistantDockPosition>,
     pub default_width: Option<f32>,
     pub default_height: Option<f32>,
+    pub default_open_ai_model: Option<OpenAIModel>,
 }
 
 impl Setting for AssistantSettings {

crates/ai/src/streaming_diff.rs 🔗

@@ -0,0 +1,293 @@
+use collections::HashMap;
+use ordered_float::OrderedFloat;
+use std::{
+    cmp,
+    fmt::{self, Debug},
+    ops::Range,
+};
+
+struct Matrix {
+    cells: Vec<f64>,
+    rows: usize,
+    cols: usize,
+}
+
+impl Matrix {
+    fn new() -> Self {
+        Self {
+            cells: Vec::new(),
+            rows: 0,
+            cols: 0,
+        }
+    }
+
+    fn resize(&mut self, rows: usize, cols: usize) {
+        self.cells.resize(rows * cols, 0.);
+        self.rows = rows;
+        self.cols = cols;
+    }
+
+    fn get(&self, row: usize, col: usize) -> f64 {
+        if row >= self.rows {
+            panic!("row out of bounds")
+        }
+
+        if col >= self.cols {
+            panic!("col out of bounds")
+        }
+        self.cells[col * self.rows + row]
+    }
+
+    fn set(&mut self, row: usize, col: usize, value: f64) {
+        if row >= self.rows {
+            panic!("row out of bounds")
+        }
+
+        if col >= self.cols {
+            panic!("col out of bounds")
+        }
+
+        self.cells[col * self.rows + row] = value;
+    }
+}
+
+impl Debug for Matrix {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        writeln!(f)?;
+        for i in 0..self.rows {
+            for j in 0..self.cols {
+                write!(f, "{:5}", self.get(i, j))?;
+            }
+            writeln!(f)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug)]
+pub enum Hunk {
+    Insert { text: String },
+    Remove { len: usize },
+    Keep { len: usize },
+}
+
+pub struct StreamingDiff {
+    old: Vec<char>,
+    new: Vec<char>,
+    scores: Matrix,
+    old_text_ix: usize,
+    new_text_ix: usize,
+    equal_runs: HashMap<(usize, usize), u32>,
+}
+
+impl StreamingDiff {
+    const INSERTION_SCORE: f64 = -1.;
+    const DELETION_SCORE: f64 = -20.;
+    const EQUALITY_BASE: f64 = 1.8;
+    const MAX_EQUALITY_EXPONENT: i32 = 16;
+
+    pub fn new(old: String) -> Self {
+        let old = old.chars().collect::<Vec<_>>();
+        let mut scores = Matrix::new();
+        scores.resize(old.len() + 1, 1);
+        for i in 0..=old.len() {
+            scores.set(i, 0, i as f64 * Self::DELETION_SCORE);
+        }
+        Self {
+            old,
+            new: Vec::new(),
+            scores,
+            old_text_ix: 0,
+            new_text_ix: 0,
+            equal_runs: Default::default(),
+        }
+    }
+
+    pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
+        self.new.extend(text.chars());
+        self.scores.resize(self.old.len() + 1, self.new.len() + 1);
+
+        for j in self.new_text_ix + 1..=self.new.len() {
+            self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE);
+            for i in 1..=self.old.len() {
+                let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE;
+                let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE;
+                let equality_score = if self.old[i - 1] == self.new[j - 1] {
+                    let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0);
+                    equal_run += 1;
+                    self.equal_runs.insert((i, j), equal_run);
+
+                    let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT);
+                    self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent)
+                } else {
+                    f64::NEG_INFINITY
+                };
+
+                let score = insertion_score.max(deletion_score).max(equality_score);
+                self.scores.set(i, j, score);
+            }
+        }
+
+        let mut max_score = f64::NEG_INFINITY;
+        let mut next_old_text_ix = self.old_text_ix;
+        let next_new_text_ix = self.new.len();
+        for i in self.old_text_ix..=self.old.len() {
+            let score = self.scores.get(i, next_new_text_ix);
+            if score > max_score {
+                max_score = score;
+                next_old_text_ix = i;
+            }
+        }
+
+        let hunks = self.backtrack(next_old_text_ix, next_new_text_ix);
+        self.old_text_ix = next_old_text_ix;
+        self.new_text_ix = next_new_text_ix;
+        hunks
+    }
+
+    fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
+        let mut pending_insert: Option<Range<usize>> = None;
+        let mut hunks = Vec::new();
+        let mut i = old_text_ix;
+        let mut j = new_text_ix;
+        while (i, j) != (self.old_text_ix, self.new_text_ix) {
+            let insertion_score = if j > self.new_text_ix {
+                Some((i, j - 1))
+            } else {
+                None
+            };
+            let deletion_score = if i > self.old_text_ix {
+                Some((i - 1, j))
+            } else {
+                None
+            };
+            let equality_score = if i > self.old_text_ix && j > self.new_text_ix {
+                if self.old[i - 1] == self.new[j - 1] {
+                    Some((i - 1, j - 1))
+                } else {
+                    None
+                }
+            } else {
+                None
+            };
+
+            let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score]
+                .iter()
+                .max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j))))
+                .unwrap()
+                .unwrap();
+
+            if prev_i == i && prev_j == j - 1 {
+                if let Some(pending_insert) = pending_insert.as_mut() {
+                    pending_insert.start = prev_j;
+                } else {
+                    pending_insert = Some(prev_j..j);
+                }
+            } else {
+                if let Some(range) = pending_insert.take() {
+                    hunks.push(Hunk::Insert {
+                        text: self.new[range].iter().collect(),
+                    });
+                }
+
+                let char_len = self.old[i - 1].len_utf8();
+                if prev_i == i - 1 && prev_j == j {
+                    if let Some(Hunk::Remove { len }) = hunks.last_mut() {
+                        *len += char_len;
+                    } else {
+                        hunks.push(Hunk::Remove { len: char_len })
+                    }
+                } else {
+                    if let Some(Hunk::Keep { len }) = hunks.last_mut() {
+                        *len += char_len;
+                    } else {
+                        hunks.push(Hunk::Keep { len: char_len })
+                    }
+                }
+            }
+
+            i = prev_i;
+            j = prev_j;
+        }
+
+        if let Some(range) = pending_insert.take() {
+            hunks.push(Hunk::Insert {
+                text: self.new[range].iter().collect(),
+            });
+        }
+
+        hunks.reverse();
+        hunks
+    }
+
+    pub fn finish(self) -> Vec<Hunk> {
+        self.backtrack(self.old.len(), self.new.len())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::env;
+
+    use super::*;
+    use rand::prelude::*;
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_diffs(mut rng: StdRng) {
+        let old_text_len = env::var("OLD_TEXT_LEN")
+            .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
+            .unwrap_or(10);
+        let new_text_len = env::var("NEW_TEXT_LEN")
+            .map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
+            .unwrap_or(10);
+
+        let old = util::RandomCharIter::new(&mut rng)
+            .take(old_text_len)
+            .collect::<String>();
+        log::info!("old text: {:?}", old);
+
+        let mut diff = StreamingDiff::new(old.clone());
+        let mut hunks = Vec::new();
+        let mut new_len = 0;
+        let mut new = String::new();
+        while new_len < new_text_len {
+            let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
+            let new_chunk = util::RandomCharIter::new(&mut rng)
+                .take(new_len)
+                .collect::<String>();
+            log::info!("new chunk: {:?}", new_chunk);
+            new_len += new_chunk_len;
+            new.push_str(&new_chunk);
+            let new_hunks = diff.push_new(&new_chunk);
+            log::info!("hunks: {:?}", new_hunks);
+            hunks.extend(new_hunks);
+        }
+        let final_hunks = diff.finish();
+        log::info!("final hunks: {:?}", final_hunks);
+        hunks.extend(final_hunks);
+
+        log::info!("new text: {:?}", new);
+        let mut old_ix = 0;
+        let mut new_ix = 0;
+        let mut patched = String::new();
+        for hunk in hunks {
+            match hunk {
+                Hunk::Keep { len } => {
+                    assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
+                    patched.push_str(&old[old_ix..old_ix + len]);
+                    old_ix += len;
+                    new_ix += len;
+                }
+                Hunk::Remove { len } => {
+                    old_ix += len;
+                }
+                Hunk::Insert { text } => {
+                    assert_eq!(text, &new[new_ix..new_ix + text.len()]);
+                    patched.push_str(&text);
+                    new_ix += text.len();
+                }
+            }
+        }
+        assert_eq!(patched, new);
+    }
+}

crates/audio/Cargo.toml 🔗

@@ -13,7 +13,7 @@ gpui = { path = "../gpui" }
 collections = { path = "../collections" }
 util = { path = "../util" }
 
-rodio = "0.17.1"
+rodio ={version = "0.17.1", default-features=false, features = ["wav"]}
 
 log.workspace = true
 

crates/audio/src/audio.rs 🔗

@@ -39,29 +39,43 @@ pub struct Audio {
 
 impl Audio {
     pub fn new() -> Self {
-        let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
-
         Self {
-            _output_stream,
-            output_handle,
+            _output_stream: None,
+            output_handle: None,
         }
     }
 
-    pub fn play_sound(sound: Sound, cx: &AppContext) {
+    fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
+        if self.output_handle.is_none() {
+            let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
+            self.output_handle = output_handle;
+            self._output_stream = _output_stream;
+        }
+
+        self.output_handle.as_ref()
+    }
+
+    pub fn play_sound(sound: Sound, cx: &mut AppContext) {
         if !cx.has_global::<Self>() {
             return;
         }
 
-        let this = cx.global::<Self>();
+        cx.update_global::<Self, _, _>(|this, cx| {
+            let output_handle = this.ensure_output_exists()?;
+            let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
+            output_handle.play_raw(source).log_err()?;
+            Some(())
+        });
+    }
 
-        let Some(output_handle) = this.output_handle.as_ref() else {
+    pub fn end_call(cx: &mut AppContext) {
+        if !cx.has_global::<Self>() {
             return;
-        };
-
-        let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
-        return;
-    };
+        }
 
-        output_handle.play_raw(source).log_err();
+        cx.update_global::<Self, _, _>(|this, _| {
+            this._output_stream.take();
+            this.output_handle.take();
+        });
     }
 }

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -50,7 +50,7 @@ impl View for Breadcrumbs {
         let not_editor = active_item.downcast::<editor::Editor>().is_none();
 
         let theme = theme::current(cx).clone();
-        let style = &theme.workspace.breadcrumbs;
+        let style = &theme.workspace.toolbar.breadcrumbs;
 
         let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
             Some(breadcrumbs) => breadcrumbs,
@@ -60,7 +60,7 @@ impl View for Breadcrumbs {
         .map(|breadcrumb| {
             Text::new(
                 breadcrumb.text,
-                theme.workspace.breadcrumbs.default.text.clone(),
+                theme.workspace.toolbar.breadcrumbs.default.text.clone(),
             )
             .with_highlights(breadcrumb.highlights.unwrap_or_default())
             .into_any()
@@ -68,10 +68,10 @@ impl View for Breadcrumbs {
 
         let crumbs = Flex::row()
             .with_children(Itertools::intersperse_with(breadcrumbs, || {
-                Label::new(" 〉 ", style.default.text.clone()).into_any()
+                Label::new(" › ", style.default.text.clone()).into_any()
             }))
             .constrained()
-            .with_height(theme.workspace.breadcrumb_height)
+            .with_height(theme.workspace.toolbar.breadcrumb_height)
             .contained();
 
         if not_editor || !self.pane_focused {

crates/call/Cargo.toml 🔗

@@ -20,6 +20,7 @@ test-support = [
 
 [dependencies]
 audio = { path = "../audio" }
+channel = { path = "../channel" }
 client = { path = "../client" }
 collections = { path = "../collections" }
 gpui = { path = "../gpui" }

crates/call/src/call.rs 🔗

@@ -5,7 +5,9 @@ pub mod room;
 use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
+use audio::Audio;
 use call_settings::CallSettings;
+use channel::ChannelId;
 use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
@@ -75,6 +77,10 @@ impl ActiveCall {
         }
     }
 
+    pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
+        self.room()?.read(cx).channel_id()
+    }
+
     async fn handle_incoming_call(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::IncomingCall>,
@@ -267,16 +273,43 @@ impl ActiveCall {
             .borrow_mut()
             .take()
             .ok_or_else(|| anyhow!("no incoming call"))?;
-        Self::report_call_event_for_room("decline incoming", call.room_id, &self.client, cx);
+        Self::report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
         self.client.send(proto::DeclineCall {
             room_id: call.room_id,
         })?;
         Ok(())
     }
 
+    pub fn join_channel(
+        &mut self,
+        channel_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if let Some(room) = self.room().cloned() {
+            if room.read(cx).channel_id() == Some(channel_id) {
+                return Task::ready(Ok(()));
+            } else {
+                room.update(cx, |room, cx| room.clear_state(cx));
+            }
+        }
+
+        let join = Room::join_channel(channel_id, self.client.clone(), self.user_store.clone(), cx);
+
+        cx.spawn(|this, mut cx| async move {
+            let room = join.await?;
+            this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
+                .await?;
+            this.update(&mut cx, |this, cx| {
+                this.report_call_event("join channel", cx)
+            });
+            Ok(())
+        })
+    }
+
     pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         cx.notify();
         self.report_call_event("hang up", cx);
+        Audio::end_call(cx);
         if let Some((room, _)) = self.room.take() {
             room.update(cx, |room, cx| room.leave(cx))
         } else {
@@ -372,19 +405,31 @@ impl ActiveCall {
 
     fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
         if let Some(room) = self.room() {
-            Self::report_call_event_for_room(operation, room.read(cx).id(), &self.client, cx)
+            let room = room.read(cx);
+            Self::report_call_event_for_room(
+                operation,
+                room.id(),
+                room.channel_id(),
+                &self.client,
+                cx,
+            )
         }
     }
 
     pub fn report_call_event_for_room(
         operation: &'static str,
         room_id: u64,
+        channel_id: Option<u64>,
         client: &Arc<Client>,
         cx: &AppContext,
     ) {
         let telemetry = client.telemetry();
         let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
-        let event = ClickhouseEvent::Call { operation, room_id };
+        let event = ClickhouseEvent::Call {
+            operation,
+            room_id,
+            channel_id,
+        };
         telemetry.report_clickhouse_event(event, telemetry_settings);
     }
 }

crates/call/src/room.rs 🔗

@@ -49,6 +49,7 @@ pub enum Event {
 
 pub struct Room {
     id: u64,
+    channel_id: Option<u64>,
     live_kit: Option<LiveKitRoom>,
     status: RoomStatus,
     shared_projects: HashSet<WeakModelHandle<Project>>,
@@ -93,8 +94,25 @@ impl Entity for Room {
 }
 
 impl Room {
+    pub fn channel_id(&self) -> Option<u64> {
+        self.channel_id
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn is_connected(&self) -> bool {
+        if let Some(live_kit) = self.live_kit.as_ref() {
+            matches!(
+                *live_kit.room.status().borrow(),
+                live_kit_client::ConnectionState::Connected { .. }
+            )
+        } else {
+            false
+        }
+    }
+
     fn new(
         id: u64,
+        channel_id: Option<u64>,
         live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
@@ -185,6 +203,7 @@ impl Room {
 
         Self {
             id,
+            channel_id,
             live_kit: live_kit_room,
             status: RoomStatus::Online,
             shared_projects: Default::default(),
@@ -217,6 +236,7 @@ impl Room {
             let room = cx.add_model(|cx| {
                 Self::new(
                     room_proto.id,
+                    None,
                     response.live_kit_connection_info,
                     client,
                     user_store,
@@ -248,35 +268,64 @@ impl Room {
         })
     }
 
+    pub(crate) fn join_channel(
+        channel_id: u64,
+        client: Arc<Client>,
+        user_store: ModelHandle<UserStore>,
+        cx: &mut AppContext,
+    ) -> Task<Result<ModelHandle<Self>>> {
+        cx.spawn(|cx| async move {
+            Self::from_join_response(
+                client.request(proto::JoinChannel { channel_id }).await?,
+                client,
+                user_store,
+                cx,
+            )
+        })
+    }
+
     pub(crate) fn join(
         call: &IncomingCall,
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
         cx: &mut AppContext,
     ) -> Task<Result<ModelHandle<Self>>> {
-        let room_id = call.room_id;
-        cx.spawn(|mut cx| async move {
-            let response = client.request(proto::JoinRoom { id: room_id }).await?;
-            let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
-            let room = cx.add_model(|cx| {
-                Self::new(
-                    room_id,
-                    response.live_kit_connection_info,
-                    client,
-                    user_store,
-                    cx,
-                )
-            });
-            room.update(&mut cx, |room, cx| {
-                room.leave_when_empty = true;
-                room.apply_room_update(room_proto, cx)?;
-                anyhow::Ok(())
-            })?;
-
-            Ok(room)
+        let id = call.room_id;
+        cx.spawn(|cx| async move {
+            Self::from_join_response(
+                client.request(proto::JoinRoom { id }).await?,
+                client,
+                user_store,
+                cx,
+            )
         })
     }
 
+    fn from_join_response(
+        response: proto::JoinRoomResponse,
+        client: Arc<Client>,
+        user_store: ModelHandle<UserStore>,
+        mut cx: AsyncAppContext,
+    ) -> Result<ModelHandle<Self>> {
+        let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
+        let room = cx.add_model(|cx| {
+            Self::new(
+                room_proto.id,
+                response.channel_id,
+                response.live_kit_connection_info,
+                client,
+                user_store,
+                cx,
+            )
+        });
+        room.update(&mut cx, |room, cx| {
+            room.leave_when_empty = room.channel_id.is_none();
+            room.apply_room_update(room_proto, cx)?;
+            anyhow::Ok(())
+        })?;
+        Ok(room)
+    }
+
     fn should_leave(&self) -> bool {
         self.leave_when_empty
             && self.pending_room_update.is_none()
@@ -297,7 +346,18 @@ impl Room {
         }
 
         log::info!("leaving room");
+        Audio::play_sound(Sound::Leave, cx);
 
+        self.clear_state(cx);
+
+        let leave_room = self.client.request(proto::LeaveRoom {});
+        cx.background().spawn(async move {
+            leave_room.await?;
+            anyhow::Ok(())
+        })
+    }
+
+    pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
         for project in self.shared_projects.drain() {
             if let Some(project) = project.upgrade(cx) {
                 project.update(cx, |project, cx| {
@@ -314,8 +374,6 @@ impl Room {
             }
         }
 
-        Audio::play_sound(Sound::Leave, cx);
-
         self.status = RoomStatus::Offline;
         self.remote_participants.clear();
         self.pending_participants.clear();
@@ -324,12 +382,6 @@ impl Room {
         self.live_kit.take();
         self.pending_room_update.take();
         self.maintain_connection.take();
-
-        let leave_room = self.client.request(proto::LeaveRoom {});
-        cx.background().spawn(async move {
-            leave_room.await?;
-            anyhow::Ok(())
-        })
     }
 
     async fn maintain_connection(
@@ -592,7 +644,9 @@ impl Room {
 
                 if let Some(participants) = remote_participants.log_err() {
                     for (participant, user) in room.participants.into_iter().zip(participants) {
-                        let Some(peer_id) = participant.peer_id else { continue };
+                        let Some(peer_id) = participant.peer_id else {
+                            continue;
+                        };
                         this.participant_user_ids.insert(participant.user_id);
 
                         let old_projects = this
@@ -1066,11 +1120,11 @@ impl Room {
         })
     }
 
-    pub fn is_muted(&self) -> bool {
+    pub fn is_muted(&self, cx: &AppContext) -> bool {
         self.live_kit
             .as_ref()
             .and_then(|live_kit| match &live_kit.microphone_track {
-                LocalTrack::None => Some(true),
+                LocalTrack::None => Some(settings::get::<CallSettings>(cx).mute_on_join),
                 LocalTrack::Pending { muted, .. } => Some(*muted),
                 LocalTrack::Published { muted, .. } => Some(*muted),
             })
@@ -1260,7 +1314,7 @@ impl Room {
     }
 
     pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
-        let should_mute = !self.is_muted();
+        let should_mute = !self.is_muted(cx);
         if let Some(live_kit) = self.live_kit.as_mut() {
             if matches!(live_kit.microphone_track, LocalTrack::None) {
                 return Ok(self.share_microphone(cx));

crates/channel/Cargo.toml 🔗

@@ -0,0 +1,51 @@
+[package]
+name = "channel"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/channel.rs"
+doctest = false
+
+[features]
+test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
+
+[dependencies]
+client = { path = "../client" }
+collections = { path = "../collections" }
+db = { path = "../db" }
+gpui = { path = "../gpui" }
+util = { path = "../util" }
+rpc = { path = "../rpc" }
+text = { path = "../text" }
+language = { path = "../language" }
+settings = { path = "../settings" }
+feature_flags = { path = "../feature_flags" }
+sum_tree = { path = "../sum_tree" }
+
+anyhow.workspace = true
+futures.workspace = true
+image = "0.23"
+lazy_static.workspace = true
+log.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+rand.workspace = true
+schemars.workspace = true
+smol.workspace = true
+thiserror.workspace = true
+time.workspace = true
+tiny_http = "0.8"
+uuid = { version = "1.1.2", features = ["v4"] }
+url = "2.2"
+serde.workspace = true
+serde_derive.workspace = true
+tempfile = "3"
+
+[dev-dependencies]
+collections = { path = "../collections", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }

crates/channel/src/channel.rs 🔗

@@ -0,0 +1,14 @@
+mod channel_store;
+
+pub mod channel_buffer;
+use std::sync::Arc;
+
+pub use channel_store::*;
+use client::Client;
+
+#[cfg(test)]
+mod channel_store_tests;
+
+pub fn init(client: &Arc<Client>) {
+    channel_buffer::init(client);
+}

crates/channel/src/channel_buffer.rs 🔗

@@ -0,0 +1,197 @@
+use crate::Channel;
+use anyhow::Result;
+use client::Client;
+use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
+use rpc::{proto, TypedEnvelope};
+use std::sync::Arc;
+use util::ResultExt;
+
+pub(crate) fn init(client: &Arc<Client>) {
+    client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
+    client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
+    client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
+}
+
+pub struct ChannelBuffer {
+    pub(crate) channel: Arc<Channel>,
+    connected: bool,
+    collaborators: Vec<proto::Collaborator>,
+    buffer: ModelHandle<language::Buffer>,
+    client: Arc<Client>,
+    subscription: Option<client::Subscription>,
+}
+
+pub enum Event {
+    CollaboratorsChanged,
+    Disconnected,
+}
+
+impl Entity for ChannelBuffer {
+    type Event = Event;
+
+    fn release(&mut self, _: &mut AppContext) {
+        if self.connected {
+            self.client
+                .send(proto::LeaveChannelBuffer {
+                    channel_id: self.channel.id,
+                })
+                .log_err();
+        }
+    }
+}
+
+impl ChannelBuffer {
+    pub(crate) async fn new(
+        channel: Arc<Channel>,
+        client: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<ModelHandle<Self>> {
+        let response = client
+            .request(proto::JoinChannelBuffer {
+                channel_id: channel.id,
+            })
+            .await?;
+
+        let base_text = response.base_text;
+        let operations = response
+            .operations
+            .into_iter()
+            .map(language::proto::deserialize_operation)
+            .collect::<Result<Vec<_>, _>>()?;
+
+        let collaborators = response.collaborators;
+
+        let buffer = cx.add_model(|_| {
+            language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
+        });
+        buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
+
+        let subscription = client.subscribe_to_entity(channel.id)?;
+
+        anyhow::Ok(cx.add_model(|cx| {
+            cx.subscribe(&buffer, Self::on_buffer_update).detach();
+
+            Self {
+                buffer,
+                client,
+                connected: true,
+                collaborators,
+                channel,
+                subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
+            }
+        }))
+    }
+
+    async fn handle_update_channel_buffer(
+        this: ModelHandle<Self>,
+        update_channel_buffer: TypedEnvelope<proto::UpdateChannelBuffer>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let ops = update_channel_buffer
+            .payload
+            .operations
+            .into_iter()
+            .map(language::proto::deserialize_operation)
+            .collect::<Result<Vec<_>, _>>()?;
+
+        this.update(&mut cx, |this, cx| {
+            cx.notify();
+            this.buffer
+                .update(cx, |buffer, cx| buffer.apply_ops(ops, cx))
+        })?;
+
+        Ok(())
+    }
+
+    async fn handle_add_channel_buffer_collaborator(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let collaborator = envelope.payload.collaborator.ok_or_else(|| {
+            anyhow::anyhow!(
+                "Should have gotten a collaborator in the AddChannelBufferCollaborator message"
+            )
+        })?;
+
+        this.update(&mut cx, |this, cx| {
+            this.collaborators.push(collaborator);
+            cx.emit(Event::CollaboratorsChanged);
+            cx.notify();
+        });
+
+        Ok(())
+    }
+
+    async fn handle_remove_channel_buffer_collaborator(
+        this: ModelHandle<Self>,
+        message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            this.collaborators.retain(|collaborator| {
+                if collaborator.peer_id == message.payload.peer_id {
+                    this.buffer.update(cx, |buffer, cx| {
+                        buffer.remove_peer(collaborator.replica_id as u16, cx)
+                    });
+                    false
+                } else {
+                    true
+                }
+            });
+            cx.emit(Event::CollaboratorsChanged);
+            cx.notify();
+        });
+
+        Ok(())
+    }
+
+    fn on_buffer_update(
+        &mut self,
+        _: ModelHandle<language::Buffer>,
+        event: &language::Event,
+        _: &mut ModelContext<Self>,
+    ) {
+        if let language::Event::Operation(operation) = event {
+            let operation = language::proto::serialize_operation(operation);
+            self.client
+                .send(proto::UpdateChannelBuffer {
+                    channel_id: self.channel.id,
+                    operations: vec![operation],
+                })
+                .log_err();
+        }
+    }
+
+    pub fn buffer(&self) -> ModelHandle<language::Buffer> {
+        self.buffer.clone()
+    }
+
+    pub fn collaborators(&self) -> &[proto::Collaborator] {
+        &self.collaborators
+    }
+
+    pub fn channel(&self) -> Arc<Channel> {
+        self.channel.clone()
+    }
+
+    pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
+        if self.connected {
+            self.connected = false;
+            self.subscription.take();
+            cx.emit(Event::Disconnected);
+            cx.notify()
+        }
+    }
+
+    pub fn is_connected(&self) -> bool {
+        self.connected
+    }
+
+    pub fn replica_id(&self, cx: &AppContext) -> u16 {
+        self.buffer.read(cx).replica_id()
+    }
+}

crates/channel/src/channel_store.rs 🔗

@@ -0,0 +1,656 @@
+use crate::channel_buffer::ChannelBuffer;
+use anyhow::{anyhow, Result};
+use client::{Client, Status, Subscription, User, UserId, UserStore};
+use collections::{hash_map, HashMap, HashSet};
+use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
+use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
+use rpc::{proto, TypedEnvelope};
+use std::sync::Arc;
+use util::ResultExt;
+
+pub type ChannelId = u64;
+
+pub struct ChannelStore {
+    channels_by_id: HashMap<ChannelId, Arc<Channel>>,
+    channel_paths: Vec<Vec<ChannelId>>,
+    channel_invitations: Vec<Arc<Channel>>,
+    channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
+    channels_with_admin_privileges: HashSet<ChannelId>,
+    outgoing_invites: HashSet<(ChannelId, UserId)>,
+    update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
+    opened_buffers: HashMap<ChannelId, OpenedChannelBuffer>,
+    client: Arc<Client>,
+    user_store: ModelHandle<UserStore>,
+    _rpc_subscription: Subscription,
+    _watch_connection_status: Task<()>,
+    _update_channels: Task<()>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Channel {
+    pub id: ChannelId,
+    pub name: String,
+}
+
+pub struct ChannelMembership {
+    pub user: Arc<User>,
+    pub kind: proto::channel_member::Kind,
+    pub admin: bool,
+}
+
+pub enum ChannelEvent {
+    ChannelCreated(ChannelId),
+    ChannelRenamed(ChannelId),
+}
+
+impl Entity for ChannelStore {
+    type Event = ChannelEvent;
+}
+
+pub enum ChannelMemberStatus {
+    Invited,
+    Member,
+    NotMember,
+}
+
+enum OpenedChannelBuffer {
+    Open(WeakModelHandle<ChannelBuffer>),
+    Loading(Shared<Task<Result<ModelHandle<ChannelBuffer>, Arc<anyhow::Error>>>>),
+}
+
+impl ChannelStore {
+    pub fn new(
+        client: Arc<Client>,
+        user_store: ModelHandle<UserStore>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        let rpc_subscription =
+            client.add_message_handler(cx.handle(), Self::handle_update_channels);
+
+        let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
+        let mut connection_status = client.status();
+        let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
+            while let Some(status) = connection_status.next().await {
+                if !status.is_connected() {
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, cx| {
+                            if matches!(status, Status::ConnectionLost | Status::SignedOut) {
+                                this.handle_disconnect(cx);
+                            } else {
+                                this.disconnect_buffers(cx);
+                            }
+                        });
+                    } else {
+                        break;
+                    }
+                }
+            }
+        });
+
+        Self {
+            channels_by_id: HashMap::default(),
+            channel_invitations: Vec::default(),
+            channel_paths: Vec::default(),
+            channel_participants: Default::default(),
+            channels_with_admin_privileges: Default::default(),
+            outgoing_invites: Default::default(),
+            opened_buffers: Default::default(),
+            update_channels_tx,
+            client,
+            user_store,
+            _rpc_subscription: rpc_subscription,
+            _watch_connection_status: watch_connection_status,
+            _update_channels: cx.spawn_weak(|this, mut cx| async move {
+                while let Some(update_channels) = update_channels_rx.next().await {
+                    if let Some(this) = this.upgrade(&cx) {
+                        let update_task = this.update(&mut cx, |this, cx| {
+                            this.update_channels(update_channels, cx)
+                        });
+                        if let Some(update_task) = update_task {
+                            update_task.await.log_err();
+                        }
+                    }
+                }
+            }),
+        }
+    }
+
+    pub fn has_children(&self, channel_id: ChannelId) -> bool {
+        self.channel_paths.iter().any(|path| {
+            if let Some(ix) = path.iter().position(|id| *id == channel_id) {
+                path.len() > ix + 1
+            } else {
+                false
+            }
+        })
+    }
+
+    pub fn channel_count(&self) -> usize {
+        self.channel_paths.len()
+    }
+
+    pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
+        self.channel_paths.iter().map(move |path| {
+            let id = path.last().unwrap();
+            let channel = self.channel_for_id(*id).unwrap();
+            (path.len() - 1, channel)
+        })
+    }
+
+    pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc<Channel>)> {
+        let path = self.channel_paths.get(ix)?;
+        let id = path.last().unwrap();
+        let channel = self.channel_for_id(*id).unwrap();
+        Some((path.len() - 1, channel))
+    }
+
+    pub fn channel_invitations(&self) -> &[Arc<Channel>] {
+        &self.channel_invitations
+    }
+
+    pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc<Channel>> {
+        self.channels_by_id.get(&channel_id)
+    }
+
+    pub fn open_channel_buffer(
+        &mut self,
+        channel_id: ChannelId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
+        // Make sure that a given channel buffer is only opened once per
+        // app instance, even if this method is called multiple times
+        // with the same channel id while the first task is still running.
+        let task = loop {
+            match self.opened_buffers.entry(channel_id) {
+                hash_map::Entry::Occupied(e) => match e.get() {
+                    OpenedChannelBuffer::Open(buffer) => {
+                        if let Some(buffer) = buffer.upgrade(cx) {
+                            break Task::ready(Ok(buffer)).shared();
+                        } else {
+                            self.opened_buffers.remove(&channel_id);
+                            continue;
+                        }
+                    }
+                    OpenedChannelBuffer::Loading(task) => break task.clone(),
+                },
+                hash_map::Entry::Vacant(e) => {
+                    let client = self.client.clone();
+                    let task = cx
+                        .spawn(|this, cx| async move {
+                            let channel = this.read_with(&cx, |this, _| {
+                                this.channel_for_id(channel_id).cloned().ok_or_else(|| {
+                                    Arc::new(anyhow!("no channel for id: {}", channel_id))
+                                })
+                            })?;
+
+                            ChannelBuffer::new(channel, client, cx)
+                                .await
+                                .map_err(Arc::new)
+                        })
+                        .shared();
+                    e.insert(OpenedChannelBuffer::Loading(task.clone()));
+                    cx.spawn({
+                        let task = task.clone();
+                        |this, mut cx| async move {
+                            let result = task.await;
+                            this.update(&mut cx, |this, cx| match result {
+                                Ok(buffer) => {
+                                    cx.observe_release(&buffer, move |this, _, _| {
+                                        this.opened_buffers.remove(&channel_id);
+                                    })
+                                    .detach();
+                                    this.opened_buffers.insert(
+                                        channel_id,
+                                        OpenedChannelBuffer::Open(buffer.downgrade()),
+                                    );
+                                }
+                                Err(error) => {
+                                    log::error!("failed to open channel buffer {error:?}");
+                                    this.opened_buffers.remove(&channel_id);
+                                }
+                            });
+                        }
+                    })
+                    .detach();
+                    break task;
+                }
+            }
+        };
+        cx.foreground()
+            .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) })
+    }
+
+    pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
+        self.channel_paths.iter().any(|path| {
+            if let Some(ix) = path.iter().position(|id| *id == channel_id) {
+                path[..=ix]
+                    .iter()
+                    .any(|id| self.channels_with_admin_privileges.contains(id))
+            } else {
+                false
+            }
+        })
+    }
+
+    pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
+        self.channel_participants
+            .get(&channel_id)
+            .map_or(&[], |v| v.as_slice())
+    }
+
+    pub fn create_channel(
+        &self,
+        name: &str,
+        parent_id: Option<ChannelId>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ChannelId>> {
+        let client = self.client.clone();
+        let name = name.trim_start_matches("#").to_owned();
+        cx.spawn(|this, mut cx| async move {
+            let channel = client
+                .request(proto::CreateChannel { name, parent_id })
+                .await?
+                .channel
+                .ok_or_else(|| anyhow!("missing channel in response"))?;
+
+            let channel_id = channel.id;
+
+            this.update(&mut cx, |this, cx| {
+                let task = this.update_channels(
+                    proto::UpdateChannels {
+                        channels: vec![channel],
+                        ..Default::default()
+                    },
+                    cx,
+                );
+                assert!(task.is_none());
+
+                // This event is emitted because the collab panel wants to clear the pending edit state
+                // before this frame is rendered. But we can't guarantee that the collab panel's future
+                // will resolve before this flush_effects finishes. Synchronously emitting this event
+                // ensures that the collab panel will observe this creation before the frame completes
+                cx.emit(ChannelEvent::ChannelCreated(channel_id));
+            });
+
+            Ok(channel_id)
+        })
+    }
+
+    pub fn invite_member(
+        &mut self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        admin: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if !self.outgoing_invites.insert((channel_id, user_id)) {
+            return Task::ready(Err(anyhow!("invite request already in progress")));
+        }
+
+        cx.notify();
+        let client = self.client.clone();
+        cx.spawn(|this, mut cx| async move {
+            let result = client
+                .request(proto::InviteChannelMember {
+                    channel_id,
+                    user_id,
+                    admin,
+                })
+                .await;
+
+            this.update(&mut cx, |this, cx| {
+                this.outgoing_invites.remove(&(channel_id, user_id));
+                cx.notify();
+            });
+
+            result?;
+
+            Ok(())
+        })
+    }
+
+    pub fn remove_member(
+        &mut self,
+        channel_id: ChannelId,
+        user_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if !self.outgoing_invites.insert((channel_id, user_id)) {
+            return Task::ready(Err(anyhow!("invite request already in progress")));
+        }
+
+        cx.notify();
+        let client = self.client.clone();
+        cx.spawn(|this, mut cx| async move {
+            let result = client
+                .request(proto::RemoveChannelMember {
+                    channel_id,
+                    user_id,
+                })
+                .await;
+
+            this.update(&mut cx, |this, cx| {
+                this.outgoing_invites.remove(&(channel_id, user_id));
+                cx.notify();
+            });
+            result?;
+            Ok(())
+        })
+    }
+
+    pub fn set_member_admin(
+        &mut self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        admin: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if !self.outgoing_invites.insert((channel_id, user_id)) {
+            return Task::ready(Err(anyhow!("member request already in progress")));
+        }
+
+        cx.notify();
+        let client = self.client.clone();
+        cx.spawn(|this, mut cx| async move {
+            let result = client
+                .request(proto::SetChannelMemberAdmin {
+                    channel_id,
+                    user_id,
+                    admin,
+                })
+                .await;
+
+            this.update(&mut cx, |this, cx| {
+                this.outgoing_invites.remove(&(channel_id, user_id));
+                cx.notify();
+            });
+
+            result?;
+            Ok(())
+        })
+    }
+
+    pub fn rename(
+        &mut self,
+        channel_id: ChannelId,
+        new_name: &str,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let client = self.client.clone();
+        let name = new_name.to_string();
+        cx.spawn(|this, mut cx| async move {
+            let channel = client
+                .request(proto::RenameChannel { channel_id, name })
+                .await?
+                .channel
+                .ok_or_else(|| anyhow!("missing channel in response"))?;
+            this.update(&mut cx, |this, cx| {
+                let task = this.update_channels(
+                    proto::UpdateChannels {
+                        channels: vec![channel],
+                        ..Default::default()
+                    },
+                    cx,
+                );
+                assert!(task.is_none());
+
+                // This event is emitted because the collab panel wants to clear the pending edit state
+                // before this frame is rendered. But we can't guarantee that the collab panel's future
+                // will resolve before this flush_effects finishes. Synchronously emitting this event
+                // ensures that the collab panel will observe this creation before the frame complete
+                cx.emit(ChannelEvent::ChannelRenamed(channel_id))
+            });
+            Ok(())
+        })
+    }
+
+    pub fn respond_to_channel_invite(
+        &mut self,
+        channel_id: ChannelId,
+        accept: bool,
+    ) -> impl Future<Output = Result<()>> {
+        let client = self.client.clone();
+        async move {
+            client
+                .request(proto::RespondToChannelInvite { channel_id, accept })
+                .await?;
+            Ok(())
+        }
+    }
+
+    pub fn get_channel_member_details(
+        &self,
+        channel_id: ChannelId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<ChannelMembership>>> {
+        let client = self.client.clone();
+        let user_store = self.user_store.downgrade();
+        cx.spawn(|_, mut cx| async move {
+            let response = client
+                .request(proto::GetChannelMembers { channel_id })
+                .await?;
+
+            let user_ids = response.members.iter().map(|m| m.user_id).collect();
+            let user_store = user_store
+                .upgrade(&cx)
+                .ok_or_else(|| anyhow!("user store dropped"))?;
+            let users = user_store
+                .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))
+                .await?;
+
+            Ok(users
+                .into_iter()
+                .zip(response.members)
+                .filter_map(|(user, member)| {
+                    Some(ChannelMembership {
+                        user,
+                        admin: member.admin,
+                        kind: proto::channel_member::Kind::from_i32(member.kind)?,
+                    })
+                })
+                .collect())
+        })
+    }
+
+    pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
+        let client = self.client.clone();
+        async move {
+            client.request(proto::RemoveChannel { channel_id }).await?;
+            Ok(())
+        }
+    }
+
+    pub fn has_pending_channel_invite_response(&self, _: &Arc<Channel>) -> bool {
+        false
+    }
+
+    pub fn has_pending_channel_invite(&self, channel_id: ChannelId, user_id: UserId) -> bool {
+        self.outgoing_invites.contains(&(channel_id, user_id))
+    }
+
+    async fn handle_update_channels(
+        this: ModelHandle<Self>,
+        message: TypedEnvelope<proto::UpdateChannels>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, _| {
+            this.update_channels_tx
+                .unbounded_send(message.payload)
+                .unwrap();
+        });
+        Ok(())
+    }
+
+    fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) {
+        self.disconnect_buffers(cx);
+        self.channels_by_id.clear();
+        self.channel_invitations.clear();
+        self.channel_participants.clear();
+        self.channels_with_admin_privileges.clear();
+        self.channel_paths.clear();
+        self.outgoing_invites.clear();
+        cx.notify();
+    }
+
+    fn disconnect_buffers(&mut self, cx: &mut ModelContext<ChannelStore>) {
+        for (_, buffer) in self.opened_buffers.drain() {
+            if let OpenedChannelBuffer::Open(buffer) = buffer {
+                if let Some(buffer) = buffer.upgrade(cx) {
+                    buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
+                }
+            }
+        }
+    }
+
+    pub(crate) fn update_channels(
+        &mut self,
+        payload: proto::UpdateChannels,
+        cx: &mut ModelContext<ChannelStore>,
+    ) -> Option<Task<Result<()>>> {
+        if !payload.remove_channel_invitations.is_empty() {
+            self.channel_invitations
+                .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id));
+        }
+        for channel in payload.channel_invitations {
+            match self
+                .channel_invitations
+                .binary_search_by_key(&channel.id, |c| c.id)
+            {
+                Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
+                Err(ix) => self.channel_invitations.insert(
+                    ix,
+                    Arc::new(Channel {
+                        id: channel.id,
+                        name: channel.name,
+                    }),
+                ),
+            }
+        }
+
+        let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty();
+        if channels_changed {
+            if !payload.remove_channels.is_empty() {
+                self.channels_by_id
+                    .retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
+                self.channel_participants
+                    .retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
+                self.channels_with_admin_privileges
+                    .retain(|channel_id| !payload.remove_channels.contains(channel_id));
+
+                for channel_id in &payload.remove_channels {
+                    let channel_id = *channel_id;
+                    if let Some(OpenedChannelBuffer::Open(buffer)) =
+                        self.opened_buffers.remove(&channel_id)
+                    {
+                        if let Some(buffer) = buffer.upgrade(cx) {
+                            buffer.update(cx, ChannelBuffer::disconnect);
+                        }
+                    }
+                }
+            }
+
+            for channel_proto in payload.channels {
+                if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
+                    Arc::make_mut(existing_channel).name = channel_proto.name;
+                } else {
+                    let channel = Arc::new(Channel {
+                        id: channel_proto.id,
+                        name: channel_proto.name,
+                    });
+                    self.channels_by_id.insert(channel.id, channel.clone());
+
+                    if let Some(parent_id) = channel_proto.parent_id {
+                        let mut ix = 0;
+                        while ix < self.channel_paths.len() {
+                            let path = &self.channel_paths[ix];
+                            if path.ends_with(&[parent_id]) {
+                                let mut new_path = path.clone();
+                                new_path.push(channel.id);
+                                self.channel_paths.insert(ix + 1, new_path);
+                                ix += 1;
+                            }
+                            ix += 1;
+                        }
+                    } else {
+                        self.channel_paths.push(vec![channel.id]);
+                    }
+                }
+            }
+
+            self.channel_paths.sort_by(|a, b| {
+                let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
+                let b = Self::channel_path_sorting_key(b, &self.channels_by_id);
+                a.cmp(b)
+            });
+            self.channel_paths.dedup();
+            self.channel_paths.retain(|path| {
+                path.iter()
+                    .all(|channel_id| self.channels_by_id.contains_key(channel_id))
+            });
+        }
+
+        for permission in payload.channel_permissions {
+            if permission.is_admin {
+                self.channels_with_admin_privileges
+                    .insert(permission.channel_id);
+            } else {
+                self.channels_with_admin_privileges
+                    .remove(&permission.channel_id);
+            }
+        }
+
+        cx.notify();
+        if payload.channel_participants.is_empty() {
+            return None;
+        }
+
+        let mut all_user_ids = Vec::new();
+        let channel_participants = payload.channel_participants;
+        for entry in &channel_participants {
+            for user_id in entry.participant_user_ids.iter() {
+                if let Err(ix) = all_user_ids.binary_search(user_id) {
+                    all_user_ids.insert(ix, *user_id);
+                }
+            }
+        }
+
+        let users = self
+            .user_store
+            .update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx));
+        Some(cx.spawn(|this, mut cx| async move {
+            let users = users.await?;
+
+            this.update(&mut cx, |this, cx| {
+                for entry in &channel_participants {
+                    let mut participants: Vec<_> = entry
+                        .participant_user_ids
+                        .iter()
+                        .filter_map(|user_id| {
+                            users
+                                .binary_search_by_key(&user_id, |user| &user.id)
+                                .ok()
+                                .map(|ix| users[ix].clone())
+                        })
+                        .collect();
+
+                    participants.sort_by_key(|u| u.id);
+
+                    this.channel_participants
+                        .insert(entry.channel_id, participants);
+                }
+
+                cx.notify();
+            });
+            anyhow::Ok(())
+        }))
+    }
+
+    fn channel_path_sorting_key<'a>(
+        path: &'a [ChannelId],
+        channels_by_id: &'a HashMap<ChannelId, Arc<Channel>>,
+    ) -> impl 'a + Iterator<Item = Option<&'a str>> {
+        path.iter()
+            .map(|id| Some(channels_by_id.get(id)?.name.as_str()))
+    }
+}

crates/channel/src/channel_store_tests.rs 🔗

@@ -0,0 +1,168 @@
+use super::*;
+use client::{Client, UserStore};
+use gpui::{AppContext, ModelHandle};
+use rpc::proto;
+use util::http::FakeHttpClient;
+
+#[gpui::test]
+fn test_update_channels(cx: &mut AppContext) {
+    let http = FakeHttpClient::with_404_response();
+    let client = Client::new(http.clone(), cx);
+    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+
+    let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
+
+    update_channels(
+        &channel_store,
+        proto::UpdateChannels {
+            channels: vec![
+                proto::Channel {
+                    id: 1,
+                    name: "b".to_string(),
+                    parent_id: None,
+                },
+                proto::Channel {
+                    id: 2,
+                    name: "a".to_string(),
+                    parent_id: None,
+                },
+            ],
+            channel_permissions: vec![proto::ChannelPermission {
+                channel_id: 1,
+                is_admin: true,
+            }],
+            ..Default::default()
+        },
+        cx,
+    );
+    assert_channels(
+        &channel_store,
+        &[
+            //
+            (0, "a".to_string(), false),
+            (0, "b".to_string(), true),
+        ],
+        cx,
+    );
+
+    update_channels(
+        &channel_store,
+        proto::UpdateChannels {
+            channels: vec![
+                proto::Channel {
+                    id: 3,
+                    name: "x".to_string(),
+                    parent_id: Some(1),
+                },
+                proto::Channel {
+                    id: 4,
+                    name: "y".to_string(),
+                    parent_id: Some(2),
+                },
+            ],
+            ..Default::default()
+        },
+        cx,
+    );
+    assert_channels(
+        &channel_store,
+        &[
+            (0, "a".to_string(), false),
+            (1, "y".to_string(), false),
+            (0, "b".to_string(), true),
+            (1, "x".to_string(), true),
+        ],
+        cx,
+    );
+}
+
+#[gpui::test]
+fn test_dangling_channel_paths(cx: &mut AppContext) {
+    let http = FakeHttpClient::with_404_response();
+    let client = Client::new(http.clone(), cx);
+    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+
+    let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
+
+    update_channels(
+        &channel_store,
+        proto::UpdateChannels {
+            channels: vec![
+                proto::Channel {
+                    id: 0,
+                    name: "a".to_string(),
+                    parent_id: None,
+                },
+                proto::Channel {
+                    id: 1,
+                    name: "b".to_string(),
+                    parent_id: Some(0),
+                },
+                proto::Channel {
+                    id: 2,
+                    name: "c".to_string(),
+                    parent_id: Some(1),
+                },
+            ],
+            channel_permissions: vec![proto::ChannelPermission {
+                channel_id: 0,
+                is_admin: true,
+            }],
+            ..Default::default()
+        },
+        cx,
+    );
+    // Sanity check
+    assert_channels(
+        &channel_store,
+        &[
+            //
+            (0, "a".to_string(), true),
+            (1, "b".to_string(), true),
+            (2, "c".to_string(), true),
+        ],
+        cx,
+    );
+
+    update_channels(
+        &channel_store,
+        proto::UpdateChannels {
+            remove_channels: vec![1, 2],
+            ..Default::default()
+        },
+        cx,
+    );
+
+    // Make sure that the 1/2/3 path is gone
+    assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx);
+}
+
+fn update_channels(
+    channel_store: &ModelHandle<ChannelStore>,
+    message: proto::UpdateChannels,
+    cx: &mut AppContext,
+) {
+    let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx));
+    assert!(task.is_none());
+}
+
+#[track_caller]
+fn assert_channels(
+    channel_store: &ModelHandle<ChannelStore>,
+    expected_channels: &[(usize, String, bool)],
+    cx: &AppContext,
+) {
+    let actual = channel_store.read_with(cx, |store, _| {
+        store
+            .channels()
+            .map(|(depth, channel)| {
+                (
+                    depth,
+                    channel.name.to_string(),
+                    store.is_user_admin(channel.id),
+                )
+            })
+            .collect::<Vec<_>>()
+    });
+    assert_eq!(actual, expected_channels);
+}

crates/client/Cargo.toml 🔗

@@ -17,8 +17,9 @@ db = { path = "../db" }
 gpui = { path = "../gpui" }
 util = { path = "../util" }
 rpc = { path = "../rpc" }
+text = { path = "../text" }
 settings = { path = "../settings" }
-staff_mode = { path = "../staff_mode" }
+feature_flags = { path = "../feature_flags" }
 sum_tree = { path = "../sum_tree" }
 
 anyhow.workspace = true

crates/client/src/client.rs 🔗

@@ -535,6 +535,7 @@ impl Client {
         }
     }
 
+    #[track_caller]
     pub fn add_message_handler<M, E, H, F>(
         self: &Arc<Self>,
         model: ModelHandle<E>,
@@ -570,7 +571,13 @@ impl Client {
             }),
         );
         if prev_handler.is_some() {
-            panic!("registered handler for the same message twice");
+            let location = std::panic::Location::caller();
+            panic!(
+                "{}:{} registered handler for the same message {} twice",
+                location.file(),
+                location.line(),
+                std::any::type_name::<M>()
+            );
         }
 
         Subscription::Message {

crates/client/src/telemetry.rs 🔗

@@ -74,6 +74,7 @@ pub enum ClickhouseEvent {
     Call {
         operation: &'static str,
         room_id: u64,
+        channel_id: Option<u64>,
     },
 }
 
@@ -134,8 +135,6 @@ impl Telemetry {
         }
     }
 
-    /// This method takes the entire TelemetrySettings struct in order to force client code
-    /// to pull the struct out of the settings global. Do not remove!
     pub fn set_authenticated_user_info(
         self: &Arc<Self>,
         metrics_id: Option<String>,

crates/client/src/test.rs 🔗

@@ -168,6 +168,7 @@ impl FakeServer {
                     GetPrivateUserInfoResponse {
                         metrics_id: "the-metrics-id".into(),
                         staff: false,
+                        flags: Default::default(),
                     },
                 )
                 .await;

crates/client/src/user.rs 🔗

@@ -1,18 +1,20 @@
 use super::{proto, Client, Status, TypedEnvelope};
 use anyhow::{anyhow, Context, Result};
 use collections::{hash_map::Entry, HashMap, HashSet};
+use feature_flags::FeatureFlagAppExt;
 use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
 use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
 use postage::{sink::Sink, watch};
 use rpc::proto::{RequestMessage, UsersResponse};
-use staff_mode::StaffMode;
 use std::sync::{Arc, Weak};
 use util::http::HttpClient;
 use util::TryFutureExt as _;
 
+pub type UserId = u64;
+
 #[derive(Default, Debug)]
 pub struct User {
-    pub id: u64,
+    pub id: UserId,
     pub github_login: String,
     pub avatar: Option<Arc<ImageData>>,
 }
@@ -143,39 +145,48 @@ impl UserStore {
                                 let fetch_metrics_id =
                                     client.request(proto::GetPrivateUserInfo {}).log_err();
                                 let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
-                                cx.read(|cx| {
-                                    client.telemetry.set_authenticated_user_info(
-                                        info.as_ref().map(|info| info.metrics_id.clone()),
-                                        info.as_ref().map(|info| info.staff).unwrap_or(false),
-                                        cx,
-                                    )
-                                });
 
-                                cx.update(|cx| {
-                                    cx.update_default_global(|staff_mode: &mut StaffMode, _| {
-                                        if !staff_mode.0 {
-                                            *staff_mode = StaffMode(
-                                                info.as_ref()
-                                                    .map(|info| info.staff)
-                                                    .unwrap_or_default(),
-                                            )
-                                        }
-                                        ()
+                                if let Some(info) = info {
+                                    cx.update(|cx| {
+                                        cx.update_flags(info.staff, info.flags);
+                                        client.telemetry.set_authenticated_user_info(
+                                            Some(info.metrics_id.clone()),
+                                            info.staff,
+                                            cx,
+                                        )
                                     });
-                                });
+                                } else {
+                                    cx.read(|cx| {
+                                        client
+                                            .telemetry
+                                            .set_authenticated_user_info(None, false, cx)
+                                    });
+                                }
 
                                 current_user_tx.send(user).await.ok();
+
+                                this.update(&mut cx, |_, cx| {
+                                    cx.notify();
+                                });
                             }
                         }
                         Status::SignedOut => {
                             current_user_tx.send(None).await.ok();
                             if let Some(this) = this.upgrade(&cx) {
-                                this.update(&mut cx, |this, _| this.clear_contacts()).await;
+                                this.update(&mut cx, |this, cx| {
+                                    cx.notify();
+                                    this.clear_contacts()
+                                })
+                                .await;
                             }
                         }
                         Status::ConnectionLost => {
                             if let Some(this) = this.upgrade(&cx) {
-                                this.update(&mut cx, |this, _| this.clear_contacts()).await;
+                                this.update(&mut cx, |this, cx| {
+                                    cx.notify();
+                                    this.clear_contacts()
+                                })
+                                .await;
                             }
                         }
                         _ => {}

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.16.0"
+version = "0.19.0"
 publish = false
 
 [[bin]]
@@ -14,8 +14,10 @@ name = "seed"
 required-features = ["seed-support"]
 
 [dependencies]
+clock = { path = "../clock" }
 collections = { path = "../collections" }
 live_kit_server = { path = "../live_kit_server" }
+text = { path = "../text" }
 rpc = { path = "../rpc" }
 util = { path = "../util" }
 
@@ -35,6 +37,7 @@ log.workspace = true
 nanoid = "0.4"
 parking_lot.workspace = true
 prometheus = "0.13"
+prost.workspace = true
 rand.workspace = true
 reqwest = { version = "0.11", features = ["json"], optional = true }
 scrypt = "0.7"
@@ -62,6 +65,7 @@ collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 call = { path = "../call", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"] }
+channel = { path = "../channel" }
 editor = { path = "../editor", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 fs = { path = "../fs", features = ["test-support"] }
@@ -74,6 +78,7 @@ rpc = { path = "../rpc", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
 theme = { path = "../theme" }
 workspace = { path = "../workspace", features = ["test-support"] }
+collab_ui = { path = "../collab_ui", features = ["test-support"] }
 
 ctor.workspace = true
 env_logger.workspace = true

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

@@ -36,7 +36,8 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
 
 CREATE TABLE "rooms" (
     "id" INTEGER PRIMARY KEY AUTOINCREMENT,
-    "live_kit_room" VARCHAR NOT NULL
+    "live_kit_room" VARCHAR NOT NULL,
+    "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
 );
 
 CREATE TABLE "projects" (
@@ -184,3 +185,86 @@ CREATE UNIQUE INDEX
     "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
 ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
 CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
+
+CREATE TABLE "channels" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "name" VARCHAR NOT NULL,
+    "created_at" TIMESTAMP NOT NULL DEFAULT now
+);
+
+CREATE TABLE "channel_paths" (
+    "id_path" TEXT NOT NULL PRIMARY KEY,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE
+);
+CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id");
+
+CREATE TABLE "channel_members" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "admin" BOOLEAN NOT NULL DEFAULT false,
+    "accepted" BOOLEAN NOT NULL DEFAULT false,
+    "updated_at" TIMESTAMP NOT NULL DEFAULT now
+);
+
+CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");
+
+CREATE TABLE "buffers" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "epoch" INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id");
+
+CREATE TABLE "buffer_operations" (
+    "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+    "epoch" INTEGER NOT NULL,
+    "replica_id" INTEGER NOT NULL,
+    "lamport_timestamp" INTEGER NOT NULL,
+    "value" BLOB NOT NULL,
+    PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id)
+);
+
+CREATE TABLE "buffer_snapshots" (
+    "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+    "epoch" INTEGER NOT NULL,
+    "text" TEXT NOT NULL,
+    "operation_serialization_version" INTEGER NOT NULL,
+    PRIMARY KEY(buffer_id, epoch)
+);
+
+CREATE TABLE "channel_buffer_collaborators" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "connection_id" INTEGER NOT NULL,
+    "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "connection_lost" BOOLEAN NOT NULL DEFAULT false,
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "replica_id" INTEGER NOT NULL
+);
+
+CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id");
+CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id");
+CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id");
+CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id");
+CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id");
+
+
+CREATE TABLE "feature_flags" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "flag" TEXT NOT NULL UNIQUE
+);
+
+CREATE INDEX "index_feature_flags" ON "feature_flags" ("id");
+
+
+CREATE TABLE "user_features" (
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "feature_id" INTEGER NOT NULL REFERENCES feature_flags (id) ON DELETE CASCADE,
+    PRIMARY KEY (user_id, feature_id)
+);
+
+CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id");
+CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id");
+CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id");

crates/collab/migrations/20230727150500_add_channels.sql 🔗

@@ -0,0 +1,30 @@
+DROP TABLE "channel_messages";
+DROP TABLE "channel_memberships";
+DROP TABLE "org_memberships";
+DROP TABLE "orgs";
+DROP TABLE "channels";
+
+CREATE TABLE "channels" (
+    "id" SERIAL PRIMARY KEY,
+    "name" VARCHAR NOT NULL,
+    "created_at" TIMESTAMP NOT NULL DEFAULT now()
+);
+
+CREATE TABLE "channel_paths" (
+    "id_path" VARCHAR NOT NULL PRIMARY KEY,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE
+);
+CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id");
+
+CREATE TABLE "channel_members" (
+    "id" SERIAL PRIMARY KEY,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "admin" BOOLEAN NOT NULL DEFAULT false,
+    "accepted" BOOLEAN NOT NULL DEFAULT false,
+    "updated_at" TIMESTAMP NOT NULL DEFAULT now()
+);
+
+CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");
+
+ALTER TABLE rooms ADD COLUMN "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE;

crates/collab/migrations/20230819154600_add_channel_buffers.sql 🔗

@@ -0,0 +1,40 @@
+CREATE TABLE "buffers" (
+    "id" SERIAL PRIMARY KEY,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "epoch" INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id");
+
+CREATE TABLE "buffer_operations" (
+    "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+    "epoch" INTEGER NOT NULL,
+    "replica_id" INTEGER NOT NULL,
+    "lamport_timestamp" INTEGER NOT NULL,
+    "value" BYTEA NOT NULL,
+    PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id)
+);
+
+CREATE TABLE "buffer_snapshots" (
+    "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+    "epoch" INTEGER NOT NULL,
+    "text" TEXT NOT NULL,
+    "operation_serialization_version" INTEGER NOT NULL,
+    PRIMARY KEY(buffer_id, epoch)
+);
+
+CREATE TABLE "channel_buffer_collaborators" (
+    "id" SERIAL PRIMARY KEY,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "connection_id" INTEGER NOT NULL,
+    "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "connection_lost" BOOLEAN NOT NULL DEFAULT FALSE,
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "replica_id" INTEGER NOT NULL
+);
+
+CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id");
+CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id");
+CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id");
+CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id");
+CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id");

crates/collab/migrations/20230825190322_add_server_feature_flags.sql 🔗

@@ -0,0 +1,16 @@
+CREATE TABLE "feature_flags" (
+    "id" SERIAL PRIMARY KEY,
+    "flag" VARCHAR(255) NOT NULL UNIQUE
+);
+
+CREATE UNIQUE INDEX "index_feature_flags" ON "feature_flags" ("id");
+
+CREATE TABLE "user_features" (
+    "user_id" INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+    "feature_id" INTEGER NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE,
+    PRIMARY KEY (user_id, feature_id)
+);
+
+CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id");
+CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id");
+CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id");

crates/collab/src/bin/seed.rs 🔗

@@ -64,9 +64,9 @@ async fn main() {
             .expect("failed to fetch user")
             .is_none()
         {
-            if let Some(email) = &github_user.email {
+            if admin {
                 db.create_user(
-                    email,
+                    &format!("{}@zed.dev", github_user.login),
                     admin,
                     db::NewUserParams {
                         github_login: github_user.login,
@@ -76,15 +76,11 @@ async fn main() {
                 )
                 .await
                 .expect("failed to insert user");
-            } else if admin {
-                db.create_user(
-                    &format!("{}@zed.dev", github_user.login),
-                    admin,
-                    db::NewUserParams {
-                        github_login: github_user.login,
-                        github_user_id: github_user.id,
-                        invite_count: 5,
-                    },
+            } else {
+                db.get_or_create_user_by_github_account(
+                    &github_user.login,
+                    Some(github_user.id),
+                    github_user.email.as_deref(),
                 )
                 .await
                 .expect("failed to insert user");

crates/collab/src/db.rs 🔗

@@ -1,3030 +1,117 @@
-mod access_token;
-mod contact;
-mod follower;
-mod language_server;
-mod project;
-mod project_collaborator;
-mod room;
-mod room_participant;
-mod server;
-mod signup;
 #[cfg(test)]
-mod tests;
-mod user;
-mod worktree;
-mod worktree_diagnostic_summary;
-mod worktree_entry;
-mod worktree_repository;
-mod worktree_repository_statuses;
-mod worktree_settings_file;
+pub mod tests;
 
-use crate::executor::Executor;
-use crate::{Error, Result};
-use anyhow::anyhow;
-use collections::{BTreeMap, HashMap, HashSet};
-pub use contact::Contact;
-use dashmap::DashMap;
-use futures::StreamExt;
-use hyper::StatusCode;
-use rand::prelude::StdRng;
-use rand::{Rng, SeedableRng};
-use rpc::{proto, ConnectionId};
-use sea_orm::Condition;
-pub use sea_orm::ConnectOptions;
-use sea_orm::{
-    entity::prelude::*, ActiveValue, ConnectionTrait, DatabaseConnection, DatabaseTransaction,
-    DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect,
-    Statement, TransactionTrait,
-};
-use sea_query::{Alias, Expr, OnConflict, Query};
-use serde::{Deserialize, Serialize};
-pub use signup::{Invite, NewSignup, WaitlistSummary};
-use sqlx::migrate::{Migrate, Migration, MigrationSource};
-use sqlx::Connection;
-use std::ops::{Deref, DerefMut};
-use std::path::Path;
-use std::time::Duration;
-use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc};
-use tokio::sync::{Mutex, OwnedMutexGuard};
-pub use user::Model as User;
-
-pub struct Database {
-    options: ConnectOptions,
-    pool: DatabaseConnection,
-    rooms: DashMap<RoomId, Arc<Mutex<()>>>,
-    rng: Mutex<StdRng>,
-    executor: Executor,
-    #[cfg(test)]
-    runtime: Option<tokio::runtime::Runtime>,
-}
-
-impl Database {
-    pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
-        Ok(Self {
-            options: options.clone(),
-            pool: sea_orm::Database::connect(options).await?,
-            rooms: DashMap::with_capacity(16384),
-            rng: Mutex::new(StdRng::seed_from_u64(0)),
-            executor,
-            #[cfg(test)]
-            runtime: None,
-        })
-    }
-
-    #[cfg(test)]
-    pub fn reset(&self) {
-        self.rooms.clear();
-    }
-
-    pub async fn migrate(
-        &self,
-        migrations_path: &Path,
-        ignore_checksum_mismatch: bool,
-    ) -> anyhow::Result<Vec<(Migration, Duration)>> {
-        let migrations = MigrationSource::resolve(migrations_path)
-            .await
-            .map_err(|err| anyhow!("failed to load migrations: {err:?}"))?;
-
-        let mut connection = sqlx::AnyConnection::connect(self.options.get_url()).await?;
-
-        connection.ensure_migrations_table().await?;
-        let applied_migrations: HashMap<_, _> = connection
-            .list_applied_migrations()
-            .await?
-            .into_iter()
-            .map(|m| (m.version, m))
-            .collect();
-
-        let mut new_migrations = Vec::new();
-        for migration in migrations {
-            match applied_migrations.get(&migration.version) {
-                Some(applied_migration) => {
-                    if migration.checksum != applied_migration.checksum && !ignore_checksum_mismatch
-                    {
-                        Err(anyhow!(
-                            "checksum mismatch for applied migration {}",
-                            migration.description
-                        ))?;
-                    }
-                }
-                None => {
-                    let elapsed = connection.apply(&migration).await?;
-                    new_migrations.push((migration, elapsed));
-                }
-            }
-        }
-
-        Ok(new_migrations)
-    }
-
-    pub async fn create_server(&self, environment: &str) -> Result<ServerId> {
-        self.transaction(|tx| async move {
-            let server = server::ActiveModel {
-                environment: ActiveValue::set(environment.into()),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-            Ok(server.id)
-        })
-        .await
-    }
-
-    pub async fn stale_room_ids(
-        &self,
-        environment: &str,
-        new_server_id: ServerId,
-    ) -> Result<Vec<RoomId>> {
-        self.transaction(|tx| async move {
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryAs {
-                RoomId,
-            }
-
-            let stale_server_epochs = self
-                .stale_server_ids(environment, new_server_id, &tx)
-                .await?;
-            Ok(room_participant::Entity::find()
-                .select_only()
-                .column(room_participant::Column::RoomId)
-                .distinct()
-                .filter(
-                    room_participant::Column::AnsweringConnectionServerId
-                        .is_in(stale_server_epochs),
-                )
-                .into_values::<_, QueryAs>()
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn refresh_room(
-        &self,
-        room_id: RoomId,
-        new_server_id: ServerId,
-    ) -> Result<RoomGuard<RefreshedRoom>> {
-        self.room_transaction(room_id, |tx| async move {
-            let stale_participant_filter = Condition::all()
-                .add(room_participant::Column::RoomId.eq(room_id))
-                .add(room_participant::Column::AnsweringConnectionId.is_not_null())
-                .add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id));
-
-            let stale_participant_user_ids = room_participant::Entity::find()
-                .filter(stale_participant_filter.clone())
-                .all(&*tx)
-                .await?
-                .into_iter()
-                .map(|participant| participant.user_id)
-                .collect::<Vec<_>>();
-
-            // Delete participants who failed to reconnect and cancel their calls.
-            let mut canceled_calls_to_user_ids = Vec::new();
-            room_participant::Entity::delete_many()
-                .filter(stale_participant_filter)
-                .exec(&*tx)
-                .await?;
-            let called_participants = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::CallingUserId
-                                .is_in(stale_participant_user_ids.iter().copied()),
-                        )
-                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
-                )
-                .all(&*tx)
-                .await?;
-            room_participant::Entity::delete_many()
-                .filter(
-                    room_participant::Column::Id
-                        .is_in(called_participants.iter().map(|participant| participant.id)),
-                )
-                .exec(&*tx)
-                .await?;
-            canceled_calls_to_user_ids.extend(
-                called_participants
-                    .into_iter()
-                    .map(|participant| participant.user_id),
-            );
-
-            let room = self.get_room(room_id, &tx).await?;
-            // Delete the room if it becomes empty.
-            if room.participants.is_empty() {
-                project::Entity::delete_many()
-                    .filter(project::Column::RoomId.eq(room_id))
-                    .exec(&*tx)
-                    .await?;
-                room::Entity::delete_by_id(room_id).exec(&*tx).await?;
-            }
-
-            Ok(RefreshedRoom {
-                room,
-                stale_participant_user_ids,
-                canceled_calls_to_user_ids,
-            })
-        })
-        .await
-    }
-
-    pub async fn delete_stale_servers(
-        &self,
-        environment: &str,
-        new_server_id: ServerId,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            server::Entity::delete_many()
-                .filter(
-                    Condition::all()
-                        .add(server::Column::Environment.eq(environment))
-                        .add(server::Column::Id.ne(new_server_id)),
-                )
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    async fn stale_server_ids(
-        &self,
-        environment: &str,
-        new_server_id: ServerId,
-        tx: &DatabaseTransaction,
-    ) -> Result<Vec<ServerId>> {
-        let stale_servers = server::Entity::find()
-            .filter(
-                Condition::all()
-                    .add(server::Column::Environment.eq(environment))
-                    .add(server::Column::Id.ne(new_server_id)),
-            )
-            .all(&*tx)
-            .await?;
-        Ok(stale_servers.into_iter().map(|server| server.id).collect())
-    }
-
-    // users
-
-    pub async fn create_user(
-        &self,
-        email_address: &str,
-        admin: bool,
-        params: NewUserParams,
-    ) -> Result<NewUserResult> {
-        self.transaction(|tx| async {
-            let tx = tx;
-            let user = user::Entity::insert(user::ActiveModel {
-                email_address: ActiveValue::set(Some(email_address.into())),
-                github_login: ActiveValue::set(params.github_login.clone()),
-                github_user_id: ActiveValue::set(Some(params.github_user_id)),
-                admin: ActiveValue::set(admin),
-                metrics_id: ActiveValue::set(Uuid::new_v4()),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(user::Column::GithubLogin)
-                    .update_column(user::Column::GithubLogin)
-                    .to_owned(),
-            )
-            .exec_with_returning(&*tx)
-            .await?;
-
-            Ok(NewUserResult {
-                user_id: user.id,
-                metrics_id: user.metrics_id.to_string(),
-                signup_device_id: None,
-                inviting_user_id: None,
-            })
-        })
-        .await
-    }
-
-    pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
-        self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
-            .await
-    }
-
-    pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
-        self.transaction(|tx| async {
-            let tx = tx;
-            Ok(user::Entity::find()
-                .filter(user::Column::Id.is_in(ids.iter().copied()))
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
-        self.transaction(|tx| async move {
-            Ok(user::Entity::find()
-                .filter(user::Column::GithubLogin.eq(github_login))
-                .one(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn get_or_create_user_by_github_account(
-        &self,
-        github_login: &str,
-        github_user_id: Option<i32>,
-        github_email: Option<&str>,
-    ) -> Result<Option<User>> {
-        self.transaction(|tx| async move {
-            let tx = &*tx;
-            if let Some(github_user_id) = github_user_id {
-                if let Some(user_by_github_user_id) = user::Entity::find()
-                    .filter(user::Column::GithubUserId.eq(github_user_id))
-                    .one(tx)
-                    .await?
-                {
-                    let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
-                    user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
-                    Ok(Some(user_by_github_user_id.update(tx).await?))
-                } else if let Some(user_by_github_login) = user::Entity::find()
-                    .filter(user::Column::GithubLogin.eq(github_login))
-                    .one(tx)
-                    .await?
-                {
-                    let mut user_by_github_login = user_by_github_login.into_active_model();
-                    user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
-                    Ok(Some(user_by_github_login.update(tx).await?))
-                } else {
-                    let user = user::Entity::insert(user::ActiveModel {
-                        email_address: ActiveValue::set(github_email.map(|email| email.into())),
-                        github_login: ActiveValue::set(github_login.into()),
-                        github_user_id: ActiveValue::set(Some(github_user_id)),
-                        admin: ActiveValue::set(false),
-                        invite_count: ActiveValue::set(0),
-                        invite_code: ActiveValue::set(None),
-                        metrics_id: ActiveValue::set(Uuid::new_v4()),
-                        ..Default::default()
-                    })
-                    .exec_with_returning(&*tx)
-                    .await?;
-                    Ok(Some(user))
-                }
-            } else {
-                Ok(user::Entity::find()
-                    .filter(user::Column::GithubLogin.eq(github_login))
-                    .one(tx)
-                    .await?)
-            }
-        })
-        .await
-    }
-
-    pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
-        self.transaction(|tx| async move {
-            Ok(user::Entity::find()
-                .order_by_asc(user::Column::GithubLogin)
-                .limit(limit as u64)
-                .offset(page as u64 * limit as u64)
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn get_users_with_no_invites(
-        &self,
-        invited_by_another_user: bool,
-    ) -> Result<Vec<User>> {
-        self.transaction(|tx| async move {
-            Ok(user::Entity::find()
-                .filter(
-                    user::Column::InviteCount
-                        .eq(0)
-                        .and(if invited_by_another_user {
-                            user::Column::InviterId.is_not_null()
-                        } else {
-                            user::Column::InviterId.is_null()
-                        }),
-                )
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
-        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-        enum QueryAs {
-            MetricsId,
-        }
-
-        self.transaction(|tx| async move {
-            let metrics_id: Uuid = user::Entity::find_by_id(id)
-                .select_only()
-                .column(user::Column::MetricsId)
-                .into_values::<_, QueryAs>()
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("could not find user"))?;
-            Ok(metrics_id.to_string())
-        })
-        .await
-    }
-
-    pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> {
-        self.transaction(|tx| async move {
-            user::Entity::update_many()
-                .filter(user::Column::Id.eq(id))
-                .set(user::ActiveModel {
-                    admin: ActiveValue::set(is_admin),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
-        self.transaction(|tx| async move {
-            user::Entity::update_many()
-                .filter(user::Column::Id.eq(id))
-                .set(user::ActiveModel {
-                    connected_once: ActiveValue::set(connected_once),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn destroy_user(&self, id: UserId) -> Result<()> {
-        self.transaction(|tx| async move {
-            access_token::Entity::delete_many()
-                .filter(access_token::Column::UserId.eq(id))
-                .exec(&*tx)
-                .await?;
-            user::Entity::delete_by_id(id).exec(&*tx).await?;
-            Ok(())
-        })
-        .await
-    }
-
-    // contacts
-
-    pub async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
-        #[derive(Debug, FromQueryResult)]
-        struct ContactWithUserBusyStatuses {
-            user_id_a: UserId,
-            user_id_b: UserId,
-            a_to_b: bool,
-            accepted: bool,
-            should_notify: bool,
-            user_a_busy: bool,
-            user_b_busy: bool,
-        }
-
-        self.transaction(|tx| async move {
-            let user_a_participant = Alias::new("user_a_participant");
-            let user_b_participant = Alias::new("user_b_participant");
-            let mut db_contacts = contact::Entity::find()
-                .column_as(
-                    Expr::tbl(user_a_participant.clone(), room_participant::Column::Id)
-                        .is_not_null(),
-                    "user_a_busy",
-                )
-                .column_as(
-                    Expr::tbl(user_b_participant.clone(), room_participant::Column::Id)
-                        .is_not_null(),
-                    "user_b_busy",
-                )
-                .filter(
-                    contact::Column::UserIdA
-                        .eq(user_id)
-                        .or(contact::Column::UserIdB.eq(user_id)),
-                )
-                .join_as(
-                    JoinType::LeftJoin,
-                    contact::Relation::UserARoomParticipant.def(),
-                    user_a_participant,
-                )
-                .join_as(
-                    JoinType::LeftJoin,
-                    contact::Relation::UserBRoomParticipant.def(),
-                    user_b_participant,
-                )
-                .into_model::<ContactWithUserBusyStatuses>()
-                .stream(&*tx)
-                .await?;
-
-            let mut contacts = Vec::new();
-            while let Some(db_contact) = db_contacts.next().await {
-                let db_contact = db_contact?;
-                if db_contact.user_id_a == user_id {
-                    if db_contact.accepted {
-                        contacts.push(Contact::Accepted {
-                            user_id: db_contact.user_id_b,
-                            should_notify: db_contact.should_notify && db_contact.a_to_b,
-                            busy: db_contact.user_b_busy,
-                        });
-                    } else if db_contact.a_to_b {
-                        contacts.push(Contact::Outgoing {
-                            user_id: db_contact.user_id_b,
-                        })
-                    } else {
-                        contacts.push(Contact::Incoming {
-                            user_id: db_contact.user_id_b,
-                            should_notify: db_contact.should_notify,
-                        });
-                    }
-                } else if db_contact.accepted {
-                    contacts.push(Contact::Accepted {
-                        user_id: db_contact.user_id_a,
-                        should_notify: db_contact.should_notify && !db_contact.a_to_b,
-                        busy: db_contact.user_a_busy,
-                    });
-                } else if db_contact.a_to_b {
-                    contacts.push(Contact::Incoming {
-                        user_id: db_contact.user_id_a,
-                        should_notify: db_contact.should_notify,
-                    });
-                } else {
-                    contacts.push(Contact::Outgoing {
-                        user_id: db_contact.user_id_a,
-                    });
-                }
-            }
-
-            contacts.sort_unstable_by_key(|contact| contact.user_id());
-
-            Ok(contacts)
-        })
-        .await
-    }
-
-    pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
-        self.transaction(|tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(room_participant::Column::UserId.eq(user_id))
-                .one(&*tx)
-                .await?;
-            Ok(participant.is_some())
-        })
-        .await
-    }
-
-    pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result<bool> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b) = if user_id_1 < user_id_2 {
-                (user_id_1, user_id_2)
-            } else {
-                (user_id_2, user_id_1)
-            };
-
-            Ok(contact::Entity::find()
-                .filter(
-                    contact::Column::UserIdA
-                        .eq(id_a)
-                        .and(contact::Column::UserIdB.eq(id_b))
-                        .and(contact::Column::Accepted.eq(true)),
-                )
-                .one(&*tx)
-                .await?
-                .is_some())
-        })
-        .await
-    }
-
-    pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b, a_to_b) = if sender_id < receiver_id {
-                (sender_id, receiver_id, true)
-            } else {
-                (receiver_id, sender_id, false)
-            };
-
-            let rows_affected = contact::Entity::insert(contact::ActiveModel {
-                user_id_a: ActiveValue::set(id_a),
-                user_id_b: ActiveValue::set(id_b),
-                a_to_b: ActiveValue::set(a_to_b),
-                accepted: ActiveValue::set(false),
-                should_notify: ActiveValue::set(true),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB])
-                    .values([
-                        (contact::Column::Accepted, true.into()),
-                        (contact::Column::ShouldNotify, false.into()),
-                    ])
-                    .action_and_where(
-                        contact::Column::Accepted.eq(false).and(
-                            contact::Column::AToB
-                                .eq(a_to_b)
-                                .and(contact::Column::UserIdA.eq(id_b))
-                                .or(contact::Column::AToB
-                                    .ne(a_to_b)
-                                    .and(contact::Column::UserIdA.eq(id_a))),
-                        ),
-                    )
-                    .to_owned(),
-            )
-            .exec_without_returning(&*tx)
-            .await?;
-
-            if rows_affected == 1 {
-                Ok(())
-            } else {
-                Err(anyhow!("contact already requested"))?
-            }
-        })
-        .await
-    }
-
-    /// Returns a bool indicating whether the removed contact had originally accepted or not
-    ///
-    /// Deletes the contact identified by the requester and responder ids, and then returns
-    /// whether the deleted contact had originally accepted or was a pending contact request.
-    ///
-    /// # Arguments
-    ///
-    /// * `requester_id` - The user that initiates this request
-    /// * `responder_id` - The user that will be removed
-    pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<bool> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b) = if responder_id < requester_id {
-                (responder_id, requester_id)
-            } else {
-                (requester_id, responder_id)
-            };
-
-            let contact = contact::Entity::find()
-                .filter(
-                    contact::Column::UserIdA
-                        .eq(id_a)
-                        .and(contact::Column::UserIdB.eq(id_b)),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such contact"))?;
-
-            contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
-            Ok(contact.accepted)
-        })
-        .await
-    }
-
-    pub async fn dismiss_contact_notification(
-        &self,
-        user_id: UserId,
-        contact_user_id: UserId,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b, a_to_b) = if user_id < contact_user_id {
-                (user_id, contact_user_id, true)
-            } else {
-                (contact_user_id, user_id, false)
-            };
-
-            let result = contact::Entity::update_many()
-                .set(contact::ActiveModel {
-                    should_notify: ActiveValue::set(false),
-                    ..Default::default()
-                })
-                .filter(
-                    contact::Column::UserIdA
-                        .eq(id_a)
-                        .and(contact::Column::UserIdB.eq(id_b))
-                        .and(
-                            contact::Column::AToB
-                                .eq(a_to_b)
-                                .and(contact::Column::Accepted.eq(true))
-                                .or(contact::Column::AToB
-                                    .ne(a_to_b)
-                                    .and(contact::Column::Accepted.eq(false))),
-                        ),
-                )
-                .exec(&*tx)
-                .await?;
-            if result.rows_affected == 0 {
-                Err(anyhow!("no such contact request"))?
-            } else {
-                Ok(())
-            }
-        })
-        .await
-    }
-
-    pub async fn respond_to_contact_request(
-        &self,
-        responder_id: UserId,
-        requester_id: UserId,
-        accept: bool,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b, a_to_b) = if responder_id < requester_id {
-                (responder_id, requester_id, false)
-            } else {
-                (requester_id, responder_id, true)
-            };
-            let rows_affected = if accept {
-                let result = contact::Entity::update_many()
-                    .set(contact::ActiveModel {
-                        accepted: ActiveValue::set(true),
-                        should_notify: ActiveValue::set(true),
-                        ..Default::default()
-                    })
-                    .filter(
-                        contact::Column::UserIdA
-                            .eq(id_a)
-                            .and(contact::Column::UserIdB.eq(id_b))
-                            .and(contact::Column::AToB.eq(a_to_b)),
-                    )
-                    .exec(&*tx)
-                    .await?;
-                result.rows_affected
-            } else {
-                let result = contact::Entity::delete_many()
-                    .filter(
-                        contact::Column::UserIdA
-                            .eq(id_a)
-                            .and(contact::Column::UserIdB.eq(id_b))
-                            .and(contact::Column::AToB.eq(a_to_b))
-                            .and(contact::Column::Accepted.eq(false)),
-                    )
-                    .exec(&*tx)
-                    .await?;
-
-                result.rows_affected
-            };
-
-            if rows_affected == 1 {
-                Ok(())
-            } else {
-                Err(anyhow!("no such contact request"))?
-            }
-        })
-        .await
-    }
-
-    pub fn fuzzy_like_string(string: &str) -> String {
-        let mut result = String::with_capacity(string.len() * 2 + 1);
-        for c in string.chars() {
-            if c.is_alphanumeric() {
-                result.push('%');
-                result.push(c);
-            }
-        }
-        result.push('%');
-        result
-    }
-
-    pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
-        self.transaction(|tx| async {
-            let tx = tx;
-            let like_string = Self::fuzzy_like_string(name_query);
-            let query = "
-                SELECT users.*
-                FROM users
-                WHERE github_login ILIKE $1
-                ORDER BY github_login <-> $2
-                LIMIT $3
-            ";
-
-            Ok(user::Entity::find()
-                .from_raw_sql(Statement::from_sql_and_values(
-                    self.pool.get_database_backend(),
-                    query.into(),
-                    vec![like_string.into(), name_query.into(), limit.into()],
-                ))
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    // signups
-
-    pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> {
-        self.transaction(|tx| async move {
-            signup::Entity::insert(signup::ActiveModel {
-                email_address: ActiveValue::set(signup.email_address.clone()),
-                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
-                email_confirmation_sent: ActiveValue::set(false),
-                platform_mac: ActiveValue::set(signup.platform_mac),
-                platform_windows: ActiveValue::set(signup.platform_windows),
-                platform_linux: ActiveValue::set(signup.platform_linux),
-                platform_unknown: ActiveValue::set(false),
-                editor_features: ActiveValue::set(Some(signup.editor_features.clone())),
-                programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())),
-                device_id: ActiveValue::set(signup.device_id.clone()),
-                added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(signup::Column::EmailAddress)
-                    .update_columns([
-                        signup::Column::PlatformMac,
-                        signup::Column::PlatformWindows,
-                        signup::Column::PlatformLinux,
-                        signup::Column::EditorFeatures,
-                        signup::Column::ProgrammingLanguages,
-                        signup::Column::DeviceId,
-                        signup::Column::AddedToMailingList,
-                    ])
-                    .to_owned(),
-            )
-            .exec(&*tx)
-            .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_signup(&self, email_address: &str) -> Result<signup::Model> {
-        self.transaction(|tx| async move {
-            let signup = signup::Entity::find()
-                .filter(signup::Column::EmailAddress.eq(email_address))
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| {
-                    anyhow!("signup with email address {} doesn't exist", email_address)
-                })?;
-
-            Ok(signup)
-        })
-        .await
-    }
-
-    pub async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
-        self.transaction(|tx| async move {
-            let query = "
-                SELECT
-                    COUNT(*) as count,
-                    COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
-                    COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
-                    COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
-                    COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
-                FROM (
-                    SELECT *
-                    FROM signups
-                    WHERE
-                        NOT email_confirmation_sent
-                ) AS unsent
-            ";
-            Ok(
-                WaitlistSummary::find_by_statement(Statement::from_sql_and_values(
-                    self.pool.get_database_backend(),
-                    query.into(),
-                    vec![],
-                ))
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("invalid result"))?,
-            )
-        })
-        .await
-    }
-
-    pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> {
-        let emails = invites
-            .iter()
-            .map(|s| s.email_address.as_str())
-            .collect::<Vec<_>>();
-        self.transaction(|tx| async {
-            let tx = tx;
-            signup::Entity::update_many()
-                .filter(signup::Column::EmailAddress.is_in(emails.iter().copied()))
-                .set(signup::ActiveModel {
-                    email_confirmation_sent: ActiveValue::set(true),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>> {
-        self.transaction(|tx| async move {
-            Ok(signup::Entity::find()
-                .select_only()
-                .column(signup::Column::EmailAddress)
-                .column(signup::Column::EmailConfirmationCode)
-                .filter(
-                    signup::Column::EmailConfirmationSent.eq(false).and(
-                        signup::Column::PlatformMac
-                            .eq(true)
-                            .or(signup::Column::PlatformUnknown.eq(true)),
-                    ),
-                )
-                .order_by_asc(signup::Column::CreatedAt)
-                .limit(count as u64)
-                .into_model()
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    // invite codes
-
-    pub async fn create_invite_from_code(
-        &self,
-        code: &str,
-        email_address: &str,
-        device_id: Option<&str>,
-        added_to_mailing_list: bool,
-    ) -> Result<Invite> {
-        self.transaction(|tx| async move {
-            let existing_user = user::Entity::find()
-                .filter(user::Column::EmailAddress.eq(email_address))
-                .one(&*tx)
-                .await?;
-
-            if existing_user.is_some() {
-                Err(anyhow!("email address is already in use"))?;
-            }
-
-            let inviting_user_with_invites = match user::Entity::find()
-                .filter(
-                    user::Column::InviteCode
-                        .eq(code)
-                        .and(user::Column::InviteCount.gt(0)),
-                )
-                .one(&*tx)
-                .await?
-            {
-                Some(inviting_user) => inviting_user,
-                None => {
-                    return Err(Error::Http(
-                        StatusCode::UNAUTHORIZED,
-                        "unable to find an invite code with invites remaining".to_string(),
-                    ))?
-                }
-            };
-            user::Entity::update_many()
-                .filter(
-                    user::Column::Id
-                        .eq(inviting_user_with_invites.id)
-                        .and(user::Column::InviteCount.gt(0)),
-                )
-                .col_expr(
-                    user::Column::InviteCount,
-                    Expr::col(user::Column::InviteCount).sub(1),
-                )
-                .exec(&*tx)
-                .await?;
-
-            let signup = signup::Entity::insert(signup::ActiveModel {
-                email_address: ActiveValue::set(email_address.into()),
-                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
-                email_confirmation_sent: ActiveValue::set(false),
-                inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)),
-                platform_linux: ActiveValue::set(false),
-                platform_mac: ActiveValue::set(false),
-                platform_windows: ActiveValue::set(false),
-                platform_unknown: ActiveValue::set(true),
-                device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())),
-                added_to_mailing_list: ActiveValue::set(added_to_mailing_list),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(signup::Column::EmailAddress)
-                    .update_column(signup::Column::InvitingUserId)
-                    .to_owned(),
-            )
-            .exec_with_returning(&*tx)
-            .await?;
-
-            Ok(Invite {
-                email_address: signup.email_address,
-                email_confirmation_code: signup.email_confirmation_code,
-            })
-        })
-        .await
-    }
-
-    pub async fn create_user_from_invite(
-        &self,
-        invite: &Invite,
-        user: NewUserParams,
-    ) -> Result<Option<NewUserResult>> {
-        self.transaction(|tx| async {
-            let tx = tx;
-            let signup = signup::Entity::find()
-                .filter(
-                    signup::Column::EmailAddress
-                        .eq(invite.email_address.as_str())
-                        .and(
-                            signup::Column::EmailConfirmationCode
-                                .eq(invite.email_confirmation_code.as_str()),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
-
-            if signup.user_id.is_some() {
-                return Ok(None);
-            }
-
-            let user = user::Entity::insert(user::ActiveModel {
-                email_address: ActiveValue::set(Some(invite.email_address.clone())),
-                github_login: ActiveValue::set(user.github_login.clone()),
-                github_user_id: ActiveValue::set(Some(user.github_user_id)),
-                admin: ActiveValue::set(false),
-                invite_count: ActiveValue::set(user.invite_count),
-                invite_code: ActiveValue::set(Some(random_invite_code())),
-                metrics_id: ActiveValue::set(Uuid::new_v4()),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(user::Column::GithubLogin)
-                    .update_columns([
-                        user::Column::EmailAddress,
-                        user::Column::GithubUserId,
-                        user::Column::Admin,
-                    ])
-                    .to_owned(),
-            )
-            .exec_with_returning(&*tx)
-            .await?;
-
-            let mut signup = signup.into_active_model();
-            signup.user_id = ActiveValue::set(Some(user.id));
-            let signup = signup.update(&*tx).await?;
-
-            if let Some(inviting_user_id) = signup.inviting_user_id {
-                let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id {
-                    (inviting_user_id, user.id, true)
-                } else {
-                    (user.id, inviting_user_id, false)
-                };
-
-                contact::Entity::insert(contact::ActiveModel {
-                    user_id_a: ActiveValue::set(user_id_a),
-                    user_id_b: ActiveValue::set(user_id_b),
-                    a_to_b: ActiveValue::set(a_to_b),
-                    should_notify: ActiveValue::set(true),
-                    accepted: ActiveValue::set(true),
-                    ..Default::default()
-                })
-                .on_conflict(OnConflict::new().do_nothing().to_owned())
-                .exec_without_returning(&*tx)
-                .await?;
-            }
-
-            Ok(Some(NewUserResult {
-                user_id: user.id,
-                metrics_id: user.metrics_id.to_string(),
-                inviting_user_id: signup.inviting_user_id,
-                signup_device_id: signup.device_id,
-            }))
-        })
-        .await
-    }
-
-    pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> {
-        self.transaction(|tx| async move {
-            if count > 0 {
-                user::Entity::update_many()
-                    .filter(
-                        user::Column::Id
-                            .eq(id)
-                            .and(user::Column::InviteCode.is_null()),
-                    )
-                    .set(user::ActiveModel {
-                        invite_code: ActiveValue::set(Some(random_invite_code())),
-                        ..Default::default()
-                    })
-                    .exec(&*tx)
-                    .await?;
-            }
-
-            user::Entity::update_many()
-                .filter(user::Column::Id.eq(id))
-                .set(user::ActiveModel {
-                    invite_count: ActiveValue::set(count),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_invite_code_for_user(&self, id: UserId) -> Result<Option<(String, i32)>> {
-        self.transaction(|tx| async move {
-            match user::Entity::find_by_id(id).one(&*tx).await? {
-                Some(user) if user.invite_code.is_some() => {
-                    Ok(Some((user.invite_code.unwrap(), user.invite_count)))
-                }
-                _ => Ok(None),
-            }
-        })
-        .await
-    }
-
-    pub async fn get_user_for_invite_code(&self, code: &str) -> Result<User> {
-        self.transaction(|tx| async move {
-            user::Entity::find()
-                .filter(user::Column::InviteCode.eq(code))
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| {
-                    Error::Http(
-                        StatusCode::NOT_FOUND,
-                        "that invite code does not exist".to_string(),
-                    )
-                })
-        })
-        .await
-    }
-
-    // rooms
-
-    pub async fn incoming_call_for_user(
-        &self,
-        user_id: UserId,
-    ) -> Result<Option<proto::IncomingCall>> {
-        self.transaction(|tx| async move {
-            let pending_participant = room_participant::Entity::find()
-                .filter(
-                    room_participant::Column::UserId
-                        .eq(user_id)
-                        .and(room_participant::Column::AnsweringConnectionId.is_null()),
-                )
-                .one(&*tx)
-                .await?;
-
-            if let Some(pending_participant) = pending_participant {
-                let room = self.get_room(pending_participant.room_id, &tx).await?;
-                Ok(Self::build_incoming_call(&room, user_id))
-            } else {
-                Ok(None)
-            }
-        })
-        .await
-    }
-
-    pub async fn create_room(
-        &self,
-        user_id: UserId,
-        connection: ConnectionId,
-        live_kit_room: &str,
-    ) -> Result<proto::Room> {
-        self.transaction(|tx| async move {
-            let room = room::ActiveModel {
-                live_kit_room: ActiveValue::set(live_kit_room.into()),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-            room_participant::ActiveModel {
-                room_id: ActiveValue::set(room.id),
-                user_id: ActiveValue::set(user_id),
-                answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                answering_connection_server_id: ActiveValue::set(Some(ServerId(
-                    connection.owner_id as i32,
-                ))),
-                answering_connection_lost: ActiveValue::set(false),
-                calling_user_id: ActiveValue::set(user_id),
-                calling_connection_id: ActiveValue::set(connection.id as i32),
-                calling_connection_server_id: ActiveValue::set(Some(ServerId(
-                    connection.owner_id as i32,
-                ))),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            let room = self.get_room(room.id, &tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn call(
-        &self,
-        room_id: RoomId,
-        calling_user_id: UserId,
-        calling_connection: ConnectionId,
-        called_user_id: UserId,
-        initial_project_id: Option<ProjectId>,
-    ) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
-        self.room_transaction(room_id, |tx| async move {
-            room_participant::ActiveModel {
-                room_id: ActiveValue::set(room_id),
-                user_id: ActiveValue::set(called_user_id),
-                answering_connection_lost: ActiveValue::set(false),
-                calling_user_id: ActiveValue::set(calling_user_id),
-                calling_connection_id: ActiveValue::set(calling_connection.id as i32),
-                calling_connection_server_id: ActiveValue::set(Some(ServerId(
-                    calling_connection.owner_id as i32,
-                ))),
-                initial_project_id: ActiveValue::set(initial_project_id),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            let room = self.get_room(room_id, &tx).await?;
-            let incoming_call = Self::build_incoming_call(&room, called_user_id)
-                .ok_or_else(|| anyhow!("failed to build incoming call"))?;
-            Ok((room, incoming_call))
-        })
-        .await
-    }
-
-    pub async fn call_failed(
-        &self,
-        room_id: RoomId,
-        called_user_id: UserId,
-    ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(room_id, |tx| async move {
-            room_participant::Entity::delete_many()
-                .filter(
-                    room_participant::Column::RoomId
-                        .eq(room_id)
-                        .and(room_participant::Column::UserId.eq(called_user_id)),
-                )
-                .exec(&*tx)
-                .await?;
-            let room = self.get_room(room_id, &tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn decline_call(
-        &self,
-        expected_room_id: Option<RoomId>,
-        user_id: UserId,
-    ) -> Result<Option<RoomGuard<proto::Room>>> {
-        self.optional_room_transaction(|tx| async move {
-            let mut filter = Condition::all()
-                .add(room_participant::Column::UserId.eq(user_id))
-                .add(room_participant::Column::AnsweringConnectionId.is_null());
-            if let Some(room_id) = expected_room_id {
-                filter = filter.add(room_participant::Column::RoomId.eq(room_id));
-            }
-            let participant = room_participant::Entity::find()
-                .filter(filter)
-                .one(&*tx)
-                .await?;
-
-            let participant = if let Some(participant) = participant {
-                participant
-            } else if expected_room_id.is_some() {
-                return Err(anyhow!("could not find call to decline"))?;
-            } else {
-                return Ok(None);
-            };
-
-            let room_id = participant.room_id;
-            room_participant::Entity::delete(participant.into_active_model())
-                .exec(&*tx)
-                .await?;
-
-            let room = self.get_room(room_id, &tx).await?;
-            Ok(Some((room_id, room)))
-        })
-        .await
-    }
-
-    pub async fn cancel_call(
-        &self,
-        room_id: RoomId,
-        calling_connection: ConnectionId,
-        called_user_id: UserId,
-    ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(room_id, |tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(room_participant::Column::UserId.eq(called_user_id))
-                        .add(room_participant::Column::RoomId.eq(room_id))
-                        .add(
-                            room_participant::Column::CallingConnectionId
-                                .eq(calling_connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::CallingConnectionServerId
-                                .eq(calling_connection.owner_id as i32),
-                        )
-                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no call to cancel"))?;
-
-            room_participant::Entity::delete(participant.into_active_model())
-                .exec(&*tx)
-                .await?;
-
-            let room = self.get_room(room_id, &tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn join_room(
-        &self,
-        room_id: RoomId,
-        user_id: UserId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(room_id, |tx| async move {
-            let result = room_participant::Entity::update_many()
-                .filter(
-                    Condition::all()
-                        .add(room_participant::Column::RoomId.eq(room_id))
-                        .add(room_participant::Column::UserId.eq(user_id))
-                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
-                )
-                .set(room_participant::ActiveModel {
-                    answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                    answering_connection_server_id: ActiveValue::set(Some(ServerId(
-                        connection.owner_id as i32,
-                    ))),
-                    answering_connection_lost: ActiveValue::set(false),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            if result.rows_affected == 0 {
-                Err(anyhow!("room does not exist or was already joined"))?
-            } else {
-                let room = self.get_room(room_id, &tx).await?;
-                Ok(room)
-            }
-        })
-        .await
-    }
-
-    pub async fn rejoin_room(
-        &self,
-        rejoin_room: proto::RejoinRoom,
-        user_id: UserId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<RejoinedRoom>> {
-        let room_id = RoomId::from_proto(rejoin_room.id);
-        self.room_transaction(room_id, |tx| async {
-            let tx = tx;
-            let participant_update = room_participant::Entity::update_many()
-                .filter(
-                    Condition::all()
-                        .add(room_participant::Column::RoomId.eq(room_id))
-                        .add(room_participant::Column::UserId.eq(user_id))
-                        .add(room_participant::Column::AnsweringConnectionId.is_not_null())
-                        .add(
-                            Condition::any()
-                                .add(room_participant::Column::AnsweringConnectionLost.eq(true))
-                                .add(
-                                    room_participant::Column::AnsweringConnectionServerId
-                                        .ne(connection.owner_id as i32),
-                                ),
-                        ),
-                )
-                .set(room_participant::ActiveModel {
-                    answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                    answering_connection_server_id: ActiveValue::set(Some(ServerId(
-                        connection.owner_id as i32,
-                    ))),
-                    answering_connection_lost: ActiveValue::set(false),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            if participant_update.rows_affected == 0 {
-                return Err(anyhow!("room does not exist or was already joined"))?;
-            }
-
-            let mut reshared_projects = Vec::new();
-            for reshared_project in &rejoin_room.reshared_projects {
-                let project_id = ProjectId::from_proto(reshared_project.project_id);
-                let project = project::Entity::find_by_id(project_id)
-                    .one(&*tx)
-                    .await?
-                    .ok_or_else(|| anyhow!("project does not exist"))?;
-                if project.host_user_id != user_id {
-                    return Err(anyhow!("no such project"))?;
-                }
-
-                let mut collaborators = project
-                    .find_related(project_collaborator::Entity)
-                    .all(&*tx)
-                    .await?;
-                let host_ix = collaborators
-                    .iter()
-                    .position(|collaborator| {
-                        collaborator.user_id == user_id && collaborator.is_host
-                    })
-                    .ok_or_else(|| anyhow!("host not found among collaborators"))?;
-                let host = collaborators.swap_remove(host_ix);
-                let old_connection_id = host.connection();
-
-                project::Entity::update(project::ActiveModel {
-                    host_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                    host_connection_server_id: ActiveValue::set(Some(ServerId(
-                        connection.owner_id as i32,
-                    ))),
-                    ..project.into_active_model()
-                })
-                .exec(&*tx)
-                .await?;
-                project_collaborator::Entity::update(project_collaborator::ActiveModel {
-                    connection_id: ActiveValue::set(connection.id as i32),
-                    connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
-                    ..host.into_active_model()
-                })
-                .exec(&*tx)
-                .await?;
-
-                self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
-                    .await?;
-
-                reshared_projects.push(ResharedProject {
-                    id: project_id,
-                    old_connection_id,
-                    collaborators: collaborators
-                        .iter()
-                        .map(|collaborator| ProjectCollaborator {
-                            connection_id: collaborator.connection(),
-                            user_id: collaborator.user_id,
-                            replica_id: collaborator.replica_id,
-                            is_host: collaborator.is_host,
-                        })
-                        .collect(),
-                    worktrees: reshared_project.worktrees.clone(),
-                });
-            }
-
-            project::Entity::delete_many()
-                .filter(
-                    Condition::all()
-                        .add(project::Column::RoomId.eq(room_id))
-                        .add(project::Column::HostUserId.eq(user_id))
-                        .add(
-                            project::Column::Id
-                                .is_not_in(reshared_projects.iter().map(|project| project.id)),
-                        ),
-                )
-                .exec(&*tx)
-                .await?;
-
-            let mut rejoined_projects = Vec::new();
-            for rejoined_project in &rejoin_room.rejoined_projects {
-                let project_id = ProjectId::from_proto(rejoined_project.id);
-                let Some(project) = project::Entity::find_by_id(project_id)
-                    .one(&*tx)
-                    .await? else { continue };
-
-                let mut worktrees = Vec::new();
-                let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
-                for db_worktree in db_worktrees {
-                    let mut worktree = RejoinedWorktree {
-                        id: db_worktree.id as u64,
-                        abs_path: db_worktree.abs_path,
-                        root_name: db_worktree.root_name,
-                        visible: db_worktree.visible,
-                        updated_entries: Default::default(),
-                        removed_entries: Default::default(),
-                        updated_repositories: Default::default(),
-                        removed_repositories: Default::default(),
-                        diagnostic_summaries: Default::default(),
-                        settings_files: Default::default(),
-                        scan_id: db_worktree.scan_id as u64,
-                        completed_scan_id: db_worktree.completed_scan_id as u64,
-                    };
-
-                    let rejoined_worktree = rejoined_project
-                        .worktrees
-                        .iter()
-                        .find(|worktree| worktree.id == db_worktree.id as u64);
-
-                    // File entries
-                    {
-                        let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
-                            worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
-                        } else {
-                            worktree_entry::Column::IsDeleted.eq(false)
-                        };
-
-                        let mut db_entries = worktree_entry::Entity::find()
-                            .filter(
-                                Condition::all()
-                                    .add(worktree_entry::Column::ProjectId.eq(project.id))
-                                    .add(worktree_entry::Column::WorktreeId.eq(worktree.id))
-                                    .add(entry_filter),
-                            )
-                            .stream(&*tx)
-                            .await?;
-
-                        while let Some(db_entry) = db_entries.next().await {
-                            let db_entry = db_entry?;
-                            if db_entry.is_deleted {
-                                worktree.removed_entries.push(db_entry.id as u64);
-                            } else {
-                                worktree.updated_entries.push(proto::Entry {
-                                    id: db_entry.id as u64,
-                                    is_dir: db_entry.is_dir,
-                                    path: db_entry.path,
-                                    inode: db_entry.inode as u64,
-                                    mtime: Some(proto::Timestamp {
-                                        seconds: db_entry.mtime_seconds as u64,
-                                        nanos: db_entry.mtime_nanos as u32,
-                                    }),
-                                    is_symlink: db_entry.is_symlink,
-                                    is_ignored: db_entry.is_ignored,
-                                    is_external: db_entry.is_external,
-                                    git_status: db_entry.git_status.map(|status| status as i32),
-                                });
-                            }
-                        }
-                    }
-
-                    // Repository Entries
-                    {
-                        let repository_entry_filter =
-                            if let Some(rejoined_worktree) = rejoined_worktree {
-                                worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
-                            } else {
-                                worktree_repository::Column::IsDeleted.eq(false)
-                            };
-
-                        let mut db_repositories = worktree_repository::Entity::find()
-                            .filter(
-                                Condition::all()
-                                    .add(worktree_repository::Column::ProjectId.eq(project.id))
-                                    .add(worktree_repository::Column::WorktreeId.eq(worktree.id))
-                                    .add(repository_entry_filter),
-                            )
-                            .stream(&*tx)
-                            .await?;
-
-                        while let Some(db_repository) = db_repositories.next().await {
-                            let db_repository = db_repository?;
-                            if db_repository.is_deleted {
-                                worktree
-                                    .removed_repositories
-                                    .push(db_repository.work_directory_id as u64);
-                            } else {
-                                worktree.updated_repositories.push(proto::RepositoryEntry {
-                                    work_directory_id: db_repository.work_directory_id as u64,
-                                    branch: db_repository.branch,
-                                });
-                            }
-                        }
-                    }
-
-                    worktrees.push(worktree);
-                }
-
-                let language_servers = project
-                    .find_related(language_server::Entity)
-                    .all(&*tx)
-                    .await?
-                    .into_iter()
-                    .map(|language_server| proto::LanguageServer {
-                        id: language_server.id as u64,
-                        name: language_server.name,
-                    })
-                    .collect::<Vec<_>>();
-
-                {
-                    let mut db_settings_files = worktree_settings_file::Entity::find()
-                        .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
-                        .stream(&*tx)
-                        .await?;
-                    while let Some(db_settings_file) = db_settings_files.next().await {
-                        let db_settings_file = db_settings_file?;
-                        if let Some(worktree) = worktrees
-                            .iter_mut()
-                            .find(|w| w.id == db_settings_file.worktree_id as u64)
-                        {
-                            worktree.settings_files.push(WorktreeSettingsFile {
-                                path: db_settings_file.path,
-                                content: db_settings_file.content,
-                            });
-                        }
-                    }
-                }
-
-                let mut collaborators = project
-                    .find_related(project_collaborator::Entity)
-                    .all(&*tx)
-                    .await?;
-                let self_collaborator = if let Some(self_collaborator_ix) = collaborators
-                    .iter()
-                    .position(|collaborator| collaborator.user_id == user_id)
-                {
-                    collaborators.swap_remove(self_collaborator_ix)
-                } else {
-                    continue;
-                };
-                let old_connection_id = self_collaborator.connection();
-                project_collaborator::Entity::update(project_collaborator::ActiveModel {
-                    connection_id: ActiveValue::set(connection.id as i32),
-                    connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
-                    ..self_collaborator.into_active_model()
-                })
-                .exec(&*tx)
-                .await?;
-
-                let collaborators = collaborators
-                    .into_iter()
-                    .map(|collaborator| ProjectCollaborator {
-                        connection_id: collaborator.connection(),
-                        user_id: collaborator.user_id,
-                        replica_id: collaborator.replica_id,
-                        is_host: collaborator.is_host,
-                    })
-                    .collect::<Vec<_>>();
-
-                rejoined_projects.push(RejoinedProject {
-                    id: project_id,
-                    old_connection_id,
-                    collaborators,
-                    worktrees,
-                    language_servers,
-                });
-            }
-
-            let room = self.get_room(room_id, &tx).await?;
-            Ok(RejoinedRoom {
-                room,
-                rejoined_projects,
-                reshared_projects,
-            })
-        })
-        .await
-    }
-
-    pub async fn leave_room(
-        &self,
-        connection: ConnectionId,
-    ) -> Result<Option<RoomGuard<LeftRoom>>> {
-        self.optional_room_transaction(|tx| async move {
-            let leaving_participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?;
-
-            if let Some(leaving_participant) = leaving_participant {
-                // Leave room.
-                let room_id = leaving_participant.room_id;
-                room_participant::Entity::delete_by_id(leaving_participant.id)
-                    .exec(&*tx)
-                    .await?;
-
-                // Cancel pending calls initiated by the leaving user.
-                let called_participants = room_participant::Entity::find()
-                    .filter(
-                        Condition::all()
-                            .add(
-                                room_participant::Column::CallingUserId
-                                    .eq(leaving_participant.user_id),
-                            )
-                            .add(room_participant::Column::AnsweringConnectionId.is_null()),
-                    )
-                    .all(&*tx)
-                    .await?;
-                room_participant::Entity::delete_many()
-                    .filter(
-                        room_participant::Column::Id
-                            .is_in(called_participants.iter().map(|participant| participant.id)),
-                    )
-                    .exec(&*tx)
-                    .await?;
-                let canceled_calls_to_user_ids = called_participants
-                    .into_iter()
-                    .map(|participant| participant.user_id)
-                    .collect();
-
-                // Detect left projects.
-                #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-                enum QueryProjectIds {
-                    ProjectId,
-                }
-                let project_ids: Vec<ProjectId> = project_collaborator::Entity::find()
-                    .select_only()
-                    .column_as(
-                        project_collaborator::Column::ProjectId,
-                        QueryProjectIds::ProjectId,
-                    )
-                    .filter(
-                        Condition::all()
-                            .add(
-                                project_collaborator::Column::ConnectionId.eq(connection.id as i32),
-                            )
-                            .add(
-                                project_collaborator::Column::ConnectionServerId
-                                    .eq(connection.owner_id as i32),
-                            ),
-                    )
-                    .into_values::<_, QueryProjectIds>()
-                    .all(&*tx)
-                    .await?;
-                let mut left_projects = HashMap::default();
-                let mut collaborators = project_collaborator::Entity::find()
-                    .filter(project_collaborator::Column::ProjectId.is_in(project_ids))
-                    .stream(&*tx)
-                    .await?;
-                while let Some(collaborator) = collaborators.next().await {
-                    let collaborator = collaborator?;
-                    let left_project =
-                        left_projects
-                            .entry(collaborator.project_id)
-                            .or_insert(LeftProject {
-                                id: collaborator.project_id,
-                                host_user_id: Default::default(),
-                                connection_ids: Default::default(),
-                                host_connection_id: Default::default(),
-                            });
-
-                    let collaborator_connection_id = collaborator.connection();
-                    if collaborator_connection_id != connection {
-                        left_project.connection_ids.push(collaborator_connection_id);
-                    }
-
-                    if collaborator.is_host {
-                        left_project.host_user_id = collaborator.user_id;
-                        left_project.host_connection_id = collaborator_connection_id;
-                    }
-                }
-                drop(collaborators);
-
-                // Leave projects.
-                project_collaborator::Entity::delete_many()
-                    .filter(
-                        Condition::all()
-                            .add(
-                                project_collaborator::Column::ConnectionId.eq(connection.id as i32),
-                            )
-                            .add(
-                                project_collaborator::Column::ConnectionServerId
-                                    .eq(connection.owner_id as i32),
-                            ),
-                    )
-                    .exec(&*tx)
-                    .await?;
-
-                // Unshare projects.
-                project::Entity::delete_many()
-                    .filter(
-                        Condition::all()
-                            .add(project::Column::RoomId.eq(room_id))
-                            .add(project::Column::HostConnectionId.eq(connection.id as i32))
-                            .add(
-                                project::Column::HostConnectionServerId
-                                    .eq(connection.owner_id as i32),
-                            ),
-                    )
-                    .exec(&*tx)
-                    .await?;
-
-                let room = self.get_room(room_id, &tx).await?;
-                if room.participants.is_empty() {
-                    room::Entity::delete_by_id(room_id).exec(&*tx).await?;
-                }
-
-                let left_room = LeftRoom {
-                    room,
-                    left_projects,
-                    canceled_calls_to_user_ids,
-                };
-
-                if left_room.room.participants.is_empty() {
-                    self.rooms.remove(&room_id);
-                }
-
-                Ok(Some((room_id, left_room)))
-            } else {
-                Ok(None)
-            }
-        })
-        .await
-    }
-
-    pub async fn follow(
-        &self,
-        project_id: ProjectId,
-        leader_connection: ConnectionId,
-        follower_connection: ConnectionId,
-    ) -> Result<RoomGuard<proto::Room>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            follower::ActiveModel {
-                room_id: ActiveValue::set(room_id),
-                project_id: ActiveValue::set(project_id),
-                leader_connection_server_id: ActiveValue::set(ServerId(
-                    leader_connection.owner_id as i32,
-                )),
-                leader_connection_id: ActiveValue::set(leader_connection.id as i32),
-                follower_connection_server_id: ActiveValue::set(ServerId(
-                    follower_connection.owner_id as i32,
-                )),
-                follower_connection_id: ActiveValue::set(follower_connection.id as i32),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            let room = self.get_room(room_id, &*tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn unfollow(
-        &self,
-        project_id: ProjectId,
-        leader_connection: ConnectionId,
-        follower_connection: ConnectionId,
-    ) -> Result<RoomGuard<proto::Room>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            follower::Entity::delete_many()
-                .filter(
-                    Condition::all()
-                        .add(follower::Column::ProjectId.eq(project_id))
-                        .add(
-                            follower::Column::LeaderConnectionServerId
-                                .eq(leader_connection.owner_id),
-                        )
-                        .add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
-                        .add(
-                            follower::Column::FollowerConnectionServerId
-                                .eq(follower_connection.owner_id),
-                        )
-                        .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
-                )
-                .exec(&*tx)
-                .await?;
-
-            let room = self.get_room(room_id, &*tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn update_room_participant_location(
-        &self,
-        room_id: RoomId,
-        connection: ConnectionId,
-        location: proto::ParticipantLocation,
-    ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(room_id, |tx| async {
-            let tx = tx;
-            let location_kind;
-            let location_project_id;
-            match location
-                .variant
-                .as_ref()
-                .ok_or_else(|| anyhow!("invalid location"))?
-            {
-                proto::participant_location::Variant::SharedProject(project) => {
-                    location_kind = 0;
-                    location_project_id = Some(ProjectId::from_proto(project.id));
-                }
-                proto::participant_location::Variant::UnsharedProject(_) => {
-                    location_kind = 1;
-                    location_project_id = None;
-                }
-                proto::participant_location::Variant::External(_) => {
-                    location_kind = 2;
-                    location_project_id = None;
-                }
-            }
-
-            let result = room_participant::Entity::update_many()
-                .filter(
-                    Condition::all()
-                        .add(room_participant::Column::RoomId.eq(room_id))
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .set(room_participant::ActiveModel {
-                    location_kind: ActiveValue::set(Some(location_kind)),
-                    location_project_id: ActiveValue::set(location_project_id),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-
-            if result.rows_affected == 1 {
-                let room = self.get_room(room_id, &tx).await?;
-                Ok(room)
-            } else {
-                Err(anyhow!("could not update room participant location"))?
-            }
-        })
-        .await
-    }
-
-    pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
-        self.transaction(|tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("not a participant in any room"))?;
-
-            room_participant::Entity::update(room_participant::ActiveModel {
-                answering_connection_lost: ActiveValue::set(true),
-                ..participant.into_active_model()
-            })
-            .exec(&*tx)
-            .await?;
-
-            Ok(())
-        })
-        .await
-    }
-
-    fn build_incoming_call(
-        room: &proto::Room,
-        called_user_id: UserId,
-    ) -> Option<proto::IncomingCall> {
-        let pending_participant = room
-            .pending_participants
-            .iter()
-            .find(|participant| participant.user_id == called_user_id.to_proto())?;
-
-        Some(proto::IncomingCall {
-            room_id: room.id,
-            calling_user_id: pending_participant.calling_user_id,
-            participant_user_ids: room
-                .participants
-                .iter()
-                .map(|participant| participant.user_id)
-                .collect(),
-            initial_project: room.participants.iter().find_map(|participant| {
-                let initial_project_id = pending_participant.initial_project_id?;
-                participant
-                    .projects
-                    .iter()
-                    .find(|project| project.id == initial_project_id)
-                    .cloned()
-            }),
-        })
-    }
-
-    async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result<proto::Room> {
-        let db_room = room::Entity::find_by_id(room_id)
-            .one(tx)
-            .await?
-            .ok_or_else(|| anyhow!("could not find room"))?;
-
-        let mut db_participants = db_room
-            .find_related(room_participant::Entity)
-            .stream(tx)
-            .await?;
-        let mut participants = HashMap::default();
-        let mut pending_participants = Vec::new();
-        while let Some(db_participant) = db_participants.next().await {
-            let db_participant = db_participant?;
-            if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
-                .answering_connection_id
-                .zip(db_participant.answering_connection_server_id)
-            {
-                let location = match (
-                    db_participant.location_kind,
-                    db_participant.location_project_id,
-                ) {
-                    (Some(0), Some(project_id)) => {
-                        Some(proto::participant_location::Variant::SharedProject(
-                            proto::participant_location::SharedProject {
-                                id: project_id.to_proto(),
-                            },
-                        ))
-                    }
-                    (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject(
-                        Default::default(),
-                    )),
-                    _ => Some(proto::participant_location::Variant::External(
-                        Default::default(),
-                    )),
-                };
-
-                let answering_connection = ConnectionId {
-                    owner_id: answering_connection_server_id.0 as u32,
-                    id: answering_connection_id as u32,
-                };
-                participants.insert(
-                    answering_connection,
-                    proto::Participant {
-                        user_id: db_participant.user_id.to_proto(),
-                        peer_id: Some(answering_connection.into()),
-                        projects: Default::default(),
-                        location: Some(proto::ParticipantLocation { variant: location }),
-                    },
-                );
-            } else {
-                pending_participants.push(proto::PendingParticipant {
-                    user_id: db_participant.user_id.to_proto(),
-                    calling_user_id: db_participant.calling_user_id.to_proto(),
-                    initial_project_id: db_participant.initial_project_id.map(|id| id.to_proto()),
-                });
-            }
-        }
-        drop(db_participants);
-
-        let mut db_projects = db_room
-            .find_related(project::Entity)
-            .find_with_related(worktree::Entity)
-            .stream(tx)
-            .await?;
-
-        while let Some(row) = db_projects.next().await {
-            let (db_project, db_worktree) = row?;
-            let host_connection = db_project.host_connection()?;
-            if let Some(participant) = participants.get_mut(&host_connection) {
-                let project = if let Some(project) = participant
-                    .projects
-                    .iter_mut()
-                    .find(|project| project.id == db_project.id.to_proto())
-                {
-                    project
-                } else {
-                    participant.projects.push(proto::ParticipantProject {
-                        id: db_project.id.to_proto(),
-                        worktree_root_names: Default::default(),
-                    });
-                    participant.projects.last_mut().unwrap()
-                };
-
-                if let Some(db_worktree) = db_worktree {
-                    if db_worktree.visible {
-                        project.worktree_root_names.push(db_worktree.root_name);
-                    }
-                }
-            }
-        }
-        drop(db_projects);
-
-        let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
-        let mut followers = Vec::new();
-        while let Some(db_follower) = db_followers.next().await {
-            let db_follower = db_follower?;
-            followers.push(proto::Follower {
-                leader_id: Some(db_follower.leader_connection().into()),
-                follower_id: Some(db_follower.follower_connection().into()),
-                project_id: db_follower.project_id.to_proto(),
-            });
-        }
-
-        Ok(proto::Room {
-            id: db_room.id.to_proto(),
-            live_kit_room: db_room.live_kit_room,
-            participants: participants.into_values().collect(),
-            pending_participants,
-            followers,
-        })
-    }
-
-    // projects
-
-    pub async fn project_count_excluding_admins(&self) -> Result<usize> {
-        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-        enum QueryAs {
-            Count,
-        }
-
-        self.transaction(|tx| async move {
-            Ok(project::Entity::find()
-                .select_only()
-                .column_as(project::Column::Id.count(), QueryAs::Count)
-                .inner_join(user::Entity)
-                .filter(user::Column::Admin.eq(false))
-                .into_values::<_, QueryAs>()
-                .one(&*tx)
-                .await?
-                .unwrap_or(0i64) as usize)
-        })
-        .await
-    }
-
-    pub async fn share_project(
-        &self,
-        room_id: RoomId,
-        connection: ConnectionId,
-        worktrees: &[proto::WorktreeMetadata],
-    ) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
-        self.room_transaction(room_id, |tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("could not find participant"))?;
-            if participant.room_id != room_id {
-                return Err(anyhow!("shared project on unexpected room"))?;
-            }
-
-            let project = project::ActiveModel {
-                room_id: ActiveValue::set(participant.room_id),
-                host_user_id: ActiveValue::set(participant.user_id),
-                host_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                host_connection_server_id: ActiveValue::set(Some(ServerId(
-                    connection.owner_id as i32,
-                ))),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            if !worktrees.is_empty() {
-                worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
-                    worktree::ActiveModel {
-                        id: ActiveValue::set(worktree.id as i64),
-                        project_id: ActiveValue::set(project.id),
-                        abs_path: ActiveValue::set(worktree.abs_path.clone()),
-                        root_name: ActiveValue::set(worktree.root_name.clone()),
-                        visible: ActiveValue::set(worktree.visible),
-                        scan_id: ActiveValue::set(0),
-                        completed_scan_id: ActiveValue::set(0),
-                    }
-                }))
-                .exec(&*tx)
-                .await?;
-            }
-
-            project_collaborator::ActiveModel {
-                project_id: ActiveValue::set(project.id),
-                connection_id: ActiveValue::set(connection.id as i32),
-                connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
-                user_id: ActiveValue::set(participant.user_id),
-                replica_id: ActiveValue::set(ReplicaId(0)),
-                is_host: ActiveValue::set(true),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            let room = self.get_room(room_id, &tx).await?;
-            Ok((project.id, room))
-        })
-        .await
-    }
-
-    pub async fn unshare_project(
-        &self,
-        project_id: ProjectId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("project not found"))?;
-            if project.host_connection()? == connection {
-                project::Entity::delete(project.into_active_model())
-                    .exec(&*tx)
-                    .await?;
-                let room = self.get_room(room_id, &tx).await?;
-                Ok((room, guest_connection_ids))
-            } else {
-                Err(anyhow!("cannot unshare a project hosted by another user"))?
-            }
-        })
-        .await
-    }
-
-    pub async fn update_project(
-        &self,
-        project_id: ProjectId,
-        connection: ConnectionId,
-        worktrees: &[proto::WorktreeMetadata],
-    ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let project = project::Entity::find_by_id(project_id)
-                .filter(
-                    Condition::all()
-                        .add(project::Column::HostConnectionId.eq(connection.id as i32))
-                        .add(
-                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-
-            self.update_project_worktrees(project.id, worktrees, &tx)
-                .await?;
-
-            let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
-            let room = self.get_room(project.room_id, &tx).await?;
-            Ok((room, guest_connection_ids))
-        })
-        .await
-    }
-
-    async fn update_project_worktrees(
-        &self,
-        project_id: ProjectId,
-        worktrees: &[proto::WorktreeMetadata],
-        tx: &DatabaseTransaction,
-    ) -> Result<()> {
-        if !worktrees.is_empty() {
-            worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel {
-                id: ActiveValue::set(worktree.id as i64),
-                project_id: ActiveValue::set(project_id),
-                abs_path: ActiveValue::set(worktree.abs_path.clone()),
-                root_name: ActiveValue::set(worktree.root_name.clone()),
-                visible: ActiveValue::set(worktree.visible),
-                scan_id: ActiveValue::set(0),
-                completed_scan_id: ActiveValue::set(0),
-            }))
-            .on_conflict(
-                OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id])
-                    .update_column(worktree::Column::RootName)
-                    .to_owned(),
-            )
-            .exec(&*tx)
-            .await?;
-        }
-
-        worktree::Entity::delete_many()
-            .filter(worktree::Column::ProjectId.eq(project_id).and(
-                worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)),
-            ))
-            .exec(&*tx)
-            .await?;
-
-        Ok(())
-    }
-
-    pub async fn update_worktree(
-        &self,
-        update: &proto::UpdateWorktree,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        let project_id = ProjectId::from_proto(update.project_id);
-        let worktree_id = update.worktree_id as i64;
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            // Ensure the update comes from the host.
-            let _project = project::Entity::find_by_id(project_id)
-                .filter(
-                    Condition::all()
-                        .add(project::Column::HostConnectionId.eq(connection.id as i32))
-                        .add(
-                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-
-            // Update metadata.
-            worktree::Entity::update(worktree::ActiveModel {
-                id: ActiveValue::set(worktree_id),
-                project_id: ActiveValue::set(project_id),
-                root_name: ActiveValue::set(update.root_name.clone()),
-                scan_id: ActiveValue::set(update.scan_id as i64),
-                completed_scan_id: if update.is_last_update {
-                    ActiveValue::set(update.scan_id as i64)
-                } else {
-                    ActiveValue::default()
-                },
-                abs_path: ActiveValue::set(update.abs_path.clone()),
-                ..Default::default()
-            })
-            .exec(&*tx)
-            .await?;
-
-            if !update.updated_entries.is_empty() {
-                worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| {
-                    let mtime = entry.mtime.clone().unwrap_or_default();
-                    worktree_entry::ActiveModel {
-                        project_id: ActiveValue::set(project_id),
-                        worktree_id: ActiveValue::set(worktree_id),
-                        id: ActiveValue::set(entry.id as i64),
-                        is_dir: ActiveValue::set(entry.is_dir),
-                        path: ActiveValue::set(entry.path.clone()),
-                        inode: ActiveValue::set(entry.inode as i64),
-                        mtime_seconds: ActiveValue::set(mtime.seconds as i64),
-                        mtime_nanos: ActiveValue::set(mtime.nanos as i32),
-                        is_symlink: ActiveValue::set(entry.is_symlink),
-                        is_ignored: ActiveValue::set(entry.is_ignored),
-                        is_external: ActiveValue::set(entry.is_external),
-                        git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
-                        is_deleted: ActiveValue::set(false),
-                        scan_id: ActiveValue::set(update.scan_id as i64),
-                    }
-                }))
-                .on_conflict(
-                    OnConflict::columns([
-                        worktree_entry::Column::ProjectId,
-                        worktree_entry::Column::WorktreeId,
-                        worktree_entry::Column::Id,
-                    ])
-                    .update_columns([
-                        worktree_entry::Column::IsDir,
-                        worktree_entry::Column::Path,
-                        worktree_entry::Column::Inode,
-                        worktree_entry::Column::MtimeSeconds,
-                        worktree_entry::Column::MtimeNanos,
-                        worktree_entry::Column::IsSymlink,
-                        worktree_entry::Column::IsIgnored,
-                        worktree_entry::Column::GitStatus,
-                        worktree_entry::Column::ScanId,
-                    ])
-                    .to_owned(),
-                )
-                .exec(&*tx)
-                .await?;
-            }
-
-            if !update.removed_entries.is_empty() {
-                worktree_entry::Entity::update_many()
-                    .filter(
-                        worktree_entry::Column::ProjectId
-                            .eq(project_id)
-                            .and(worktree_entry::Column::WorktreeId.eq(worktree_id))
-                            .and(
-                                worktree_entry::Column::Id
-                                    .is_in(update.removed_entries.iter().map(|id| *id as i64)),
-                            ),
-                    )
-                    .set(worktree_entry::ActiveModel {
-                        is_deleted: ActiveValue::Set(true),
-                        scan_id: ActiveValue::Set(update.scan_id as i64),
-                        ..Default::default()
-                    })
-                    .exec(&*tx)
-                    .await?;
-            }
-
-            if !update.updated_repositories.is_empty() {
-                worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
-                    |repository| worktree_repository::ActiveModel {
-                        project_id: ActiveValue::set(project_id),
-                        worktree_id: ActiveValue::set(worktree_id),
-                        work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
-                        scan_id: ActiveValue::set(update.scan_id as i64),
-                        branch: ActiveValue::set(repository.branch.clone()),
-                        is_deleted: ActiveValue::set(false),
-                    },
-                ))
-                .on_conflict(
-                    OnConflict::columns([
-                        worktree_repository::Column::ProjectId,
-                        worktree_repository::Column::WorktreeId,
-                        worktree_repository::Column::WorkDirectoryId,
-                    ])
-                    .update_columns([
-                        worktree_repository::Column::ScanId,
-                        worktree_repository::Column::Branch,
-                    ])
-                    .to_owned(),
-                )
-                .exec(&*tx)
-                .await?;
-            }
-
-            if !update.removed_repositories.is_empty() {
-                worktree_repository::Entity::update_many()
-                    .filter(
-                        worktree_repository::Column::ProjectId
-                            .eq(project_id)
-                            .and(worktree_repository::Column::WorktreeId.eq(worktree_id))
-                            .and(
-                                worktree_repository::Column::WorkDirectoryId
-                                    .is_in(update.removed_repositories.iter().map(|id| *id as i64)),
-                            ),
-                    )
-                    .set(worktree_repository::ActiveModel {
-                        is_deleted: ActiveValue::Set(true),
-                        scan_id: ActiveValue::Set(update.scan_id as i64),
-                        ..Default::default()
-                    })
-                    .exec(&*tx)
-                    .await?;
-            }
-
-            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok(connection_ids)
-        })
-        .await
-    }
-
-    pub async fn update_diagnostic_summary(
-        &self,
-        update: &proto::UpdateDiagnosticSummary,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        let project_id = ProjectId::from_proto(update.project_id);
-        let worktree_id = update.worktree_id as i64;
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let summary = update
-                .summary
-                .as_ref()
-                .ok_or_else(|| anyhow!("invalid summary"))?;
-
-            // Ensure the update comes from the host.
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            if project.host_connection()? != connection {
-                return Err(anyhow!("can't update a project hosted by someone else"))?;
-            }
-
-            // Update summary.
-            worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel {
-                project_id: ActiveValue::set(project_id),
-                worktree_id: ActiveValue::set(worktree_id),
-                path: ActiveValue::set(summary.path.clone()),
-                language_server_id: ActiveValue::set(summary.language_server_id as i64),
-                error_count: ActiveValue::set(summary.error_count as i32),
-                warning_count: ActiveValue::set(summary.warning_count as i32),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::columns([
-                    worktree_diagnostic_summary::Column::ProjectId,
-                    worktree_diagnostic_summary::Column::WorktreeId,
-                    worktree_diagnostic_summary::Column::Path,
-                ])
-                .update_columns([
-                    worktree_diagnostic_summary::Column::LanguageServerId,
-                    worktree_diagnostic_summary::Column::ErrorCount,
-                    worktree_diagnostic_summary::Column::WarningCount,
-                ])
-                .to_owned(),
-            )
-            .exec(&*tx)
-            .await?;
+#[cfg(test)]
+pub use tests::TestDb;
 
-            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok(connection_ids)
-        })
-        .await
-    }
+mod ids;
+mod queries;
+mod tables;
 
-    pub async fn start_language_server(
-        &self,
-        update: &proto::StartLanguageServer,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        let project_id = ProjectId::from_proto(update.project_id);
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let server = update
-                .server
-                .as_ref()
-                .ok_or_else(|| anyhow!("invalid language server"))?;
+use crate::{executor::Executor, Error, Result};
+use anyhow::anyhow;
+use collections::{BTreeMap, HashMap, HashSet};
+use dashmap::DashMap;
+use futures::StreamExt;
+use rand::{prelude::StdRng, Rng, SeedableRng};
+use rpc::{proto, ConnectionId};
+use sea_orm::{
+    entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection,
+    DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType,
+    QueryOrder, QuerySelect, Statement, TransactionTrait,
+};
+use sea_query::{Alias, Expr, OnConflict, Query};
+use serde::{Deserialize, Serialize};
+use sqlx::{
+    migrate::{Migrate, Migration, MigrationSource},
+    Connection,
+};
+use std::{
+    fmt::Write as _,
+    future::Future,
+    marker::PhantomData,
+    ops::{Deref, DerefMut},
+    path::Path,
+    rc::Rc,
+    sync::Arc,
+    time::Duration,
+};
+use tables::*;
+use tokio::sync::{Mutex, OwnedMutexGuard};
 
-            // Ensure the update comes from the host.
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            if project.host_connection()? != connection {
-                return Err(anyhow!("can't update a project hosted by someone else"))?;
-            }
+pub use ids::*;
+pub use sea_orm::ConnectOptions;
+pub use tables::user::Model as User;
 
-            // Add the newly-started language server.
-            language_server::Entity::insert(language_server::ActiveModel {
-                project_id: ActiveValue::set(project_id),
-                id: ActiveValue::set(server.id as i64),
-                name: ActiveValue::set(server.name.clone()),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::columns([
-                    language_server::Column::ProjectId,
-                    language_server::Column::Id,
-                ])
-                .update_column(language_server::Column::Name)
-                .to_owned(),
-            )
-            .exec(&*tx)
-            .await?;
+pub struct Database {
+    options: ConnectOptions,
+    pool: DatabaseConnection,
+    rooms: DashMap<RoomId, Arc<Mutex<()>>>,
+    rng: Mutex<StdRng>,
+    executor: Executor,
+    #[cfg(test)]
+    runtime: Option<tokio::runtime::Runtime>,
+}
 
-            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok(connection_ids)
+// The `Database` type has so many methods that its impl blocks are split into
+// separate files in the `queries` folder.
+impl Database {
+    pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
+        Ok(Self {
+            options: options.clone(),
+            pool: sea_orm::Database::connect(options).await?,
+            rooms: DashMap::with_capacity(16384),
+            rng: Mutex::new(StdRng::seed_from_u64(0)),
+            executor,
+            #[cfg(test)]
+            runtime: None,
         })
-        .await
     }
 
-    pub async fn update_worktree_settings(
-        &self,
-        update: &proto::UpdateWorktreeSettings,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        let project_id = ProjectId::from_proto(update.project_id);
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            // Ensure the update comes from the host.
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            if project.host_connection()? != connection {
-                return Err(anyhow!("can't update a project hosted by someone else"))?;
-            }
-
-            if let Some(content) = &update.content {
-                worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
-                    project_id: ActiveValue::Set(project_id),
-                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
-                    path: ActiveValue::Set(update.path.clone()),
-                    content: ActiveValue::Set(content.clone()),
-                })
-                .on_conflict(
-                    OnConflict::columns([
-                        worktree_settings_file::Column::ProjectId,
-                        worktree_settings_file::Column::WorktreeId,
-                        worktree_settings_file::Column::Path,
-                    ])
-                    .update_column(worktree_settings_file::Column::Content)
-                    .to_owned(),
-                )
-                .exec(&*tx)
-                .await?;
-            } else {
-                worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
-                    project_id: ActiveValue::Set(project_id),
-                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
-                    path: ActiveValue::Set(update.path.clone()),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            }
-
-            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok(connection_ids)
-        })
-        .await
+    #[cfg(test)]
+    pub fn reset(&self) {
+        self.rooms.clear();
     }
 
-    pub async fn join_project(
+    pub async fn migrate(
         &self,
-        project_id: ProjectId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<(Project, ReplicaId)>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("must join a room first"))?;
-
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            if project.room_id != participant.room_id {
-                return Err(anyhow!("no such project"))?;
-            }
-
-            let mut collaborators = project
-                .find_related(project_collaborator::Entity)
-                .all(&*tx)
-                .await?;
-            let replica_ids = collaborators
-                .iter()
-                .map(|c| c.replica_id)
-                .collect::<HashSet<_>>();
-            let mut replica_id = ReplicaId(1);
-            while replica_ids.contains(&replica_id) {
-                replica_id.0 += 1;
-            }
-            let new_collaborator = project_collaborator::ActiveModel {
-                project_id: ActiveValue::set(project_id),
-                connection_id: ActiveValue::set(connection.id as i32),
-                connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
-                user_id: ActiveValue::set(participant.user_id),
-                replica_id: ActiveValue::set(replica_id),
-                is_host: ActiveValue::set(false),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-            collaborators.push(new_collaborator);
+        migrations_path: &Path,
+        ignore_checksum_mismatch: bool,
+    ) -> anyhow::Result<Vec<(Migration, Duration)>> {
+        let migrations = MigrationSource::resolve(migrations_path)
+            .await
+            .map_err(|err| anyhow!("failed to load migrations: {err:?}"))?;
 
-            let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
-            let mut worktrees = db_worktrees
-                .into_iter()
-                .map(|db_worktree| {
-                    (
-                        db_worktree.id as u64,
-                        Worktree {
-                            id: db_worktree.id as u64,
-                            abs_path: db_worktree.abs_path,
-                            root_name: db_worktree.root_name,
-                            visible: db_worktree.visible,
-                            entries: Default::default(),
-                            repository_entries: Default::default(),
-                            diagnostic_summaries: Default::default(),
-                            settings_files: Default::default(),
-                            scan_id: db_worktree.scan_id as u64,
-                            completed_scan_id: db_worktree.completed_scan_id as u64,
-                        },
-                    )
-                })
-                .collect::<BTreeMap<_, _>>();
+        let mut connection = sqlx::AnyConnection::connect(self.options.get_url()).await?;
 
-            // Populate worktree entries.
-            {
-                let mut db_entries = worktree_entry::Entity::find()
-                    .filter(
-                        Condition::all()
-                            .add(worktree_entry::Column::ProjectId.eq(project_id))
-                            .add(worktree_entry::Column::IsDeleted.eq(false)),
-                    )
-                    .stream(&*tx)
-                    .await?;
-                while let Some(db_entry) = db_entries.next().await {
-                    let db_entry = db_entry?;
-                    if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
-                        worktree.entries.push(proto::Entry {
-                            id: db_entry.id as u64,
-                            is_dir: db_entry.is_dir,
-                            path: db_entry.path,
-                            inode: db_entry.inode as u64,
-                            mtime: Some(proto::Timestamp {
-                                seconds: db_entry.mtime_seconds as u64,
-                                nanos: db_entry.mtime_nanos as u32,
-                            }),
-                            is_symlink: db_entry.is_symlink,
-                            is_ignored: db_entry.is_ignored,
-                            is_external: db_entry.is_external,
-                            git_status: db_entry.git_status.map(|status| status as i32),
-                        });
-                    }
-                }
-            }
+        connection.ensure_migrations_table().await?;
+        let applied_migrations: HashMap<_, _> = connection
+            .list_applied_migrations()
+            .await?
+            .into_iter()
+            .map(|m| (m.version, m))
+            .collect();
 
-            // Populate repository entries.
-            {
-                let mut db_repository_entries = worktree_repository::Entity::find()
-                    .filter(
-                        Condition::all()
-                            .add(worktree_repository::Column::ProjectId.eq(project_id))
-                            .add(worktree_repository::Column::IsDeleted.eq(false)),
-                    )
-                    .stream(&*tx)
-                    .await?;
-                while let Some(db_repository_entry) = db_repository_entries.next().await {
-                    let db_repository_entry = db_repository_entry?;
-                    if let Some(worktree) =
-                        worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
+        let mut new_migrations = Vec::new();
+        for migration in migrations {
+            match applied_migrations.get(&migration.version) {
+                Some(applied_migration) => {
+                    if migration.checksum != applied_migration.checksum && !ignore_checksum_mismatch
                     {
-                        worktree.repository_entries.insert(
-                            db_repository_entry.work_directory_id as u64,
-                            proto::RepositoryEntry {
-                                work_directory_id: db_repository_entry.work_directory_id as u64,
-                                branch: db_repository_entry.branch,
-                            },
-                        );
-                    }
-                }
-            }
-
-            // Populate worktree diagnostic summaries.
-            {
-                let mut db_summaries = worktree_diagnostic_summary::Entity::find()
-                    .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id))
-                    .stream(&*tx)
-                    .await?;
-                while let Some(db_summary) = db_summaries.next().await {
-                    let db_summary = db_summary?;
-                    if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
-                        worktree
-                            .diagnostic_summaries
-                            .push(proto::DiagnosticSummary {
-                                path: db_summary.path,
-                                language_server_id: db_summary.language_server_id as u64,
-                                error_count: db_summary.error_count as u32,
-                                warning_count: db_summary.warning_count as u32,
-                            });
+                        Err(anyhow!(
+                            "checksum mismatch for applied migration {}",
+                            migration.description
+                        ))?;
                     }
                 }
-            }
-
-            // Populate worktree settings files
-            {
-                let mut db_settings_files = worktree_settings_file::Entity::find()
-                    .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
-                    .stream(&*tx)
-                    .await?;
-                while let Some(db_settings_file) = db_settings_files.next().await {
-                    let db_settings_file = db_settings_file?;
-                    if let Some(worktree) =
-                        worktrees.get_mut(&(db_settings_file.worktree_id as u64))
-                    {
-                        worktree.settings_files.push(WorktreeSettingsFile {
-                            path: db_settings_file.path,
-                            content: db_settings_file.content,
-                        });
-                    }
+                None => {
+                    let elapsed = connection.apply(&migration).await?;
+                    new_migrations.push((migration, elapsed));
                 }
             }
-
-            // Populate language servers.
-            let language_servers = project
-                .find_related(language_server::Entity)
-                .all(&*tx)
-                .await?;
-
-            let project = Project {
-                collaborators: collaborators
-                    .into_iter()
-                    .map(|collaborator| ProjectCollaborator {
-                        connection_id: collaborator.connection(),
-                        user_id: collaborator.user_id,
-                        replica_id: collaborator.replica_id,
-                        is_host: collaborator.is_host,
-                    })
-                    .collect(),
-                worktrees,
-                language_servers: language_servers
-                    .into_iter()
-                    .map(|language_server| proto::LanguageServer {
-                        id: language_server.id as u64,
-                        name: language_server.name,
-                    })
-                    .collect(),
-            };
-            Ok((project, replica_id as ReplicaId))
-        })
-        .await
-    }
-
-    pub async fn leave_project(
-        &self,
-        project_id: ProjectId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let result = project_collaborator::Entity::delete_many()
-                .filter(
-                    Condition::all()
-                        .add(project_collaborator::Column::ProjectId.eq(project_id))
-                        .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
-                        .add(
-                            project_collaborator::Column::ConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .exec(&*tx)
-                .await?;
-            if result.rows_affected == 0 {
-                Err(anyhow!("not a collaborator on this project"))?;
-            }
-
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            let collaborators = project
-                .find_related(project_collaborator::Entity)
-                .all(&*tx)
-                .await?;
-            let connection_ids = collaborators
-                .into_iter()
-                .map(|collaborator| collaborator.connection())
-                .collect();
-
-            follower::Entity::delete_many()
-                .filter(
-                    Condition::any()
-                        .add(
-                            Condition::all()
-                                .add(follower::Column::ProjectId.eq(project_id))
-                                .add(
-                                    follower::Column::LeaderConnectionServerId
-                                        .eq(connection.owner_id),
-                                )
-                                .add(follower::Column::LeaderConnectionId.eq(connection.id)),
-                        )
-                        .add(
-                            Condition::all()
-                                .add(follower::Column::ProjectId.eq(project_id))
-                                .add(
-                                    follower::Column::FollowerConnectionServerId
-                                        .eq(connection.owner_id),
-                                )
-                                .add(follower::Column::FollowerConnectionId.eq(connection.id)),
-                        ),
-                )
-                .exec(&*tx)
-                .await?;
-
-            let room = self.get_room(project.room_id, &tx).await?;
-            let left_project = LeftProject {
-                id: project_id,
-                host_user_id: project.host_user_id,
-                host_connection_id: project.host_connection()?,
-                connection_ids,
-            };
-            Ok((room, left_project))
-        })
-        .await
-    }
-
-    pub async fn project_collaborators(
-        &self,
-        project_id: ProjectId,
-        connection_id: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let collaborators = project_collaborator::Entity::find()
-                .filter(project_collaborator::Column::ProjectId.eq(project_id))
-                .all(&*tx)
-                .await?
-                .into_iter()
-                .map(|collaborator| ProjectCollaborator {
-                    connection_id: collaborator.connection(),
-                    user_id: collaborator.user_id,
-                    replica_id: collaborator.replica_id,
-                    is_host: collaborator.is_host,
-                })
-                .collect::<Vec<_>>();
-
-            if collaborators
-                .iter()
-                .any(|collaborator| collaborator.connection_id == connection_id)
-            {
-                Ok(collaborators)
-            } else {
-                Err(anyhow!("no such project"))?
-            }
-        })
-        .await
-    }
-
-    pub async fn project_connection_ids(
-        &self,
-        project_id: ProjectId,
-        connection_id: ConnectionId,
-    ) -> Result<RoomGuard<HashSet<ConnectionId>>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let mut collaborators = project_collaborator::Entity::find()
-                .filter(project_collaborator::Column::ProjectId.eq(project_id))
-                .stream(&*tx)
-                .await?;
-
-            let mut connection_ids = HashSet::default();
-            while let Some(collaborator) = collaborators.next().await {
-                let collaborator = collaborator?;
-                connection_ids.insert(collaborator.connection());
-            }
-
-            if connection_ids.contains(&connection_id) {
-                Ok(connection_ids)
-            } else {
-                Err(anyhow!("no such project"))?
-            }
-        })
-        .await
-    }
-
-    async fn project_guest_connection_ids(
-        &self,
-        project_id: ProjectId,
-        tx: &DatabaseTransaction,
-    ) -> Result<Vec<ConnectionId>> {
-        let mut collaborators = project_collaborator::Entity::find()
-            .filter(
-                project_collaborator::Column::ProjectId
-                    .eq(project_id)
-                    .and(project_collaborator::Column::IsHost.eq(false)),
-            )
-            .stream(tx)
-            .await?;
-
-        let mut guest_connection_ids = Vec::new();
-        while let Some(collaborator) = collaborators.next().await {
-            let collaborator = collaborator?;
-            guest_connection_ids.push(collaborator.connection());
         }
-        Ok(guest_connection_ids)
-    }
-
-    async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
-        self.transaction(|tx| async move {
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("project {} not found", project_id))?;
-            Ok(project.room_id)
-        })
-        .await
-    }
-
-    // access tokens
-
-    pub async fn create_access_token(
-        &self,
-        user_id: UserId,
-        access_token_hash: &str,
-        max_access_token_count: usize,
-    ) -> Result<AccessTokenId> {
-        self.transaction(|tx| async {
-            let tx = tx;
 
-            let token = access_token::ActiveModel {
-                user_id: ActiveValue::set(user_id),
-                hash: ActiveValue::set(access_token_hash.into()),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            access_token::Entity::delete_many()
-                .filter(
-                    access_token::Column::Id.in_subquery(
-                        Query::select()
-                            .column(access_token::Column::Id)
-                            .from(access_token::Entity)
-                            .and_where(access_token::Column::UserId.eq(user_id))
-                            .order_by(access_token::Column::Id, sea_orm::Order::Desc)
-                            .limit(10000)
-                            .offset(max_access_token_count as u64)
-                            .to_owned(),
-                    ),
-                )
-                .exec(&*tx)
-                .await?;
-            Ok(token.id)
-        })
-        .await
-    }
-
-    pub async fn get_access_token(
-        &self,
-        access_token_id: AccessTokenId,
-    ) -> Result<access_token::Model> {
-        self.transaction(|tx| async move {
-            Ok(access_token::Entity::find_by_id(access_token_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such access token"))?)
-        })
-        .await
+        Ok(new_migrations)
     }
 
     async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>

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

@@ -0,0 +1,128 @@
+use crate::Result;
+use sea_orm::DbErr;
+use sea_query::{Value, ValueTypeErr};
+use serde::{Deserialize, Serialize};
+
+macro_rules! id_type {
+    ($name:ident) => {
+        #[derive(
+            Clone,
+            Copy,
+            Debug,
+            Default,
+            PartialEq,
+            Eq,
+            PartialOrd,
+            Ord,
+            Hash,
+            Serialize,
+            Deserialize,
+        )]
+        #[serde(transparent)]
+        pub struct $name(pub i32);
+
+        impl $name {
+            #[allow(unused)]
+            pub const MAX: Self = Self(i32::MAX);
+
+            #[allow(unused)]
+            pub fn from_proto(value: u64) -> Self {
+                Self(value as i32)
+            }
+
+            #[allow(unused)]
+            pub fn to_proto(self) -> u64 {
+                self.0 as u64
+            }
+        }
+
+        impl std::fmt::Display for $name {
+            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+                self.0.fmt(f)
+            }
+        }
+
+        impl From<$name> for sea_query::Value {
+            fn from(value: $name) -> Self {
+                sea_query::Value::Int(Some(value.0))
+            }
+        }
+
+        impl sea_orm::TryGetable for $name {
+            fn try_get(
+                res: &sea_orm::QueryResult,
+                pre: &str,
+                col: &str,
+            ) -> Result<Self, sea_orm::TryGetError> {
+                Ok(Self(i32::try_get(res, pre, col)?))
+            }
+        }
+
+        impl sea_query::ValueType for $name {
+            fn try_from(v: Value) -> Result<Self, sea_query::ValueTypeErr> {
+                Ok(Self(value_to_integer(v)?))
+            }
+
+            fn type_name() -> String {
+                stringify!($name).into()
+            }
+
+            fn array_type() -> sea_query::ArrayType {
+                sea_query::ArrayType::Int
+            }
+
+            fn column_type() -> sea_query::ColumnType {
+                sea_query::ColumnType::Integer(None)
+            }
+        }
+
+        impl sea_orm::TryFromU64 for $name {
+            fn try_from_u64(n: u64) -> Result<Self, DbErr> {
+                Ok(Self(n.try_into().map_err(|_| {
+                    DbErr::ConvertFromU64(concat!(
+                        "error converting ",
+                        stringify!($name),
+                        " to u64"
+                    ))
+                })?))
+            }
+        }
+
+        impl sea_query::Nullable for $name {
+            fn null() -> Value {
+                Value::Int(None)
+            }
+        }
+    };
+}
+
+fn value_to_integer(v: Value) -> Result<i32, ValueTypeErr> {
+    match v {
+        Value::TinyInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::SmallInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::Int(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::BigInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::TinyUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::SmallUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::Unsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::BigUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        _ => Err(ValueTypeErr),
+    }
+}
+
+id_type!(BufferId);
+id_type!(AccessTokenId);
+id_type!(ChannelId);
+id_type!(ChannelMemberId);
+id_type!(ContactId);
+id_type!(FollowerId);
+id_type!(RoomId);
+id_type!(RoomParticipantId);
+id_type!(ProjectId);
+id_type!(ProjectCollaboratorId);
+id_type!(ReplicaId);
+id_type!(ServerId);
+id_type!(SignupId);
+id_type!(UserId);
+id_type!(ChannelBufferCollaboratorId);
+id_type!(FlagId);

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

@@ -0,0 +1,11 @@
+use super::*;
+
+pub mod access_tokens;
+pub mod buffers;
+pub mod channels;
+pub mod contacts;
+pub mod projects;
+pub mod rooms;
+pub mod servers;
+pub mod signups;
+pub mod users;

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

@@ -0,0 +1,53 @@
+use super::*;
+
+impl Database {
+    pub async fn create_access_token(
+        &self,
+        user_id: UserId,
+        access_token_hash: &str,
+        max_access_token_count: usize,
+    ) -> Result<AccessTokenId> {
+        self.transaction(|tx| async {
+            let tx = tx;
+
+            let token = access_token::ActiveModel {
+                user_id: ActiveValue::set(user_id),
+                hash: ActiveValue::set(access_token_hash.into()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            access_token::Entity::delete_many()
+                .filter(
+                    access_token::Column::Id.in_subquery(
+                        Query::select()
+                            .column(access_token::Column::Id)
+                            .from(access_token::Entity)
+                            .and_where(access_token::Column::UserId.eq(user_id))
+                            .order_by(access_token::Column::Id, sea_orm::Order::Desc)
+                            .limit(10000)
+                            .offset(max_access_token_count as u64)
+                            .to_owned(),
+                    ),
+                )
+                .exec(&*tx)
+                .await?;
+            Ok(token.id)
+        })
+        .await
+    }
+
+    pub async fn get_access_token(
+        &self,
+        access_token_id: AccessTokenId,
+    ) -> Result<access_token::Model> {
+        self.transaction(|tx| async move {
+            Ok(access_token::Entity::find_by_id(access_token_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such access token"))?)
+        })
+        .await
+    }
+}

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

@@ -0,0 +1,588 @@
+use super::*;
+use prost::Message;
+use text::{EditOperation, InsertionTimestamp, UndoOperation};
+
+impl Database {
+    pub async fn join_channel_buffer(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        connection: ConnectionId,
+    ) -> Result<proto::JoinChannelBufferResponse> {
+        self.transaction(|tx| async move {
+            let tx = tx;
+
+            self.check_user_is_channel_member(channel_id, user_id, &tx)
+                .await?;
+
+            let buffer = channel::Model {
+                id: channel_id,
+                ..Default::default()
+            }
+            .find_related(buffer::Entity)
+            .one(&*tx)
+            .await?;
+
+            let buffer = if let Some(buffer) = buffer {
+                buffer
+            } else {
+                let buffer = buffer::ActiveModel {
+                    channel_id: ActiveValue::Set(channel_id),
+                    ..Default::default()
+                }
+                .insert(&*tx)
+                .await?;
+                buffer_snapshot::ActiveModel {
+                    buffer_id: ActiveValue::Set(buffer.id),
+                    epoch: ActiveValue::Set(0),
+                    text: ActiveValue::Set(String::new()),
+                    operation_serialization_version: ActiveValue::Set(
+                        storage::SERIALIZATION_VERSION,
+                    ),
+                }
+                .insert(&*tx)
+                .await?;
+                buffer
+            };
+
+            // Join the collaborators
+            let mut collaborators = channel_buffer_collaborator::Entity::find()
+                .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
+                .all(&*tx)
+                .await?;
+            let replica_ids = collaborators
+                .iter()
+                .map(|c| c.replica_id)
+                .collect::<HashSet<_>>();
+            let mut replica_id = ReplicaId(0);
+            while replica_ids.contains(&replica_id) {
+                replica_id.0 += 1;
+            }
+            let collaborator = channel_buffer_collaborator::ActiveModel {
+                channel_id: ActiveValue::Set(channel_id),
+                connection_id: ActiveValue::Set(connection.id as i32),
+                connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)),
+                user_id: ActiveValue::Set(user_id),
+                replica_id: ActiveValue::Set(replica_id),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+            collaborators.push(collaborator);
+
+            // Assemble the buffer state
+            let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?;
+
+            Ok(proto::JoinChannelBufferResponse {
+                buffer_id: buffer.id.to_proto(),
+                replica_id: replica_id.to_proto() as u32,
+                base_text,
+                operations,
+                collaborators: collaborators
+                    .into_iter()
+                    .map(|collaborator| proto::Collaborator {
+                        peer_id: Some(collaborator.connection().into()),
+                        user_id: collaborator.user_id.to_proto(),
+                        replica_id: collaborator.replica_id.0 as u32,
+                    })
+                    .collect(),
+            })
+        })
+        .await
+    }
+
+    pub async fn leave_channel_buffer(
+        &self,
+        channel_id: ChannelId,
+        connection: ConnectionId,
+    ) -> Result<Vec<ConnectionId>> {
+        self.transaction(|tx| async move {
+            self.leave_channel_buffer_internal(channel_id, connection, &*tx)
+                .await
+        })
+        .await
+    }
+
+    pub async fn leave_channel_buffer_internal(
+        &self,
+        channel_id: ChannelId,
+        connection: ConnectionId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<ConnectionId>> {
+        let result = channel_buffer_collaborator::Entity::delete_many()
+            .filter(
+                Condition::all()
+                    .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
+                    .add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32))
+                    .add(
+                        channel_buffer_collaborator::Column::ConnectionServerId
+                            .eq(connection.owner_id as i32),
+                    ),
+            )
+            .exec(&*tx)
+            .await?;
+        if result.rows_affected == 0 {
+            Err(anyhow!("not a collaborator on this project"))?;
+        }
+
+        let mut connections = Vec::new();
+        let mut rows = channel_buffer_collaborator::Entity::find()
+            .filter(
+                Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
+            )
+            .stream(&*tx)
+            .await?;
+        while let Some(row) = rows.next().await {
+            let row = row?;
+            connections.push(ConnectionId {
+                id: row.connection_id as u32,
+                owner_id: row.connection_server_id.0 as u32,
+            });
+        }
+
+        drop(rows);
+
+        if connections.is_empty() {
+            self.snapshot_buffer(channel_id, &tx).await?;
+        }
+
+        Ok(connections)
+    }
+
+    pub async fn leave_channel_buffers(
+        &self,
+        connection: ConnectionId,
+    ) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
+        self.transaction(|tx| async move {
+            #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+            enum QueryChannelIds {
+                ChannelId,
+            }
+
+            let channel_ids: Vec<ChannelId> = channel_buffer_collaborator::Entity::find()
+                .select_only()
+                .column(channel_buffer_collaborator::Column::ChannelId)
+                .filter(Condition::all().add(
+                    channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32),
+                ))
+                .into_values::<_, QueryChannelIds>()
+                .all(&*tx)
+                .await?;
+
+            let mut result = Vec::new();
+            for channel_id in channel_ids {
+                let collaborators = self
+                    .leave_channel_buffer_internal(channel_id, connection, &*tx)
+                    .await?;
+                result.push((channel_id, collaborators));
+            }
+
+            Ok(result)
+        })
+        .await
+    }
+
+    #[cfg(debug_assertions)]
+    pub async fn get_channel_buffer_collaborators(
+        &self,
+        channel_id: ChannelId,
+    ) -> Result<Vec<UserId>> {
+        self.transaction(|tx| async move {
+            #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+            enum QueryUserIds {
+                UserId,
+            }
+
+            let users: Vec<UserId> = channel_buffer_collaborator::Entity::find()
+                .select_only()
+                .column(channel_buffer_collaborator::Column::UserId)
+                .filter(
+                    Condition::all()
+                        .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
+                )
+                .into_values::<_, QueryUserIds>()
+                .all(&*tx)
+                .await?;
+
+            Ok(users)
+        })
+        .await
+    }
+
+    pub async fn update_channel_buffer(
+        &self,
+        channel_id: ChannelId,
+        user: UserId,
+        operations: &[proto::Operation],
+    ) -> Result<Vec<ConnectionId>> {
+        self.transaction(move |tx| async move {
+            self.check_user_is_channel_member(channel_id, user, &*tx)
+                .await?;
+
+            let buffer = buffer::Entity::find()
+                .filter(buffer::Column::ChannelId.eq(channel_id))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such buffer"))?;
+
+            #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+            enum QueryVersion {
+                OperationSerializationVersion,
+            }
+
+            let serialization_version: i32 = buffer
+                .find_related(buffer_snapshot::Entity)
+                .select_only()
+                .column(buffer_snapshot::Column::OperationSerializationVersion)
+                .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch))
+                .into_values::<_, QueryVersion>()
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("missing buffer snapshot"))?;
+
+            let operations = operations
+                .iter()
+                .filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
+                .collect::<Vec<_>>();
+            if !operations.is_empty() {
+                buffer_operation::Entity::insert_many(operations)
+                    .exec(&*tx)
+                    .await?;
+            }
+
+            let mut connections = Vec::new();
+            let mut rows = channel_buffer_collaborator::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
+                )
+                .stream(&*tx)
+                .await?;
+            while let Some(row) = rows.next().await {
+                let row = row?;
+                connections.push(ConnectionId {
+                    id: row.connection_id as u32,
+                    owner_id: row.connection_server_id.0 as u32,
+                });
+            }
+
+            Ok(connections)
+        })
+        .await
+    }
+
+    async fn get_buffer_state(
+        &self,
+        buffer: &buffer::Model,
+        tx: &DatabaseTransaction,
+    ) -> Result<(String, Vec<proto::Operation>)> {
+        let id = buffer.id;
+        let (base_text, version) = if buffer.epoch > 0 {
+            let snapshot = buffer_snapshot::Entity::find()
+                .filter(
+                    buffer_snapshot::Column::BufferId
+                        .eq(id)
+                        .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such snapshot"))?;
+
+            let version = snapshot.operation_serialization_version;
+            (snapshot.text, version)
+        } else {
+            (String::new(), storage::SERIALIZATION_VERSION)
+        };
+
+        let mut rows = buffer_operation::Entity::find()
+            .filter(
+                buffer_operation::Column::BufferId
+                    .eq(id)
+                    .and(buffer_operation::Column::Epoch.eq(buffer.epoch)),
+            )
+            .stream(&*tx)
+            .await?;
+        let mut operations = Vec::new();
+        while let Some(row) = rows.next().await {
+            let row = row?;
+
+            let operation = operation_from_storage(row, version)?;
+            operations.push(proto::Operation {
+                variant: Some(operation),
+            })
+        }
+
+        Ok((base_text, operations))
+    }
+
+    async fn snapshot_buffer(&self, channel_id: ChannelId, tx: &DatabaseTransaction) -> Result<()> {
+        let buffer = channel::Model {
+            id: channel_id,
+            ..Default::default()
+        }
+        .find_related(buffer::Entity)
+        .one(&*tx)
+        .await?
+        .ok_or_else(|| anyhow!("no such buffer"))?;
+
+        let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?;
+        if operations.is_empty() {
+            return Ok(());
+        }
+
+        let mut text_buffer = text::Buffer::new(0, 0, base_text);
+        text_buffer
+            .apply_ops(operations.into_iter().filter_map(operation_from_wire))
+            .unwrap();
+
+        let base_text = text_buffer.text();
+        let epoch = buffer.epoch + 1;
+
+        buffer_snapshot::Model {
+            buffer_id: buffer.id,
+            epoch,
+            text: base_text,
+            operation_serialization_version: storage::SERIALIZATION_VERSION,
+        }
+        .into_active_model()
+        .insert(tx)
+        .await?;
+
+        buffer::ActiveModel {
+            id: ActiveValue::Unchanged(buffer.id),
+            epoch: ActiveValue::Set(epoch),
+            ..Default::default()
+        }
+        .save(tx)
+        .await?;
+
+        Ok(())
+    }
+}
+
+fn operation_to_storage(
+    operation: &proto::Operation,
+    buffer: &buffer::Model,
+    _format: i32,
+) -> Option<buffer_operation::ActiveModel> {
+    let (replica_id, lamport_timestamp, value) = match operation.variant.as_ref()? {
+        proto::operation::Variant::Edit(operation) => (
+            operation.replica_id,
+            operation.lamport_timestamp,
+            storage::Operation {
+                local_timestamp: operation.local_timestamp,
+                version: version_to_storage(&operation.version),
+                is_undo: false,
+                edit_ranges: operation
+                    .ranges
+                    .iter()
+                    .map(|range| storage::Range {
+                        start: range.start,
+                        end: range.end,
+                    })
+                    .collect(),
+                edit_texts: operation.new_text.clone(),
+                undo_counts: Vec::new(),
+            },
+        ),
+        proto::operation::Variant::Undo(operation) => (
+            operation.replica_id,
+            operation.lamport_timestamp,
+            storage::Operation {
+                local_timestamp: operation.local_timestamp,
+                version: version_to_storage(&operation.version),
+                is_undo: true,
+                edit_ranges: Vec::new(),
+                edit_texts: Vec::new(),
+                undo_counts: operation
+                    .counts
+                    .iter()
+                    .map(|entry| storage::UndoCount {
+                        replica_id: entry.replica_id,
+                        local_timestamp: entry.local_timestamp,
+                        count: entry.count,
+                    })
+                    .collect(),
+            },
+        ),
+        _ => None?,
+    };
+
+    Some(buffer_operation::ActiveModel {
+        buffer_id: ActiveValue::Set(buffer.id),
+        epoch: ActiveValue::Set(buffer.epoch),
+        replica_id: ActiveValue::Set(replica_id as i32),
+        lamport_timestamp: ActiveValue::Set(lamport_timestamp as i32),
+        value: ActiveValue::Set(value.encode_to_vec()),
+    })
+}
+
+fn operation_from_storage(
+    row: buffer_operation::Model,
+    _format_version: i32,
+) -> Result<proto::operation::Variant, Error> {
+    let operation =
+        storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{}", error))?;
+    let version = version_from_storage(&operation.version);
+    Ok(if operation.is_undo {
+        proto::operation::Variant::Undo(proto::operation::Undo {
+            replica_id: row.replica_id as u32,
+            local_timestamp: operation.local_timestamp as u32,
+            lamport_timestamp: row.lamport_timestamp as u32,
+            version,
+            counts: operation
+                .undo_counts
+                .iter()
+                .map(|entry| proto::UndoCount {
+                    replica_id: entry.replica_id,
+                    local_timestamp: entry.local_timestamp,
+                    count: entry.count,
+                })
+                .collect(),
+        })
+    } else {
+        proto::operation::Variant::Edit(proto::operation::Edit {
+            replica_id: row.replica_id as u32,
+            local_timestamp: operation.local_timestamp as u32,
+            lamport_timestamp: row.lamport_timestamp as u32,
+            version,
+            ranges: operation
+                .edit_ranges
+                .into_iter()
+                .map(|range| proto::Range {
+                    start: range.start,
+                    end: range.end,
+                })
+                .collect(),
+            new_text: operation.edit_texts,
+        })
+    })
+}
+
+fn version_to_storage(version: &Vec<proto::VectorClockEntry>) -> Vec<storage::VectorClockEntry> {
+    version
+        .iter()
+        .map(|entry| storage::VectorClockEntry {
+            replica_id: entry.replica_id,
+            timestamp: entry.timestamp,
+        })
+        .collect()
+}
+
+fn version_from_storage(version: &Vec<storage::VectorClockEntry>) -> Vec<proto::VectorClockEntry> {
+    version
+        .iter()
+        .map(|entry| proto::VectorClockEntry {
+            replica_id: entry.replica_id,
+            timestamp: entry.timestamp,
+        })
+        .collect()
+}
+
+// This is currently a manual copy of the deserialization code in the client's langauge crate
+pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operation> {
+    match operation.variant? {
+        proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation {
+            timestamp: InsertionTimestamp {
+                replica_id: edit.replica_id as text::ReplicaId,
+                local: edit.local_timestamp,
+                lamport: edit.lamport_timestamp,
+            },
+            version: version_from_wire(&edit.version),
+            ranges: edit
+                .ranges
+                .into_iter()
+                .map(|range| {
+                    text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize)
+                })
+                .collect(),
+            new_text: edit.new_text.into_iter().map(Arc::from).collect(),
+        })),
+        proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo {
+            lamport_timestamp: clock::Lamport {
+                replica_id: undo.replica_id as text::ReplicaId,
+                value: undo.lamport_timestamp,
+            },
+            undo: UndoOperation {
+                id: clock::Local {
+                    replica_id: undo.replica_id as text::ReplicaId,
+                    value: undo.local_timestamp,
+                },
+                version: version_from_wire(&undo.version),
+                counts: undo
+                    .counts
+                    .into_iter()
+                    .map(|c| {
+                        (
+                            clock::Local {
+                                replica_id: c.replica_id as text::ReplicaId,
+                                value: c.local_timestamp,
+                            },
+                            c.count,
+                        )
+                    })
+                    .collect(),
+            },
+        }),
+        _ => None,
+    }
+}
+
+fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global {
+    let mut version = clock::Global::new();
+    for entry in message {
+        version.observe(clock::Local {
+            replica_id: entry.replica_id as text::ReplicaId,
+            value: entry.timestamp,
+        });
+    }
+    version
+}
+
+mod storage {
+    #![allow(non_snake_case)]
+    use prost::Message;
+    pub const SERIALIZATION_VERSION: i32 = 1;
+
+    #[derive(Message)]
+    pub struct Operation {
+        #[prost(uint32, tag = "1")]
+        pub local_timestamp: u32,
+        #[prost(message, repeated, tag = "2")]
+        pub version: Vec<VectorClockEntry>,
+        #[prost(bool, tag = "3")]
+        pub is_undo: bool,
+        #[prost(message, repeated, tag = "4")]
+        pub edit_ranges: Vec<Range>,
+        #[prost(string, repeated, tag = "5")]
+        pub edit_texts: Vec<String>,
+        #[prost(message, repeated, tag = "6")]
+        pub undo_counts: Vec<UndoCount>,
+    }
+
+    #[derive(Message)]
+    pub struct VectorClockEntry {
+        #[prost(uint32, tag = "1")]
+        pub replica_id: u32,
+        #[prost(uint32, tag = "2")]
+        pub timestamp: u32,
+    }
+
+    #[derive(Message)]
+    pub struct Range {
+        #[prost(uint64, tag = "1")]
+        pub start: u64,
+        #[prost(uint64, tag = "2")]
+        pub end: u64,
+    }
+
+    #[derive(Message)]
+    pub struct UndoCount {
+        #[prost(uint32, tag = "1")]
+        pub replica_id: u32,
+        #[prost(uint32, tag = "2")]
+        pub local_timestamp: u32,
+        #[prost(uint32, tag = "3")]
+        pub count: u32,
+    }
+}

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

@@ -0,0 +1,697 @@
+use super::*;
+
+impl Database {
+    pub async fn create_root_channel(
+        &self,
+        name: &str,
+        live_kit_room: &str,
+        creator_id: UserId,
+    ) -> Result<ChannelId> {
+        self.create_channel(name, None, live_kit_room, creator_id)
+            .await
+    }
+
+    pub async fn create_channel(
+        &self,
+        name: &str,
+        parent: Option<ChannelId>,
+        live_kit_room: &str,
+        creator_id: UserId,
+    ) -> Result<ChannelId> {
+        let name = Self::sanitize_channel_name(name)?;
+        self.transaction(move |tx| async move {
+            if let Some(parent) = parent {
+                self.check_user_is_channel_admin(parent, creator_id, &*tx)
+                    .await?;
+            }
+
+            let channel = channel::ActiveModel {
+                name: ActiveValue::Set(name.to_string()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let channel_paths_stmt;
+            if let Some(parent) = parent {
+                let sql = r#"
+                    INSERT INTO channel_paths
+                    (id_path, channel_id)
+                    SELECT
+                        id_path || $1 || '/', $2
+                    FROM
+                        channel_paths
+                    WHERE
+                        channel_id = $3
+                "#;
+                channel_paths_stmt = Statement::from_sql_and_values(
+                    self.pool.get_database_backend(),
+                    sql,
+                    [
+                        channel.id.to_proto().into(),
+                        channel.id.to_proto().into(),
+                        parent.to_proto().into(),
+                    ],
+                );
+                tx.execute(channel_paths_stmt).await?;
+            } else {
+                channel_path::Entity::insert(channel_path::ActiveModel {
+                    channel_id: ActiveValue::Set(channel.id),
+                    id_path: ActiveValue::Set(format!("/{}/", channel.id)),
+                })
+                .exec(&*tx)
+                .await?;
+            }
+
+            channel_member::ActiveModel {
+                channel_id: ActiveValue::Set(channel.id),
+                user_id: ActiveValue::Set(creator_id),
+                accepted: ActiveValue::Set(true),
+                admin: ActiveValue::Set(true),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            room::ActiveModel {
+                channel_id: ActiveValue::Set(Some(channel.id)),
+                live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            Ok(channel.id)
+        })
+        .await
+    }
+
+    pub async fn remove_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+    ) -> Result<(Vec<ChannelId>, Vec<UserId>)> {
+        self.transaction(move |tx| async move {
+            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+                .await?;
+
+            // Don't remove descendant channels that have additional parents.
+            let mut channels_to_remove = self.get_channel_descendants([channel_id], &*tx).await?;
+            {
+                let mut channels_to_keep = channel_path::Entity::find()
+                    .filter(
+                        channel_path::Column::ChannelId
+                            .is_in(
+                                channels_to_remove
+                                    .keys()
+                                    .copied()
+                                    .filter(|&id| id != channel_id),
+                            )
+                            .and(
+                                channel_path::Column::IdPath
+                                    .not_like(&format!("%/{}/%", channel_id)),
+                            ),
+                    )
+                    .stream(&*tx)
+                    .await?;
+                while let Some(row) = channels_to_keep.next().await {
+                    let row = row?;
+                    channels_to_remove.remove(&row.channel_id);
+                }
+            }
+
+            let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?;
+            let members_to_notify: Vec<UserId> = channel_member::Entity::find()
+                .filter(channel_member::Column::ChannelId.is_in(channel_ancestors))
+                .select_only()
+                .column(channel_member::Column::UserId)
+                .distinct()
+                .into_values::<_, QueryUserIds>()
+                .all(&*tx)
+                .await?;
+
+            channel::Entity::delete_many()
+                .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied()))
+                .exec(&*tx)
+                .await?;
+
+            Ok((channels_to_remove.into_keys().collect(), members_to_notify))
+        })
+        .await
+    }
+
+    pub async fn invite_channel_member(
+        &self,
+        channel_id: ChannelId,
+        invitee_id: UserId,
+        inviter_id: UserId,
+        is_admin: bool,
+    ) -> Result<()> {
+        self.transaction(move |tx| async move {
+            self.check_user_is_channel_admin(channel_id, inviter_id, &*tx)
+                .await?;
+
+            channel_member::ActiveModel {
+                channel_id: ActiveValue::Set(channel_id),
+                user_id: ActiveValue::Set(invitee_id),
+                accepted: ActiveValue::Set(false),
+                admin: ActiveValue::Set(is_admin),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            Ok(())
+        })
+        .await
+    }
+
+    fn sanitize_channel_name(name: &str) -> Result<&str> {
+        let new_name = name.trim().trim_start_matches('#');
+        if new_name == "" {
+            Err(anyhow!("channel name can't be blank"))?;
+        }
+        Ok(new_name)
+    }
+
+    pub async fn rename_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        new_name: &str,
+    ) -> Result<String> {
+        self.transaction(move |tx| async move {
+            let new_name = Self::sanitize_channel_name(new_name)?.to_string();
+
+            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+                .await?;
+
+            channel::ActiveModel {
+                id: ActiveValue::Unchanged(channel_id),
+                name: ActiveValue::Set(new_name.clone()),
+                ..Default::default()
+            }
+            .update(&*tx)
+            .await?;
+
+            Ok(new_name)
+        })
+        .await
+    }
+
+    pub async fn respond_to_channel_invite(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        accept: bool,
+    ) -> Result<()> {
+        self.transaction(move |tx| async move {
+            let rows_affected = if accept {
+                channel_member::Entity::update_many()
+                    .set(channel_member::ActiveModel {
+                        accepted: ActiveValue::Set(accept),
+                        ..Default::default()
+                    })
+                    .filter(
+                        channel_member::Column::ChannelId
+                            .eq(channel_id)
+                            .and(channel_member::Column::UserId.eq(user_id))
+                            .and(channel_member::Column::Accepted.eq(false)),
+                    )
+                    .exec(&*tx)
+                    .await?
+                    .rows_affected
+            } else {
+                channel_member::ActiveModel {
+                    channel_id: ActiveValue::Unchanged(channel_id),
+                    user_id: ActiveValue::Unchanged(user_id),
+                    ..Default::default()
+                }
+                .delete(&*tx)
+                .await?
+                .rows_affected
+            };
+
+            if rows_affected == 0 {
+                Err(anyhow!("no such invitation"))?;
+            }
+
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn remove_channel_member(
+        &self,
+        channel_id: ChannelId,
+        member_id: UserId,
+        remover_id: UserId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            self.check_user_is_channel_admin(channel_id, remover_id, &*tx)
+                .await?;
+
+            let result = channel_member::Entity::delete_many()
+                .filter(
+                    channel_member::Column::ChannelId
+                        .eq(channel_id)
+                        .and(channel_member::Column::UserId.eq(member_id)),
+                )
+                .exec(&*tx)
+                .await?;
+
+            if result.rows_affected == 0 {
+                Err(anyhow!("no such member"))?;
+            }
+
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
+        self.transaction(|tx| async move {
+            let channel_invites = channel_member::Entity::find()
+                .filter(
+                    channel_member::Column::UserId
+                        .eq(user_id)
+                        .and(channel_member::Column::Accepted.eq(false)),
+                )
+                .all(&*tx)
+                .await?;
+
+            let channels = channel::Entity::find()
+                .filter(
+                    channel::Column::Id.is_in(
+                        channel_invites
+                            .into_iter()
+                            .map(|channel_member| channel_member.channel_id),
+                    ),
+                )
+                .all(&*tx)
+                .await?;
+
+            let channels = channels
+                .into_iter()
+                .map(|channel| Channel {
+                    id: channel.id,
+                    name: channel.name,
+                    parent_id: None,
+                })
+                .collect();
+
+            Ok(channels)
+        })
+        .await
+    }
+
+    pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
+        self.transaction(|tx| async move {
+            let tx = tx;
+
+            let channel_memberships = channel_member::Entity::find()
+                .filter(
+                    channel_member::Column::UserId
+                        .eq(user_id)
+                        .and(channel_member::Column::Accepted.eq(true)),
+                )
+                .all(&*tx)
+                .await?;
+
+            let parents_by_child_id = self
+                .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
+                .await?;
+
+            let channels_with_admin_privileges = channel_memberships
+                .iter()
+                .filter_map(|membership| membership.admin.then_some(membership.channel_id))
+                .collect();
+
+            let mut channels = Vec::with_capacity(parents_by_child_id.len());
+            {
+                let mut rows = channel::Entity::find()
+                    .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
+                    .stream(&*tx)
+                    .await?;
+                while let Some(row) = rows.next().await {
+                    let row = row?;
+                    channels.push(Channel {
+                        id: row.id,
+                        name: row.name,
+                        parent_id: parents_by_child_id.get(&row.id).copied().flatten(),
+                    });
+                }
+            }
+
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryUserIdsAndChannelIds {
+                ChannelId,
+                UserId,
+            }
+
+            let mut channel_participants: HashMap<ChannelId, Vec<UserId>> = HashMap::default();
+            {
+                let mut rows = room_participant::Entity::find()
+                    .inner_join(room::Entity)
+                    .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id)))
+                    .select_only()
+                    .column(room::Column::ChannelId)
+                    .column(room_participant::Column::UserId)
+                    .into_values::<_, QueryUserIdsAndChannelIds>()
+                    .stream(&*tx)
+                    .await?;
+                while let Some(row) = rows.next().await {
+                    let row: (ChannelId, UserId) = row?;
+                    channel_participants.entry(row.0).or_default().push(row.1)
+                }
+            }
+
+            Ok(ChannelsForUser {
+                channels,
+                channel_participants,
+                channels_with_admin_privileges,
+            })
+        })
+        .await
+    }
+
+    pub async fn get_channel_members(&self, id: ChannelId) -> Result<Vec<UserId>> {
+        self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await })
+            .await
+    }
+
+    pub async fn set_channel_member_admin(
+        &self,
+        channel_id: ChannelId,
+        from: UserId,
+        for_user: UserId,
+        admin: bool,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            self.check_user_is_channel_admin(channel_id, from, &*tx)
+                .await?;
+
+            let result = channel_member::Entity::update_many()
+                .filter(
+                    channel_member::Column::ChannelId
+                        .eq(channel_id)
+                        .and(channel_member::Column::UserId.eq(for_user)),
+                )
+                .set(channel_member::ActiveModel {
+                    admin: ActiveValue::set(admin),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+
+            if result.rows_affected == 0 {
+                Err(anyhow!("no such member"))?;
+            }
+
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_channel_member_details(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+    ) -> Result<Vec<proto::ChannelMember>> {
+        self.transaction(|tx| async move {
+            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+                .await?;
+
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryMemberDetails {
+                UserId,
+                Admin,
+                IsDirectMember,
+                Accepted,
+            }
+
+            let tx = tx;
+            let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?;
+            let mut stream = channel_member::Entity::find()
+                .distinct()
+                .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied()))
+                .select_only()
+                .column(channel_member::Column::UserId)
+                .column(channel_member::Column::Admin)
+                .column_as(
+                    channel_member::Column::ChannelId.eq(channel_id),
+                    QueryMemberDetails::IsDirectMember,
+                )
+                .column(channel_member::Column::Accepted)
+                .order_by_asc(channel_member::Column::UserId)
+                .into_values::<_, QueryMemberDetails>()
+                .stream(&*tx)
+                .await?;
+
+            let mut rows = Vec::<proto::ChannelMember>::new();
+            while let Some(row) = stream.next().await {
+                let (user_id, is_admin, is_direct_member, is_invite_accepted): (
+                    UserId,
+                    bool,
+                    bool,
+                    bool,
+                ) = row?;
+                let kind = match (is_direct_member, is_invite_accepted) {
+                    (true, true) => proto::channel_member::Kind::Member,
+                    (true, false) => proto::channel_member::Kind::Invitee,
+                    (false, true) => proto::channel_member::Kind::AncestorMember,
+                    (false, false) => continue,
+                };
+                let user_id = user_id.to_proto();
+                let kind = kind.into();
+                if let Some(last_row) = rows.last_mut() {
+                    if last_row.user_id == user_id {
+                        if is_direct_member {
+                            last_row.kind = kind;
+                            last_row.admin = is_admin;
+                        }
+                        continue;
+                    }
+                }
+                rows.push(proto::ChannelMember {
+                    user_id,
+                    kind,
+                    admin: is_admin,
+                });
+            }
+
+            Ok(rows)
+        })
+        .await
+    }
+
+    pub async fn get_channel_members_internal(
+        &self,
+        id: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<UserId>> {
+        let ancestor_ids = self.get_channel_ancestors(id, tx).await?;
+        let user_ids = channel_member::Entity::find()
+            .distinct()
+            .filter(
+                channel_member::Column::ChannelId
+                    .is_in(ancestor_ids.iter().copied())
+                    .and(channel_member::Column::Accepted.eq(true)),
+            )
+            .select_only()
+            .column(channel_member::Column::UserId)
+            .into_values::<_, QueryUserIds>()
+            .all(&*tx)
+            .await?;
+        Ok(user_ids)
+    }
+
+    pub async fn check_user_is_channel_member(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
+        channel_member::Entity::find()
+            .filter(
+                channel_member::Column::ChannelId
+                    .is_in(channel_ids)
+                    .and(channel_member::Column::UserId.eq(user_id)),
+            )
+            .one(&*tx)
+            .await?
+            .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?;
+        Ok(())
+    }
+
+    pub async fn check_user_is_channel_admin(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
+        channel_member::Entity::find()
+            .filter(
+                channel_member::Column::ChannelId
+                    .is_in(channel_ids)
+                    .and(channel_member::Column::UserId.eq(user_id))
+                    .and(channel_member::Column::Admin.eq(true)),
+            )
+            .one(&*tx)
+            .await?
+            .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?;
+        Ok(())
+    }
+
+    pub async fn get_channel_ancestors(
+        &self,
+        channel_id: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<ChannelId>> {
+        let paths = channel_path::Entity::find()
+            .filter(channel_path::Column::ChannelId.eq(channel_id))
+            .all(tx)
+            .await?;
+        let mut channel_ids = Vec::new();
+        for path in paths {
+            for id in path.id_path.trim_matches('/').split('/') {
+                if let Ok(id) = id.parse() {
+                    let id = ChannelId::from_proto(id);
+                    if let Err(ix) = channel_ids.binary_search(&id) {
+                        channel_ids.insert(ix, id);
+                    }
+                }
+            }
+        }
+        Ok(channel_ids)
+    }
+
+    async fn get_channel_descendants(
+        &self,
+        channel_ids: impl IntoIterator<Item = ChannelId>,
+        tx: &DatabaseTransaction,
+    ) -> Result<HashMap<ChannelId, Option<ChannelId>>> {
+        let mut values = String::new();
+        for id in channel_ids {
+            if !values.is_empty() {
+                values.push_str(", ");
+            }
+            write!(&mut values, "({})", id).unwrap();
+        }
+
+        if values.is_empty() {
+            return Ok(HashMap::default());
+        }
+
+        let sql = format!(
+            r#"
+            SELECT
+                descendant_paths.*
+            FROM
+                channel_paths parent_paths, channel_paths descendant_paths
+            WHERE
+                parent_paths.channel_id IN ({values}) AND
+                descendant_paths.id_path LIKE (parent_paths.id_path || '%')
+        "#
+        );
+
+        let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
+
+        let mut parents_by_child_id = HashMap::default();
+        let mut paths = channel_path::Entity::find()
+            .from_raw_sql(stmt)
+            .stream(tx)
+            .await?;
+
+        while let Some(path) = paths.next().await {
+            let path = path?;
+            let ids = path.id_path.trim_matches('/').split('/');
+            let mut parent_id = None;
+            for id in ids {
+                if let Ok(id) = id.parse() {
+                    let id = ChannelId::from_proto(id);
+                    if id == path.channel_id {
+                        break;
+                    }
+                    parent_id = Some(id);
+                }
+            }
+            parents_by_child_id.insert(path.channel_id, parent_id);
+        }
+
+        Ok(parents_by_child_id)
+    }
+
+    /// Returns the channel with the given ID and:
+    /// - true if the user is a member
+    /// - false if the user hasn't accepted the invitation yet
+    pub async fn get_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+    ) -> Result<Option<(Channel, bool)>> {
+        self.transaction(|tx| async move {
+            let tx = tx;
+
+            let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?;
+
+            if let Some(channel) = channel {
+                if self
+                    .check_user_is_channel_member(channel_id, user_id, &*tx)
+                    .await
+                    .is_err()
+                {
+                    return Ok(None);
+                }
+
+                let channel_membership = channel_member::Entity::find()
+                    .filter(
+                        channel_member::Column::ChannelId
+                            .eq(channel_id)
+                            .and(channel_member::Column::UserId.eq(user_id)),
+                    )
+                    .one(&*tx)
+                    .await?;
+
+                let is_accepted = channel_membership
+                    .map(|membership| membership.accepted)
+                    .unwrap_or(false);
+
+                Ok(Some((
+                    Channel {
+                        id: channel.id,
+                        name: channel.name,
+                        parent_id: None,
+                    },
+                    is_accepted,
+                )))
+            } else {
+                Ok(None)
+            }
+        })
+        .await
+    }
+
+    pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result<RoomId> {
+        self.transaction(|tx| async move {
+            let tx = tx;
+            let room = channel::Model {
+                id: channel_id,
+                ..Default::default()
+            }
+            .find_related(room::Entity)
+            .one(&*tx)
+            .await?
+            .ok_or_else(|| anyhow!("invalid channel"))?;
+            Ok(room.id)
+        })
+        .await
+    }
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+enum QueryUserIds {
+    UserId,
+}

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

@@ -0,0 +1,298 @@
+use super::*;
+
+impl Database {
+    pub async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
+        #[derive(Debug, FromQueryResult)]
+        struct ContactWithUserBusyStatuses {
+            user_id_a: UserId,
+            user_id_b: UserId,
+            a_to_b: bool,
+            accepted: bool,
+            should_notify: bool,
+            user_a_busy: bool,
+            user_b_busy: bool,
+        }
+
+        self.transaction(|tx| async move {
+            let user_a_participant = Alias::new("user_a_participant");
+            let user_b_participant = Alias::new("user_b_participant");
+            let mut db_contacts = contact::Entity::find()
+                .column_as(
+                    Expr::tbl(user_a_participant.clone(), room_participant::Column::Id)
+                        .is_not_null(),
+                    "user_a_busy",
+                )
+                .column_as(
+                    Expr::tbl(user_b_participant.clone(), room_participant::Column::Id)
+                        .is_not_null(),
+                    "user_b_busy",
+                )
+                .filter(
+                    contact::Column::UserIdA
+                        .eq(user_id)
+                        .or(contact::Column::UserIdB.eq(user_id)),
+                )
+                .join_as(
+                    JoinType::LeftJoin,
+                    contact::Relation::UserARoomParticipant.def(),
+                    user_a_participant,
+                )
+                .join_as(
+                    JoinType::LeftJoin,
+                    contact::Relation::UserBRoomParticipant.def(),
+                    user_b_participant,
+                )
+                .into_model::<ContactWithUserBusyStatuses>()
+                .stream(&*tx)
+                .await?;
+
+            let mut contacts = Vec::new();
+            while let Some(db_contact) = db_contacts.next().await {
+                let db_contact = db_contact?;
+                if db_contact.user_id_a == user_id {
+                    if db_contact.accepted {
+                        contacts.push(Contact::Accepted {
+                            user_id: db_contact.user_id_b,
+                            should_notify: db_contact.should_notify && db_contact.a_to_b,
+                            busy: db_contact.user_b_busy,
+                        });
+                    } else if db_contact.a_to_b {
+                        contacts.push(Contact::Outgoing {
+                            user_id: db_contact.user_id_b,
+                        })
+                    } else {
+                        contacts.push(Contact::Incoming {
+                            user_id: db_contact.user_id_b,
+                            should_notify: db_contact.should_notify,
+                        });
+                    }
+                } else if db_contact.accepted {
+                    contacts.push(Contact::Accepted {
+                        user_id: db_contact.user_id_a,
+                        should_notify: db_contact.should_notify && !db_contact.a_to_b,
+                        busy: db_contact.user_a_busy,
+                    });
+                } else if db_contact.a_to_b {
+                    contacts.push(Contact::Incoming {
+                        user_id: db_contact.user_id_a,
+                        should_notify: db_contact.should_notify,
+                    });
+                } else {
+                    contacts.push(Contact::Outgoing {
+                        user_id: db_contact.user_id_a,
+                    });
+                }
+            }
+
+            contacts.sort_unstable_by_key(|contact| contact.user_id());
+
+            Ok(contacts)
+        })
+        .await
+    }
+
+    pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
+        self.transaction(|tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(room_participant::Column::UserId.eq(user_id))
+                .one(&*tx)
+                .await?;
+            Ok(participant.is_some())
+        })
+        .await
+    }
+
+    pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result<bool> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b) = if user_id_1 < user_id_2 {
+                (user_id_1, user_id_2)
+            } else {
+                (user_id_2, user_id_1)
+            };
+
+            Ok(contact::Entity::find()
+                .filter(
+                    contact::Column::UserIdA
+                        .eq(id_a)
+                        .and(contact::Column::UserIdB.eq(id_b))
+                        .and(contact::Column::Accepted.eq(true)),
+                )
+                .one(&*tx)
+                .await?
+                .is_some())
+        })
+        .await
+    }
+
+    pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b, a_to_b) = if sender_id < receiver_id {
+                (sender_id, receiver_id, true)
+            } else {
+                (receiver_id, sender_id, false)
+            };
+
+            let rows_affected = contact::Entity::insert(contact::ActiveModel {
+                user_id_a: ActiveValue::set(id_a),
+                user_id_b: ActiveValue::set(id_b),
+                a_to_b: ActiveValue::set(a_to_b),
+                accepted: ActiveValue::set(false),
+                should_notify: ActiveValue::set(true),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB])
+                    .values([
+                        (contact::Column::Accepted, true.into()),
+                        (contact::Column::ShouldNotify, false.into()),
+                    ])
+                    .action_and_where(
+                        contact::Column::Accepted.eq(false).and(
+                            contact::Column::AToB
+                                .eq(a_to_b)
+                                .and(contact::Column::UserIdA.eq(id_b))
+                                .or(contact::Column::AToB
+                                    .ne(a_to_b)
+                                    .and(contact::Column::UserIdA.eq(id_a))),
+                        ),
+                    )
+                    .to_owned(),
+            )
+            .exec_without_returning(&*tx)
+            .await?;
+
+            if rows_affected == 1 {
+                Ok(())
+            } else {
+                Err(anyhow!("contact already requested"))?
+            }
+        })
+        .await
+    }
+
+    /// Returns a bool indicating whether the removed contact had originally accepted or not
+    ///
+    /// Deletes the contact identified by the requester and responder ids, and then returns
+    /// whether the deleted contact had originally accepted or was a pending contact request.
+    ///
+    /// # Arguments
+    ///
+    /// * `requester_id` - The user that initiates this request
+    /// * `responder_id` - The user that will be removed
+    pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<bool> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b) = if responder_id < requester_id {
+                (responder_id, requester_id)
+            } else {
+                (requester_id, responder_id)
+            };
+
+            let contact = contact::Entity::find()
+                .filter(
+                    contact::Column::UserIdA
+                        .eq(id_a)
+                        .and(contact::Column::UserIdB.eq(id_b)),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such contact"))?;
+
+            contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
+            Ok(contact.accepted)
+        })
+        .await
+    }
+
+    pub async fn dismiss_contact_notification(
+        &self,
+        user_id: UserId,
+        contact_user_id: UserId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b, a_to_b) = if user_id < contact_user_id {
+                (user_id, contact_user_id, true)
+            } else {
+                (contact_user_id, user_id, false)
+            };
+
+            let result = contact::Entity::update_many()
+                .set(contact::ActiveModel {
+                    should_notify: ActiveValue::set(false),
+                    ..Default::default()
+                })
+                .filter(
+                    contact::Column::UserIdA
+                        .eq(id_a)
+                        .and(contact::Column::UserIdB.eq(id_b))
+                        .and(
+                            contact::Column::AToB
+                                .eq(a_to_b)
+                                .and(contact::Column::Accepted.eq(true))
+                                .or(contact::Column::AToB
+                                    .ne(a_to_b)
+                                    .and(contact::Column::Accepted.eq(false))),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+            if result.rows_affected == 0 {
+                Err(anyhow!("no such contact request"))?
+            } else {
+                Ok(())
+            }
+        })
+        .await
+    }
+
+    pub async fn respond_to_contact_request(
+        &self,
+        responder_id: UserId,
+        requester_id: UserId,
+        accept: bool,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b, a_to_b) = if responder_id < requester_id {
+                (responder_id, requester_id, false)
+            } else {
+                (requester_id, responder_id, true)
+            };
+            let rows_affected = if accept {
+                let result = contact::Entity::update_many()
+                    .set(contact::ActiveModel {
+                        accepted: ActiveValue::set(true),
+                        should_notify: ActiveValue::set(true),
+                        ..Default::default()
+                    })
+                    .filter(
+                        contact::Column::UserIdA
+                            .eq(id_a)
+                            .and(contact::Column::UserIdB.eq(id_b))
+                            .and(contact::Column::AToB.eq(a_to_b)),
+                    )
+                    .exec(&*tx)
+                    .await?;
+                result.rows_affected
+            } else {
+                let result = contact::Entity::delete_many()
+                    .filter(
+                        contact::Column::UserIdA
+                            .eq(id_a)
+                            .and(contact::Column::UserIdB.eq(id_b))
+                            .and(contact::Column::AToB.eq(a_to_b))
+                            .and(contact::Column::Accepted.eq(false)),
+                    )
+                    .exec(&*tx)
+                    .await?;
+
+                result.rows_affected
+            };
+
+            if rows_affected == 1 {
+                Ok(())
+            } else {
+                Err(anyhow!("no such contact request"))?
+            }
+        })
+        .await
+    }
+}

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

@@ -0,0 +1,926 @@
+use super::*;
+
+impl Database {
+    pub async fn project_count_excluding_admins(&self) -> Result<usize> {
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryAs {
+            Count,
+        }
+
+        self.transaction(|tx| async move {
+            Ok(project::Entity::find()
+                .select_only()
+                .column_as(project::Column::Id.count(), QueryAs::Count)
+                .inner_join(user::Entity)
+                .filter(user::Column::Admin.eq(false))
+                .into_values::<_, QueryAs>()
+                .one(&*tx)
+                .await?
+                .unwrap_or(0i64) as usize)
+        })
+        .await
+    }
+
+    pub async fn share_project(
+        &self,
+        room_id: RoomId,
+        connection: ConnectionId,
+        worktrees: &[proto::WorktreeMetadata],
+    ) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
+        self.room_transaction(room_id, |tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("could not find participant"))?;
+            if participant.room_id != room_id {
+                return Err(anyhow!("shared project on unexpected room"))?;
+            }
+
+            let project = project::ActiveModel {
+                room_id: ActiveValue::set(participant.room_id),
+                host_user_id: ActiveValue::set(participant.user_id),
+                host_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                host_connection_server_id: ActiveValue::set(Some(ServerId(
+                    connection.owner_id as i32,
+                ))),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            if !worktrees.is_empty() {
+                worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
+                    worktree::ActiveModel {
+                        id: ActiveValue::set(worktree.id as i64),
+                        project_id: ActiveValue::set(project.id),
+                        abs_path: ActiveValue::set(worktree.abs_path.clone()),
+                        root_name: ActiveValue::set(worktree.root_name.clone()),
+                        visible: ActiveValue::set(worktree.visible),
+                        scan_id: ActiveValue::set(0),
+                        completed_scan_id: ActiveValue::set(0),
+                    }
+                }))
+                .exec(&*tx)
+                .await?;
+            }
+
+            project_collaborator::ActiveModel {
+                project_id: ActiveValue::set(project.id),
+                connection_id: ActiveValue::set(connection.id as i32),
+                connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
+                user_id: ActiveValue::set(participant.user_id),
+                replica_id: ActiveValue::set(ReplicaId(0)),
+                is_host: ActiveValue::set(true),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room_id, &tx).await?;
+            Ok((project.id, room))
+        })
+        .await
+    }
+
+    pub async fn unshare_project(
+        &self,
+        project_id: ProjectId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("project not found"))?;
+            if project.host_connection()? == connection {
+                project::Entity::delete(project.into_active_model())
+                    .exec(&*tx)
+                    .await?;
+                let room = self.get_room(room_id, &tx).await?;
+                Ok((room, guest_connection_ids))
+            } else {
+                Err(anyhow!("cannot unshare a project hosted by another user"))?
+            }
+        })
+        .await
+    }
+
+    pub async fn update_project(
+        &self,
+        project_id: ProjectId,
+        connection: ConnectionId,
+        worktrees: &[proto::WorktreeMetadata],
+    ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let project = project::Entity::find_by_id(project_id)
+                .filter(
+                    Condition::all()
+                        .add(project::Column::HostConnectionId.eq(connection.id as i32))
+                        .add(
+                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+
+            self.update_project_worktrees(project.id, worktrees, &tx)
+                .await?;
+
+            let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
+            let room = self.get_room(project.room_id, &tx).await?;
+            Ok((room, guest_connection_ids))
+        })
+        .await
+    }
+
+    pub(in crate::db) async fn update_project_worktrees(
+        &self,
+        project_id: ProjectId,
+        worktrees: &[proto::WorktreeMetadata],
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        if !worktrees.is_empty() {
+            worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel {
+                id: ActiveValue::set(worktree.id as i64),
+                project_id: ActiveValue::set(project_id),
+                abs_path: ActiveValue::set(worktree.abs_path.clone()),
+                root_name: ActiveValue::set(worktree.root_name.clone()),
+                visible: ActiveValue::set(worktree.visible),
+                scan_id: ActiveValue::set(0),
+                completed_scan_id: ActiveValue::set(0),
+            }))
+            .on_conflict(
+                OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id])
+                    .update_column(worktree::Column::RootName)
+                    .to_owned(),
+            )
+            .exec(&*tx)
+            .await?;
+        }
+
+        worktree::Entity::delete_many()
+            .filter(worktree::Column::ProjectId.eq(project_id).and(
+                worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)),
+            ))
+            .exec(&*tx)
+            .await?;
+
+        Ok(())
+    }
+
+    pub async fn update_worktree(
+        &self,
+        update: &proto::UpdateWorktree,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
+        let project_id = ProjectId::from_proto(update.project_id);
+        let worktree_id = update.worktree_id as i64;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            // Ensure the update comes from the host.
+            let _project = project::Entity::find_by_id(project_id)
+                .filter(
+                    Condition::all()
+                        .add(project::Column::HostConnectionId.eq(connection.id as i32))
+                        .add(
+                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+
+            // Update metadata.
+            worktree::Entity::update(worktree::ActiveModel {
+                id: ActiveValue::set(worktree_id),
+                project_id: ActiveValue::set(project_id),
+                root_name: ActiveValue::set(update.root_name.clone()),
+                scan_id: ActiveValue::set(update.scan_id as i64),
+                completed_scan_id: if update.is_last_update {
+                    ActiveValue::set(update.scan_id as i64)
+                } else {
+                    ActiveValue::default()
+                },
+                abs_path: ActiveValue::set(update.abs_path.clone()),
+                ..Default::default()
+            })
+            .exec(&*tx)
+            .await?;
+
+            if !update.updated_entries.is_empty() {
+                worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| {
+                    let mtime = entry.mtime.clone().unwrap_or_default();
+                    worktree_entry::ActiveModel {
+                        project_id: ActiveValue::set(project_id),
+                        worktree_id: ActiveValue::set(worktree_id),
+                        id: ActiveValue::set(entry.id as i64),
+                        is_dir: ActiveValue::set(entry.is_dir),
+                        path: ActiveValue::set(entry.path.clone()),
+                        inode: ActiveValue::set(entry.inode as i64),
+                        mtime_seconds: ActiveValue::set(mtime.seconds as i64),
+                        mtime_nanos: ActiveValue::set(mtime.nanos as i32),
+                        is_symlink: ActiveValue::set(entry.is_symlink),
+                        is_ignored: ActiveValue::set(entry.is_ignored),
+                        is_external: ActiveValue::set(entry.is_external),
+                        git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
+                        is_deleted: ActiveValue::set(false),
+                        scan_id: ActiveValue::set(update.scan_id as i64),
+                    }
+                }))
+                .on_conflict(
+                    OnConflict::columns([
+                        worktree_entry::Column::ProjectId,
+                        worktree_entry::Column::WorktreeId,
+                        worktree_entry::Column::Id,
+                    ])
+                    .update_columns([
+                        worktree_entry::Column::IsDir,
+                        worktree_entry::Column::Path,
+                        worktree_entry::Column::Inode,
+                        worktree_entry::Column::MtimeSeconds,
+                        worktree_entry::Column::MtimeNanos,
+                        worktree_entry::Column::IsSymlink,
+                        worktree_entry::Column::IsIgnored,
+                        worktree_entry::Column::GitStatus,
+                        worktree_entry::Column::ScanId,
+                    ])
+                    .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            }
+
+            if !update.removed_entries.is_empty() {
+                worktree_entry::Entity::update_many()
+                    .filter(
+                        worktree_entry::Column::ProjectId
+                            .eq(project_id)
+                            .and(worktree_entry::Column::WorktreeId.eq(worktree_id))
+                            .and(
+                                worktree_entry::Column::Id
+                                    .is_in(update.removed_entries.iter().map(|id| *id as i64)),
+                            ),
+                    )
+                    .set(worktree_entry::ActiveModel {
+                        is_deleted: ActiveValue::Set(true),
+                        scan_id: ActiveValue::Set(update.scan_id as i64),
+                        ..Default::default()
+                    })
+                    .exec(&*tx)
+                    .await?;
+            }
+
+            if !update.updated_repositories.is_empty() {
+                worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
+                    |repository| worktree_repository::ActiveModel {
+                        project_id: ActiveValue::set(project_id),
+                        worktree_id: ActiveValue::set(worktree_id),
+                        work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
+                        scan_id: ActiveValue::set(update.scan_id as i64),
+                        branch: ActiveValue::set(repository.branch.clone()),
+                        is_deleted: ActiveValue::set(false),
+                    },
+                ))
+                .on_conflict(
+                    OnConflict::columns([
+                        worktree_repository::Column::ProjectId,
+                        worktree_repository::Column::WorktreeId,
+                        worktree_repository::Column::WorkDirectoryId,
+                    ])
+                    .update_columns([
+                        worktree_repository::Column::ScanId,
+                        worktree_repository::Column::Branch,
+                    ])
+                    .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            }
+
+            if !update.removed_repositories.is_empty() {
+                worktree_repository::Entity::update_many()
+                    .filter(
+                        worktree_repository::Column::ProjectId
+                            .eq(project_id)
+                            .and(worktree_repository::Column::WorktreeId.eq(worktree_id))
+                            .and(
+                                worktree_repository::Column::WorkDirectoryId
+                                    .is_in(update.removed_repositories.iter().map(|id| *id as i64)),
+                            ),
+                    )
+                    .set(worktree_repository::ActiveModel {
+                        is_deleted: ActiveValue::Set(true),
+                        scan_id: ActiveValue::Set(update.scan_id as i64),
+                        ..Default::default()
+                    })
+                    .exec(&*tx)
+                    .await?;
+            }
+
+            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+            Ok(connection_ids)
+        })
+        .await
+    }
+
+    pub async fn update_diagnostic_summary(
+        &self,
+        update: &proto::UpdateDiagnosticSummary,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
+        let project_id = ProjectId::from_proto(update.project_id);
+        let worktree_id = update.worktree_id as i64;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let summary = update
+                .summary
+                .as_ref()
+                .ok_or_else(|| anyhow!("invalid summary"))?;
+
+            // Ensure the update comes from the host.
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            if project.host_connection()? != connection {
+                return Err(anyhow!("can't update a project hosted by someone else"))?;
+            }
+
+            // Update summary.
+            worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel {
+                project_id: ActiveValue::set(project_id),
+                worktree_id: ActiveValue::set(worktree_id),
+                path: ActiveValue::set(summary.path.clone()),
+                language_server_id: ActiveValue::set(summary.language_server_id as i64),
+                error_count: ActiveValue::set(summary.error_count as i32),
+                warning_count: ActiveValue::set(summary.warning_count as i32),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::columns([
+                    worktree_diagnostic_summary::Column::ProjectId,
+                    worktree_diagnostic_summary::Column::WorktreeId,
+                    worktree_diagnostic_summary::Column::Path,
+                ])
+                .update_columns([
+                    worktree_diagnostic_summary::Column::LanguageServerId,
+                    worktree_diagnostic_summary::Column::ErrorCount,
+                    worktree_diagnostic_summary::Column::WarningCount,
+                ])
+                .to_owned(),
+            )
+            .exec(&*tx)
+            .await?;
+
+            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+            Ok(connection_ids)
+        })
+        .await
+    }
+
+    pub async fn start_language_server(
+        &self,
+        update: &proto::StartLanguageServer,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
+        let project_id = ProjectId::from_proto(update.project_id);
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let server = update
+                .server
+                .as_ref()
+                .ok_or_else(|| anyhow!("invalid language server"))?;
+
+            // Ensure the update comes from the host.
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            if project.host_connection()? != connection {
+                return Err(anyhow!("can't update a project hosted by someone else"))?;
+            }
+
+            // Add the newly-started language server.
+            language_server::Entity::insert(language_server::ActiveModel {
+                project_id: ActiveValue::set(project_id),
+                id: ActiveValue::set(server.id as i64),
+                name: ActiveValue::set(server.name.clone()),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::columns([
+                    language_server::Column::ProjectId,
+                    language_server::Column::Id,
+                ])
+                .update_column(language_server::Column::Name)
+                .to_owned(),
+            )
+            .exec(&*tx)
+            .await?;
+
+            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+            Ok(connection_ids)
+        })
+        .await
+    }
+
+    pub async fn update_worktree_settings(
+        &self,
+        update: &proto::UpdateWorktreeSettings,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
+        let project_id = ProjectId::from_proto(update.project_id);
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            // Ensure the update comes from the host.
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            if project.host_connection()? != connection {
+                return Err(anyhow!("can't update a project hosted by someone else"))?;
+            }
+
+            if let Some(content) = &update.content {
+                worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
+                    project_id: ActiveValue::Set(project_id),
+                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
+                    path: ActiveValue::Set(update.path.clone()),
+                    content: ActiveValue::Set(content.clone()),
+                })
+                .on_conflict(
+                    OnConflict::columns([
+                        worktree_settings_file::Column::ProjectId,
+                        worktree_settings_file::Column::WorktreeId,
+                        worktree_settings_file::Column::Path,
+                    ])
+                    .update_column(worktree_settings_file::Column::Content)
+                    .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            } else {
+                worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
+                    project_id: ActiveValue::Set(project_id),
+                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
+                    path: ActiveValue::Set(update.path.clone()),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            }
+
+            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+            Ok(connection_ids)
+        })
+        .await
+    }
+
+    pub async fn join_project(
+        &self,
+        project_id: ProjectId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<(Project, ReplicaId)>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("must join a room first"))?;
+
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            if project.room_id != participant.room_id {
+                return Err(anyhow!("no such project"))?;
+            }
+
+            let mut collaborators = project
+                .find_related(project_collaborator::Entity)
+                .all(&*tx)
+                .await?;
+            let replica_ids = collaborators
+                .iter()
+                .map(|c| c.replica_id)
+                .collect::<HashSet<_>>();
+            let mut replica_id = ReplicaId(1);
+            while replica_ids.contains(&replica_id) {
+                replica_id.0 += 1;
+            }
+            let new_collaborator = project_collaborator::ActiveModel {
+                project_id: ActiveValue::set(project_id),
+                connection_id: ActiveValue::set(connection.id as i32),
+                connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
+                user_id: ActiveValue::set(participant.user_id),
+                replica_id: ActiveValue::set(replica_id),
+                is_host: ActiveValue::set(false),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+            collaborators.push(new_collaborator);
+
+            let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
+            let mut worktrees = db_worktrees
+                .into_iter()
+                .map(|db_worktree| {
+                    (
+                        db_worktree.id as u64,
+                        Worktree {
+                            id: db_worktree.id as u64,
+                            abs_path: db_worktree.abs_path,
+                            root_name: db_worktree.root_name,
+                            visible: db_worktree.visible,
+                            entries: Default::default(),
+                            repository_entries: Default::default(),
+                            diagnostic_summaries: Default::default(),
+                            settings_files: Default::default(),
+                            scan_id: db_worktree.scan_id as u64,
+                            completed_scan_id: db_worktree.completed_scan_id as u64,
+                        },
+                    )
+                })
+                .collect::<BTreeMap<_, _>>();
+
+            // Populate worktree entries.
+            {
+                let mut db_entries = worktree_entry::Entity::find()
+                    .filter(
+                        Condition::all()
+                            .add(worktree_entry::Column::ProjectId.eq(project_id))
+                            .add(worktree_entry::Column::IsDeleted.eq(false)),
+                    )
+                    .stream(&*tx)
+                    .await?;
+                while let Some(db_entry) = db_entries.next().await {
+                    let db_entry = db_entry?;
+                    if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
+                        worktree.entries.push(proto::Entry {
+                            id: db_entry.id as u64,
+                            is_dir: db_entry.is_dir,
+                            path: db_entry.path,
+                            inode: db_entry.inode as u64,
+                            mtime: Some(proto::Timestamp {
+                                seconds: db_entry.mtime_seconds as u64,
+                                nanos: db_entry.mtime_nanos as u32,
+                            }),
+                            is_symlink: db_entry.is_symlink,
+                            is_ignored: db_entry.is_ignored,
+                            is_external: db_entry.is_external,
+                            git_status: db_entry.git_status.map(|status| status as i32),
+                        });
+                    }
+                }
+            }
+
+            // Populate repository entries.
+            {
+                let mut db_repository_entries = worktree_repository::Entity::find()
+                    .filter(
+                        Condition::all()
+                            .add(worktree_repository::Column::ProjectId.eq(project_id))
+                            .add(worktree_repository::Column::IsDeleted.eq(false)),
+                    )
+                    .stream(&*tx)
+                    .await?;
+                while let Some(db_repository_entry) = db_repository_entries.next().await {
+                    let db_repository_entry = db_repository_entry?;
+                    if let Some(worktree) =
+                        worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
+                    {
+                        worktree.repository_entries.insert(
+                            db_repository_entry.work_directory_id as u64,
+                            proto::RepositoryEntry {
+                                work_directory_id: db_repository_entry.work_directory_id as u64,
+                                branch: db_repository_entry.branch,
+                            },
+                        );
+                    }
+                }
+            }
+
+            // Populate worktree diagnostic summaries.
+            {
+                let mut db_summaries = worktree_diagnostic_summary::Entity::find()
+                    .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id))
+                    .stream(&*tx)
+                    .await?;
+                while let Some(db_summary) = db_summaries.next().await {
+                    let db_summary = db_summary?;
+                    if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
+                        worktree
+                            .diagnostic_summaries
+                            .push(proto::DiagnosticSummary {
+                                path: db_summary.path,
+                                language_server_id: db_summary.language_server_id as u64,
+                                error_count: db_summary.error_count as u32,
+                                warning_count: db_summary.warning_count as u32,
+                            });
+                    }
+                }
+            }
+
+            // Populate worktree settings files
+            {
+                let mut db_settings_files = worktree_settings_file::Entity::find()
+                    .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
+                    .stream(&*tx)
+                    .await?;
+                while let Some(db_settings_file) = db_settings_files.next().await {
+                    let db_settings_file = db_settings_file?;
+                    if let Some(worktree) =
+                        worktrees.get_mut(&(db_settings_file.worktree_id as u64))
+                    {
+                        worktree.settings_files.push(WorktreeSettingsFile {
+                            path: db_settings_file.path,
+                            content: db_settings_file.content,
+                        });
+                    }
+                }
+            }
+
+            // Populate language servers.
+            let language_servers = project
+                .find_related(language_server::Entity)
+                .all(&*tx)
+                .await?;
+
+            let project = Project {
+                collaborators: collaborators
+                    .into_iter()
+                    .map(|collaborator| ProjectCollaborator {
+                        connection_id: collaborator.connection(),
+                        user_id: collaborator.user_id,
+                        replica_id: collaborator.replica_id,
+                        is_host: collaborator.is_host,
+                    })
+                    .collect(),
+                worktrees,
+                language_servers: language_servers
+                    .into_iter()
+                    .map(|language_server| proto::LanguageServer {
+                        id: language_server.id as u64,
+                        name: language_server.name,
+                    })
+                    .collect(),
+            };
+            Ok((project, replica_id as ReplicaId))
+        })
+        .await
+    }
+
+    pub async fn leave_project(
+        &self,
+        project_id: ProjectId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let result = project_collaborator::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(project_collaborator::Column::ProjectId.eq(project_id))
+                        .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
+                        .add(
+                            project_collaborator::Column::ConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+            if result.rows_affected == 0 {
+                Err(anyhow!("not a collaborator on this project"))?;
+            }
+
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            let collaborators = project
+                .find_related(project_collaborator::Entity)
+                .all(&*tx)
+                .await?;
+            let connection_ids = collaborators
+                .into_iter()
+                .map(|collaborator| collaborator.connection())
+                .collect();
+
+            follower::Entity::delete_many()
+                .filter(
+                    Condition::any()
+                        .add(
+                            Condition::all()
+                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(
+                                    follower::Column::LeaderConnectionServerId
+                                        .eq(connection.owner_id),
+                                )
+                                .add(follower::Column::LeaderConnectionId.eq(connection.id)),
+                        )
+                        .add(
+                            Condition::all()
+                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(
+                                    follower::Column::FollowerConnectionServerId
+                                        .eq(connection.owner_id),
+                                )
+                                .add(follower::Column::FollowerConnectionId.eq(connection.id)),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(project.room_id, &tx).await?;
+            let left_project = LeftProject {
+                id: project_id,
+                host_user_id: project.host_user_id,
+                host_connection_id: project.host_connection()?,
+                connection_ids,
+            };
+            Ok((room, left_project))
+        })
+        .await
+    }
+
+    pub async fn project_collaborators(
+        &self,
+        project_id: ProjectId,
+        connection_id: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let collaborators = project_collaborator::Entity::find()
+                .filter(project_collaborator::Column::ProjectId.eq(project_id))
+                .all(&*tx)
+                .await?
+                .into_iter()
+                .map(|collaborator| ProjectCollaborator {
+                    connection_id: collaborator.connection(),
+                    user_id: collaborator.user_id,
+                    replica_id: collaborator.replica_id,
+                    is_host: collaborator.is_host,
+                })
+                .collect::<Vec<_>>();
+
+            if collaborators
+                .iter()
+                .any(|collaborator| collaborator.connection_id == connection_id)
+            {
+                Ok(collaborators)
+            } else {
+                Err(anyhow!("no such project"))?
+            }
+        })
+        .await
+    }
+
+    pub async fn project_connection_ids(
+        &self,
+        project_id: ProjectId,
+        connection_id: ConnectionId,
+    ) -> Result<RoomGuard<HashSet<ConnectionId>>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let mut collaborators = project_collaborator::Entity::find()
+                .filter(project_collaborator::Column::ProjectId.eq(project_id))
+                .stream(&*tx)
+                .await?;
+
+            let mut connection_ids = HashSet::default();
+            while let Some(collaborator) = collaborators.next().await {
+                let collaborator = collaborator?;
+                connection_ids.insert(collaborator.connection());
+            }
+
+            if connection_ids.contains(&connection_id) {
+                Ok(connection_ids)
+            } else {
+                Err(anyhow!("no such project"))?
+            }
+        })
+        .await
+    }
+
+    async fn project_guest_connection_ids(
+        &self,
+        project_id: ProjectId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<ConnectionId>> {
+        let mut collaborators = project_collaborator::Entity::find()
+            .filter(
+                project_collaborator::Column::ProjectId
+                    .eq(project_id)
+                    .and(project_collaborator::Column::IsHost.eq(false)),
+            )
+            .stream(tx)
+            .await?;
+
+        let mut guest_connection_ids = Vec::new();
+        while let Some(collaborator) = collaborators.next().await {
+            let collaborator = collaborator?;
+            guest_connection_ids.push(collaborator.connection());
+        }
+        Ok(guest_connection_ids)
+    }
+
+    pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
+        self.transaction(|tx| async move {
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("project {} not found", project_id))?;
+            Ok(project.room_id)
+        })
+        .await
+    }
+
+    pub async fn follow(
+        &self,
+        project_id: ProjectId,
+        leader_connection: ConnectionId,
+        follower_connection: ConnectionId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            follower::ActiveModel {
+                room_id: ActiveValue::set(room_id),
+                project_id: ActiveValue::set(project_id),
+                leader_connection_server_id: ActiveValue::set(ServerId(
+                    leader_connection.owner_id as i32,
+                )),
+                leader_connection_id: ActiveValue::set(leader_connection.id as i32),
+                follower_connection_server_id: ActiveValue::set(ServerId(
+                    follower_connection.owner_id as i32,
+                )),
+                follower_connection_id: ActiveValue::set(follower_connection.id as i32),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room_id, &*tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn unfollow(
+        &self,
+        project_id: ProjectId,
+        leader_connection: ConnectionId,
+        follower_connection: ConnectionId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            follower::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(follower::Column::ProjectId.eq(project_id))
+                        .add(
+                            follower::Column::LeaderConnectionServerId
+                                .eq(leader_connection.owner_id),
+                        )
+                        .add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
+                        .add(
+                            follower::Column::FollowerConnectionServerId
+                                .eq(follower_connection.owner_id),
+                        )
+                        .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(room_id, &*tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+}

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

@@ -0,0 +1,1093 @@
+use super::*;
+
+impl Database {
+    pub async fn refresh_room(
+        &self,
+        room_id: RoomId,
+        new_server_id: ServerId,
+    ) -> Result<RoomGuard<RefreshedRoom>> {
+        self.room_transaction(room_id, |tx| async move {
+            let stale_participant_filter = Condition::all()
+                .add(room_participant::Column::RoomId.eq(room_id))
+                .add(room_participant::Column::AnsweringConnectionId.is_not_null())
+                .add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id));
+
+            let stale_participant_user_ids = room_participant::Entity::find()
+                .filter(stale_participant_filter.clone())
+                .all(&*tx)
+                .await?
+                .into_iter()
+                .map(|participant| participant.user_id)
+                .collect::<Vec<_>>();
+
+            // Delete participants who failed to reconnect and cancel their calls.
+            let mut canceled_calls_to_user_ids = Vec::new();
+            room_participant::Entity::delete_many()
+                .filter(stale_participant_filter)
+                .exec(&*tx)
+                .await?;
+            let called_participants = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::CallingUserId
+                                .is_in(stale_participant_user_ids.iter().copied()),
+                        )
+                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
+                )
+                .all(&*tx)
+                .await?;
+            room_participant::Entity::delete_many()
+                .filter(
+                    room_participant::Column::Id
+                        .is_in(called_participants.iter().map(|participant| participant.id)),
+                )
+                .exec(&*tx)
+                .await?;
+            canceled_calls_to_user_ids.extend(
+                called_participants
+                    .into_iter()
+                    .map(|participant| participant.user_id),
+            );
+
+            let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
+            let channel_members;
+            if let Some(channel_id) = channel_id {
+                channel_members = self.get_channel_members_internal(channel_id, &tx).await?;
+            } else {
+                channel_members = Vec::new();
+
+                // Delete the room if it becomes empty.
+                if room.participants.is_empty() {
+                    project::Entity::delete_many()
+                        .filter(project::Column::RoomId.eq(room_id))
+                        .exec(&*tx)
+                        .await?;
+                    room::Entity::delete_by_id(room_id).exec(&*tx).await?;
+                }
+            };
+
+            Ok(RefreshedRoom {
+                room,
+                channel_id,
+                channel_members,
+                stale_participant_user_ids,
+                canceled_calls_to_user_ids,
+            })
+        })
+        .await
+    }
+
+    pub async fn incoming_call_for_user(
+        &self,
+        user_id: UserId,
+    ) -> Result<Option<proto::IncomingCall>> {
+        self.transaction(|tx| async move {
+            let pending_participant = room_participant::Entity::find()
+                .filter(
+                    room_participant::Column::UserId
+                        .eq(user_id)
+                        .and(room_participant::Column::AnsweringConnectionId.is_null()),
+                )
+                .one(&*tx)
+                .await?;
+
+            if let Some(pending_participant) = pending_participant {
+                let room = self.get_room(pending_participant.room_id, &tx).await?;
+                Ok(Self::build_incoming_call(&room, user_id))
+            } else {
+                Ok(None)
+            }
+        })
+        .await
+    }
+
+    pub async fn create_room(
+        &self,
+        user_id: UserId,
+        connection: ConnectionId,
+        live_kit_room: &str,
+    ) -> Result<proto::Room> {
+        self.transaction(|tx| async move {
+            let room = room::ActiveModel {
+                live_kit_room: ActiveValue::set(live_kit_room.into()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+            room_participant::ActiveModel {
+                room_id: ActiveValue::set(room.id),
+                user_id: ActiveValue::set(user_id),
+                answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                answering_connection_server_id: ActiveValue::set(Some(ServerId(
+                    connection.owner_id as i32,
+                ))),
+                answering_connection_lost: ActiveValue::set(false),
+                calling_user_id: ActiveValue::set(user_id),
+                calling_connection_id: ActiveValue::set(connection.id as i32),
+                calling_connection_server_id: ActiveValue::set(Some(ServerId(
+                    connection.owner_id as i32,
+                ))),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room.id, &tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn call(
+        &self,
+        room_id: RoomId,
+        calling_user_id: UserId,
+        calling_connection: ConnectionId,
+        called_user_id: UserId,
+        initial_project_id: Option<ProjectId>,
+    ) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
+        self.room_transaction(room_id, |tx| async move {
+            room_participant::ActiveModel {
+                room_id: ActiveValue::set(room_id),
+                user_id: ActiveValue::set(called_user_id),
+                answering_connection_lost: ActiveValue::set(false),
+                calling_user_id: ActiveValue::set(calling_user_id),
+                calling_connection_id: ActiveValue::set(calling_connection.id as i32),
+                calling_connection_server_id: ActiveValue::set(Some(ServerId(
+                    calling_connection.owner_id as i32,
+                ))),
+                initial_project_id: ActiveValue::set(initial_project_id),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room_id, &tx).await?;
+            let incoming_call = Self::build_incoming_call(&room, called_user_id)
+                .ok_or_else(|| anyhow!("failed to build incoming call"))?;
+            Ok((room, incoming_call))
+        })
+        .await
+    }
+
+    pub async fn call_failed(
+        &self,
+        room_id: RoomId,
+        called_user_id: UserId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        self.room_transaction(room_id, |tx| async move {
+            room_participant::Entity::delete_many()
+                .filter(
+                    room_participant::Column::RoomId
+                        .eq(room_id)
+                        .and(room_participant::Column::UserId.eq(called_user_id)),
+                )
+                .exec(&*tx)
+                .await?;
+            let room = self.get_room(room_id, &tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn decline_call(
+        &self,
+        expected_room_id: Option<RoomId>,
+        user_id: UserId,
+    ) -> Result<Option<RoomGuard<proto::Room>>> {
+        self.optional_room_transaction(|tx| async move {
+            let mut filter = Condition::all()
+                .add(room_participant::Column::UserId.eq(user_id))
+                .add(room_participant::Column::AnsweringConnectionId.is_null());
+            if let Some(room_id) = expected_room_id {
+                filter = filter.add(room_participant::Column::RoomId.eq(room_id));
+            }
+            let participant = room_participant::Entity::find()
+                .filter(filter)
+                .one(&*tx)
+                .await?;
+
+            let participant = if let Some(participant) = participant {
+                participant
+            } else if expected_room_id.is_some() {
+                return Err(anyhow!("could not find call to decline"))?;
+            } else {
+                return Ok(None);
+            };
+
+            let room_id = participant.room_id;
+            room_participant::Entity::delete(participant.into_active_model())
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(room_id, &tx).await?;
+            Ok(Some((room_id, room)))
+        })
+        .await
+    }
+
+    pub async fn cancel_call(
+        &self,
+        room_id: RoomId,
+        calling_connection: ConnectionId,
+        called_user_id: UserId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        self.room_transaction(room_id, |tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(room_participant::Column::UserId.eq(called_user_id))
+                        .add(room_participant::Column::RoomId.eq(room_id))
+                        .add(
+                            room_participant::Column::CallingConnectionId
+                                .eq(calling_connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::CallingConnectionServerId
+                                .eq(calling_connection.owner_id as i32),
+                        )
+                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no call to cancel"))?;
+
+            room_participant::Entity::delete(participant.into_active_model())
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(room_id, &tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn join_room(
+        &self,
+        room_id: RoomId,
+        user_id: UserId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<JoinRoom>> {
+        self.room_transaction(room_id, |tx| async move {
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryChannelId {
+                ChannelId,
+            }
+            let channel_id: Option<ChannelId> = room::Entity::find()
+                .select_only()
+                .column(room::Column::ChannelId)
+                .filter(room::Column::Id.eq(room_id))
+                .into_values::<_, QueryChannelId>()
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such room"))?;
+
+            if let Some(channel_id) = channel_id {
+                self.check_user_is_channel_member(channel_id, user_id, &*tx)
+                    .await?;
+
+                room_participant::Entity::insert_many([room_participant::ActiveModel {
+                    room_id: ActiveValue::set(room_id),
+                    user_id: ActiveValue::set(user_id),
+                    answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                    answering_connection_server_id: ActiveValue::set(Some(ServerId(
+                        connection.owner_id as i32,
+                    ))),
+                    answering_connection_lost: ActiveValue::set(false),
+                    calling_user_id: ActiveValue::set(user_id),
+                    calling_connection_id: ActiveValue::set(connection.id as i32),
+                    calling_connection_server_id: ActiveValue::set(Some(ServerId(
+                        connection.owner_id as i32,
+                    ))),
+                    ..Default::default()
+                }])
+                .on_conflict(
+                    OnConflict::columns([room_participant::Column::UserId])
+                        .update_columns([
+                            room_participant::Column::AnsweringConnectionId,
+                            room_participant::Column::AnsweringConnectionServerId,
+                            room_participant::Column::AnsweringConnectionLost,
+                        ])
+                        .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            } else {
+                let result = room_participant::Entity::update_many()
+                    .filter(
+                        Condition::all()
+                            .add(room_participant::Column::RoomId.eq(room_id))
+                            .add(room_participant::Column::UserId.eq(user_id))
+                            .add(room_participant::Column::AnsweringConnectionId.is_null()),
+                    )
+                    .set(room_participant::ActiveModel {
+                        answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                        answering_connection_server_id: ActiveValue::set(Some(ServerId(
+                            connection.owner_id as i32,
+                        ))),
+                        answering_connection_lost: ActiveValue::set(false),
+                        ..Default::default()
+                    })
+                    .exec(&*tx)
+                    .await?;
+                if result.rows_affected == 0 {
+                    Err(anyhow!("room does not exist or was already joined"))?;
+                }
+            }
+
+            let room = self.get_room(room_id, &tx).await?;
+            let channel_members = if let Some(channel_id) = channel_id {
+                self.get_channel_members_internal(channel_id, &tx).await?
+            } else {
+                Vec::new()
+            };
+            Ok(JoinRoom {
+                room,
+                channel_id,
+                channel_members,
+            })
+        })
+        .await
+    }
+
+    pub async fn rejoin_room(
+        &self,
+        rejoin_room: proto::RejoinRoom,
+        user_id: UserId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<RejoinedRoom>> {
+        let room_id = RoomId::from_proto(rejoin_room.id);
+        self.room_transaction(room_id, |tx| async {
+            let tx = tx;
+            let participant_update = room_participant::Entity::update_many()
+                .filter(
+                    Condition::all()
+                        .add(room_participant::Column::RoomId.eq(room_id))
+                        .add(room_participant::Column::UserId.eq(user_id))
+                        .add(room_participant::Column::AnsweringConnectionId.is_not_null())
+                        .add(
+                            Condition::any()
+                                .add(room_participant::Column::AnsweringConnectionLost.eq(true))
+                                .add(
+                                    room_participant::Column::AnsweringConnectionServerId
+                                        .ne(connection.owner_id as i32),
+                                ),
+                        ),
+                )
+                .set(room_participant::ActiveModel {
+                    answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                    answering_connection_server_id: ActiveValue::set(Some(ServerId(
+                        connection.owner_id as i32,
+                    ))),
+                    answering_connection_lost: ActiveValue::set(false),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            if participant_update.rows_affected == 0 {
+                return Err(anyhow!("room does not exist or was already joined"))?;
+            }
+
+            let mut reshared_projects = Vec::new();
+            for reshared_project in &rejoin_room.reshared_projects {
+                let project_id = ProjectId::from_proto(reshared_project.project_id);
+                let project = project::Entity::find_by_id(project_id)
+                    .one(&*tx)
+                    .await?
+                    .ok_or_else(|| anyhow!("project does not exist"))?;
+                if project.host_user_id != user_id {
+                    return Err(anyhow!("no such project"))?;
+                }
+
+                let mut collaborators = project
+                    .find_related(project_collaborator::Entity)
+                    .all(&*tx)
+                    .await?;
+                let host_ix = collaborators
+                    .iter()
+                    .position(|collaborator| {
+                        collaborator.user_id == user_id && collaborator.is_host
+                    })
+                    .ok_or_else(|| anyhow!("host not found among collaborators"))?;
+                let host = collaborators.swap_remove(host_ix);
+                let old_connection_id = host.connection();
+
+                project::Entity::update(project::ActiveModel {
+                    host_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                    host_connection_server_id: ActiveValue::set(Some(ServerId(
+                        connection.owner_id as i32,
+                    ))),
+                    ..project.into_active_model()
+                })
+                .exec(&*tx)
+                .await?;
+                project_collaborator::Entity::update(project_collaborator::ActiveModel {
+                    connection_id: ActiveValue::set(connection.id as i32),
+                    connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
+                    ..host.into_active_model()
+                })
+                .exec(&*tx)
+                .await?;
+
+                self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
+                    .await?;
+
+                reshared_projects.push(ResharedProject {
+                    id: project_id,
+                    old_connection_id,
+                    collaborators: collaborators
+                        .iter()
+                        .map(|collaborator| ProjectCollaborator {
+                            connection_id: collaborator.connection(),
+                            user_id: collaborator.user_id,
+                            replica_id: collaborator.replica_id,
+                            is_host: collaborator.is_host,
+                        })
+                        .collect(),
+                    worktrees: reshared_project.worktrees.clone(),
+                });
+            }
+
+            project::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(project::Column::RoomId.eq(room_id))
+                        .add(project::Column::HostUserId.eq(user_id))
+                        .add(
+                            project::Column::Id
+                                .is_not_in(reshared_projects.iter().map(|project| project.id)),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let mut rejoined_projects = Vec::new();
+            for rejoined_project in &rejoin_room.rejoined_projects {
+                let project_id = ProjectId::from_proto(rejoined_project.id);
+                let Some(project) = project::Entity::find_by_id(project_id).one(&*tx).await? else {
+                    continue;
+                };
+
+                let mut worktrees = Vec::new();
+                let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
+                for db_worktree in db_worktrees {
+                    let mut worktree = RejoinedWorktree {
+                        id: db_worktree.id as u64,
+                        abs_path: db_worktree.abs_path,
+                        root_name: db_worktree.root_name,
+                        visible: db_worktree.visible,
+                        updated_entries: Default::default(),
+                        removed_entries: Default::default(),
+                        updated_repositories: Default::default(),
+                        removed_repositories: Default::default(),
+                        diagnostic_summaries: Default::default(),
+                        settings_files: Default::default(),
+                        scan_id: db_worktree.scan_id as u64,
+                        completed_scan_id: db_worktree.completed_scan_id as u64,
+                    };
+
+                    let rejoined_worktree = rejoined_project
+                        .worktrees
+                        .iter()
+                        .find(|worktree| worktree.id == db_worktree.id as u64);
+
+                    // File entries
+                    {
+                        let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
+                            worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
+                        } else {
+                            worktree_entry::Column::IsDeleted.eq(false)
+                        };
+
+                        let mut db_entries = worktree_entry::Entity::find()
+                            .filter(
+                                Condition::all()
+                                    .add(worktree_entry::Column::ProjectId.eq(project.id))
+                                    .add(worktree_entry::Column::WorktreeId.eq(worktree.id))
+                                    .add(entry_filter),
+                            )
+                            .stream(&*tx)
+                            .await?;
+
+                        while let Some(db_entry) = db_entries.next().await {
+                            let db_entry = db_entry?;
+                            if db_entry.is_deleted {
+                                worktree.removed_entries.push(db_entry.id as u64);
+                            } else {
+                                worktree.updated_entries.push(proto::Entry {
+                                    id: db_entry.id as u64,
+                                    is_dir: db_entry.is_dir,
+                                    path: db_entry.path,
+                                    inode: db_entry.inode as u64,
+                                    mtime: Some(proto::Timestamp {
+                                        seconds: db_entry.mtime_seconds as u64,
+                                        nanos: db_entry.mtime_nanos as u32,
+                                    }),
+                                    is_symlink: db_entry.is_symlink,
+                                    is_ignored: db_entry.is_ignored,
+                                    is_external: db_entry.is_external,
+                                    git_status: db_entry.git_status.map(|status| status as i32),
+                                });
+                            }
+                        }
+                    }
+
+                    // Repository Entries
+                    {
+                        let repository_entry_filter =
+                            if let Some(rejoined_worktree) = rejoined_worktree {
+                                worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
+                            } else {
+                                worktree_repository::Column::IsDeleted.eq(false)
+                            };
+
+                        let mut db_repositories = worktree_repository::Entity::find()
+                            .filter(
+                                Condition::all()
+                                    .add(worktree_repository::Column::ProjectId.eq(project.id))
+                                    .add(worktree_repository::Column::WorktreeId.eq(worktree.id))
+                                    .add(repository_entry_filter),
+                            )
+                            .stream(&*tx)
+                            .await?;
+
+                        while let Some(db_repository) = db_repositories.next().await {
+                            let db_repository = db_repository?;
+                            if db_repository.is_deleted {
+                                worktree
+                                    .removed_repositories
+                                    .push(db_repository.work_directory_id as u64);
+                            } else {
+                                worktree.updated_repositories.push(proto::RepositoryEntry {
+                                    work_directory_id: db_repository.work_directory_id as u64,
+                                    branch: db_repository.branch,
+                                });
+                            }
+                        }
+                    }
+
+                    worktrees.push(worktree);
+                }
+
+                let language_servers = project
+                    .find_related(language_server::Entity)
+                    .all(&*tx)
+                    .await?
+                    .into_iter()
+                    .map(|language_server| proto::LanguageServer {
+                        id: language_server.id as u64,
+                        name: language_server.name,
+                    })
+                    .collect::<Vec<_>>();
+
+                {
+                    let mut db_settings_files = worktree_settings_file::Entity::find()
+                        .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
+                        .stream(&*tx)
+                        .await?;
+                    while let Some(db_settings_file) = db_settings_files.next().await {
+                        let db_settings_file = db_settings_file?;
+                        if let Some(worktree) = worktrees
+                            .iter_mut()
+                            .find(|w| w.id == db_settings_file.worktree_id as u64)
+                        {
+                            worktree.settings_files.push(WorktreeSettingsFile {
+                                path: db_settings_file.path,
+                                content: db_settings_file.content,
+                            });
+                        }
+                    }
+                }
+
+                let mut collaborators = project
+                    .find_related(project_collaborator::Entity)
+                    .all(&*tx)
+                    .await?;
+                let self_collaborator = if let Some(self_collaborator_ix) = collaborators
+                    .iter()
+                    .position(|collaborator| collaborator.user_id == user_id)
+                {
+                    collaborators.swap_remove(self_collaborator_ix)
+                } else {
+                    continue;
+                };
+                let old_connection_id = self_collaborator.connection();
+                project_collaborator::Entity::update(project_collaborator::ActiveModel {
+                    connection_id: ActiveValue::set(connection.id as i32),
+                    connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
+                    ..self_collaborator.into_active_model()
+                })
+                .exec(&*tx)
+                .await?;
+
+                let collaborators = collaborators
+                    .into_iter()
+                    .map(|collaborator| ProjectCollaborator {
+                        connection_id: collaborator.connection(),
+                        user_id: collaborator.user_id,
+                        replica_id: collaborator.replica_id,
+                        is_host: collaborator.is_host,
+                    })
+                    .collect::<Vec<_>>();
+
+                rejoined_projects.push(RejoinedProject {
+                    id: project_id,
+                    old_connection_id,
+                    collaborators,
+                    worktrees,
+                    language_servers,
+                });
+            }
+
+            let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
+            let channel_members = if let Some(channel_id) = channel_id {
+                self.get_channel_members_internal(channel_id, &tx).await?
+            } else {
+                Vec::new()
+            };
+
+            Ok(RejoinedRoom {
+                room,
+                channel_id,
+                channel_members,
+                rejoined_projects,
+                reshared_projects,
+            })
+        })
+        .await
+    }
+
+    pub async fn leave_room(
+        &self,
+        connection: ConnectionId,
+    ) -> Result<Option<RoomGuard<LeftRoom>>> {
+        self.optional_room_transaction(|tx| async move {
+            let leaving_participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?;
+
+            if let Some(leaving_participant) = leaving_participant {
+                // Leave room.
+                let room_id = leaving_participant.room_id;
+                room_participant::Entity::delete_by_id(leaving_participant.id)
+                    .exec(&*tx)
+                    .await?;
+
+                // Cancel pending calls initiated by the leaving user.
+                let called_participants = room_participant::Entity::find()
+                    .filter(
+                        Condition::all()
+                            .add(
+                                room_participant::Column::CallingUserId
+                                    .eq(leaving_participant.user_id),
+                            )
+                            .add(room_participant::Column::AnsweringConnectionId.is_null()),
+                    )
+                    .all(&*tx)
+                    .await?;
+                room_participant::Entity::delete_many()
+                    .filter(
+                        room_participant::Column::Id
+                            .is_in(called_participants.iter().map(|participant| participant.id)),
+                    )
+                    .exec(&*tx)
+                    .await?;
+                let canceled_calls_to_user_ids = called_participants
+                    .into_iter()
+                    .map(|participant| participant.user_id)
+                    .collect();
+
+                // Detect left projects.
+                #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+                enum QueryProjectIds {
+                    ProjectId,
+                }
+                let project_ids: Vec<ProjectId> = project_collaborator::Entity::find()
+                    .select_only()
+                    .column_as(
+                        project_collaborator::Column::ProjectId,
+                        QueryProjectIds::ProjectId,
+                    )
+                    .filter(
+                        Condition::all()
+                            .add(
+                                project_collaborator::Column::ConnectionId.eq(connection.id as i32),
+                            )
+                            .add(
+                                project_collaborator::Column::ConnectionServerId
+                                    .eq(connection.owner_id as i32),
+                            ),
+                    )
+                    .into_values::<_, QueryProjectIds>()
+                    .all(&*tx)
+                    .await?;
+                let mut left_projects = HashMap::default();
+                let mut collaborators = project_collaborator::Entity::find()
+                    .filter(project_collaborator::Column::ProjectId.is_in(project_ids))
+                    .stream(&*tx)
+                    .await?;
+                while let Some(collaborator) = collaborators.next().await {
+                    let collaborator = collaborator?;
+                    let left_project =
+                        left_projects
+                            .entry(collaborator.project_id)
+                            .or_insert(LeftProject {
+                                id: collaborator.project_id,
+                                host_user_id: Default::default(),
+                                connection_ids: Default::default(),
+                                host_connection_id: Default::default(),
+                            });
+
+                    let collaborator_connection_id = collaborator.connection();
+                    if collaborator_connection_id != connection {
+                        left_project.connection_ids.push(collaborator_connection_id);
+                    }
+
+                    if collaborator.is_host {
+                        left_project.host_user_id = collaborator.user_id;
+                        left_project.host_connection_id = collaborator_connection_id;
+                    }
+                }
+                drop(collaborators);
+
+                // Leave projects.
+                project_collaborator::Entity::delete_many()
+                    .filter(
+                        Condition::all()
+                            .add(
+                                project_collaborator::Column::ConnectionId.eq(connection.id as i32),
+                            )
+                            .add(
+                                project_collaborator::Column::ConnectionServerId
+                                    .eq(connection.owner_id as i32),
+                            ),
+                    )
+                    .exec(&*tx)
+                    .await?;
+
+                // Unshare projects.
+                project::Entity::delete_many()
+                    .filter(
+                        Condition::all()
+                            .add(project::Column::RoomId.eq(room_id))
+                            .add(project::Column::HostConnectionId.eq(connection.id as i32))
+                            .add(
+                                project::Column::HostConnectionServerId
+                                    .eq(connection.owner_id as i32),
+                            ),
+                    )
+                    .exec(&*tx)
+                    .await?;
+
+                let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
+                let deleted = if room.participants.is_empty() {
+                    let result = room::Entity::delete_by_id(room_id)
+                        .filter(room::Column::ChannelId.is_null())
+                        .exec(&*tx)
+                        .await?;
+                    result.rows_affected > 0
+                } else {
+                    false
+                };
+
+                let channel_members = if let Some(channel_id) = channel_id {
+                    self.get_channel_members_internal(channel_id, &tx).await?
+                } else {
+                    Vec::new()
+                };
+                let left_room = LeftRoom {
+                    room,
+                    channel_id,
+                    channel_members,
+                    left_projects,
+                    canceled_calls_to_user_ids,
+                    deleted,
+                };
+
+                if left_room.room.participants.is_empty() {
+                    self.rooms.remove(&room_id);
+                }
+
+                Ok(Some((room_id, left_room)))
+            } else {
+                Ok(None)
+            }
+        })
+        .await
+    }
+
+    pub async fn update_room_participant_location(
+        &self,
+        room_id: RoomId,
+        connection: ConnectionId,
+        location: proto::ParticipantLocation,
+    ) -> Result<RoomGuard<proto::Room>> {
+        self.room_transaction(room_id, |tx| async {
+            let tx = tx;
+            let location_kind;
+            let location_project_id;
+            match location
+                .variant
+                .as_ref()
+                .ok_or_else(|| anyhow!("invalid location"))?
+            {
+                proto::participant_location::Variant::SharedProject(project) => {
+                    location_kind = 0;
+                    location_project_id = Some(ProjectId::from_proto(project.id));
+                }
+                proto::participant_location::Variant::UnsharedProject(_) => {
+                    location_kind = 1;
+                    location_project_id = None;
+                }
+                proto::participant_location::Variant::External(_) => {
+                    location_kind = 2;
+                    location_project_id = None;
+                }
+            }
+
+            let result = room_participant::Entity::update_many()
+                .filter(
+                    Condition::all()
+                        .add(room_participant::Column::RoomId.eq(room_id))
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .set(room_participant::ActiveModel {
+                    location_kind: ActiveValue::set(Some(location_kind)),
+                    location_project_id: ActiveValue::set(location_project_id),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+
+            if result.rows_affected == 1 {
+                let room = self.get_room(room_id, &tx).await?;
+                Ok(room)
+            } else {
+                Err(anyhow!("could not update room participant location"))?
+            }
+        })
+        .await
+    }
+
+    pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
+        self.transaction(|tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?;
+
+            if let Some(participant) = participant {
+                room_participant::Entity::update(room_participant::ActiveModel {
+                    answering_connection_lost: ActiveValue::set(true),
+                    ..participant.into_active_model()
+                })
+                .exec(&*tx)
+                .await?;
+            }
+
+            channel_buffer_collaborator::Entity::update_many()
+                .filter(
+                    Condition::all()
+                        .add(
+                            channel_buffer_collaborator::Column::ConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            channel_buffer_collaborator::Column::ConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .set(channel_buffer_collaborator::ActiveModel {
+                    connection_lost: ActiveValue::set(true),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+
+            Ok(())
+        })
+        .await
+    }
+
+    fn build_incoming_call(
+        room: &proto::Room,
+        called_user_id: UserId,
+    ) -> Option<proto::IncomingCall> {
+        let pending_participant = room
+            .pending_participants
+            .iter()
+            .find(|participant| participant.user_id == called_user_id.to_proto())?;
+
+        Some(proto::IncomingCall {
+            room_id: room.id,
+            calling_user_id: pending_participant.calling_user_id,
+            participant_user_ids: room
+                .participants
+                .iter()
+                .map(|participant| participant.user_id)
+                .collect(),
+            initial_project: room.participants.iter().find_map(|participant| {
+                let initial_project_id = pending_participant.initial_project_id?;
+                participant
+                    .projects
+                    .iter()
+                    .find(|project| project.id == initial_project_id)
+                    .cloned()
+            }),
+        })
+    }
+
+    pub async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result<proto::Room> {
+        let (_, room) = self.get_channel_room(room_id, tx).await?;
+        Ok(room)
+    }
+
+    async fn get_channel_room(
+        &self,
+        room_id: RoomId,
+        tx: &DatabaseTransaction,
+    ) -> Result<(Option<ChannelId>, proto::Room)> {
+        let db_room = room::Entity::find_by_id(room_id)
+            .one(tx)
+            .await?
+            .ok_or_else(|| anyhow!("could not find room"))?;
+
+        let mut db_participants = db_room
+            .find_related(room_participant::Entity)
+            .stream(tx)
+            .await?;
+        let mut participants = HashMap::default();
+        let mut pending_participants = Vec::new();
+        while let Some(db_participant) = db_participants.next().await {
+            let db_participant = db_participant?;
+            if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
+                .answering_connection_id
+                .zip(db_participant.answering_connection_server_id)
+            {
+                let location = match (
+                    db_participant.location_kind,
+                    db_participant.location_project_id,
+                ) {
+                    (Some(0), Some(project_id)) => {
+                        Some(proto::participant_location::Variant::SharedProject(
+                            proto::participant_location::SharedProject {
+                                id: project_id.to_proto(),
+                            },
+                        ))
+                    }
+                    (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject(
+                        Default::default(),
+                    )),
+                    _ => Some(proto::participant_location::Variant::External(
+                        Default::default(),
+                    )),
+                };
+
+                let answering_connection = ConnectionId {
+                    owner_id: answering_connection_server_id.0 as u32,
+                    id: answering_connection_id as u32,
+                };
+                participants.insert(
+                    answering_connection,
+                    proto::Participant {
+                        user_id: db_participant.user_id.to_proto(),
+                        peer_id: Some(answering_connection.into()),
+                        projects: Default::default(),
+                        location: Some(proto::ParticipantLocation { variant: location }),
+                    },
+                );
+            } else {
+                pending_participants.push(proto::PendingParticipant {
+                    user_id: db_participant.user_id.to_proto(),
+                    calling_user_id: db_participant.calling_user_id.to_proto(),
+                    initial_project_id: db_participant.initial_project_id.map(|id| id.to_proto()),
+                });
+            }
+        }
+        drop(db_participants);
+
+        let mut db_projects = db_room
+            .find_related(project::Entity)
+            .find_with_related(worktree::Entity)
+            .stream(tx)
+            .await?;
+
+        while let Some(row) = db_projects.next().await {
+            let (db_project, db_worktree) = row?;
+            let host_connection = db_project.host_connection()?;
+            if let Some(participant) = participants.get_mut(&host_connection) {
+                let project = if let Some(project) = participant
+                    .projects
+                    .iter_mut()
+                    .find(|project| project.id == db_project.id.to_proto())
+                {
+                    project
+                } else {
+                    participant.projects.push(proto::ParticipantProject {
+                        id: db_project.id.to_proto(),
+                        worktree_root_names: Default::default(),
+                    });
+                    participant.projects.last_mut().unwrap()
+                };
+
+                if let Some(db_worktree) = db_worktree {
+                    if db_worktree.visible {
+                        project.worktree_root_names.push(db_worktree.root_name);
+                    }
+                }
+            }
+        }
+        drop(db_projects);
+
+        let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
+        let mut followers = Vec::new();
+        while let Some(db_follower) = db_followers.next().await {
+            let db_follower = db_follower?;
+            followers.push(proto::Follower {
+                leader_id: Some(db_follower.leader_connection().into()),
+                follower_id: Some(db_follower.follower_connection().into()),
+                project_id: db_follower.project_id.to_proto(),
+            });
+        }
+
+        Ok((
+            db_room.channel_id,
+            proto::Room {
+                id: db_room.id.to_proto(),
+                live_kit_room: db_room.live_kit_room,
+                participants: participants.into_values().collect(),
+                pending_participants,
+                followers,
+            },
+        ))
+    }
+}

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

@@ -0,0 +1,81 @@
+use super::*;
+
+impl Database {
+    pub async fn create_server(&self, environment: &str) -> Result<ServerId> {
+        self.transaction(|tx| async move {
+            let server = server::ActiveModel {
+                environment: ActiveValue::set(environment.into()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+            Ok(server.id)
+        })
+        .await
+    }
+
+    pub async fn stale_room_ids(
+        &self,
+        environment: &str,
+        new_server_id: ServerId,
+    ) -> Result<Vec<RoomId>> {
+        self.transaction(|tx| async move {
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryAs {
+                RoomId,
+            }
+
+            let stale_server_epochs = self
+                .stale_server_ids(environment, new_server_id, &tx)
+                .await?;
+            Ok(room_participant::Entity::find()
+                .select_only()
+                .column(room_participant::Column::RoomId)
+                .distinct()
+                .filter(
+                    room_participant::Column::AnsweringConnectionServerId
+                        .is_in(stale_server_epochs),
+                )
+                .into_values::<_, QueryAs>()
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn delete_stale_servers(
+        &self,
+        environment: &str,
+        new_server_id: ServerId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            server::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(server::Column::Environment.eq(environment))
+                        .add(server::Column::Id.ne(new_server_id)),
+                )
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    async fn stale_server_ids(
+        &self,
+        environment: &str,
+        new_server_id: ServerId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<ServerId>> {
+        let stale_servers = server::Entity::find()
+            .filter(
+                Condition::all()
+                    .add(server::Column::Environment.eq(environment))
+                    .add(server::Column::Id.ne(new_server_id)),
+            )
+            .all(&*tx)
+            .await?;
+        Ok(stale_servers.into_iter().map(|server| server.id).collect())
+    }
+}

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

@@ -0,0 +1,349 @@
+use super::*;
+use hyper::StatusCode;
+
+impl Database {
+    pub async fn create_invite_from_code(
+        &self,
+        code: &str,
+        email_address: &str,
+        device_id: Option<&str>,
+        added_to_mailing_list: bool,
+    ) -> Result<Invite> {
+        self.transaction(|tx| async move {
+            let existing_user = user::Entity::find()
+                .filter(user::Column::EmailAddress.eq(email_address))
+                .one(&*tx)
+                .await?;
+
+            if existing_user.is_some() {
+                Err(anyhow!("email address is already in use"))?;
+            }
+
+            let inviting_user_with_invites = match user::Entity::find()
+                .filter(
+                    user::Column::InviteCode
+                        .eq(code)
+                        .and(user::Column::InviteCount.gt(0)),
+                )
+                .one(&*tx)
+                .await?
+            {
+                Some(inviting_user) => inviting_user,
+                None => {
+                    return Err(Error::Http(
+                        StatusCode::UNAUTHORIZED,
+                        "unable to find an invite code with invites remaining".to_string(),
+                    ))?
+                }
+            };
+            user::Entity::update_many()
+                .filter(
+                    user::Column::Id
+                        .eq(inviting_user_with_invites.id)
+                        .and(user::Column::InviteCount.gt(0)),
+                )
+                .col_expr(
+                    user::Column::InviteCount,
+                    Expr::col(user::Column::InviteCount).sub(1),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let signup = signup::Entity::insert(signup::ActiveModel {
+                email_address: ActiveValue::set(email_address.into()),
+                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
+                email_confirmation_sent: ActiveValue::set(false),
+                inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)),
+                platform_linux: ActiveValue::set(false),
+                platform_mac: ActiveValue::set(false),
+                platform_windows: ActiveValue::set(false),
+                platform_unknown: ActiveValue::set(true),
+                device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())),
+                added_to_mailing_list: ActiveValue::set(added_to_mailing_list),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::column(signup::Column::EmailAddress)
+                    .update_column(signup::Column::InvitingUserId)
+                    .to_owned(),
+            )
+            .exec_with_returning(&*tx)
+            .await?;
+
+            Ok(Invite {
+                email_address: signup.email_address,
+                email_confirmation_code: signup.email_confirmation_code,
+            })
+        })
+        .await
+    }
+
+    pub async fn create_user_from_invite(
+        &self,
+        invite: &Invite,
+        user: NewUserParams,
+    ) -> Result<Option<NewUserResult>> {
+        self.transaction(|tx| async {
+            let tx = tx;
+            let signup = signup::Entity::find()
+                .filter(
+                    signup::Column::EmailAddress
+                        .eq(invite.email_address.as_str())
+                        .and(
+                            signup::Column::EmailConfirmationCode
+                                .eq(invite.email_confirmation_code.as_str()),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
+
+            if signup.user_id.is_some() {
+                return Ok(None);
+            }
+
+            let user = user::Entity::insert(user::ActiveModel {
+                email_address: ActiveValue::set(Some(invite.email_address.clone())),
+                github_login: ActiveValue::set(user.github_login.clone()),
+                github_user_id: ActiveValue::set(Some(user.github_user_id)),
+                admin: ActiveValue::set(false),
+                invite_count: ActiveValue::set(user.invite_count),
+                invite_code: ActiveValue::set(Some(random_invite_code())),
+                metrics_id: ActiveValue::set(Uuid::new_v4()),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::column(user::Column::GithubLogin)
+                    .update_columns([
+                        user::Column::EmailAddress,
+                        user::Column::GithubUserId,
+                        user::Column::Admin,
+                    ])
+                    .to_owned(),
+            )
+            .exec_with_returning(&*tx)
+            .await?;
+
+            let mut signup = signup.into_active_model();
+            signup.user_id = ActiveValue::set(Some(user.id));
+            let signup = signup.update(&*tx).await?;
+
+            if let Some(inviting_user_id) = signup.inviting_user_id {
+                let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id {
+                    (inviting_user_id, user.id, true)
+                } else {
+                    (user.id, inviting_user_id, false)
+                };
+
+                contact::Entity::insert(contact::ActiveModel {
+                    user_id_a: ActiveValue::set(user_id_a),
+                    user_id_b: ActiveValue::set(user_id_b),
+                    a_to_b: ActiveValue::set(a_to_b),
+                    should_notify: ActiveValue::set(true),
+                    accepted: ActiveValue::set(true),
+                    ..Default::default()
+                })
+                .on_conflict(OnConflict::new().do_nothing().to_owned())
+                .exec_without_returning(&*tx)
+                .await?;
+            }
+
+            Ok(Some(NewUserResult {
+                user_id: user.id,
+                metrics_id: user.metrics_id.to_string(),
+                inviting_user_id: signup.inviting_user_id,
+                signup_device_id: signup.device_id,
+            }))
+        })
+        .await
+    }
+
+    pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> {
+        self.transaction(|tx| async move {
+            if count > 0 {
+                user::Entity::update_many()
+                    .filter(
+                        user::Column::Id
+                            .eq(id)
+                            .and(user::Column::InviteCode.is_null()),
+                    )
+                    .set(user::ActiveModel {
+                        invite_code: ActiveValue::set(Some(random_invite_code())),
+                        ..Default::default()
+                    })
+                    .exec(&*tx)
+                    .await?;
+            }
+
+            user::Entity::update_many()
+                .filter(user::Column::Id.eq(id))
+                .set(user::ActiveModel {
+                    invite_count: ActiveValue::set(count),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_invite_code_for_user(&self, id: UserId) -> Result<Option<(String, i32)>> {
+        self.transaction(|tx| async move {
+            match user::Entity::find_by_id(id).one(&*tx).await? {
+                Some(user) if user.invite_code.is_some() => {
+                    Ok(Some((user.invite_code.unwrap(), user.invite_count)))
+                }
+                _ => Ok(None),
+            }
+        })
+        .await
+    }
+
+    pub async fn get_user_for_invite_code(&self, code: &str) -> Result<User> {
+        self.transaction(|tx| async move {
+            user::Entity::find()
+                .filter(user::Column::InviteCode.eq(code))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| {
+                    Error::Http(
+                        StatusCode::NOT_FOUND,
+                        "that invite code does not exist".to_string(),
+                    )
+                })
+        })
+        .await
+    }
+
+    pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> {
+        self.transaction(|tx| async move {
+            signup::Entity::insert(signup::ActiveModel {
+                email_address: ActiveValue::set(signup.email_address.clone()),
+                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
+                email_confirmation_sent: ActiveValue::set(false),
+                platform_mac: ActiveValue::set(signup.platform_mac),
+                platform_windows: ActiveValue::set(signup.platform_windows),
+                platform_linux: ActiveValue::set(signup.platform_linux),
+                platform_unknown: ActiveValue::set(false),
+                editor_features: ActiveValue::set(Some(signup.editor_features.clone())),
+                programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())),
+                device_id: ActiveValue::set(signup.device_id.clone()),
+                added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::column(signup::Column::EmailAddress)
+                    .update_columns([
+                        signup::Column::PlatformMac,
+                        signup::Column::PlatformWindows,
+                        signup::Column::PlatformLinux,
+                        signup::Column::EditorFeatures,
+                        signup::Column::ProgrammingLanguages,
+                        signup::Column::DeviceId,
+                        signup::Column::AddedToMailingList,
+                    ])
+                    .to_owned(),
+            )
+            .exec(&*tx)
+            .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_signup(&self, email_address: &str) -> Result<signup::Model> {
+        self.transaction(|tx| async move {
+            let signup = signup::Entity::find()
+                .filter(signup::Column::EmailAddress.eq(email_address))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| {
+                    anyhow!("signup with email address {} doesn't exist", email_address)
+                })?;
+
+            Ok(signup)
+        })
+        .await
+    }
+
+    pub async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
+        self.transaction(|tx| async move {
+            let query = "
+                SELECT
+                    COUNT(*) as count,
+                    COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
+                    COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
+                    COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
+                    COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
+                FROM (
+                    SELECT *
+                    FROM signups
+                    WHERE
+                        NOT email_confirmation_sent
+                ) AS unsent
+            ";
+            Ok(
+                WaitlistSummary::find_by_statement(Statement::from_sql_and_values(
+                    self.pool.get_database_backend(),
+                    query.into(),
+                    vec![],
+                ))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("invalid result"))?,
+            )
+        })
+        .await
+    }
+
+    pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> {
+        let emails = invites
+            .iter()
+            .map(|s| s.email_address.as_str())
+            .collect::<Vec<_>>();
+        self.transaction(|tx| async {
+            let tx = tx;
+            signup::Entity::update_many()
+                .filter(signup::Column::EmailAddress.is_in(emails.iter().copied()))
+                .set(signup::ActiveModel {
+                    email_confirmation_sent: ActiveValue::set(true),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>> {
+        self.transaction(|tx| async move {
+            Ok(signup::Entity::find()
+                .select_only()
+                .column(signup::Column::EmailAddress)
+                .column(signup::Column::EmailConfirmationCode)
+                .filter(
+                    signup::Column::EmailConfirmationSent.eq(false).and(
+                        signup::Column::PlatformMac
+                            .eq(true)
+                            .or(signup::Column::PlatformUnknown.eq(true)),
+                    ),
+                )
+                .order_by_asc(signup::Column::CreatedAt)
+                .limit(count as u64)
+                .into_model()
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+}
+
+fn random_invite_code() -> String {
+    nanoid::nanoid!(16)
+}
+
+fn random_email_confirmation_code() -> String {
+    nanoid::nanoid!(64)
+}

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

@@ -0,0 +1,297 @@
+use super::*;
+
+impl Database {
+    pub async fn create_user(
+        &self,
+        email_address: &str,
+        admin: bool,
+        params: NewUserParams,
+    ) -> Result<NewUserResult> {
+        self.transaction(|tx| async {
+            let tx = tx;
+            let user = user::Entity::insert(user::ActiveModel {
+                email_address: ActiveValue::set(Some(email_address.into())),
+                github_login: ActiveValue::set(params.github_login.clone()),
+                github_user_id: ActiveValue::set(Some(params.github_user_id)),
+                admin: ActiveValue::set(admin),
+                metrics_id: ActiveValue::set(Uuid::new_v4()),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::column(user::Column::GithubLogin)
+                    .update_column(user::Column::GithubLogin)
+                    .to_owned(),
+            )
+            .exec_with_returning(&*tx)
+            .await?;
+
+            Ok(NewUserResult {
+                user_id: user.id,
+                metrics_id: user.metrics_id.to_string(),
+                signup_device_id: None,
+                inviting_user_id: None,
+            })
+        })
+        .await
+    }
+
+    pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
+        self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
+            .await
+    }
+
+    pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
+        self.transaction(|tx| async {
+            let tx = tx;
+            Ok(user::Entity::find()
+                .filter(user::Column::Id.is_in(ids.iter().copied()))
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
+        self.transaction(|tx| async move {
+            Ok(user::Entity::find()
+                .filter(user::Column::GithubLogin.eq(github_login))
+                .one(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn get_or_create_user_by_github_account(
+        &self,
+        github_login: &str,
+        github_user_id: Option<i32>,
+        github_email: Option<&str>,
+    ) -> Result<Option<User>> {
+        self.transaction(|tx| async move {
+            let tx = &*tx;
+            if let Some(github_user_id) = github_user_id {
+                if let Some(user_by_github_user_id) = user::Entity::find()
+                    .filter(user::Column::GithubUserId.eq(github_user_id))
+                    .one(tx)
+                    .await?
+                {
+                    let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
+                    user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
+                    Ok(Some(user_by_github_user_id.update(tx).await?))
+                } else if let Some(user_by_github_login) = user::Entity::find()
+                    .filter(user::Column::GithubLogin.eq(github_login))
+                    .one(tx)
+                    .await?
+                {
+                    let mut user_by_github_login = user_by_github_login.into_active_model();
+                    user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
+                    Ok(Some(user_by_github_login.update(tx).await?))
+                } else {
+                    let user = user::Entity::insert(user::ActiveModel {
+                        email_address: ActiveValue::set(github_email.map(|email| email.into())),
+                        github_login: ActiveValue::set(github_login.into()),
+                        github_user_id: ActiveValue::set(Some(github_user_id)),
+                        admin: ActiveValue::set(false),
+                        invite_count: ActiveValue::set(0),
+                        invite_code: ActiveValue::set(None),
+                        metrics_id: ActiveValue::set(Uuid::new_v4()),
+                        ..Default::default()
+                    })
+                    .exec_with_returning(&*tx)
+                    .await?;
+                    Ok(Some(user))
+                }
+            } else {
+                Ok(user::Entity::find()
+                    .filter(user::Column::GithubLogin.eq(github_login))
+                    .one(tx)
+                    .await?)
+            }
+        })
+        .await
+    }
+
+    pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
+        self.transaction(|tx| async move {
+            Ok(user::Entity::find()
+                .order_by_asc(user::Column::GithubLogin)
+                .limit(limit as u64)
+                .offset(page as u64 * limit as u64)
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn get_users_with_no_invites(
+        &self,
+        invited_by_another_user: bool,
+    ) -> Result<Vec<User>> {
+        self.transaction(|tx| async move {
+            Ok(user::Entity::find()
+                .filter(
+                    user::Column::InviteCount
+                        .eq(0)
+                        .and(if invited_by_another_user {
+                            user::Column::InviterId.is_not_null()
+                        } else {
+                            user::Column::InviterId.is_null()
+                        }),
+                )
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryAs {
+            MetricsId,
+        }
+
+        self.transaction(|tx| async move {
+            let metrics_id: Uuid = user::Entity::find_by_id(id)
+                .select_only()
+                .column(user::Column::MetricsId)
+                .into_values::<_, QueryAs>()
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("could not find user"))?;
+            Ok(metrics_id.to_string())
+        })
+        .await
+    }
+
+    pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> {
+        self.transaction(|tx| async move {
+            user::Entity::update_many()
+                .filter(user::Column::Id.eq(id))
+                .set(user::ActiveModel {
+                    admin: ActiveValue::set(is_admin),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
+        self.transaction(|tx| async move {
+            user::Entity::update_many()
+                .filter(user::Column::Id.eq(id))
+                .set(user::ActiveModel {
+                    connected_once: ActiveValue::set(connected_once),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn destroy_user(&self, id: UserId) -> Result<()> {
+        self.transaction(|tx| async move {
+            access_token::Entity::delete_many()
+                .filter(access_token::Column::UserId.eq(id))
+                .exec(&*tx)
+                .await?;
+            user::Entity::delete_by_id(id).exec(&*tx).await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
+        self.transaction(|tx| async {
+            let tx = tx;
+            let like_string = Self::fuzzy_like_string(name_query);
+            let query = "
+                SELECT users.*
+                FROM users
+                WHERE github_login ILIKE $1
+                ORDER BY github_login <-> $2
+                LIMIT $3
+            ";
+
+            Ok(user::Entity::find()
+                .from_raw_sql(Statement::from_sql_and_values(
+                    self.pool.get_database_backend(),
+                    query.into(),
+                    vec![like_string.into(), name_query.into(), limit.into()],
+                ))
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub fn fuzzy_like_string(string: &str) -> String {
+        let mut result = String::with_capacity(string.len() * 2 + 1);
+        for c in string.chars() {
+            if c.is_alphanumeric() {
+                result.push('%');
+                result.push(c);
+            }
+        }
+        result.push('%');
+        result
+    }
+
+    #[cfg(debug_assertions)]
+    pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
+        self.transaction(|tx| async move {
+            let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
+                flag: ActiveValue::set(flag.to_string()),
+                ..Default::default()
+            })
+            .exec(&*tx)
+            .await?
+            .last_insert_id;
+
+            Ok(flag)
+        })
+        .await
+    }
+
+    #[cfg(debug_assertions)]
+    pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
+        self.transaction(|tx| async move {
+            user_feature::Entity::insert(user_feature::ActiveModel {
+                user_id: ActiveValue::set(user),
+                feature_id: ActiveValue::set(flag),
+            })
+            .exec(&*tx)
+            .await?;
+
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
+        self.transaction(|tx| async move {
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryAs {
+                Flag,
+            }
+
+            let flags = user::Model {
+                id: user,
+                ..Default::default()
+            }
+            .find_linked(user::UserFlags)
+            .select_only()
+            .column(feature_flag::Column::Flag)
+            .into_values::<_, QueryAs>()
+            .all(&*tx)
+            .await?;
+
+            Ok(flags)
+        })
+        .await
+    }
+}

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

@@ -1,57 +0,0 @@
-use super::{SignupId, UserId};
-use sea_orm::{entity::prelude::*, FromQueryResult};
-use serde::{Deserialize, Serialize};
-
-#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "signups")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: SignupId,
-    pub email_address: String,
-    pub email_confirmation_code: String,
-    pub email_confirmation_sent: bool,
-    pub created_at: DateTime,
-    pub device_id: Option<String>,
-    pub user_id: Option<UserId>,
-    pub inviting_user_id: Option<UserId>,
-    pub platform_mac: bool,
-    pub platform_linux: bool,
-    pub platform_windows: bool,
-    pub platform_unknown: bool,
-    pub editor_features: Option<Vec<String>>,
-    pub programming_languages: Option<Vec<String>>,
-    pub added_to_mailing_list: bool,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {}
-
-impl ActiveModelBehavior for ActiveModel {}
-
-#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
-pub struct Invite {
-    pub email_address: String,
-    pub email_confirmation_code: String,
-}
-
-#[derive(Clone, Debug, Deserialize)]
-pub struct NewSignup {
-    pub email_address: String,
-    pub platform_mac: bool,
-    pub platform_windows: bool,
-    pub platform_linux: bool,
-    pub editor_features: Vec<String>,
-    pub programming_languages: Vec<String>,
-    pub device_id: Option<String>,
-    pub added_to_mailing_list: bool,
-    pub created_at: Option<DateTime>,
-}
-
-#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)]
-pub struct WaitlistSummary {
-    pub count: i64,
-    pub linux_count: i64,
-    pub mac_count: i64,
-    pub windows_count: i64,
-    pub unknown_count: i64,
-}

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

@@ -0,0 +1,26 @@
+pub mod access_token;
+pub mod buffer;
+pub mod buffer_operation;
+pub mod buffer_snapshot;
+pub mod channel;
+pub mod channel_buffer_collaborator;
+pub mod channel_member;
+pub mod channel_path;
+pub mod contact;
+pub mod feature_flag;
+pub mod follower;
+pub mod language_server;
+pub mod project;
+pub mod project_collaborator;
+pub mod room;
+pub mod room_participant;
+pub mod server;
+pub mod signup;
+pub mod user;
+pub mod user_feature;
+pub mod worktree;
+pub mod worktree_diagnostic_summary;
+pub mod worktree_entry;
+pub mod worktree_repository;
+pub mod worktree_repository_statuses;
+pub mod worktree_settings_file;

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

@@ -1,4 +1,4 @@
-use super::{AccessTokenId, UserId};
+use crate::db::{AccessTokenId, UserId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

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

@@ -0,0 +1,45 @@
+use crate::db::{BufferId, ChannelId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "buffers")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: BufferId,
+    pub epoch: i32,
+    pub channel_id: ChannelId,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(has_many = "super::buffer_operation::Entity")]
+    Operations,
+    #[sea_orm(has_many = "super::buffer_snapshot::Entity")]
+    Snapshots,
+    #[sea_orm(
+        belongs_to = "super::channel::Entity",
+        from = "Column::ChannelId",
+        to = "super::channel::Column::Id"
+    )]
+    Channel,
+}
+
+impl Related<super::buffer_operation::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Operations.def()
+    }
+}
+
+impl Related<super::buffer_snapshot::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Snapshots.def()
+    }
+}
+
+impl Related<super::channel::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Channel.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -0,0 +1,34 @@
+use crate::db::BufferId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "buffer_operations")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub buffer_id: BufferId,
+    #[sea_orm(primary_key)]
+    pub epoch: i32,
+    #[sea_orm(primary_key)]
+    pub lamport_timestamp: i32,
+    #[sea_orm(primary_key)]
+    pub replica_id: i32,
+    pub value: Vec<u8>,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::buffer::Entity",
+        from = "Column::BufferId",
+        to = "super::buffer::Column::Id"
+    )]
+    Buffer,
+}
+
+impl Related<super::buffer::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Buffer.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -0,0 +1,31 @@
+use crate::db::BufferId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "buffer_snapshots")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub buffer_id: BufferId,
+    #[sea_orm(primary_key)]
+    pub epoch: i32,
+    pub text: String,
+    pub operation_serialization_version: i32,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::buffer::Entity",
+        from = "Column::BufferId",
+        to = "super::buffer::Column::Id"
+    )]
+    Buffer,
+}
+
+impl Related<super::buffer::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Buffer.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -0,0 +1,48 @@
+use crate::db::ChannelId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "channels")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: ChannelId,
+    pub name: String,
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(has_one = "super::room::Entity")]
+    Room,
+    #[sea_orm(has_one = "super::buffer::Entity")]
+    Buffer,
+    #[sea_orm(has_many = "super::channel_member::Entity")]
+    Member,
+    #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")]
+    BufferCollaborators,
+}
+
+impl Related<super::channel_member::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Member.def()
+    }
+}
+
+impl Related<super::room::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Room.def()
+    }
+}
+
+impl Related<super::buffer::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Buffer.def()
+    }
+}
+
+impl Related<super::channel_buffer_collaborator::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::BufferCollaborators.def()
+    }
+}

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

@@ -0,0 +1,43 @@
+use crate::db::{ChannelBufferCollaboratorId, ChannelId, ReplicaId, ServerId, UserId};
+use rpc::ConnectionId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "channel_buffer_collaborators")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: ChannelBufferCollaboratorId,
+    pub channel_id: ChannelId,
+    pub connection_id: i32,
+    pub connection_server_id: ServerId,
+    pub connection_lost: bool,
+    pub user_id: UserId,
+    pub replica_id: ReplicaId,
+}
+
+impl Model {
+    pub fn connection(&self) -> ConnectionId {
+        ConnectionId {
+            owner_id: self.connection_server_id.0 as u32,
+            id: self.connection_id as u32,
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::channel::Entity",
+        from = "Column::ChannelId",
+        to = "super::channel::Column::Id"
+    )]
+    Channel,
+}
+
+impl Related<super::channel::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Channel.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -0,0 +1,59 @@
+use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "channel_members")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: ChannelMemberId,
+    pub channel_id: ChannelId,
+    pub user_id: UserId,
+    pub accepted: bool,
+    pub admin: bool,
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::channel::Entity",
+        from = "Column::ChannelId",
+        to = "super::channel::Column::Id"
+    )]
+    Channel,
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::UserId",
+        to = "super::user::Column::Id"
+    )]
+    User,
+}
+
+impl Related<super::channel::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Channel.def()
+    }
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::User.def()
+    }
+}
+
+#[derive(Debug)]
+pub struct UserToChannel;
+
+impl Linked for UserToChannel {
+    type FromEntity = super::user::Entity;
+
+    type ToEntity = super::channel::Entity;
+
+    fn link(&self) -> Vec<RelationDef> {
+        vec![
+            channel_member::Relation::User.def().rev(),
+            channel_member::Relation::Channel.def(),
+        ]
+    }
+}

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

@@ -0,0 +1,15 @@
+use crate::db::ChannelId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "channel_paths")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id_path: String,
+    pub channel_id: ChannelId,
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}

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

@@ -1,4 +1,4 @@
-use super::{ContactId, UserId};
+use crate::db::{ContactId, UserId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
@@ -30,29 +30,3 @@ pub enum Relation {
 }
 
 impl ActiveModelBehavior for ActiveModel {}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Contact {
-    Accepted {
-        user_id: UserId,
-        should_notify: bool,
-        busy: bool,
-    },
-    Outgoing {
-        user_id: UserId,
-    },
-    Incoming {
-        user_id: UserId,
-        should_notify: bool,
-    },
-}
-
-impl Contact {
-    pub fn user_id(&self) -> UserId {
-        match self {
-            Contact::Accepted { user_id, .. } => *user_id,
-            Contact::Outgoing { user_id } => *user_id,
-            Contact::Incoming { user_id, .. } => *user_id,
-        }
-    }
-}

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

@@ -0,0 +1,40 @@
+use sea_orm::entity::prelude::*;
+
+use crate::db::FlagId;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "feature_flags")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: FlagId,
+    pub flag: String,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(has_many = "super::user_feature::Entity")]
+    UserFeature,
+}
+
+impl Related<super::user_feature::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::UserFeature.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+pub struct FlaggedUsers;
+
+impl Linked for FlaggedUsers {
+    type FromEntity = Entity;
+
+    type ToEntity = super::user::Entity;
+
+    fn link(&self) -> Vec<RelationDef> {
+        vec![
+            super::user_feature::Relation::Flag.def().rev(),
+            super::user_feature::Relation::User.def(),
+        ]
+    }
+}

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

@@ -1,9 +1,8 @@
-use super::{FollowerId, ProjectId, RoomId, ServerId};
+use crate::db::{FollowerId, ProjectId, RoomId, ServerId};
 use rpc::ConnectionId;
 use sea_orm::entity::prelude::*;
-use serde::Serialize;
 
-#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
 #[sea_orm(table_name = "followers")]
 pub struct Model {
     #[sea_orm(primary_key)]

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

@@ -1,4 +1,4 @@
-use super::ProjectId;
+use crate::db::ProjectId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

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

@@ -1,4 +1,4 @@
-use super::{ProjectId, Result, RoomId, ServerId, UserId};
+use crate::db::{ProjectId, Result, RoomId, ServerId, UserId};
 use anyhow::anyhow;
 use rpc::ConnectionId;
 use sea_orm::entity::prelude::*;

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

@@ -1,4 +1,4 @@
-use super::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId};
+use crate::db::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId};
 use rpc::ConnectionId;
 use sea_orm::entity::prelude::*;
 

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

@@ -1,12 +1,13 @@
-use super::RoomId;
+use crate::db::{ChannelId, RoomId};
 use sea_orm::entity::prelude::*;
 
-#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)]
 #[sea_orm(table_name = "rooms")]
 pub struct Model {
     #[sea_orm(primary_key)]
     pub id: RoomId,
     pub live_kit_room: String,
+    pub channel_id: Option<ChannelId>,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -17,6 +18,12 @@ pub enum Relation {
     Project,
     #[sea_orm(has_many = "super::follower::Entity")]
     Follower,
+    #[sea_orm(
+        belongs_to = "super::channel::Entity",
+        from = "Column::ChannelId",
+        to = "super::channel::Column::Id"
+    )]
+    Channel,
 }
 
 impl Related<super::room_participant::Entity> for Entity {
@@ -37,4 +44,10 @@ impl Related<super::follower::Entity> for Entity {
     }
 }
 
+impl Related<super::channel::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Channel.def()
+    }
+}
+
 impl ActiveModelBehavior for ActiveModel {}

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

@@ -1,4 +1,4 @@
-use super::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
+use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

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

@@ -1,4 +1,4 @@
-use super::ServerId;
+use crate::db::ServerId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

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

@@ -0,0 +1,28 @@
+use crate::db::{SignupId, UserId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "signups")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: SignupId,
+    pub email_address: String,
+    pub email_confirmation_code: String,
+    pub email_confirmation_sent: bool,
+    pub created_at: DateTime,
+    pub device_id: Option<String>,
+    pub user_id: Option<UserId>,
+    pub inviting_user_id: Option<UserId>,
+    pub platform_mac: bool,
+    pub platform_linux: bool,
+    pub platform_windows: bool,
+    pub platform_unknown: bool,
+    pub editor_features: Option<Vec<String>>,
+    pub programming_languages: Option<Vec<String>>,
+    pub added_to_mailing_list: bool,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -1,4 +1,4 @@
-use super::UserId;
+use crate::db::UserId;
 use sea_orm::entity::prelude::*;
 use serde::Serialize;
 
@@ -26,6 +26,10 @@ pub enum Relation {
     RoomParticipant,
     #[sea_orm(has_many = "super::project::Entity")]
     HostedProjects,
+    #[sea_orm(has_many = "super::channel_member::Entity")]
+    ChannelMemberships,
+    #[sea_orm(has_many = "super::user_feature::Entity")]
+    UserFeatures,
 }
 
 impl Related<super::access_token::Entity> for Entity {
@@ -46,4 +50,31 @@ impl Related<super::project::Entity> for Entity {
     }
 }
 
+impl Related<super::channel_member::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::ChannelMemberships.def()
+    }
+}
+
+impl Related<super::user_feature::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::UserFeatures.def()
+    }
+}
+
 impl ActiveModelBehavior for ActiveModel {}
+
+pub struct UserFlags;
+
+impl Linked for UserFlags {
+    type FromEntity = Entity;
+
+    type ToEntity = super::feature_flag::Entity;
+
+    fn link(&self) -> Vec<RelationDef> {
+        vec![
+            super::user_feature::Relation::User.def().rev(),
+            super::user_feature::Relation::Flag.def(),
+        ]
+    }
+}

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

@@ -0,0 +1,42 @@
+use sea_orm::entity::prelude::*;
+
+use crate::db::{FlagId, UserId};
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "user_features")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub user_id: UserId,
+    #[sea_orm(primary_key)]
+    pub feature_id: FlagId,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::feature_flag::Entity",
+        from = "Column::FeatureId",
+        to = "super::feature_flag::Column::Id"
+    )]
+    Flag,
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::UserId",
+        to = "super::user::Column::Id"
+    )]
+    User,
+}
+
+impl Related<super::feature_flag::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Flag.def()
+    }
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::User.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -1,4 +1,4 @@
-use super::ProjectId;
+use crate::db::ProjectId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

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

@@ -1,4 +1,4 @@
-use super::ProjectId;
+use crate::db::ProjectId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

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

@@ -1,1123 +1,144 @@
+mod buffer_tests;
+mod db_tests;
+mod feature_flag_tests;
+
 use super::*;
-use gpui::executor::{Background, Deterministic};
+use gpui::executor::Background;
+use parking_lot::Mutex;
+use sea_orm::ConnectionTrait;
+use sqlx::migrate::MigrateDatabase;
 use std::sync::Arc;
 
-#[cfg(test)]
-use pretty_assertions::{assert_eq, assert_ne};
-
-macro_rules! test_both_dbs {
-    ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => {
-        #[gpui::test]
-        async fn $postgres_test_name() {
-            let test_db = TestDb::postgres(Deterministic::new(0).build_background());
-            let $db = test_db.db();
-            $body
-        }
-
-        #[gpui::test]
-        async fn $sqlite_test_name() {
-            let test_db = TestDb::sqlite(Deterministic::new(0).build_background());
-            let $db = test_db.db();
-            $body
-        }
-    };
+pub struct TestDb {
+    pub db: Option<Arc<Database>>,
+    pub connection: Option<sqlx::AnyConnection>,
 }
 
-test_both_dbs!(
-    test_get_users_by_ids_postgres,
-    test_get_users_by_ids_sqlite,
-    db,
-    {
-        let mut user_ids = Vec::new();
-        let mut user_metric_ids = Vec::new();
-        for i in 1..=4 {
-            let user = db
-                .create_user(
-                    &format!("user{i}@example.com"),
-                    false,
-                    NewUserParams {
-                        github_login: format!("user{i}"),
-                        github_user_id: i,
-                        invite_count: 0,
-                    },
-                )
-                .await
-                .unwrap();
-            user_ids.push(user.user_id);
-            user_metric_ids.push(user.metrics_id);
-        }
-
-        assert_eq!(
-            db.get_users_by_ids(user_ids.clone()).await.unwrap(),
-            vec![
-                User {
-                    id: user_ids[0],
-                    github_login: "user1".to_string(),
-                    github_user_id: Some(1),
-                    email_address: Some("user1@example.com".to_string()),
-                    admin: false,
-                    metrics_id: user_metric_ids[0].parse().unwrap(),
-                    ..Default::default()
-                },
-                User {
-                    id: user_ids[1],
-                    github_login: "user2".to_string(),
-                    github_user_id: Some(2),
-                    email_address: Some("user2@example.com".to_string()),
-                    admin: false,
-                    metrics_id: user_metric_ids[1].parse().unwrap(),
-                    ..Default::default()
-                },
-                User {
-                    id: user_ids[2],
-                    github_login: "user3".to_string(),
-                    github_user_id: Some(3),
-                    email_address: Some("user3@example.com".to_string()),
-                    admin: false,
-                    metrics_id: user_metric_ids[2].parse().unwrap(),
-                    ..Default::default()
-                },
-                User {
-                    id: user_ids[3],
-                    github_login: "user4".to_string(),
-                    github_user_id: Some(4),
-                    email_address: Some("user4@example.com".to_string()),
-                    admin: false,
-                    metrics_id: user_metric_ids[3].parse().unwrap(),
-                    ..Default::default()
-                }
-            ]
-        );
-    }
-);
-
-test_both_dbs!(
-    test_get_or_create_user_by_github_account_postgres,
-    test_get_or_create_user_by_github_account_sqlite,
-    db,
-    {
-        let user_id1 = db
-            .create_user(
-                "user1@example.com",
-                false,
-                NewUserParams {
-                    github_login: "login1".into(),
-                    github_user_id: 101,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap()
-            .user_id;
-        let user_id2 = db
-            .create_user(
-                "user2@example.com",
-                false,
-                NewUserParams {
-                    github_login: "login2".into(),
-                    github_user_id: 102,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap()
-            .user_id;
-
-        let user = db
-            .get_or_create_user_by_github_account("login1", None, None)
-            .await
-            .unwrap()
+impl TestDb {
+    pub fn sqlite(background: Arc<Background>) -> Self {
+        let url = format!("sqlite::memory:");
+        let runtime = tokio::runtime::Builder::new_current_thread()
+            .enable_io()
+            .enable_time()
+            .build()
             .unwrap();
-        assert_eq!(user.id, user_id1);
-        assert_eq!(&user.github_login, "login1");
-        assert_eq!(user.github_user_id, Some(101));
 
-        assert!(db
-            .get_or_create_user_by_github_account("non-existent-login", None, None)
-            .await
-            .unwrap()
-            .is_none());
+        let mut db = runtime.block_on(async {
+            let mut options = ConnectOptions::new(url);
+            options.max_connections(5);
+            let db = Database::new(options, Executor::Deterministic(background))
+                .await
+                .unwrap();
+            let sql = include_str!(concat!(
+                env!("CARGO_MANIFEST_DIR"),
+                "/migrations.sqlite/20221109000000_test_schema.sql"
+            ));
+            db.pool
+                .execute(sea_orm::Statement::from_string(
+                    db.pool.get_database_backend(),
+                    sql.into(),
+                ))
+                .await
+                .unwrap();
+            db
+        });
 
-        let user = db
-            .get_or_create_user_by_github_account("the-new-login2", Some(102), None)
-            .await
-            .unwrap()
-            .unwrap();
-        assert_eq!(user.id, user_id2);
-        assert_eq!(&user.github_login, "the-new-login2");
-        assert_eq!(user.github_user_id, Some(102));
+        db.runtime = Some(runtime);
 
-        let user = db
-            .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com"))
-            .await
-            .unwrap()
-            .unwrap();
-        assert_eq!(&user.github_login, "login3");
-        assert_eq!(user.github_user_id, Some(103));
-        assert_eq!(user.email_address, Some("user3@example.com".into()));
+        Self {
+            db: Some(Arc::new(db)),
+            connection: None,
+        }
     }
-);
-
-test_both_dbs!(
-    test_create_access_tokens_postgres,
-    test_create_access_tokens_sqlite,
-    db,
-    {
-        let user = db
-            .create_user(
-                "u1@example.com",
-                false,
-                NewUserParams {
-                    github_login: "u1".into(),
-                    github_user_id: 1,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap()
-            .user_id;
-
-        let token_1 = db.create_access_token(user, "h1", 2).await.unwrap();
-        let token_2 = db.create_access_token(user, "h2", 2).await.unwrap();
-        assert_eq!(
-            db.get_access_token(token_1).await.unwrap(),
-            access_token::Model {
-                id: token_1,
-                user_id: user,
-                hash: "h1".into(),
-            }
-        );
-        assert_eq!(
-            db.get_access_token(token_2).await.unwrap(),
-            access_token::Model {
-                id: token_2,
-                user_id: user,
-                hash: "h2".into()
-            }
-        );
-
-        let token_3 = db.create_access_token(user, "h3", 2).await.unwrap();
-        assert_eq!(
-            db.get_access_token(token_3).await.unwrap(),
-            access_token::Model {
-                id: token_3,
-                user_id: user,
-                hash: "h3".into()
-            }
-        );
-        assert_eq!(
-            db.get_access_token(token_2).await.unwrap(),
-            access_token::Model {
-                id: token_2,
-                user_id: user,
-                hash: "h2".into()
-            }
-        );
-        assert!(db.get_access_token(token_1).await.is_err());
 
-        let token_4 = db.create_access_token(user, "h4", 2).await.unwrap();
-        assert_eq!(
-            db.get_access_token(token_4).await.unwrap(),
-            access_token::Model {
-                id: token_4,
-                user_id: user,
-                hash: "h4".into()
-            }
-        );
-        assert_eq!(
-            db.get_access_token(token_3).await.unwrap(),
-            access_token::Model {
-                id: token_3,
-                user_id: user,
-                hash: "h3".into()
-            }
-        );
-        assert!(db.get_access_token(token_2).await.is_err());
-        assert!(db.get_access_token(token_1).await.is_err());
-    }
-);
+    pub fn postgres(background: Arc<Background>) -> Self {
+        static LOCK: Mutex<()> = Mutex::new(());
 
-test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
-    let mut user_ids = Vec::new();
-    for i in 0..3 {
-        user_ids.push(
-            db.create_user(
-                &format!("user{i}@example.com"),
-                false,
-                NewUserParams {
-                    github_login: format!("user{i}"),
-                    github_user_id: i,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap()
-            .user_id,
+        let _guard = LOCK.lock();
+        let mut rng = StdRng::from_entropy();
+        let url = format!(
+            "postgres://postgres@localhost/zed-test-{}",
+            rng.gen::<u128>()
         );
-    }
-
-    let user_1 = user_ids[0];
-    let user_2 = user_ids[1];
-    let user_3 = user_ids[2];
-
-    // User starts with no contacts
-    assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
-
-    // User requests a contact. Both users see the pending request.
-    db.send_contact_request(user_1, user_2).await.unwrap();
-    assert!(!db.has_contact(user_1, user_2).await.unwrap());
-    assert!(!db.has_contact(user_2, user_1).await.unwrap());
-    assert_eq!(
-        db.get_contacts(user_1).await.unwrap(),
-        &[Contact::Outgoing { user_id: user_2 }],
-    );
-    assert_eq!(
-        db.get_contacts(user_2).await.unwrap(),
-        &[Contact::Incoming {
-            user_id: user_1,
-            should_notify: true
-        }]
-    );
-
-    // User 2 dismisses the contact request notification without accepting or rejecting.
-    // We shouldn't notify them again.
-    db.dismiss_contact_notification(user_1, user_2)
-        .await
-        .unwrap_err();
-    db.dismiss_contact_notification(user_2, user_1)
-        .await
-        .unwrap();
-    assert_eq!(
-        db.get_contacts(user_2).await.unwrap(),
-        &[Contact::Incoming {
-            user_id: user_1,
-            should_notify: false
-        }]
-    );
-
-    // User can't accept their own contact request
-    db.respond_to_contact_request(user_1, user_2, true)
-        .await
-        .unwrap_err();
-
-    // User accepts a contact request. Both users see the contact.
-    db.respond_to_contact_request(user_2, user_1, true)
-        .await
-        .unwrap();
-    assert_eq!(
-        db.get_contacts(user_1).await.unwrap(),
-        &[Contact::Accepted {
-            user_id: user_2,
-            should_notify: true,
-            busy: false,
-        }],
-    );
-    assert!(db.has_contact(user_1, user_2).await.unwrap());
-    assert!(db.has_contact(user_2, user_1).await.unwrap());
-    assert_eq!(
-        db.get_contacts(user_2).await.unwrap(),
-        &[Contact::Accepted {
-            user_id: user_1,
-            should_notify: false,
-            busy: false,
-        }]
-    );
-
-    // Users cannot re-request existing contacts.
-    db.send_contact_request(user_1, user_2).await.unwrap_err();
-    db.send_contact_request(user_2, user_1).await.unwrap_err();
-
-    // Users can't dismiss notifications of them accepting other users' requests.
-    db.dismiss_contact_notification(user_2, user_1)
-        .await
-        .unwrap_err();
-    assert_eq!(
-        db.get_contacts(user_1).await.unwrap(),
-        &[Contact::Accepted {
-            user_id: user_2,
-            should_notify: true,
-            busy: false,
-        }]
-    );
-
-    // Users can dismiss notifications of other users accepting their requests.
-    db.dismiss_contact_notification(user_1, user_2)
-        .await
-        .unwrap();
-    assert_eq!(
-        db.get_contacts(user_1).await.unwrap(),
-        &[Contact::Accepted {
-            user_id: user_2,
-            should_notify: false,
-            busy: false,
-        }]
-    );
-
-    // Users send each other concurrent contact requests and
-    // see that they are immediately accepted.
-    db.send_contact_request(user_1, user_3).await.unwrap();
-    db.send_contact_request(user_3, user_1).await.unwrap();
-    assert_eq!(
-        db.get_contacts(user_1).await.unwrap(),
-        &[
-            Contact::Accepted {
-                user_id: user_2,
-                should_notify: false,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user_3,
-                should_notify: false,
-                busy: false,
-            }
-        ]
-    );
-    assert_eq!(
-        db.get_contacts(user_3).await.unwrap(),
-        &[Contact::Accepted {
-            user_id: user_1,
-            should_notify: false,
-            busy: false,
-        }],
-    );
-
-    // User declines a contact request. Both users see that it is gone.
-    db.send_contact_request(user_2, user_3).await.unwrap();
-    db.respond_to_contact_request(user_3, user_2, false)
-        .await
-        .unwrap();
-    assert!(!db.has_contact(user_2, user_3).await.unwrap());
-    assert!(!db.has_contact(user_3, user_2).await.unwrap());
-    assert_eq!(
-        db.get_contacts(user_2).await.unwrap(),
-        &[Contact::Accepted {
-            user_id: user_1,
-            should_notify: false,
-            busy: false,
-        }]
-    );
-    assert_eq!(
-        db.get_contacts(user_3).await.unwrap(),
-        &[Contact::Accepted {
-            user_id: user_1,
-            should_notify: false,
-            busy: false,
-        }],
-    );
-});
-
-test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
-    let NewUserResult {
-        user_id: user1,
-        metrics_id: metrics_id1,
-        ..
-    } = db
-        .create_user(
-            "person1@example.com",
-            false,
-            NewUserParams {
-                github_login: "person1".into(),
-                github_user_id: 101,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap();
-    let NewUserResult {
-        user_id: user2,
-        metrics_id: metrics_id2,
-        ..
-    } = db
-        .create_user(
-            "person2@example.com",
-            false,
-            NewUserParams {
-                github_login: "person2".into(),
-                github_user_id: 102,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap();
-
-    assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
-    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
-    assert_eq!(metrics_id1.len(), 36);
-    assert_eq!(metrics_id2.len(), 36);
-    assert_ne!(metrics_id1, metrics_id2);
-});
-
-test_both_dbs!(
-    test_project_count_postgres,
-    test_project_count_sqlite,
-    db,
-    {
-        let owner_id = db.create_server("test").await.unwrap().0 as u32;
-
-        let user1 = db
-            .create_user(
-                &format!("admin@example.com"),
-                true,
-                NewUserParams {
-                    github_login: "admin".into(),
-                    github_user_id: 0,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap();
-        let user2 = db
-            .create_user(
-                &format!("user@example.com"),
-                false,
-                NewUserParams {
-                    github_login: "user".into(),
-                    github_user_id: 1,
-                    invite_count: 0,
-                },
-            )
-            .await
+        let runtime = tokio::runtime::Builder::new_current_thread()
+            .enable_io()
+            .enable_time()
+            .build()
             .unwrap();
 
-        let room_id = RoomId::from_proto(
-            db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
+        let mut db = runtime.block_on(async {
+            sqlx::Postgres::create_database(&url)
                 .await
-                .unwrap()
-                .id,
-        );
-        db.call(
-            room_id,
-            user1.user_id,
-            ConnectionId { owner_id, id: 0 },
-            user2.user_id,
-            None,
-        )
-        .await
-        .unwrap();
-        db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
-            .await
-            .unwrap();
-        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
-
-        db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
-            .await
-            .unwrap();
-        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
-
-        db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
-            .await
-            .unwrap();
-        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
-
-        // Projects shared by admins aren't counted.
-        db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
-            .await
-            .unwrap();
-        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
+                .expect("failed to create test db");
+            let mut options = ConnectOptions::new(url);
+            options
+                .max_connections(5)
+                .idle_timeout(Duration::from_secs(0));
+            let db = Database::new(options, Executor::Deterministic(background))
+                .await
+                .unwrap();
+            let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
+            db.migrate(Path::new(migrations_path), false).await.unwrap();
+            db
+        });
 
-        db.leave_room(ConnectionId { owner_id, id: 1 })
-            .await
-            .unwrap();
-        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
-    }
-);
+        db.runtime = Some(runtime);
 
-#[test]
-fn test_fuzzy_like_string() {
-    assert_eq!(Database::fuzzy_like_string("abcd"), "%a%b%c%d%");
-    assert_eq!(Database::fuzzy_like_string("x y"), "%x%y%");
-    assert_eq!(Database::fuzzy_like_string(" z  "), "%z%");
-}
-
-#[gpui::test]
-async fn test_fuzzy_search_users() {
-    let test_db = TestDb::postgres(build_background_executor());
-    let db = test_db.db();
-    for (i, github_login) in [
-        "California",
-        "colorado",
-        "oregon",
-        "washington",
-        "florida",
-        "delaware",
-        "rhode-island",
-    ]
-    .into_iter()
-    .enumerate()
-    {
-        db.create_user(
-            &format!("{github_login}@example.com"),
-            false,
-            NewUserParams {
-                github_login: github_login.into(),
-                github_user_id: i as i32,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap();
+        Self {
+            db: Some(Arc::new(db)),
+            connection: None,
+        }
     }
 
-    assert_eq!(
-        fuzzy_search_user_names(db, "clr").await,
-        &["colorado", "California"]
-    );
-    assert_eq!(
-        fuzzy_search_user_names(db, "ro").await,
-        &["rhode-island", "colorado", "oregon"],
-    );
-
-    async fn fuzzy_search_user_names(db: &Database, query: &str) -> Vec<String> {
-        db.fuzzy_search_users(query, 10)
-            .await
-            .unwrap()
-            .into_iter()
-            .map(|user| user.github_login)
-            .collect::<Vec<_>>()
+    pub fn db(&self) -> &Arc<Database> {
+        self.db.as_ref().unwrap()
     }
 }
 
-#[gpui::test]
-async fn test_invite_codes() {
-    let test_db = TestDb::postgres(build_background_executor());
-    let db = test_db.db();
-
-    let NewUserResult { user_id: user1, .. } = db
-        .create_user(
-            "user1@example.com",
-            false,
-            NewUserParams {
-                github_login: "user1".into(),
-                github_user_id: 0,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap();
-
-    // Initially, user 1 has no invite code
-    assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
-
-    // Setting invite count to 0 when no code is assigned does not assign a new code
-    db.set_invite_count_for_user(user1, 0).await.unwrap();
-    assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
-
-    // User 1 creates an invite code that can be used twice.
-    db.set_invite_count_for_user(user1, 2).await.unwrap();
-    let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 2);
-
-    // User 2 redeems the invite code and becomes a contact of user 1.
-    let user2_invite = db
-        .create_invite_from_code(
-            &invite_code,
-            "user2@example.com",
-            Some("user-2-device-id"),
-            true,
-        )
-        .await
-        .unwrap();
-    let NewUserResult {
-        user_id: user2,
-        inviting_user_id,
-        signup_device_id,
-        metrics_id,
-    } = db
-        .create_user_from_invite(
-            &user2_invite,
-            NewUserParams {
-                github_login: "user2".into(),
-                github_user_id: 2,
-                invite_count: 7,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap();
-    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 1);
-    assert_eq!(inviting_user_id, Some(user1));
-    assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
-    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
-    assert_eq!(
-        db.get_contacts(user1).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user2,
-            should_notify: true,
-            busy: false,
-        }]
-    );
-    assert_eq!(
-        db.get_contacts(user2).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user1,
-            should_notify: false,
-            busy: false,
-        }]
-    );
-    assert!(db.has_contact(user1, user2).await.unwrap());
-    assert!(db.has_contact(user2, user1).await.unwrap());
-    assert_eq!(
-        db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
-        7
-    );
-
-    // User 3 redeems the invite code and becomes a contact of user 1.
-    let user3_invite = db
-        .create_invite_from_code(&invite_code, "user3@example.com", None, true)
-        .await
-        .unwrap();
-    let NewUserResult {
-        user_id: user3,
-        inviting_user_id,
-        signup_device_id,
-        ..
-    } = db
-        .create_user_from_invite(
-            &user3_invite,
-            NewUserParams {
-                github_login: "user-3".into(),
-                github_user_id: 3,
-                invite_count: 3,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap();
-    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 0);
-    assert_eq!(inviting_user_id, Some(user1));
-    assert!(signup_device_id.is_none());
-    assert_eq!(
-        db.get_contacts(user1).await.unwrap(),
-        [
-            Contact::Accepted {
-                user_id: user2,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user3,
-                should_notify: true,
-                busy: false,
-            }
-        ]
-    );
-    assert_eq!(
-        db.get_contacts(user3).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user1,
-            should_notify: false,
-            busy: false,
-        }]
-    );
-    assert!(db.has_contact(user1, user3).await.unwrap());
-    assert!(db.has_contact(user3, user1).await.unwrap());
-    assert_eq!(
-        db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
-        3
-    );
-
-    // Trying to reedem the code for the third time results in an error.
-    db.create_invite_from_code(
-        &invite_code,
-        "user4@example.com",
-        Some("user-4-device-id"),
-        true,
-    )
-    .await
-    .unwrap_err();
-
-    // Invite count can be updated after the code has been created.
-    db.set_invite_count_for_user(user1, 2).await.unwrap();
-    let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
-    assert_eq!(invite_count, 2);
-
-    // User 4 can now redeem the invite code and becomes a contact of user 1.
-    let user4_invite = db
-        .create_invite_from_code(
-            &invite_code,
-            "user4@example.com",
-            Some("user-4-device-id"),
-            true,
-        )
-        .await
-        .unwrap();
-    let user4 = db
-        .create_user_from_invite(
-            &user4_invite,
-            NewUserParams {
-                github_login: "user-4".into(),
-                github_user_id: 4,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap()
-        .user_id;
-
-    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 1);
-    assert_eq!(
-        db.get_contacts(user1).await.unwrap(),
-        [
-            Contact::Accepted {
-                user_id: user2,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user3,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user4,
-                should_notify: true,
-                busy: false,
-            }
-        ]
-    );
-    assert_eq!(
-        db.get_contacts(user4).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user1,
-            should_notify: false,
-            busy: false,
-        }]
-    );
-    assert!(db.has_contact(user1, user4).await.unwrap());
-    assert!(db.has_contact(user4, user1).await.unwrap());
-    assert_eq!(
-        db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
-        5
-    );
-
-    // An existing user cannot redeem invite codes.
-    db.create_invite_from_code(
-        &invite_code,
-        "user2@example.com",
-        Some("user-2-device-id"),
-        true,
-    )
-    .await
-    .unwrap_err();
-    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 1);
-
-    // A newer user can invite an existing one via a different email address
-    // than the one they used to sign up.
-    let user5 = db
-        .create_user(
-            "user5@example.com",
-            false,
-            NewUserParams {
-                github_login: "user5".into(),
-                github_user_id: 5,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-    db.set_invite_count_for_user(user5, 5).await.unwrap();
-    let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
-    let user5_invite_to_user1 = db
-        .create_invite_from_code(&user5_invite_code, "user1@different.com", None, true)
-        .await
-        .unwrap();
-    let user1_2 = db
-        .create_user_from_invite(
-            &user5_invite_to_user1,
-            NewUserParams {
-                github_login: "user1".into(),
-                github_user_id: 1,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap()
-        .user_id;
-    assert_eq!(user1_2, user1);
-    assert_eq!(
-        db.get_contacts(user1).await.unwrap(),
-        [
-            Contact::Accepted {
-                user_id: user2,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user3,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user4,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user5,
-                should_notify: false,
-                busy: false,
-            }
-        ]
-    );
-    assert_eq!(
-        db.get_contacts(user5).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user1,
-            should_notify: true,
-            busy: false,
-        }]
-    );
-    assert!(db.has_contact(user1, user5).await.unwrap());
-    assert!(db.has_contact(user5, user1).await.unwrap());
-}
-
-#[gpui::test]
-async fn test_multiple_signup_overwrite() {
-    let test_db = TestDb::postgres(build_background_executor());
-    let db = test_db.db();
-
-    let email_address = "user_1@example.com".to_string();
-
-    let initial_signup_created_at_milliseconds = 0;
-
-    let initial_signup = NewSignup {
-        email_address: email_address.clone(),
-        platform_mac: false,
-        platform_linux: true,
-        platform_windows: false,
-        editor_features: vec!["speed".into()],
-        programming_languages: vec!["rust".into(), "c".into()],
-        device_id: Some(format!("device_id")),
-        added_to_mailing_list: false,
-        created_at: Some(
-            DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(),
-        ),
-    };
-
-    db.create_signup(&initial_signup).await.unwrap();
-
-    let initial_signup_from_db = db.get_signup(&email_address).await.unwrap();
-
-    assert_eq!(
-        initial_signup_from_db.clone(),
-        signup::Model {
-            email_address: initial_signup.email_address,
-            platform_mac: initial_signup.platform_mac,
-            platform_linux: initial_signup.platform_linux,
-            platform_windows: initial_signup.platform_windows,
-            editor_features: Some(initial_signup.editor_features),
-            programming_languages: Some(initial_signup.programming_languages),
-            added_to_mailing_list: initial_signup.added_to_mailing_list,
-            ..initial_signup_from_db
+#[macro_export]
+macro_rules! test_both_dbs {
+    ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
+        #[gpui::test]
+        async fn $postgres_test_name() {
+            let test_db = crate::db::TestDb::postgres(
+                gpui::executor::Deterministic::new(0).build_background(),
+            );
+            $test_name(test_db.db()).await;
         }
-    );
-
-    let subsequent_signup = NewSignup {
-        email_address: email_address.clone(),
-        platform_mac: true,
-        platform_linux: false,
-        platform_windows: true,
-        editor_features: vec!["git integration".into(), "clean design".into()],
-        programming_languages: vec!["d".into(), "elm".into()],
-        device_id: Some(format!("different_device_id")),
-        added_to_mailing_list: true,
-        // subsequent signup happens next day
-        created_at: Some(
-            DateTime::from_timestamp_millis(
-                initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24),
-            )
-            .unwrap(),
-        ),
-    };
-
-    db.create_signup(&subsequent_signup).await.unwrap();
 
-    let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap();
-
-    assert_eq!(
-        subsequent_signup_from_db.clone(),
-        signup::Model {
-            platform_mac: subsequent_signup.platform_mac,
-            platform_linux: subsequent_signup.platform_linux,
-            platform_windows: subsequent_signup.platform_windows,
-            editor_features: Some(subsequent_signup.editor_features),
-            programming_languages: Some(subsequent_signup.programming_languages),
-            device_id: subsequent_signup.device_id,
-            added_to_mailing_list: subsequent_signup.added_to_mailing_list,
-            // shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line
-            created_at: initial_signup_from_db.created_at,
-            ..subsequent_signup_from_db
+        #[gpui::test]
+        async fn $sqlite_test_name() {
+            let test_db =
+                crate::db::TestDb::sqlite(gpui::executor::Deterministic::new(0).build_background());
+            $test_name(test_db.db()).await;
         }
-    );
+    };
 }
 
-#[gpui::test]
-async fn test_signups() {
-    let test_db = TestDb::postgres(build_background_executor());
-    let db = test_db.db();
-
-    let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
-
-    let all_signups = usernames
-        .iter()
-        .enumerate()
-        .map(|(i, username)| NewSignup {
-            email_address: format!("{username}@example.com"),
-            platform_mac: true,
-            platform_linux: i % 2 == 0,
-            platform_windows: i % 4 == 0,
-            editor_features: vec!["speed".into()],
-            programming_languages: vec!["rust".into(), "c".into()],
-            device_id: Some(format!("device_id_{i}")),
-            added_to_mailing_list: i != 0, // One user failed to subscribe
-            created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive
-        })
-        .collect::<Vec<NewSignup>>();
-
-    // people sign up on the waitlist
-    for signup in &all_signups {
-        // users can sign up multiple times without issues
-        for _ in 0..2 {
-            db.create_signup(&signup).await.unwrap();
+impl Drop for TestDb {
+    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.into(),
+                    ))
+                    .await
+                    .log_err();
+                sqlx::Postgres::drop_database(db.options.get_url())
+                    .await
+                    .log_err();
+            })
         }
     }
-
-    assert_eq!(
-        db.get_waitlist_summary().await.unwrap(),
-        WaitlistSummary {
-            count: 8,
-            mac_count: 8,
-            linux_count: 4,
-            windows_count: 2,
-            unknown_count: 0,
-        }
-    );
-
-    // retrieve the next batch of signup emails to send
-    let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
-    let addresses = signups_batch1
-        .iter()
-        .map(|s| &s.email_address)
-        .collect::<Vec<_>>();
-    assert_eq!(
-        addresses,
-        &[
-            all_signups[0].email_address.as_str(),
-            all_signups[1].email_address.as_str(),
-            all_signups[2].email_address.as_str()
-        ]
-    );
-    assert_ne!(
-        signups_batch1[0].email_confirmation_code,
-        signups_batch1[1].email_confirmation_code
-    );
-
-    // the waitlist isn't updated until we record that the emails
-    // were successfully sent.
-    let signups_batch = db.get_unsent_invites(3).await.unwrap();
-    assert_eq!(signups_batch, signups_batch1);
-
-    // once the emails go out, we can retrieve the next batch
-    // of signups.
-    db.record_sent_invites(&signups_batch1).await.unwrap();
-    let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
-    let addresses = signups_batch2
-        .iter()
-        .map(|s| &s.email_address)
-        .collect::<Vec<_>>();
-    assert_eq!(
-        addresses,
-        &[
-            all_signups[3].email_address.as_str(),
-            all_signups[4].email_address.as_str(),
-            all_signups[5].email_address.as_str()
-        ]
-    );
-
-    // the sent invites are excluded from the summary.
-    assert_eq!(
-        db.get_waitlist_summary().await.unwrap(),
-        WaitlistSummary {
-            count: 5,
-            mac_count: 5,
-            linux_count: 2,
-            windows_count: 1,
-            unknown_count: 0,
-        }
-    );
-
-    // user completes the signup process by providing their
-    // github account.
-    let NewUserResult {
-        user_id,
-        inviting_user_id,
-        signup_device_id,
-        ..
-    } = db
-        .create_user_from_invite(
-            &Invite {
-                ..signups_batch1[0].clone()
-            },
-            NewUserParams {
-                github_login: usernames[0].clone(),
-                github_user_id: 0,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap();
-    let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
-    assert!(inviting_user_id.is_none());
-    assert_eq!(user.github_login, usernames[0]);
-    assert_eq!(
-        user.email_address,
-        Some(all_signups[0].email_address.clone())
-    );
-    assert_eq!(user.invite_count, 5);
-    assert_eq!(signup_device_id.unwrap(), "device_id_0");
-
-    // cannot redeem the same signup again.
-    assert!(db
-        .create_user_from_invite(
-            &Invite {
-                email_address: signups_batch1[0].email_address.clone(),
-                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
-            },
-            NewUserParams {
-                github_login: "some-other-github_account".into(),
-                github_user_id: 1,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap()
-        .is_none());
-
-    // cannot redeem a signup with the wrong confirmation code.
-    db.create_user_from_invite(
-        &Invite {
-            email_address: signups_batch1[1].email_address.clone(),
-            email_confirmation_code: "the-wrong-code".to_string(),
-        },
-        NewUserParams {
-            github_login: usernames[1].clone(),
-            github_user_id: 2,
-            invite_count: 5,
-        },
-    )
-    .await
-    .unwrap_err();
-}
-
-fn build_background_executor() -> Arc<Background> {
-    Deterministic::new(0).build_background()
 }

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

@@ -0,0 +1,165 @@
+use super::*;
+use crate::test_both_dbs;
+use language::proto;
+use text::Buffer;
+
+test_both_dbs!(
+    test_channel_buffers,
+    test_channel_buffers_postgres,
+    test_channel_buffers_sqlite
+);
+
+async fn test_channel_buffers(db: &Arc<Database>) {
+    let a_id = db
+        .create_user(
+            "user_a@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_a".into(),
+                github_user_id: 101,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let b_id = db
+        .create_user(
+            "user_b@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_b".into(),
+                github_user_id: 102,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    // This user will not be a part of the channel
+    let c_id = db
+        .create_user(
+            "user_c@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_c".into(),
+                github_user_id: 102,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let owner_id = db.create_server("production").await.unwrap().0 as u32;
+
+    let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
+
+    db.invite_channel_member(zed_id, b_id, a_id, false)
+        .await
+        .unwrap();
+
+    db.respond_to_channel_invite(zed_id, b_id, true)
+        .await
+        .unwrap();
+
+    let connection_id_a = ConnectionId { owner_id, id: 1 };
+    let _ = db
+        .join_channel_buffer(zed_id, a_id, connection_id_a)
+        .await
+        .unwrap();
+
+    let mut buffer_a = Buffer::new(0, 0, "".to_string());
+    let mut operations = Vec::new();
+    operations.push(buffer_a.edit([(0..0, "hello world")]));
+    operations.push(buffer_a.edit([(5..5, ", cruel")]));
+    operations.push(buffer_a.edit([(0..5, "goodbye")]));
+    operations.push(buffer_a.undo().unwrap().1);
+    assert_eq!(buffer_a.text(), "hello, cruel world");
+
+    let operations = operations
+        .into_iter()
+        .map(|op| proto::serialize_operation(&language::Operation::Buffer(op)))
+        .collect::<Vec<_>>();
+
+    db.update_channel_buffer(zed_id, a_id, &operations)
+        .await
+        .unwrap();
+
+    let connection_id_b = ConnectionId { owner_id, id: 2 };
+    let buffer_response_b = db
+        .join_channel_buffer(zed_id, b_id, connection_id_b)
+        .await
+        .unwrap();
+
+    let mut buffer_b = Buffer::new(0, 0, buffer_response_b.base_text);
+    buffer_b
+        .apply_ops(buffer_response_b.operations.into_iter().map(|operation| {
+            let operation = proto::deserialize_operation(operation).unwrap();
+            if let language::Operation::Buffer(operation) = operation {
+                operation
+            } else {
+                unreachable!()
+            }
+        }))
+        .unwrap();
+
+    assert_eq!(buffer_b.text(), "hello, cruel world");
+
+    // Ensure that C fails to open the buffer
+    assert!(db
+        .join_channel_buffer(zed_id, c_id, ConnectionId { owner_id, id: 3 })
+        .await
+        .is_err());
+
+    // Ensure that both collaborators have shown up
+    assert_eq!(
+        buffer_response_b.collaborators,
+        &[
+            rpc::proto::Collaborator {
+                user_id: a_id.to_proto(),
+                peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
+                replica_id: 0,
+            },
+            rpc::proto::Collaborator {
+                user_id: b_id.to_proto(),
+                peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
+                replica_id: 1,
+            }
+        ]
+    );
+
+    // Ensure that get_channel_buffer_collaborators works
+    let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
+    assert_eq!(zed_collaborats, &[a_id, b_id]);
+
+    let collaborators = db
+        .leave_channel_buffer(zed_id, connection_id_b)
+        .await
+        .unwrap();
+
+    assert_eq!(collaborators, &[connection_id_a],);
+
+    let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
+    let _ = db
+        .join_channel_buffer(cargo_id, a_id, connection_id_a)
+        .await
+        .unwrap();
+
+    db.leave_channel_buffers(connection_id_a).await.unwrap();
+
+    let zed_collaborators = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
+    let cargo_collaborators = db.get_channel_buffer_collaborators(cargo_id).await.unwrap();
+    assert_eq!(zed_collaborators, &[]);
+    assert_eq!(cargo_collaborators, &[]);
+
+    // When everyone has left the channel, the operations are collapsed into
+    // a new base text.
+    let buffer_response_b = db
+        .join_channel_buffer(zed_id, b_id, connection_id_b)
+        .await
+        .unwrap();
+    assert_eq!(buffer_response_b.base_text, "hello, cruel world");
+    assert_eq!(buffer_response_b.operations, &[]);
+}

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

@@ -0,0 +1,1573 @@
+use super::*;
+use crate::test_both_dbs;
+use gpui::executor::{Background, Deterministic};
+use pretty_assertions::{assert_eq, assert_ne};
+use std::sync::Arc;
+use tests::TestDb;
+
+test_both_dbs!(
+    test_get_users,
+    test_get_users_by_ids_postgres,
+    test_get_users_by_ids_sqlite
+);
+
+async fn test_get_users(db: &Arc<Database>) {
+    let mut user_ids = Vec::new();
+    let mut user_metric_ids = Vec::new();
+    for i in 1..=4 {
+        let user = db
+            .create_user(
+                &format!("user{i}@example.com"),
+                false,
+                NewUserParams {
+                    github_login: format!("user{i}"),
+                    github_user_id: i,
+                    invite_count: 0,
+                },
+            )
+            .await
+            .unwrap();
+        user_ids.push(user.user_id);
+        user_metric_ids.push(user.metrics_id);
+    }
+
+    assert_eq!(
+        db.get_users_by_ids(user_ids.clone()).await.unwrap(),
+        vec![
+            User {
+                id: user_ids[0],
+                github_login: "user1".to_string(),
+                github_user_id: Some(1),
+                email_address: Some("user1@example.com".to_string()),
+                admin: false,
+                metrics_id: user_metric_ids[0].parse().unwrap(),
+                ..Default::default()
+            },
+            User {
+                id: user_ids[1],
+                github_login: "user2".to_string(),
+                github_user_id: Some(2),
+                email_address: Some("user2@example.com".to_string()),
+                admin: false,
+                metrics_id: user_metric_ids[1].parse().unwrap(),
+                ..Default::default()
+            },
+            User {
+                id: user_ids[2],
+                github_login: "user3".to_string(),
+                github_user_id: Some(3),
+                email_address: Some("user3@example.com".to_string()),
+                admin: false,
+                metrics_id: user_metric_ids[2].parse().unwrap(),
+                ..Default::default()
+            },
+            User {
+                id: user_ids[3],
+                github_login: "user4".to_string(),
+                github_user_id: Some(4),
+                email_address: Some("user4@example.com".to_string()),
+                admin: false,
+                metrics_id: user_metric_ids[3].parse().unwrap(),
+                ..Default::default()
+            }
+        ]
+    );
+}
+
+test_both_dbs!(
+    test_get_or_create_user_by_github_account,
+    test_get_or_create_user_by_github_account_postgres,
+    test_get_or_create_user_by_github_account_sqlite
+);
+
+async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
+    let user_id1 = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "login1".into(),
+                github_user_id: 101,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let user_id2 = db
+        .create_user(
+            "user2@example.com",
+            false,
+            NewUserParams {
+                github_login: "login2".into(),
+                github_user_id: 102,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let user = db
+        .get_or_create_user_by_github_account("login1", None, None)
+        .await
+        .unwrap()
+        .unwrap();
+    assert_eq!(user.id, user_id1);
+    assert_eq!(&user.github_login, "login1");
+    assert_eq!(user.github_user_id, Some(101));
+
+    assert!(db
+        .get_or_create_user_by_github_account("non-existent-login", None, None)
+        .await
+        .unwrap()
+        .is_none());
+
+    let user = db
+        .get_or_create_user_by_github_account("the-new-login2", Some(102), None)
+        .await
+        .unwrap()
+        .unwrap();
+    assert_eq!(user.id, user_id2);
+    assert_eq!(&user.github_login, "the-new-login2");
+    assert_eq!(user.github_user_id, Some(102));
+
+    let user = db
+        .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com"))
+        .await
+        .unwrap()
+        .unwrap();
+    assert_eq!(&user.github_login, "login3");
+    assert_eq!(user.github_user_id, Some(103));
+    assert_eq!(user.email_address, Some("user3@example.com".into()));
+}
+
+test_both_dbs!(
+    test_create_access_tokens,
+    test_create_access_tokens_postgres,
+    test_create_access_tokens_sqlite
+);
+
+async fn test_create_access_tokens(db: &Arc<Database>) {
+    let user = db
+        .create_user(
+            "u1@example.com",
+            false,
+            NewUserParams {
+                github_login: "u1".into(),
+                github_user_id: 1,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let token_1 = db.create_access_token(user, "h1", 2).await.unwrap();
+    let token_2 = db.create_access_token(user, "h2", 2).await.unwrap();
+    assert_eq!(
+        db.get_access_token(token_1).await.unwrap(),
+        access_token::Model {
+            id: token_1,
+            user_id: user,
+            hash: "h1".into(),
+        }
+    );
+    assert_eq!(
+        db.get_access_token(token_2).await.unwrap(),
+        access_token::Model {
+            id: token_2,
+            user_id: user,
+            hash: "h2".into()
+        }
+    );
+
+    let token_3 = db.create_access_token(user, "h3", 2).await.unwrap();
+    assert_eq!(
+        db.get_access_token(token_3).await.unwrap(),
+        access_token::Model {
+            id: token_3,
+            user_id: user,
+            hash: "h3".into()
+        }
+    );
+    assert_eq!(
+        db.get_access_token(token_2).await.unwrap(),
+        access_token::Model {
+            id: token_2,
+            user_id: user,
+            hash: "h2".into()
+        }
+    );
+    assert!(db.get_access_token(token_1).await.is_err());
+
+    let token_4 = db.create_access_token(user, "h4", 2).await.unwrap();
+    assert_eq!(
+        db.get_access_token(token_4).await.unwrap(),
+        access_token::Model {
+            id: token_4,
+            user_id: user,
+            hash: "h4".into()
+        }
+    );
+    assert_eq!(
+        db.get_access_token(token_3).await.unwrap(),
+        access_token::Model {
+            id: token_3,
+            user_id: user,
+            hash: "h3".into()
+        }
+    );
+    assert!(db.get_access_token(token_2).await.is_err());
+    assert!(db.get_access_token(token_1).await.is_err());
+}
+
+test_both_dbs!(
+    test_add_contacts,
+    test_add_contacts_postgres,
+    test_add_contacts_sqlite
+);
+
+async fn test_add_contacts(db: &Arc<Database>) {
+    let mut user_ids = Vec::new();
+    for i in 0..3 {
+        user_ids.push(
+            db.create_user(
+                &format!("user{i}@example.com"),
+                false,
+                NewUserParams {
+                    github_login: format!("user{i}"),
+                    github_user_id: i,
+                    invite_count: 0,
+                },
+            )
+            .await
+            .unwrap()
+            .user_id,
+        );
+    }
+
+    let user_1 = user_ids[0];
+    let user_2 = user_ids[1];
+    let user_3 = user_ids[2];
+
+    // User starts with no contacts
+    assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
+
+    // User requests a contact. Both users see the pending request.
+    db.send_contact_request(user_1, user_2).await.unwrap();
+    assert!(!db.has_contact(user_1, user_2).await.unwrap());
+    assert!(!db.has_contact(user_2, user_1).await.unwrap());
+    assert_eq!(
+        db.get_contacts(user_1).await.unwrap(),
+        &[Contact::Outgoing { user_id: user_2 }],
+    );
+    assert_eq!(
+        db.get_contacts(user_2).await.unwrap(),
+        &[Contact::Incoming {
+            user_id: user_1,
+            should_notify: true
+        }]
+    );
+
+    // User 2 dismisses the contact request notification without accepting or rejecting.
+    // We shouldn't notify them again.
+    db.dismiss_contact_notification(user_1, user_2)
+        .await
+        .unwrap_err();
+    db.dismiss_contact_notification(user_2, user_1)
+        .await
+        .unwrap();
+    assert_eq!(
+        db.get_contacts(user_2).await.unwrap(),
+        &[Contact::Incoming {
+            user_id: user_1,
+            should_notify: false
+        }]
+    );
+
+    // User can't accept their own contact request
+    db.respond_to_contact_request(user_1, user_2, true)
+        .await
+        .unwrap_err();
+
+    // User accepts a contact request. Both users see the contact.
+    db.respond_to_contact_request(user_2, user_1, true)
+        .await
+        .unwrap();
+    assert_eq!(
+        db.get_contacts(user_1).await.unwrap(),
+        &[Contact::Accepted {
+            user_id: user_2,
+            should_notify: true,
+            busy: false,
+        }],
+    );
+    assert!(db.has_contact(user_1, user_2).await.unwrap());
+    assert!(db.has_contact(user_2, user_1).await.unwrap());
+    assert_eq!(
+        db.get_contacts(user_2).await.unwrap(),
+        &[Contact::Accepted {
+            user_id: user_1,
+            should_notify: false,
+            busy: false,
+        }]
+    );
+
+    // Users cannot re-request existing contacts.
+    db.send_contact_request(user_1, user_2).await.unwrap_err();
+    db.send_contact_request(user_2, user_1).await.unwrap_err();
+
+    // Users can't dismiss notifications of them accepting other users' requests.
+    db.dismiss_contact_notification(user_2, user_1)
+        .await
+        .unwrap_err();
+    assert_eq!(
+        db.get_contacts(user_1).await.unwrap(),
+        &[Contact::Accepted {
+            user_id: user_2,
+            should_notify: true,
+            busy: false,
+        }]
+    );
+
+    // Users can dismiss notifications of other users accepting their requests.
+    db.dismiss_contact_notification(user_1, user_2)
+        .await
+        .unwrap();
+    assert_eq!(
+        db.get_contacts(user_1).await.unwrap(),
+        &[Contact::Accepted {
+            user_id: user_2,
+            should_notify: false,
+            busy: false,
+        }]
+    );
+
+    // Users send each other concurrent contact requests and
+    // see that they are immediately accepted.
+    db.send_contact_request(user_1, user_3).await.unwrap();
+    db.send_contact_request(user_3, user_1).await.unwrap();
+    assert_eq!(
+        db.get_contacts(user_1).await.unwrap(),
+        &[
+            Contact::Accepted {
+                user_id: user_2,
+                should_notify: false,
+                busy: false,
+            },
+            Contact::Accepted {
+                user_id: user_3,
+                should_notify: false,
+                busy: false,
+            }
+        ]
+    );
+    assert_eq!(
+        db.get_contacts(user_3).await.unwrap(),
+        &[Contact::Accepted {
+            user_id: user_1,
+            should_notify: false,
+            busy: false,
+        }],
+    );
+
+    // User declines a contact request. Both users see that it is gone.
+    db.send_contact_request(user_2, user_3).await.unwrap();
+    db.respond_to_contact_request(user_3, user_2, false)
+        .await
+        .unwrap();
+    assert!(!db.has_contact(user_2, user_3).await.unwrap());
+    assert!(!db.has_contact(user_3, user_2).await.unwrap());
+    assert_eq!(
+        db.get_contacts(user_2).await.unwrap(),
+        &[Contact::Accepted {
+            user_id: user_1,
+            should_notify: false,
+            busy: false,
+        }]
+    );
+    assert_eq!(
+        db.get_contacts(user_3).await.unwrap(),
+        &[Contact::Accepted {
+            user_id: user_1,
+            should_notify: false,
+            busy: false,
+        }],
+    );
+}
+
+test_both_dbs!(
+    test_metrics_id,
+    test_metrics_id_postgres,
+    test_metrics_id_sqlite
+);
+
+async fn test_metrics_id(db: &Arc<Database>) {
+    let NewUserResult {
+        user_id: user1,
+        metrics_id: metrics_id1,
+        ..
+    } = db
+        .create_user(
+            "person1@example.com",
+            false,
+            NewUserParams {
+                github_login: "person1".into(),
+                github_user_id: 101,
+                invite_count: 5,
+            },
+        )
+        .await
+        .unwrap();
+    let NewUserResult {
+        user_id: user2,
+        metrics_id: metrics_id2,
+        ..
+    } = db
+        .create_user(
+            "person2@example.com",
+            false,
+            NewUserParams {
+                github_login: "person2".into(),
+                github_user_id: 102,
+                invite_count: 5,
+            },
+        )
+        .await
+        .unwrap();
+
+    assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
+    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
+    assert_eq!(metrics_id1.len(), 36);
+    assert_eq!(metrics_id2.len(), 36);
+    assert_ne!(metrics_id1, metrics_id2);
+}
+
+test_both_dbs!(
+    test_project_count,
+    test_project_count_postgres,
+    test_project_count_sqlite
+);
+
+async fn test_project_count(db: &Arc<Database>) {
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+
+    let user1 = db
+        .create_user(
+            &format!("admin@example.com"),
+            true,
+            NewUserParams {
+                github_login: "admin".into(),
+                github_user_id: 0,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap();
+    let user2 = db
+        .create_user(
+            &format!("user@example.com"),
+            false,
+            NewUserParams {
+                github_login: "user".into(),
+                github_user_id: 1,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap();
+
+    let room_id = RoomId::from_proto(
+        db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
+            .await
+            .unwrap()
+            .id,
+    );
+    db.call(
+        room_id,
+        user1.user_id,
+        ConnectionId { owner_id, id: 0 },
+        user2.user_id,
+        None,
+    )
+    .await
+    .unwrap();
+    db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
+        .await
+        .unwrap();
+    assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
+
+    db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
+        .await
+        .unwrap();
+    assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
+
+    db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
+        .await
+        .unwrap();
+    assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
+
+    // Projects shared by admins aren't counted.
+    db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
+        .await
+        .unwrap();
+    assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
+
+    db.leave_room(ConnectionId { owner_id, id: 1 })
+        .await
+        .unwrap();
+    assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
+}
+
+#[test]
+fn test_fuzzy_like_string() {
+    assert_eq!(Database::fuzzy_like_string("abcd"), "%a%b%c%d%");
+    assert_eq!(Database::fuzzy_like_string("x y"), "%x%y%");
+    assert_eq!(Database::fuzzy_like_string(" z  "), "%z%");
+}
+
+#[gpui::test]
+async fn test_fuzzy_search_users() {
+    let test_db = TestDb::postgres(build_background_executor());
+    let db = test_db.db();
+    for (i, github_login) in [
+        "California",
+        "colorado",
+        "oregon",
+        "washington",
+        "florida",
+        "delaware",
+        "rhode-island",
+    ]
+    .into_iter()
+    .enumerate()
+    {
+        db.create_user(
+            &format!("{github_login}@example.com"),
+            false,
+            NewUserParams {
+                github_login: github_login.into(),
+                github_user_id: i as i32,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap();
+    }
+
+    assert_eq!(
+        fuzzy_search_user_names(db, "clr").await,
+        &["colorado", "California"]
+    );
+    assert_eq!(
+        fuzzy_search_user_names(db, "ro").await,
+        &["rhode-island", "colorado", "oregon"],
+    );
+
+    async fn fuzzy_search_user_names(db: &Database, query: &str) -> Vec<String> {
+        db.fuzzy_search_users(query, 10)
+            .await
+            .unwrap()
+            .into_iter()
+            .map(|user| user.github_login)
+            .collect::<Vec<_>>()
+    }
+}
+
+#[gpui::test]
+async fn test_invite_codes() {
+    let test_db = TestDb::postgres(build_background_executor());
+    let db = test_db.db();
+
+    let NewUserResult { user_id: user1, .. } = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 0,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap();
+
+    // Initially, user 1 has no invite code
+    assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
+
+    // Setting invite count to 0 when no code is assigned does not assign a new code
+    db.set_invite_count_for_user(user1, 0).await.unwrap();
+    assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
+
+    // User 1 creates an invite code that can be used twice.
+    db.set_invite_count_for_user(user1, 2).await.unwrap();
+    let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
+    assert_eq!(invite_count, 2);
+
+    // User 2 redeems the invite code and becomes a contact of user 1.
+    let user2_invite = db
+        .create_invite_from_code(
+            &invite_code,
+            "user2@example.com",
+            Some("user-2-device-id"),
+            true,
+        )
+        .await
+        .unwrap();
+    let NewUserResult {
+        user_id: user2,
+        inviting_user_id,
+        signup_device_id,
+        metrics_id,
+    } = db
+        .create_user_from_invite(
+            &user2_invite,
+            NewUserParams {
+                github_login: "user2".into(),
+                github_user_id: 2,
+                invite_count: 7,
+            },
+        )
+        .await
+        .unwrap()
+        .unwrap();
+    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
+    assert_eq!(invite_count, 1);
+    assert_eq!(inviting_user_id, Some(user1));
+    assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
+    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
+    assert_eq!(
+        db.get_contacts(user1).await.unwrap(),
+        [Contact::Accepted {
+            user_id: user2,
+            should_notify: true,
+            busy: false,
+        }]
+    );
+    assert_eq!(
+        db.get_contacts(user2).await.unwrap(),
+        [Contact::Accepted {
+            user_id: user1,
+            should_notify: false,
+            busy: false,
+        }]
+    );
+    assert!(db.has_contact(user1, user2).await.unwrap());
+    assert!(db.has_contact(user2, user1).await.unwrap());
+    assert_eq!(
+        db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
+        7
+    );
+
+    // User 3 redeems the invite code and becomes a contact of user 1.
+    let user3_invite = db
+        .create_invite_from_code(&invite_code, "user3@example.com", None, true)
+        .await
+        .unwrap();
+    let NewUserResult {
+        user_id: user3,
+        inviting_user_id,
+        signup_device_id,
+        ..
+    } = db
+        .create_user_from_invite(
+            &user3_invite,
+            NewUserParams {
+                github_login: "user-3".into(),
+                github_user_id: 3,
+                invite_count: 3,
+            },
+        )
+        .await
+        .unwrap()
+        .unwrap();
+    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
+    assert_eq!(invite_count, 0);
+    assert_eq!(inviting_user_id, Some(user1));
+    assert!(signup_device_id.is_none());
+    assert_eq!(
+        db.get_contacts(user1).await.unwrap(),
+        [
+            Contact::Accepted {
+                user_id: user2,
+                should_notify: true,
+                busy: false,
+            },
+            Contact::Accepted {
+                user_id: user3,
+                should_notify: true,
+                busy: false,
+            }
+        ]
+    );
+    assert_eq!(
+        db.get_contacts(user3).await.unwrap(),
+        [Contact::Accepted {
+            user_id: user1,
+            should_notify: false,
+            busy: false,
+        }]
+    );
+    assert!(db.has_contact(user1, user3).await.unwrap());
+    assert!(db.has_contact(user3, user1).await.unwrap());
+    assert_eq!(
+        db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
+        3
+    );
+
+    // Trying to reedem the code for the third time results in an error.
+    db.create_invite_from_code(
+        &invite_code,
+        "user4@example.com",
+        Some("user-4-device-id"),
+        true,
+    )
+    .await
+    .unwrap_err();
+
+    // Invite count can be updated after the code has been created.
+    db.set_invite_count_for_user(user1, 2).await.unwrap();
+    let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
+    assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
+    assert_eq!(invite_count, 2);
+
+    // User 4 can now redeem the invite code and becomes a contact of user 1.
+    let user4_invite = db
+        .create_invite_from_code(
+            &invite_code,
+            "user4@example.com",
+            Some("user-4-device-id"),
+            true,
+        )
+        .await
+        .unwrap();
+    let user4 = db
+        .create_user_from_invite(
+            &user4_invite,
+            NewUserParams {
+                github_login: "user-4".into(),
+                github_user_id: 4,
+                invite_count: 5,
+            },
+        )
+        .await
+        .unwrap()
+        .unwrap()
+        .user_id;
+
+    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
+    assert_eq!(invite_count, 1);
+    assert_eq!(
+        db.get_contacts(user1).await.unwrap(),
+        [
+            Contact::Accepted {
+                user_id: user2,
+                should_notify: true,
+                busy: false,
+            },
+            Contact::Accepted {
+                user_id: user3,
+                should_notify: true,
+                busy: false,
+            },
+            Contact::Accepted {
+                user_id: user4,
+                should_notify: true,
+                busy: false,
+            }
+        ]
+    );
+    assert_eq!(
+        db.get_contacts(user4).await.unwrap(),
+        [Contact::Accepted {
+            user_id: user1,
+            should_notify: false,
+            busy: false,
+        }]
+    );
+    assert!(db.has_contact(user1, user4).await.unwrap());
+    assert!(db.has_contact(user4, user1).await.unwrap());
+    assert_eq!(
+        db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
+        5
+    );
+
+    // An existing user cannot redeem invite codes.
+    db.create_invite_from_code(
+        &invite_code,
+        "user2@example.com",
+        Some("user-2-device-id"),
+        true,
+    )
+    .await
+    .unwrap_err();
+    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
+    assert_eq!(invite_count, 1);
+
+    // A newer user can invite an existing one via a different email address
+    // than the one they used to sign up.
+    let user5 = db
+        .create_user(
+            "user5@example.com",
+            false,
+            NewUserParams {
+                github_login: "user5".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    db.set_invite_count_for_user(user5, 5).await.unwrap();
+    let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
+    let user5_invite_to_user1 = db
+        .create_invite_from_code(&user5_invite_code, "user1@different.com", None, true)
+        .await
+        .unwrap();
+    let user1_2 = db
+        .create_user_from_invite(
+            &user5_invite_to_user1,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 1,
+                invite_count: 5,
+            },
+        )
+        .await
+        .unwrap()
+        .unwrap()
+        .user_id;
+    assert_eq!(user1_2, user1);
+    assert_eq!(
+        db.get_contacts(user1).await.unwrap(),
+        [
+            Contact::Accepted {
+                user_id: user2,
+                should_notify: true,
+                busy: false,
+            },
+            Contact::Accepted {
+                user_id: user3,
+                should_notify: true,
+                busy: false,
+            },
+            Contact::Accepted {
+                user_id: user4,
+                should_notify: true,
+                busy: false,
+            },
+            Contact::Accepted {
+                user_id: user5,
+                should_notify: false,
+                busy: false,
+            }
+        ]
+    );
+    assert_eq!(
+        db.get_contacts(user5).await.unwrap(),
+        [Contact::Accepted {
+            user_id: user1,
+            should_notify: true,
+            busy: false,
+        }]
+    );
+    assert!(db.has_contact(user1, user5).await.unwrap());
+    assert!(db.has_contact(user5, user1).await.unwrap());
+}
+
+test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
+
+async fn test_channels(db: &Arc<Database>) {
+    let a_id = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let b_id = db
+        .create_user(
+            "user2@example.com",
+            false,
+            NewUserParams {
+                github_login: "user2".into(),
+                github_user_id: 6,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
+
+    // Make sure that people cannot read channels they haven't been invited to
+    assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
+
+    db.invite_channel_member(zed_id, b_id, a_id, false)
+        .await
+        .unwrap();
+
+    db.respond_to_channel_invite(zed_id, b_id, true)
+        .await
+        .unwrap();
+
+    let crdb_id = db
+        .create_channel("crdb", Some(zed_id), "2", a_id)
+        .await
+        .unwrap();
+    let livestreaming_id = db
+        .create_channel("livestreaming", Some(zed_id), "3", a_id)
+        .await
+        .unwrap();
+    let replace_id = db
+        .create_channel("replace", Some(zed_id), "4", a_id)
+        .await
+        .unwrap();
+
+    let mut members = db.get_channel_members(replace_id).await.unwrap();
+    members.sort();
+    assert_eq!(members, &[a_id, b_id]);
+
+    let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
+    let cargo_id = db
+        .create_channel("cargo", Some(rust_id), "6", a_id)
+        .await
+        .unwrap();
+
+    let cargo_ra_id = db
+        .create_channel("cargo-ra", Some(cargo_id), "7", a_id)
+        .await
+        .unwrap();
+
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_eq!(
+        result.channels,
+        vec![
+            Channel {
+                id: zed_id,
+                name: "zed".to_string(),
+                parent_id: None,
+            },
+            Channel {
+                id: crdb_id,
+                name: "crdb".to_string(),
+                parent_id: Some(zed_id),
+            },
+            Channel {
+                id: livestreaming_id,
+                name: "livestreaming".to_string(),
+                parent_id: Some(zed_id),
+            },
+            Channel {
+                id: replace_id,
+                name: "replace".to_string(),
+                parent_id: Some(zed_id),
+            },
+            Channel {
+                id: rust_id,
+                name: "rust".to_string(),
+                parent_id: None,
+            },
+            Channel {
+                id: cargo_id,
+                name: "cargo".to_string(),
+                parent_id: Some(rust_id),
+            },
+            Channel {
+                id: cargo_ra_id,
+                name: "cargo-ra".to_string(),
+                parent_id: Some(cargo_id),
+            }
+        ]
+    );
+
+    let result = db.get_channels_for_user(b_id).await.unwrap();
+    assert_eq!(
+        result.channels,
+        vec![
+            Channel {
+                id: zed_id,
+                name: "zed".to_string(),
+                parent_id: None,
+            },
+            Channel {
+                id: crdb_id,
+                name: "crdb".to_string(),
+                parent_id: Some(zed_id),
+            },
+            Channel {
+                id: livestreaming_id,
+                name: "livestreaming".to_string(),
+                parent_id: Some(zed_id),
+            },
+            Channel {
+                id: replace_id,
+                name: "replace".to_string(),
+                parent_id: Some(zed_id),
+            },
+        ]
+    );
+
+    // Update member permissions
+    let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
+    assert!(set_subchannel_admin.is_err());
+    let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
+    assert!(set_channel_admin.is_ok());
+
+    let result = db.get_channels_for_user(b_id).await.unwrap();
+    assert_eq!(
+        result.channels,
+        vec![
+            Channel {
+                id: zed_id,
+                name: "zed".to_string(),
+                parent_id: None,
+            },
+            Channel {
+                id: crdb_id,
+                name: "crdb".to_string(),
+                parent_id: Some(zed_id),
+            },
+            Channel {
+                id: livestreaming_id,
+                name: "livestreaming".to_string(),
+                parent_id: Some(zed_id),
+            },
+            Channel {
+                id: replace_id,
+                name: "replace".to_string(),
+                parent_id: Some(zed_id),
+            },
+        ]
+    );
+
+    // Remove a single channel
+    db.remove_channel(crdb_id, a_id).await.unwrap();
+    assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
+
+    // Remove a channel tree
+    let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap();
+    channel_ids.sort();
+    assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
+    assert_eq!(user_ids, &[a_id]);
+
+    assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
+    assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
+    assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
+}
+
+test_both_dbs!(
+    test_joining_channels,
+    test_joining_channels_postgres,
+    test_joining_channels_sqlite
+);
+
+async fn test_joining_channels(db: &Arc<Database>) {
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+
+    let user_1 = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let user_2 = db
+        .create_user(
+            "user2@example.com",
+            false,
+            NewUserParams {
+                github_login: "user2".into(),
+                github_user_id: 6,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let channel_1 = db
+        .create_root_channel("channel_1", "1", user_1)
+        .await
+        .unwrap();
+    let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
+
+    // can join a room with membership to its channel
+    let joined_room = db
+        .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
+        .await
+        .unwrap();
+    assert_eq!(joined_room.room.participants.len(), 1);
+
+    drop(joined_room);
+    // cannot join a room without membership to its channel
+    assert!(db
+        .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
+        .await
+        .is_err());
+}
+
+test_both_dbs!(
+    test_channel_invites,
+    test_channel_invites_postgres,
+    test_channel_invites_sqlite
+);
+
+async fn test_channel_invites(db: &Arc<Database>) {
+    db.create_server("test").await.unwrap();
+
+    let user_1 = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let user_2 = db
+        .create_user(
+            "user2@example.com",
+            false,
+            NewUserParams {
+                github_login: "user2".into(),
+                github_user_id: 6,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let user_3 = db
+        .create_user(
+            "user3@example.com",
+            false,
+            NewUserParams {
+                github_login: "user3".into(),
+                github_user_id: 7,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let channel_1_1 = db
+        .create_root_channel("channel_1", "1", user_1)
+        .await
+        .unwrap();
+
+    let channel_1_2 = db
+        .create_root_channel("channel_2", "2", user_1)
+        .await
+        .unwrap();
+
+    db.invite_channel_member(channel_1_1, user_2, user_1, false)
+        .await
+        .unwrap();
+    db.invite_channel_member(channel_1_2, user_2, user_1, false)
+        .await
+        .unwrap();
+    db.invite_channel_member(channel_1_1, user_3, user_1, true)
+        .await
+        .unwrap();
+
+    let user_2_invites = db
+        .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
+        .await
+        .unwrap()
+        .into_iter()
+        .map(|channel| channel.id)
+        .collect::<Vec<_>>();
+
+    assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
+
+    let user_3_invites = db
+        .get_channel_invites_for_user(user_3) // -> [channel_1_1]
+        .await
+        .unwrap()
+        .into_iter()
+        .map(|channel| channel.id)
+        .collect::<Vec<_>>();
+
+    assert_eq!(user_3_invites, &[channel_1_1]);
+
+    let members = db
+        .get_channel_member_details(channel_1_1, user_1)
+        .await
+        .unwrap();
+    assert_eq!(
+        members,
+        &[
+            proto::ChannelMember {
+                user_id: user_1.to_proto(),
+                kind: proto::channel_member::Kind::Member.into(),
+                admin: true,
+            },
+            proto::ChannelMember {
+                user_id: user_2.to_proto(),
+                kind: proto::channel_member::Kind::Invitee.into(),
+                admin: false,
+            },
+            proto::ChannelMember {
+                user_id: user_3.to_proto(),
+                kind: proto::channel_member::Kind::Invitee.into(),
+                admin: true,
+            },
+        ]
+    );
+
+    db.respond_to_channel_invite(channel_1_1, user_2, true)
+        .await
+        .unwrap();
+
+    let channel_1_3 = db
+        .create_channel("channel_3", Some(channel_1_1), "1", user_1)
+        .await
+        .unwrap();
+
+    let members = db
+        .get_channel_member_details(channel_1_3, user_1)
+        .await
+        .unwrap();
+    assert_eq!(
+        members,
+        &[
+            proto::ChannelMember {
+                user_id: user_1.to_proto(),
+                kind: proto::channel_member::Kind::Member.into(),
+                admin: true,
+            },
+            proto::ChannelMember {
+                user_id: user_2.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                admin: false,
+            },
+        ]
+    );
+}
+
+test_both_dbs!(
+    test_channel_renames,
+    test_channel_renames_postgres,
+    test_channel_renames_sqlite
+);
+
+async fn test_channel_renames(db: &Arc<Database>) {
+    db.create_server("test").await.unwrap();
+
+    let user_1 = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let user_2 = db
+        .create_user(
+            "user2@example.com",
+            false,
+            NewUserParams {
+                github_login: "user2".into(),
+                github_user_id: 6,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
+
+    db.rename_channel(zed_id, user_1, "#zed-archive")
+        .await
+        .unwrap();
+
+    let zed_archive_id = zed_id;
+
+    let (channel, _) = db
+        .get_channel(zed_archive_id, user_1)
+        .await
+        .unwrap()
+        .unwrap();
+    assert_eq!(channel.name, "zed-archive");
+
+    let non_permissioned_rename = db
+        .rename_channel(zed_archive_id, user_2, "hacked-lol")
+        .await;
+    assert!(non_permissioned_rename.is_err());
+
+    let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
+    assert!(bad_name_rename.is_err())
+}
+
+#[gpui::test]
+async fn test_multiple_signup_overwrite() {
+    let test_db = TestDb::postgres(build_background_executor());
+    let db = test_db.db();
+
+    let email_address = "user_1@example.com".to_string();
+
+    let initial_signup_created_at_milliseconds = 0;
+
+    let initial_signup = NewSignup {
+        email_address: email_address.clone(),
+        platform_mac: false,
+        platform_linux: true,
+        platform_windows: false,
+        editor_features: vec!["speed".into()],
+        programming_languages: vec!["rust".into(), "c".into()],
+        device_id: Some(format!("device_id")),
+        added_to_mailing_list: false,
+        created_at: Some(
+            DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(),
+        ),
+    };
+
+    db.create_signup(&initial_signup).await.unwrap();
+
+    let initial_signup_from_db = db.get_signup(&email_address).await.unwrap();
+
+    assert_eq!(
+        initial_signup_from_db.clone(),
+        signup::Model {
+            email_address: initial_signup.email_address,
+            platform_mac: initial_signup.platform_mac,
+            platform_linux: initial_signup.platform_linux,
+            platform_windows: initial_signup.platform_windows,
+            editor_features: Some(initial_signup.editor_features),
+            programming_languages: Some(initial_signup.programming_languages),
+            added_to_mailing_list: initial_signup.added_to_mailing_list,
+            ..initial_signup_from_db
+        }
+    );
+
+    let subsequent_signup = NewSignup {
+        email_address: email_address.clone(),
+        platform_mac: true,
+        platform_linux: false,
+        platform_windows: true,
+        editor_features: vec!["git integration".into(), "clean design".into()],
+        programming_languages: vec!["d".into(), "elm".into()],
+        device_id: Some(format!("different_device_id")),
+        added_to_mailing_list: true,
+        // subsequent signup happens next day
+        created_at: Some(
+            DateTime::from_timestamp_millis(
+                initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24),
+            )
+            .unwrap(),
+        ),
+    };
+
+    db.create_signup(&subsequent_signup).await.unwrap();
+
+    let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap();
+
+    assert_eq!(
+        subsequent_signup_from_db.clone(),
+        signup::Model {
+            platform_mac: subsequent_signup.platform_mac,
+            platform_linux: subsequent_signup.platform_linux,
+            platform_windows: subsequent_signup.platform_windows,
+            editor_features: Some(subsequent_signup.editor_features),
+            programming_languages: Some(subsequent_signup.programming_languages),
+            device_id: subsequent_signup.device_id,
+            added_to_mailing_list: subsequent_signup.added_to_mailing_list,
+            // shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line
+            created_at: initial_signup_from_db.created_at,
+            ..subsequent_signup_from_db
+        }
+    );
+}
+
+#[gpui::test]
+async fn test_signups() {
+    let test_db = TestDb::postgres(build_background_executor());
+    let db = test_db.db();
+
+    let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
+
+    let all_signups = usernames
+        .iter()
+        .enumerate()
+        .map(|(i, username)| NewSignup {
+            email_address: format!("{username}@example.com"),
+            platform_mac: true,
+            platform_linux: i % 2 == 0,
+            platform_windows: i % 4 == 0,
+            editor_features: vec!["speed".into()],
+            programming_languages: vec!["rust".into(), "c".into()],
+            device_id: Some(format!("device_id_{i}")),
+            added_to_mailing_list: i != 0, // One user failed to subscribe
+            created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive
+        })
+        .collect::<Vec<NewSignup>>();
+
+    // people sign up on the waitlist
+    for signup in &all_signups {
+        // users can sign up multiple times without issues
+        for _ in 0..2 {
+            db.create_signup(&signup).await.unwrap();
+        }
+    }
+
+    assert_eq!(
+        db.get_waitlist_summary().await.unwrap(),
+        WaitlistSummary {
+            count: 8,
+            mac_count: 8,
+            linux_count: 4,
+            windows_count: 2,
+            unknown_count: 0,
+        }
+    );
+
+    // retrieve the next batch of signup emails to send
+    let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
+    let addresses = signups_batch1
+        .iter()
+        .map(|s| &s.email_address)
+        .collect::<Vec<_>>();
+    assert_eq!(
+        addresses,
+        &[
+            all_signups[0].email_address.as_str(),
+            all_signups[1].email_address.as_str(),
+            all_signups[2].email_address.as_str()
+        ]
+    );
+    assert_ne!(
+        signups_batch1[0].email_confirmation_code,
+        signups_batch1[1].email_confirmation_code
+    );
+
+    // the waitlist isn't updated until we record that the emails
+    // were successfully sent.
+    let signups_batch = db.get_unsent_invites(3).await.unwrap();
+    assert_eq!(signups_batch, signups_batch1);
+
+    // once the emails go out, we can retrieve the next batch
+    // of signups.
+    db.record_sent_invites(&signups_batch1).await.unwrap();
+    let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
+    let addresses = signups_batch2
+        .iter()
+        .map(|s| &s.email_address)
+        .collect::<Vec<_>>();
+    assert_eq!(
+        addresses,
+        &[
+            all_signups[3].email_address.as_str(),
+            all_signups[4].email_address.as_str(),
+            all_signups[5].email_address.as_str()
+        ]
+    );
+
+    // the sent invites are excluded from the summary.
+    assert_eq!(
+        db.get_waitlist_summary().await.unwrap(),
+        WaitlistSummary {
+            count: 5,
+            mac_count: 5,
+            linux_count: 2,
+            windows_count: 1,
+            unknown_count: 0,
+        }
+    );
+
+    // user completes the signup process by providing their
+    // github account.
+    let NewUserResult {
+        user_id,
+        inviting_user_id,
+        signup_device_id,
+        ..
+    } = db
+        .create_user_from_invite(
+            &Invite {
+                ..signups_batch1[0].clone()
+            },
+            NewUserParams {
+                github_login: usernames[0].clone(),
+                github_user_id: 0,
+                invite_count: 5,
+            },
+        )
+        .await
+        .unwrap()
+        .unwrap();
+    let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
+    assert!(inviting_user_id.is_none());
+    assert_eq!(user.github_login, usernames[0]);
+    assert_eq!(
+        user.email_address,
+        Some(all_signups[0].email_address.clone())
+    );
+    assert_eq!(user.invite_count, 5);
+    assert_eq!(signup_device_id.unwrap(), "device_id_0");
+
+    // cannot redeem the same signup again.
+    assert!(db
+        .create_user_from_invite(
+            &Invite {
+                email_address: signups_batch1[0].email_address.clone(),
+                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
+            },
+            NewUserParams {
+                github_login: "some-other-github_account".into(),
+                github_user_id: 1,
+                invite_count: 5,
+            },
+        )
+        .await
+        .unwrap()
+        .is_none());
+
+    // cannot redeem a signup with the wrong confirmation code.
+    db.create_user_from_invite(
+        &Invite {
+            email_address: signups_batch1[1].email_address.clone(),
+            email_confirmation_code: "the-wrong-code".to_string(),
+        },
+        NewUserParams {
+            github_login: usernames[1].clone(),
+            github_user_id: 2,
+            invite_count: 5,
+        },
+    )
+    .await
+    .unwrap_err();
+}
+
+fn build_background_executor() -> Arc<Background> {
+    Deterministic::new(0).build_background()
+}

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

@@ -0,0 +1,60 @@
+use crate::{
+    db::{Database, NewUserParams},
+    test_both_dbs,
+};
+use std::sync::Arc;
+
+test_both_dbs!(
+    test_get_user_flags,
+    test_get_user_flags_postgres,
+    test_get_user_flags_sqlite
+);
+
+async fn test_get_user_flags(db: &Arc<Database>) {
+    let user_1 = db
+        .create_user(
+            &format!("user1@example.com"),
+            false,
+            NewUserParams {
+                github_login: format!("user1"),
+                github_user_id: 1,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let user_2 = db
+        .create_user(
+            &format!("user2@example.com"),
+            false,
+            NewUserParams {
+                github_login: format!("user2"),
+                github_user_id: 2,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    const CHANNELS_ALPHA: &'static str = "channels-alpha";
+    const NEW_SEARCH: &'static str = "new-search";
+
+    let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap();
+    let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap();
+
+    db.add_user_flag(user_1, channels_flag).await.unwrap();
+    db.add_user_flag(user_1, search_flag).await.unwrap();
+
+    db.add_user_flag(user_2, channels_flag).await.unwrap();
+
+    let mut user_1_flags = db.get_user_flags(user_1).await.unwrap();
+    user_1_flags.sort();
+    assert_eq!(user_1_flags, &[CHANNELS_ALPHA, NEW_SEARCH]);
+
+    let mut user_2_flags = db.get_user_flags(user_2).await.unwrap();
+    user_2_flags.sort();
+    assert_eq!(user_2_flags, &[CHANNELS_ALPHA]);
+}

crates/collab/src/rpc.rs 🔗

@@ -2,7 +2,7 @@ mod connection_pool;
 
 use crate::{
     auth,
-    db::{self, Database, ProjectId, RoomId, ServerId, User, UserId},
+    db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId},
     executor::Executor,
     AppState, Result,
 };
@@ -34,7 +34,10 @@ use futures::{
 use lazy_static::lazy_static;
 use prometheus::{register_int_gauge, IntGauge};
 use rpc::{
-    proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
+    proto::{
+        self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage,
+        LiveKitConnectionInfo, RequestMessage,
+    },
     Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
 };
 use serde::{Serialize, Serializer};
@@ -239,6 +242,18 @@ impl Server {
             .add_request_handler(request_contact)
             .add_request_handler(remove_contact)
             .add_request_handler(respond_to_contact_request)
+            .add_request_handler(create_channel)
+            .add_request_handler(remove_channel)
+            .add_request_handler(invite_channel_member)
+            .add_request_handler(remove_channel_member)
+            .add_request_handler(set_channel_member_admin)
+            .add_request_handler(rename_channel)
+            .add_request_handler(join_channel_buffer)
+            .add_request_handler(leave_channel_buffer)
+            .add_message_handler(update_channel_buffer)
+            .add_request_handler(get_channel_members)
+            .add_request_handler(respond_to_channel_invite)
+            .add_request_handler(join_channel)
             .add_request_handler(follow)
             .add_message_handler(unfollow)
             .add_message_handler(update_followers)
@@ -287,6 +302,15 @@ impl Server {
                                 "refreshed room"
                             );
                             room_updated(&refreshed_room.room, &peer);
+                            if let Some(channel_id) = refreshed_room.channel_id {
+                                channel_updated(
+                                    channel_id,
+                                    &refreshed_room.room,
+                                    &refreshed_room.channel_members,
+                                    &peer,
+                                    &*pool.lock(),
+                                );
+                            }
                             contacts_to_update
                                 .extend(refreshed_room.stale_participant_user_ids.iter().copied());
                             contacts_to_update
@@ -508,15 +532,21 @@ impl Server {
                 this.app_state.db.set_user_connected_once(user_id, true).await?;
             }
 
-            let (contacts, invite_code) = future::try_join(
+            let (contacts, invite_code, channels_for_user, channel_invites) = future::try_join4(
                 this.app_state.db.get_contacts(user_id),
-                this.app_state.db.get_invite_code_for_user(user_id)
+                this.app_state.db.get_invite_code_for_user(user_id),
+                this.app_state.db.get_channels_for_user(user_id),
+                this.app_state.db.get_channel_invites_for_user(user_id)
             ).await?;
 
             {
                 let mut pool = this.connection_pool.lock();
                 pool.add_connection(connection_id, user_id, user.admin);
                 this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
+                this.peer.send(connection_id, build_initial_channels_update(
+                    channels_for_user,
+                    channel_invites
+                ))?;
 
                 if let Some((code, count)) = invite_code {
                     this.peer.send(connection_id, proto::UpdateInviteInfo {
@@ -824,6 +854,10 @@ async fn connection_lost(
         .await
         .trace_err();
 
+    leave_channel_buffers_for_session(&session)
+        .await
+        .trace_err();
+
     futures::select_biased! {
         _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => {
             leave_room_for_session(&session).await.trace_err();
@@ -839,6 +873,8 @@ async fn connection_lost(
                 }
             }
             update_user_contacts(session.user_id, &session).await?;
+
+
         }
         _ = teardown.changed().fuse() => {}
     }
@@ -857,42 +893,41 @@ async fn create_room(
     session: Session,
 ) -> Result<()> {
     let live_kit_room = nanoid::nanoid!(30);
-    let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
-        if let Some(_) = live_kit
-            .create_room(live_kit_room.clone())
-            .await
-            .trace_err()
-        {
-            if let Some(token) = live_kit
-                .room_token(&live_kit_room, &session.user_id.to_string())
-                .trace_err()
-            {
-                Some(proto::LiveKitConnectionInfo {
-                    server_url: live_kit.url().into(),
-                    token,
-                })
-            } else {
-                None
-            }
-        } else {
-            None
-        }
-    } else {
-        None
-    };
 
-    {
-        let room = session
-            .db()
-            .await
-            .create_room(session.user_id, session.connection_id, &live_kit_room)
-            .await?;
+    let live_kit_connection_info = {
+        let live_kit_room = live_kit_room.clone();
+        let live_kit = session.live_kit_client.as_ref();
 
-        response.send(proto::CreateRoomResponse {
-            room: Some(room.clone()),
-            live_kit_connection_info,
-        })?;
+        util::async_iife!({
+            let live_kit = live_kit?;
+
+            live_kit
+                .create_room(live_kit_room.clone())
+                .await
+                .trace_err()?;
+
+            let token = live_kit
+                .room_token(&live_kit_room, &session.user_id.to_string())
+                .trace_err()?;
+
+            Some(proto::LiveKitConnectionInfo {
+                server_url: live_kit.url().into(),
+                token,
+            })
+        })
     }
+    .await;
+
+    let room = session
+        .db()
+        .await
+        .create_room(session.user_id, session.connection_id, &live_kit_room)
+        .await?;
+
+    response.send(proto::CreateRoomResponse {
+        room: Some(room.clone()),
+        live_kit_connection_info,
+    })?;
 
     update_user_contacts(session.user_id, &session).await?;
     Ok(())
@@ -904,16 +939,26 @@ async fn join_room(
     session: Session,
 ) -> Result<()> {
     let room_id = RoomId::from_proto(request.id);
-    let room = {
+    let joined_room = {
         let room = session
             .db()
             .await
             .join_room(room_id, session.user_id, session.connection_id)
             .await?;
-        room_updated(&room, &session.peer);
-        room.clone()
+        room_updated(&room.room, &session.peer);
+        room.into_inner()
     };
 
+    if let Some(channel_id) = joined_room.channel_id {
+        channel_updated(
+            channel_id,
+            &joined_room.room,
+            &joined_room.channel_members,
+            &session.peer,
+            &*session.connection_pool().await,
+        )
+    }
+
     for connection_id in session
         .connection_pool()
         .await
@@ -932,7 +977,10 @@ async fn join_room(
 
     let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
         if let Some(token) = live_kit
-            .room_token(&room.live_kit_room, &session.user_id.to_string())
+            .room_token(
+                &joined_room.room.live_kit_room,
+                &session.user_id.to_string(),
+            )
             .trace_err()
         {
             Some(proto::LiveKitConnectionInfo {
@@ -947,7 +995,8 @@ async fn join_room(
     };
 
     response.send(proto::JoinRoomResponse {
-        room: Some(room),
+        room: Some(joined_room.room),
+        channel_id: joined_room.channel_id.map(|id| id.to_proto()),
         live_kit_connection_info,
     })?;
 
@@ -960,6 +1009,9 @@ async fn rejoin_room(
     response: Response<proto::RejoinRoom>,
     session: Session,
 ) -> Result<()> {
+    let room;
+    let channel_id;
+    let channel_members;
     {
         let mut rejoined_room = session
             .db()
@@ -1121,6 +1173,22 @@ async fn rejoin_room(
                 )?;
             }
         }
+
+        let rejoined_room = rejoined_room.into_inner();
+
+        room = rejoined_room.room;
+        channel_id = rejoined_room.channel_id;
+        channel_members = rejoined_room.channel_members;
+    }
+
+    if let Some(channel_id) = channel_id {
+        channel_updated(
+            channel_id,
+            &room,
+            &channel_members,
+            &session.peer,
+            &*session.connection_pool().await,
+        );
     }
 
     update_user_contacts(session.user_id, &session).await?;
@@ -1282,11 +1350,12 @@ async fn update_participant_location(
     let location = request
         .location
         .ok_or_else(|| anyhow!("invalid location"))?;
-    let room = session
-        .db()
-        .await
+
+    let db = session.db().await;
+    let room = db
         .update_room_participant_location(room_id, session.connection_id, location)
         .await?;
+
     room_updated(&room, &session.peer);
     response.send(proto::Ack {})?;
     Ok(())
@@ -2084,6 +2153,438 @@ async fn remove_contact(
     Ok(())
 }
 
+async fn create_channel(
+    request: proto::CreateChannel,
+    response: Response<proto::CreateChannel>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
+
+    if let Some(live_kit) = session.live_kit_client.as_ref() {
+        live_kit.create_room(live_kit_room.clone()).await?;
+    }
+
+    let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
+    let id = db
+        .create_channel(&request.name, parent_id, &live_kit_room, session.user_id)
+        .await?;
+
+    let channel = proto::Channel {
+        id: id.to_proto(),
+        name: request.name,
+        parent_id: request.parent_id,
+    };
+
+    response.send(proto::ChannelResponse {
+        channel: Some(channel.clone()),
+    })?;
+
+    let mut update = proto::UpdateChannels::default();
+    update.channels.push(channel);
+
+    let user_ids_to_notify = if let Some(parent_id) = parent_id {
+        db.get_channel_members(parent_id).await?
+    } else {
+        vec![session.user_id]
+    };
+
+    let connection_pool = session.connection_pool().await;
+    for user_id in user_ids_to_notify {
+        for connection_id in connection_pool.user_connection_ids(user_id) {
+            let mut update = update.clone();
+            if user_id == session.user_id {
+                update.channel_permissions.push(proto::ChannelPermission {
+                    channel_id: id.to_proto(),
+                    is_admin: true,
+                });
+            }
+            session.peer.send(connection_id, update)?;
+        }
+    }
+
+    Ok(())
+}
+
+async fn remove_channel(
+    request: proto::RemoveChannel,
+    response: Response<proto::RemoveChannel>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+
+    let channel_id = request.channel_id;
+    let (removed_channels, member_ids) = db
+        .remove_channel(ChannelId::from_proto(channel_id), session.user_id)
+        .await?;
+    response.send(proto::Ack {})?;
+
+    // Notify members of removed channels
+    let mut update = proto::UpdateChannels::default();
+    update
+        .remove_channels
+        .extend(removed_channels.into_iter().map(|id| id.to_proto()));
+
+    let connection_pool = session.connection_pool().await;
+    for member_id in member_ids {
+        for connection_id in connection_pool.user_connection_ids(member_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    }
+
+    Ok(())
+}
+
+async fn invite_channel_member(
+    request: proto::InviteChannelMember,
+    response: Response<proto::InviteChannelMember>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let invitee_id = UserId::from_proto(request.user_id);
+    db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin)
+        .await?;
+
+    let (channel, _) = db
+        .get_channel(channel_id, session.user_id)
+        .await?
+        .ok_or_else(|| anyhow!("channel not found"))?;
+
+    let mut update = proto::UpdateChannels::default();
+    update.channel_invitations.push(proto::Channel {
+        id: channel.id.to_proto(),
+        name: channel.name,
+        parent_id: None,
+    });
+    for connection_id in session
+        .connection_pool()
+        .await
+        .user_connection_ids(invitee_id)
+    {
+        session.peer.send(connection_id, update.clone())?;
+    }
+
+    response.send(proto::Ack {})?;
+    Ok(())
+}
+
+async fn remove_channel_member(
+    request: proto::RemoveChannelMember,
+    response: Response<proto::RemoveChannelMember>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let member_id = UserId::from_proto(request.user_id);
+
+    db.remove_channel_member(channel_id, member_id, session.user_id)
+        .await?;
+
+    let mut update = proto::UpdateChannels::default();
+    update.remove_channels.push(channel_id.to_proto());
+
+    for connection_id in session
+        .connection_pool()
+        .await
+        .user_connection_ids(member_id)
+    {
+        session.peer.send(connection_id, update.clone())?;
+    }
+
+    response.send(proto::Ack {})?;
+    Ok(())
+}
+
+async fn set_channel_member_admin(
+    request: proto::SetChannelMemberAdmin,
+    response: Response<proto::SetChannelMemberAdmin>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let member_id = UserId::from_proto(request.user_id);
+    db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin)
+        .await?;
+
+    let (channel, has_accepted) = db
+        .get_channel(channel_id, member_id)
+        .await?
+        .ok_or_else(|| anyhow!("channel not found"))?;
+
+    let mut update = proto::UpdateChannels::default();
+    if has_accepted {
+        update.channel_permissions.push(proto::ChannelPermission {
+            channel_id: channel.id.to_proto(),
+            is_admin: request.admin,
+        });
+    }
+
+    for connection_id in session
+        .connection_pool()
+        .await
+        .user_connection_ids(member_id)
+    {
+        session.peer.send(connection_id, update.clone())?;
+    }
+
+    response.send(proto::Ack {})?;
+    Ok(())
+}
+
+async fn rename_channel(
+    request: proto::RenameChannel,
+    response: Response<proto::RenameChannel>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let new_name = db
+        .rename_channel(channel_id, session.user_id, &request.name)
+        .await?;
+
+    let channel = proto::Channel {
+        id: request.channel_id,
+        name: new_name,
+        parent_id: None,
+    };
+    response.send(proto::ChannelResponse {
+        channel: Some(channel.clone()),
+    })?;
+    let mut update = proto::UpdateChannels::default();
+    update.channels.push(channel);
+
+    let member_ids = db.get_channel_members(channel_id).await?;
+
+    let connection_pool = session.connection_pool().await;
+    for member_id in member_ids {
+        for connection_id in connection_pool.user_connection_ids(member_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    }
+
+    Ok(())
+}
+
+async fn get_channel_members(
+    request: proto::GetChannelMembers,
+    response: Response<proto::GetChannelMembers>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let members = db
+        .get_channel_member_details(channel_id, session.user_id)
+        .await?;
+    response.send(proto::GetChannelMembersResponse { members })?;
+    Ok(())
+}
+
+async fn respond_to_channel_invite(
+    request: proto::RespondToChannelInvite,
+    response: Response<proto::RespondToChannelInvite>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
+        .await?;
+
+    let mut update = proto::UpdateChannels::default();
+    update
+        .remove_channel_invitations
+        .push(channel_id.to_proto());
+    if request.accept {
+        let result = db.get_channels_for_user(session.user_id).await?;
+        update
+            .channels
+            .extend(result.channels.into_iter().map(|channel| proto::Channel {
+                id: channel.id.to_proto(),
+                name: channel.name,
+                parent_id: channel.parent_id.map(ChannelId::to_proto),
+            }));
+        update
+            .channel_participants
+            .extend(
+                result
+                    .channel_participants
+                    .into_iter()
+                    .map(|(channel_id, user_ids)| proto::ChannelParticipants {
+                        channel_id: channel_id.to_proto(),
+                        participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
+                    }),
+            );
+        update
+            .channel_permissions
+            .extend(
+                result
+                    .channels_with_admin_privileges
+                    .into_iter()
+                    .map(|channel_id| proto::ChannelPermission {
+                        channel_id: channel_id.to_proto(),
+                        is_admin: true,
+                    }),
+            );
+    }
+    session.peer.send(session.connection_id, update)?;
+    response.send(proto::Ack {})?;
+
+    Ok(())
+}
+
+async fn join_channel(
+    request: proto::JoinChannel,
+    response: Response<proto::JoinChannel>,
+    session: Session,
+) -> Result<()> {
+    let channel_id = ChannelId::from_proto(request.channel_id);
+
+    let joined_room = {
+        leave_room_for_session(&session).await?;
+        let db = session.db().await;
+
+        let room_id = db.room_id_for_channel(channel_id).await?;
+
+        let joined_room = db
+            .join_room(room_id, session.user_id, session.connection_id)
+            .await?;
+
+        let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
+            let token = live_kit
+                .room_token(
+                    &joined_room.room.live_kit_room,
+                    &session.user_id.to_string(),
+                )
+                .trace_err()?;
+
+            Some(LiveKitConnectionInfo {
+                server_url: live_kit.url().into(),
+                token,
+            })
+        });
+
+        response.send(proto::JoinRoomResponse {
+            room: Some(joined_room.room.clone()),
+            channel_id: joined_room.channel_id.map(|id| id.to_proto()),
+            live_kit_connection_info,
+        })?;
+
+        room_updated(&joined_room.room, &session.peer);
+
+        joined_room.into_inner()
+    };
+
+    channel_updated(
+        channel_id,
+        &joined_room.room,
+        &joined_room.channel_members,
+        &session.peer,
+        &*session.connection_pool().await,
+    );
+
+    update_user_contacts(session.user_id, &session).await?;
+
+    Ok(())
+}
+
+async fn join_channel_buffer(
+    request: proto::JoinChannelBuffer,
+    response: Response<proto::JoinChannelBuffer>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+
+    let open_response = db
+        .join_channel_buffer(channel_id, session.user_id, session.connection_id)
+        .await?;
+
+    let replica_id = open_response.replica_id;
+    let collaborators = open_response.collaborators.clone();
+
+    response.send(open_response)?;
+
+    let update = AddChannelBufferCollaborator {
+        channel_id: channel_id.to_proto(),
+        collaborator: Some(proto::Collaborator {
+            user_id: session.user_id.to_proto(),
+            peer_id: Some(session.connection_id.into()),
+            replica_id,
+        }),
+    };
+    channel_buffer_updated(
+        session.connection_id,
+        collaborators
+            .iter()
+            .filter_map(|collaborator| Some(collaborator.peer_id?.into())),
+        &update,
+        &session.peer,
+    );
+
+    Ok(())
+}
+
+async fn update_channel_buffer(
+    request: proto::UpdateChannelBuffer,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+
+    let collaborators = db
+        .update_channel_buffer(channel_id, session.user_id, &request.operations)
+        .await?;
+
+    channel_buffer_updated(
+        session.connection_id,
+        collaborators,
+        &proto::UpdateChannelBuffer {
+            channel_id: channel_id.to_proto(),
+            operations: request.operations,
+        },
+        &session.peer,
+    );
+    Ok(())
+}
+
+async fn leave_channel_buffer(
+    request: proto::LeaveChannelBuffer,
+    response: Response<proto::LeaveChannelBuffer>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+
+    let collaborators_to_notify = db
+        .leave_channel_buffer(channel_id, session.connection_id)
+        .await?;
+
+    response.send(Ack {})?;
+
+    channel_buffer_updated(
+        session.connection_id,
+        collaborators_to_notify,
+        &proto::RemoveChannelBufferCollaborator {
+            channel_id: channel_id.to_proto(),
+            peer_id: Some(session.connection_id.into()),
+        },
+        &session.peer,
+    );
+
+    Ok(())
+}
+
+fn channel_buffer_updated<T: EnvelopedMessage>(
+    sender_id: ConnectionId,
+    collaborators: impl IntoIterator<Item = ConnectionId>,
+    message: &T,
+    peer: &Peer,
+) {
+    broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| {
+        peer.send(peer_id.into(), message.clone())
+    });
+}
+
 async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
     let project_id = ProjectId::from_proto(request.project_id);
     let project_connection_ids = session
@@ -2108,20 +2609,19 @@ async fn get_private_user_info(
     response: Response<proto::GetPrivateUserInfo>,
     session: Session,
 ) -> Result<()> {
-    let metrics_id = session
-        .db()
-        .await
-        .get_user_metrics_id(session.user_id)
-        .await?;
-    let user = session
-        .db()
-        .await
+    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?
         .ok_or_else(|| anyhow!("user not found"))?;
+    let flags = db.get_user_flags(session.user_id).await?;
+
     response.send(proto::GetPrivateUserInfoResponse {
         metrics_id,
         staff: user.admin,
+        flags,
     })?;
     Ok(())
 }
@@ -2154,6 +2654,52 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage {
     }
 }
 
+fn build_initial_channels_update(
+    channels: ChannelsForUser,
+    channel_invites: Vec<db::Channel>,
+) -> proto::UpdateChannels {
+    let mut update = proto::UpdateChannels::default();
+
+    for channel in channels.channels {
+        update.channels.push(proto::Channel {
+            id: channel.id.to_proto(),
+            name: channel.name,
+            parent_id: channel.parent_id.map(|id| id.to_proto()),
+        });
+    }
+
+    for (channel_id, participants) in channels.channel_participants {
+        update
+            .channel_participants
+            .push(proto::ChannelParticipants {
+                channel_id: channel_id.to_proto(),
+                participant_user_ids: participants.into_iter().map(|id| id.to_proto()).collect(),
+            });
+    }
+
+    update
+        .channel_permissions
+        .extend(
+            channels
+                .channels_with_admin_privileges
+                .into_iter()
+                .map(|id| proto::ChannelPermission {
+                    channel_id: id.to_proto(),
+                    is_admin: true,
+                }),
+        );
+
+    for channel in channel_invites {
+        update.channel_invitations.push(proto::Channel {
+            id: channel.id.to_proto(),
+            name: channel.name,
+            parent_id: None,
+        });
+    }
+
+    update
+}
+
 fn build_initial_contacts_update(
     contacts: Vec<db::Contact>,
     pool: &ConnectionPool,
@@ -2218,8 +2764,42 @@ fn room_updated(room: &proto::Room, peer: &Peer) {
     );
 }
 
+fn channel_updated(
+    channel_id: ChannelId,
+    room: &proto::Room,
+    channel_members: &[UserId],
+    peer: &Peer,
+    pool: &ConnectionPool,
+) {
+    let participants = room
+        .participants
+        .iter()
+        .map(|p| p.user_id)
+        .collect::<Vec<_>>();
+
+    broadcast(
+        None,
+        channel_members
+            .iter()
+            .flat_map(|user_id| pool.user_connection_ids(*user_id)),
+        |peer_id| {
+            peer.send(
+                peer_id.into(),
+                proto::UpdateChannels {
+                    channel_participants: vec![proto::ChannelParticipants {
+                        channel_id: channel_id.to_proto(),
+                        participant_user_ids: participants.clone(),
+                    }],
+                    ..Default::default()
+                },
+            )
+        },
+    );
+}
+
 async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> {
     let db = session.db().await;
+
     let contacts = db.get_contacts(user_id).await?;
     let busy = db.is_user_busy(user_id).await?;
 
@@ -2259,6 +2839,10 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
     let canceled_calls_to_user_ids;
     let live_kit_room;
     let delete_live_kit_room;
+    let room;
+    let channel_members;
+    let channel_id;
+
     if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? {
         contacts_to_update.insert(session.user_id);
 
@@ -2266,15 +2850,29 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
             project_left(project, session);
         }
 
-        room_updated(&left_room.room, &session.peer);
         room_id = RoomId::from_proto(left_room.room.id);
         canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids);
         live_kit_room = mem::take(&mut left_room.room.live_kit_room);
-        delete_live_kit_room = left_room.room.participants.is_empty();
+        delete_live_kit_room = left_room.deleted;
+        room = mem::take(&mut left_room.room);
+        channel_members = mem::take(&mut left_room.channel_members);
+        channel_id = left_room.channel_id;
+
+        room_updated(&room, &session.peer);
     } else {
         return Ok(());
     }
 
+    if let Some(channel_id) = channel_id {
+        channel_updated(
+            channel_id,
+            &room,
+            &channel_members,
+            &session.peer,
+            &*session.connection_pool().await,
+        );
+    }
+
     {
         let pool = session.connection_pool().await;
         for canceled_user_id in canceled_calls_to_user_ids {
@@ -2311,6 +2909,28 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
     Ok(())
 }
 
+async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
+    let left_channel_buffers = session
+        .db()
+        .await
+        .leave_channel_buffers(session.connection_id)
+        .await?;
+
+    for (channel_id, connections) in left_channel_buffers {
+        channel_buffer_updated(
+            session.connection_id,
+            connections,
+            &proto::RemoveChannelBufferCollaborator {
+                channel_id: channel_id.to_proto(),
+                peer_id: Some(session.connection_id.into()),
+            },
+            &session.peer,
+        );
+    }
+
+    Ok(())
+}
+
 fn project_left(project: &db::LeftProject, session: &Session) {
     for connection_id in &project.connection_ids {
         if project.host_user_id == session.user_id {

crates/collab/src/tests.rs 🔗

@@ -1,18 +1,19 @@
 use crate::{
-    db::{NewUserParams, TestDb, UserId},
+    db::{tests::TestDb, NewUserParams, UserId},
     executor::Executor,
     rpc::{Server, CLEANUP_TIMEOUT},
     AppState,
 };
 use anyhow::anyhow;
-use call::ActiveCall;
+use call::{ActiveCall, Room};
+use channel::ChannelStore;
 use client::{
     self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
 };
 use collections::{HashMap, HashSet};
 use fs::FakeFs;
 use futures::{channel::oneshot, StreamExt as _};
-use gpui::{executor::Deterministic, ModelHandle, TestAppContext, WindowHandle};
+use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
 use language::LanguageRegistry;
 use parking_lot::Mutex;
 use project::{Project, WorktreeId};
@@ -30,6 +31,8 @@ use std::{
 use util::http::FakeHttpClient;
 use workspace::Workspace;
 
+mod channel_buffer_tests;
+mod channel_tests;
 mod integration_tests;
 mod randomized_integration_tests;
 
@@ -98,6 +101,9 @@ impl TestServer {
 
     async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
         cx.update(|cx| {
+            if cx.has_global::<SettingsStore>() {
+                panic!("Same cx used to create two test clients")
+            }
             cx.set_global(SettingsStore::test(cx));
         });
 
@@ -183,13 +189,16 @@ impl TestServer {
 
         let fs = FakeFs::new(cx.background());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+        let channel_store =
+            cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
         let app_state = Arc::new(workspace::AppState {
             client: client.clone(),
             user_store: user_store.clone(),
+            channel_store: channel_store.clone(),
             languages: Arc::new(LanguageRegistry::test()),
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
-            initialize_workspace: |_, _, _, _| unimplemented!(),
+            initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             background_actions: || &[],
         });
 
@@ -202,6 +211,7 @@ impl TestServer {
             workspace::init(app_state.clone(), cx);
             audio::init((), cx);
             call::init(client.clone(), user_store.clone(), cx);
+            channel::init(&client);
         });
 
         client
@@ -210,12 +220,9 @@ impl TestServer {
             .unwrap();
 
         let client = TestClient {
-            client,
+            app_state,
             username: name.to_string(),
             state: Default::default(),
-            user_store,
-            fs,
-            language_registry: Arc::new(LanguageRegistry::test()),
         };
         client.wait_for_current_user(cx).await;
         client
@@ -243,6 +250,7 @@ impl TestServer {
             let (client_a, cx_a) = left.last_mut().unwrap();
             for (client_b, cx_b) in right {
                 client_a
+                    .app_state
                     .user_store
                     .update(*cx_a, |store, cx| {
                         store.request_contact(client_b.user_id().unwrap(), cx)
@@ -251,6 +259,7 @@ impl TestServer {
                     .unwrap();
                 cx_a.foreground().run_until_parked();
                 client_b
+                    .app_state
                     .user_store
                     .update(*cx_b, |store, cx| {
                         store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
@@ -261,6 +270,52 @@ impl TestServer {
         }
     }
 
+    async fn make_channel(
+        &self,
+        channel: &str,
+        admin: (&TestClient, &mut TestAppContext),
+        members: &mut [(&TestClient, &mut TestAppContext)],
+    ) -> u64 {
+        let (admin_client, admin_cx) = admin;
+        let channel_id = admin_client
+            .app_state
+            .channel_store
+            .update(admin_cx, |channel_store, cx| {
+                channel_store.create_channel(channel, None, cx)
+            })
+            .await
+            .unwrap();
+
+        for (member_client, member_cx) in members {
+            admin_client
+                .app_state
+                .channel_store
+                .update(admin_cx, |channel_store, cx| {
+                    channel_store.invite_member(
+                        channel_id,
+                        member_client.user_id().unwrap(),
+                        false,
+                        cx,
+                    )
+                })
+                .await
+                .unwrap();
+
+            admin_cx.foreground().run_until_parked();
+
+            member_client
+                .app_state
+                .channel_store
+                .update(*member_cx, |channels, _| {
+                    channels.respond_to_channel_invite(channel_id, true)
+                })
+                .await
+                .unwrap();
+        }
+
+        channel_id
+    }
+
     async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
         self.make_contacts(clients).await;
 
@@ -312,12 +367,9 @@ impl Drop for TestServer {
 }
 
 struct TestClient {
-    client: Arc<Client>,
     username: String,
     state: RefCell<TestClientState>,
-    pub user_store: ModelHandle<UserStore>,
-    language_registry: Arc<LanguageRegistry>,
-    fs: Arc<FakeFs>,
+    app_state: Arc<workspace::AppState>,
 }
 
 #[derive(Default)]
@@ -331,7 +383,7 @@ impl Deref for TestClient {
     type Target = Arc<Client>;
 
     fn deref(&self) -> &Self::Target {
-        &self.client
+        &self.app_state.client
     }
 }
 
@@ -342,22 +394,45 @@ struct ContactsSummary {
 }
 
 impl TestClient {
+    pub fn fs(&self) -> &FakeFs {
+        self.app_state.fs.as_fake()
+    }
+
+    pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
+        &self.app_state.channel_store
+    }
+
+    pub fn user_store(&self) -> &ModelHandle<UserStore> {
+        &self.app_state.user_store
+    }
+
+    pub fn language_registry(&self) -> &Arc<LanguageRegistry> {
+        &self.app_state.languages
+    }
+
+    pub fn client(&self) -> &Arc<Client> {
+        &self.app_state.client
+    }
+
     pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
         UserId::from_proto(
-            self.user_store
+            self.app_state
+                .user_store
                 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
         )
     }
 
     async fn wait_for_current_user(&self, cx: &TestAppContext) {
         let mut authed_user = self
+            .app_state
             .user_store
             .read_with(cx, |user_store, _| user_store.watch_current_user());
         while authed_user.next().await.unwrap().is_none() {}
     }
 
     async fn clear_contacts(&self, cx: &mut TestAppContext) {
-        self.user_store
+        self.app_state
+            .user_store
             .update(cx, |store, _| store.clear_contacts())
             .await;
     }
@@ -395,23 +470,25 @@ impl TestClient {
     }
 
     fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
-        self.user_store.read_with(cx, |store, _| ContactsSummary {
-            current: store
-                .contacts()
-                .iter()
-                .map(|contact| contact.user.github_login.clone())
-                .collect(),
-            outgoing_requests: store
-                .outgoing_contact_requests()
-                .iter()
-                .map(|user| user.github_login.clone())
-                .collect(),
-            incoming_requests: store
-                .incoming_contact_requests()
-                .iter()
-                .map(|user| user.github_login.clone())
-                .collect(),
-        })
+        self.app_state
+            .user_store
+            .read_with(cx, |store, _| ContactsSummary {
+                current: store
+                    .contacts()
+                    .iter()
+                    .map(|contact| contact.user.github_login.clone())
+                    .collect(),
+                outgoing_requests: store
+                    .outgoing_contact_requests()
+                    .iter()
+                    .map(|user| user.github_login.clone())
+                    .collect(),
+                incoming_requests: store
+                    .incoming_contact_requests()
+                    .iter()
+                    .map(|user| user.github_login.clone())
+                    .collect(),
+            })
     }
 
     async fn build_local_project(
@@ -421,10 +498,10 @@ impl TestClient {
     ) -> (ModelHandle<Project>, WorktreeId) {
         let project = cx.update(|cx| {
             Project::local(
-                self.client.clone(),
-                self.user_store.clone(),
-                self.language_registry.clone(),
-                self.fs.clone(),
+                self.client().clone(),
+                self.app_state.user_store.clone(),
+                self.app_state.languages.clone(),
+                self.app_state.fs.clone(),
                 cx,
             )
         });
@@ -450,8 +527,8 @@ impl TestClient {
         room.update(guest_cx, |room, cx| {
             room.join_project(
                 host_project_id,
-                self.language_registry.clone(),
-                self.fs.clone(),
+                self.app_state.languages.clone(),
+                self.app_state.fs.clone(),
                 cx,
             )
         })
@@ -464,12 +541,36 @@ impl TestClient {
         project: &ModelHandle<Project>,
         cx: &mut TestAppContext,
     ) -> WindowHandle<Workspace> {
-        cx.add_window(|cx| Workspace::test_new(project.clone(), cx))
+        cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
     }
 }
 
 impl Drop for TestClient {
     fn drop(&mut self) {
-        self.client.teardown();
+        self.app_state.client.teardown();
     }
 }
+
+#[derive(Debug, Eq, PartialEq)]
+struct RoomParticipants {
+    remote: Vec<String>,
+    pending: Vec<String>,
+}
+
+fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
+    room.read_with(cx, |room, _| {
+        let mut remote = room
+            .remote_participants()
+            .iter()
+            .map(|(_, participant)| participant.user.github_login.clone())
+            .collect::<Vec<_>>();
+        let mut pending = room
+            .pending_participants()
+            .iter()
+            .map(|user| user.github_login.clone())
+            .collect::<Vec<_>>();
+        remote.sort();
+        pending.sort();
+        RoomParticipants { remote, pending }
+    })
+}

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

@@ -0,0 +1,426 @@
+use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+use call::ActiveCall;
+use channel::Channel;
+use client::UserId;
+use collab_ui::channel_view::ChannelView;
+use collections::HashMap;
+use futures::future;
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
+use rpc::{proto, RECEIVE_TIMEOUT};
+use serde_json::json;
+use std::sync::Arc;
+
+#[gpui::test]
+async fn test_core_channel_buffers(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    let zed_id = server
+        .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .await;
+
+    // Client A joins the channel buffer
+    let channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx))
+        .await
+        .unwrap();
+
+    // Client A edits the buffer
+    let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer());
+
+    buffer_a.update(cx_a, |buffer, cx| {
+        buffer.edit([(0..0, "hello world")], None, cx)
+    });
+    buffer_a.update(cx_a, |buffer, cx| {
+        buffer.edit([(5..5, ", cruel")], None, cx)
+    });
+    buffer_a.update(cx_a, |buffer, cx| {
+        buffer.edit([(0..5, "goodbye")], None, cx)
+    });
+    buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx));
+    deterministic.run_until_parked();
+
+    assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world");
+
+    // Client B joins the channel buffer
+    let channel_buffer_b = client_b
+        .channel_store()
+        .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx))
+        .await
+        .unwrap();
+
+    channel_buffer_b.read_with(cx_b, |buffer, _| {
+        assert_collaborators(
+            buffer.collaborators(),
+            &[client_a.user_id(), client_b.user_id()],
+        );
+    });
+
+    // Client B sees the correct text, and then edits it
+    let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer());
+    assert_eq!(
+        buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()),
+        buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id())
+    );
+    assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world");
+    buffer_b.update(cx_b, |buffer, cx| {
+        buffer.edit([(7..12, "beautiful")], None, cx)
+    });
+
+    // Both A and B see the new edit
+    deterministic.run_until_parked();
+    assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world");
+    assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world");
+
+    // Client A closes the channel buffer.
+    cx_a.update(|_| drop(channel_buffer_a));
+    deterministic.run_until_parked();
+
+    // Client B sees that client A is gone from the channel buffer.
+    channel_buffer_b.read_with(cx_b, |buffer, _| {
+        assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
+    });
+
+    // Client A rejoins the channel buffer
+    let _channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |channels, cx| {
+            channels.open_channel_buffer(zed_id, cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    // Sanity test, make sure we saw A rejoining
+    channel_buffer_b.read_with(cx_b, |buffer, _| {
+        assert_collaborators(
+            &buffer.collaborators(),
+            &[client_b.user_id(), client_a.user_id()],
+        );
+    });
+
+    // Client A loses connection.
+    server.forbid_connections();
+    server.disconnect_client(client_a.peer_id().unwrap());
+    deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+    // Client B observes A disconnect
+    channel_buffer_b.read_with(cx_b, |buffer, _| {
+        assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
+    });
+
+    // TODO:
+    // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects
+    // - Test interaction with channel deletion while buffer is open
+}
+
+#[gpui::test]
+async fn test_channel_buffer_replica_ids(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+
+    let channel_id = server
+        .make_channel(
+            "zed",
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+    let active_call_c = cx_c.read(ActiveCall::global);
+
+    // Clients A and B join a channel.
+    active_call_a
+        .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
+        .await
+        .unwrap();
+    active_call_b
+        .update(cx_b, |call, cx| call.join_channel(channel_id, cx))
+        .await
+        .unwrap();
+
+    // Clients A, B, and C join a channel buffer
+    // C first so that the replica IDs in the project and the channel buffer are different
+    let channel_buffer_c = client_c
+        .channel_store()
+        .update(cx_c, |channel, cx| {
+            channel.open_channel_buffer(channel_id, cx)
+        })
+        .await
+        .unwrap();
+    let channel_buffer_b = client_b
+        .channel_store()
+        .update(cx_b, |channel, cx| {
+            channel.open_channel_buffer(channel_id, cx)
+        })
+        .await
+        .unwrap();
+    let channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |channel, cx| {
+            channel.open_channel_buffer(channel_id, cx)
+        })
+        .await
+        .unwrap();
+
+    // Client B shares a project
+    client_b
+        .fs()
+        .insert_tree("/dir", json!({ "file.txt": "contents" }))
+        .await;
+    let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
+    let shared_project_id = active_call_b
+        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+        .await
+        .unwrap();
+
+    // Client A joins the project
+    let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
+    deterministic.run_until_parked();
+
+    // Client C is in a separate project.
+    client_c.fs().insert_tree("/dir", json!({})).await;
+    let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
+
+    // Note that each user has a different replica id in the projects vs the
+    // channel buffer.
+    channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
+        assert_eq!(project_a.read(cx).replica_id(), 1);
+        assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
+    });
+    channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
+        assert_eq!(project_b.read(cx).replica_id(), 0);
+        assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
+    });
+    channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
+        // C is not in the project
+        assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
+    });
+
+    let channel_window_a =
+        cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
+    let channel_window_b =
+        cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
+    let channel_window_c = cx_c.add_window(|cx| {
+        ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
+    });
+
+    let channel_view_a = channel_window_a.root(cx_a);
+    let channel_view_b = channel_window_b.root(cx_b);
+    let channel_view_c = channel_window_c.root(cx_c);
+
+    // For clients A and B, the replica ids in the channel buffer are mapped
+    // so that they match the same users' replica ids in their shared project.
+    channel_view_a.read_with(cx_a, |view, cx| {
+        assert_eq!(
+            view.editor.read(cx).replica_id_map().unwrap(),
+            &[(1, 0), (2, 1)].into_iter().collect::<HashMap<_, _>>()
+        );
+    });
+    channel_view_b.read_with(cx_b, |view, cx| {
+        assert_eq!(
+            view.editor.read(cx).replica_id_map().unwrap(),
+            &[(1, 0), (2, 1)].into_iter().collect::<HashMap<u16, u16>>(),
+        )
+    });
+
+    // Client C only sees themself, as they're not part of any shared project
+    channel_view_c.read_with(cx_c, |view, cx| {
+        assert_eq!(
+            view.editor.read(cx).replica_id_map().unwrap(),
+            &[(0, 0)].into_iter().collect::<HashMap<u16, u16>>(),
+        );
+    });
+
+    // Client C joins the project that clients A and B are in.
+    active_call_c
+        .update(cx_c, |call, cx| call.join_channel(channel_id, cx))
+        .await
+        .unwrap();
+    let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
+    deterministic.run_until_parked();
+    project_c.read_with(cx_c, |project, _| {
+        assert_eq!(project.replica_id(), 2);
+    });
+
+    // For clients A and B, client C's replica id in the channel buffer is
+    // now mapped to their replica id in the shared project.
+    channel_view_a.read_with(cx_a, |view, cx| {
+        assert_eq!(
+            view.editor.read(cx).replica_id_map().unwrap(),
+            &[(1, 0), (2, 1), (0, 2)]
+                .into_iter()
+                .collect::<HashMap<_, _>>()
+        );
+    });
+    channel_view_b.read_with(cx_b, |view, cx| {
+        assert_eq!(
+            view.editor.read(cx).replica_id_map().unwrap(),
+            &[(1, 0), (2, 1), (0, 2)]
+                .into_iter()
+                .collect::<HashMap<_, _>>(),
+        )
+    });
+}
+
+#[gpui::test]
+async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+
+    let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await;
+
+    let channel_buffer_1 = client_a
+        .channel_store()
+        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
+    let channel_buffer_2 = client_a
+        .channel_store()
+        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
+    let channel_buffer_3 = client_a
+        .channel_store()
+        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx));
+
+    // All concurrent tasks for opening a channel buffer return the same model handle.
+    let (channel_buffer_1, channel_buffer_2, channel_buffer_3) =
+        future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3)
+            .await
+            .unwrap();
+    let model_id = channel_buffer_1.id();
+    assert_eq!(channel_buffer_1, channel_buffer_2);
+    assert_eq!(channel_buffer_1, channel_buffer_3);
+
+    channel_buffer_1.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "hello")], None, cx);
+        })
+    });
+    deterministic.run_until_parked();
+
+    cx_a.update(|_| {
+        drop(channel_buffer_1);
+        drop(channel_buffer_2);
+        drop(channel_buffer_3);
+    });
+    deterministic.run_until_parked();
+
+    // The channel buffer can be reopened after dropping it.
+    let channel_buffer = client_a
+        .channel_store()
+        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx))
+        .await
+        .unwrap();
+    assert_ne!(channel_buffer.id(), model_id);
+    channel_buffer.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, _| {
+            assert_eq!(buffer.text(), "hello");
+        })
+    });
+}
+
+#[gpui::test]
+async fn test_channel_buffer_disconnect(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    let channel_id = server
+        .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .await;
+
+    let channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |channel, cx| {
+            channel.open_channel_buffer(channel_id, cx)
+        })
+        .await
+        .unwrap();
+
+    let channel_buffer_b = client_b
+        .channel_store()
+        .update(cx_b, |channel, cx| {
+            channel.open_channel_buffer(channel_id, cx)
+        })
+        .await
+        .unwrap();
+
+    server.forbid_connections();
+    server.disconnect_client(client_a.peer_id().unwrap());
+    deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+    channel_buffer_a.update(cx_a, |buffer, _| {
+        assert_eq!(
+            buffer.channel().as_ref(),
+            &Channel {
+                id: channel_id,
+                name: "zed".to_string()
+            }
+        );
+        assert!(!buffer.is_connected());
+    });
+
+    deterministic.run_until_parked();
+
+    server.allow_connections();
+    deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+    deterministic.run_until_parked();
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, _| {
+            channel_store.remove_channel(channel_id)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    // Channel buffer observed the deletion
+    channel_buffer_b.update(cx_b, |buffer, _| {
+        assert_eq!(
+            buffer.channel().as_ref(),
+            &Channel {
+                id: channel_id,
+                name: "zed".to_string()
+            }
+        );
+        assert!(!buffer.is_connected());
+    });
+}
+
+#[track_caller]
+fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
+    assert_eq!(
+        collaborators
+            .into_iter()
+            .map(|collaborator| collaborator.user_id)
+            .collect::<Vec<_>>(),
+        ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
+    );
+}
+
+fn buffer_text(channel_buffer: &ModelHandle<language::Buffer>, cx: &mut TestAppContext) -> String {
+    channel_buffer.read_with(cx, |buffer, _| buffer.text())
+}

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

@@ -0,0 +1,924 @@
+use crate::{
+    rpc::RECONNECT_TIMEOUT,
+    tests::{room_participants, RoomParticipants, TestServer},
+};
+use call::ActiveCall;
+use channel::{ChannelId, ChannelMembership, ChannelStore};
+use client::User;
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
+use rpc::{proto, RECEIVE_TIMEOUT};
+use std::sync::Arc;
+
+#[gpui::test]
+async fn test_core_channels(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    let channel_a_id = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("channel-a", None, cx)
+        })
+        .await
+        .unwrap();
+    let channel_b_id = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("channel-b", Some(channel_a_id), cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    assert_channels(
+        client_a.channel_store(),
+        cx_a,
+        &[
+            ExpectedChannel {
+                id: channel_a_id,
+                name: "channel-a".to_string(),
+                depth: 0,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_b_id,
+                name: "channel-b".to_string(),
+                depth: 1,
+                user_is_admin: true,
+            },
+        ],
+    );
+
+    client_b.channel_store().read_with(cx_b, |channels, _| {
+        assert!(channels.channels().collect::<Vec<_>>().is_empty())
+    });
+
+    // Invite client B to channel A as client A.
+    client_a
+        .channel_store()
+        .update(cx_a, |store, cx| {
+            assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
+
+            let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
+
+            // Make sure we're synchronously storing the pending invite
+            assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
+            invite
+        })
+        .await
+        .unwrap();
+
+    // Client A sees that B has been invited.
+    deterministic.run_until_parked();
+    assert_channel_invitations(
+        client_b.channel_store(),
+        cx_b,
+        &[ExpectedChannel {
+            id: channel_a_id,
+            name: "channel-a".to_string(),
+            depth: 0,
+            user_is_admin: false,
+        }],
+    );
+
+    let members = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| {
+            assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
+            store.get_channel_member_details(channel_a_id, cx)
+        })
+        .await
+        .unwrap();
+    assert_members_eq(
+        &members,
+        &[
+            (
+                client_a.user_id().unwrap(),
+                true,
+                proto::channel_member::Kind::Member,
+            ),
+            (
+                client_b.user_id().unwrap(),
+                false,
+                proto::channel_member::Kind::Invitee,
+            ),
+        ],
+    );
+
+    // Client B accepts the invitation.
+    client_b
+        .channel_store()
+        .update(cx_b, |channels, _| {
+            channels.respond_to_channel_invite(channel_a_id, true)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    // Client B now sees that they are a member of channel A and its existing subchannels.
+    assert_channel_invitations(client_b.channel_store(), cx_b, &[]);
+    assert_channels(
+        client_b.channel_store(),
+        cx_b,
+        &[
+            ExpectedChannel {
+                id: channel_a_id,
+                name: "channel-a".to_string(),
+                user_is_admin: false,
+                depth: 0,
+            },
+            ExpectedChannel {
+                id: channel_b_id,
+                name: "channel-b".to_string(),
+                user_is_admin: false,
+                depth: 1,
+            },
+        ],
+    );
+
+    let channel_c_id = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("channel-c", Some(channel_b_id), cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    assert_channels(
+        client_b.channel_store(),
+        cx_b,
+        &[
+            ExpectedChannel {
+                id: channel_a_id,
+                name: "channel-a".to_string(),
+                user_is_admin: false,
+                depth: 0,
+            },
+            ExpectedChannel {
+                id: channel_b_id,
+                name: "channel-b".to_string(),
+                user_is_admin: false,
+                depth: 1,
+            },
+            ExpectedChannel {
+                id: channel_c_id,
+                name: "channel-c".to_string(),
+                user_is_admin: false,
+                depth: 2,
+            },
+        ],
+    );
+
+    // Update client B's membership to channel A to be an admin.
+    client_a
+        .channel_store()
+        .update(cx_a, |store, cx| {
+            store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    // Observe that client B is now an admin of channel A, and that
+    // their admin priveleges extend to subchannels of channel A.
+    assert_channel_invitations(client_b.channel_store(), cx_b, &[]);
+    assert_channels(
+        client_b.channel_store(),
+        cx_b,
+        &[
+            ExpectedChannel {
+                id: channel_a_id,
+                name: "channel-a".to_string(),
+                depth: 0,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_b_id,
+                name: "channel-b".to_string(),
+                depth: 1,
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                id: channel_c_id,
+                name: "channel-c".to_string(),
+                depth: 2,
+                user_is_admin: true,
+            },
+        ],
+    );
+
+    // Client A deletes the channel, deletion also deletes subchannels.
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, _| {
+            channel_store.remove_channel(channel_b_id)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    assert_channels(
+        client_a.channel_store(),
+        cx_a,
+        &[ExpectedChannel {
+            id: channel_a_id,
+            name: "channel-a".to_string(),
+            depth: 0,
+            user_is_admin: true,
+        }],
+    );
+    assert_channels(
+        client_b.channel_store(),
+        cx_b,
+        &[ExpectedChannel {
+            id: channel_a_id,
+            name: "channel-a".to_string(),
+            depth: 0,
+            user_is_admin: true,
+        }],
+    );
+
+    // Remove client B
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.remove_member(channel_a_id, client_b.user_id().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // Client A still has their channel
+    assert_channels(
+        client_a.channel_store(),
+        cx_a,
+        &[ExpectedChannel {
+            id: channel_a_id,
+            name: "channel-a".to_string(),
+            depth: 0,
+            user_is_admin: true,
+        }],
+    );
+
+    // Client B no longer has access to the channel
+    assert_channels(client_b.channel_store(), cx_b, &[]);
+
+    // When disconnected, client A sees no channels.
+    server.forbid_connections();
+    server.disconnect_client(client_a.peer_id().unwrap());
+    deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+    assert_channels(client_a.channel_store(), cx_a, &[]);
+
+    server.allow_connections();
+    deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+    assert_channels(
+        client_a.channel_store(),
+        cx_a,
+        &[ExpectedChannel {
+            id: channel_a_id,
+            name: "channel-a".to_string(),
+            depth: 0,
+            user_is_admin: true,
+        }],
+    );
+}
+
+#[track_caller]
+fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u64]) {
+    assert_eq!(
+        participants.iter().map(|p| p.id).collect::<Vec<_>>(),
+        expected_partitipants
+    );
+}
+
+#[track_caller]
+fn assert_members_eq(
+    members: &[ChannelMembership],
+    expected_members: &[(u64, bool, proto::channel_member::Kind)],
+) {
+    assert_eq!(
+        members
+            .iter()
+            .map(|member| (member.user.id, member.admin, member.kind))
+            .collect::<Vec<_>>(),
+        expected_members
+    );
+}
+
+#[gpui::test]
+async fn test_joining_channel_ancestor_member(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    let parent_id = server
+        .make_channel("parent", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .await;
+
+    let sub_id = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("sub_channel", Some(parent_id), cx)
+        })
+        .await
+        .unwrap();
+
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    assert!(active_call_b
+        .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
+        .await
+        .is_ok());
+}
+
+#[gpui::test]
+async fn test_channel_room(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+
+    let zed_id = server
+        .make_channel(
+            "zed",
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    active_call_a
+        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .await
+        .unwrap();
+
+    // Give everyone a chance to observe user A joining
+    deterministic.run_until_parked();
+
+    client_a.channel_store().read_with(cx_a, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_a.user_id().unwrap()],
+        );
+    });
+
+    assert_channels(
+        client_b.channel_store(),
+        cx_b,
+        &[ExpectedChannel {
+            id: zed_id,
+            name: "zed".to_string(),
+            depth: 0,
+            user_is_admin: false,
+        }],
+    );
+    client_b.channel_store().read_with(cx_b, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_a.user_id().unwrap()],
+        );
+    });
+
+    client_c.channel_store().read_with(cx_c, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_a.user_id().unwrap()],
+        );
+    });
+
+    active_call_b
+        .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    client_a.channel_store().read_with(cx_a, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
+        );
+    });
+
+    client_b.channel_store().read_with(cx_b, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
+        );
+    });
+
+    client_c.channel_store().read_with(cx_c, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
+        );
+    });
+
+    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
+    room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
+    assert_eq!(
+        room_participants(&room_a, cx_a),
+        RoomParticipants {
+            remote: vec!["user_b".to_string()],
+            pending: vec![]
+        }
+    );
+
+    let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
+    room_b.read_with(cx_b, |room, _| assert!(room.is_connected()));
+    assert_eq!(
+        room_participants(&room_b, cx_b),
+        RoomParticipants {
+            remote: vec!["user_a".to_string()],
+            pending: vec![]
+        }
+    );
+
+    // Make sure that leaving and rejoining works
+
+    active_call_a
+        .update(cx_a, |active_call, cx| active_call.hang_up(cx))
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    client_a.channel_store().read_with(cx_a, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_b.user_id().unwrap()],
+        );
+    });
+
+    client_b.channel_store().read_with(cx_b, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_b.user_id().unwrap()],
+        );
+    });
+
+    client_c.channel_store().read_with(cx_c, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_b.user_id().unwrap()],
+        );
+    });
+
+    active_call_b
+        .update(cx_b, |active_call, cx| active_call.hang_up(cx))
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    client_a.channel_store().read_with(cx_a, |channels, _| {
+        assert_participants_eq(channels.channel_participants(zed_id), &[]);
+    });
+
+    client_b.channel_store().read_with(cx_b, |channels, _| {
+        assert_participants_eq(channels.channel_participants(zed_id), &[]);
+    });
+
+    client_c.channel_store().read_with(cx_c, |channels, _| {
+        assert_participants_eq(channels.channel_participants(zed_id), &[]);
+    });
+
+    active_call_a
+        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .await
+        .unwrap();
+
+    active_call_b
+        .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
+    room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
+    assert_eq!(
+        room_participants(&room_a, cx_a),
+        RoomParticipants {
+            remote: vec!["user_b".to_string()],
+            pending: vec![]
+        }
+    );
+
+    let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
+    room_b.read_with(cx_b, |room, _| assert!(room.is_connected()));
+    assert_eq!(
+        room_participants(&room_b, cx_b),
+        RoomParticipants {
+            remote: vec!["user_a".to_string()],
+            pending: vec![]
+        }
+    );
+}
+
+#[gpui::test]
+async fn test_channel_jumping(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+
+    let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await;
+    let rust_id = server
+        .make_channel("rust", (&client_a, cx_a), &mut [])
+        .await;
+
+    let active_call_a = cx_a.read(ActiveCall::global);
+
+    active_call_a
+        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .await
+        .unwrap();
+
+    // Give everything a chance to observe user A joining
+    deterministic.run_until_parked();
+
+    client_a.channel_store().read_with(cx_a, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(zed_id),
+            &[client_a.user_id().unwrap()],
+        );
+        assert_participants_eq(channels.channel_participants(rust_id), &[]);
+    });
+
+    active_call_a
+        .update(cx_a, |active_call, cx| {
+            active_call.join_channel(rust_id, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    client_a.channel_store().read_with(cx_a, |channels, _| {
+        assert_participants_eq(channels.channel_participants(zed_id), &[]);
+        assert_participants_eq(
+            channels.channel_participants(rust_id),
+            &[client_a.user_id().unwrap()],
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_permissions_update_while_invited(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    let rust_id = server
+        .make_channel("rust", (&client_a, cx_a), &mut [])
+        .await;
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    assert_channel_invitations(
+        client_b.channel_store(),
+        cx_b,
+        &[ExpectedChannel {
+            depth: 0,
+            id: rust_id,
+            name: "rust".to_string(),
+            user_is_admin: false,
+        }],
+    );
+    assert_channels(client_b.channel_store(), cx_b, &[]);
+
+    // Update B's invite before they've accepted it
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    assert_channel_invitations(
+        client_b.channel_store(),
+        cx_b,
+        &[ExpectedChannel {
+            depth: 0,
+            id: rust_id,
+            name: "rust".to_string(),
+            user_is_admin: false,
+        }],
+    );
+    assert_channels(client_b.channel_store(), cx_b, &[]);
+}
+
+#[gpui::test]
+async fn test_channel_rename(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    let rust_id = server
+        .make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .await;
+
+    // Rename the channel
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.rename(rust_id, "#rust-archive", cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // Client A sees the channel with its new name.
+    assert_channels(
+        client_a.channel_store(),
+        cx_a,
+        &[ExpectedChannel {
+            depth: 0,
+            id: rust_id,
+            name: "rust-archive".to_string(),
+            user_is_admin: true,
+        }],
+    );
+
+    // Client B sees the channel with its new name.
+    assert_channels(
+        client_b.channel_store(),
+        cx_b,
+        &[ExpectedChannel {
+            depth: 0,
+            id: rust_id,
+            name: "rust-archive".to_string(),
+            user_is_admin: false,
+        }],
+    );
+}
+
+#[gpui::test]
+async fn test_call_from_channel(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+    server
+        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let channel_id = server
+        .make_channel(
+            "x",
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    active_call_a
+        .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
+        .await
+        .unwrap();
+
+    // Client A calls client B while in the channel.
+    active_call_a
+        .update(cx_a, |call, cx| {
+            call.invite(client_b.user_id().unwrap(), None, cx)
+        })
+        .await
+        .unwrap();
+
+    // Client B accepts the call.
+    deterministic.run_until_parked();
+    active_call_b
+        .update(cx_b, |call, cx| call.accept_incoming(cx))
+        .await
+        .unwrap();
+
+    // Client B sees that they are now in the channel
+    deterministic.run_until_parked();
+    active_call_b.read_with(cx_b, |call, cx| {
+        assert_eq!(call.channel_id(cx), Some(channel_id));
+    });
+    client_b.channel_store().read_with(cx_b, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(channel_id),
+            &[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
+        );
+    });
+
+    // Clients A and C also see that client B is in the channel.
+    client_a.channel_store().read_with(cx_a, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(channel_id),
+            &[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
+        );
+    });
+    client_c.channel_store().read_with(cx_c, |channels, _| {
+        assert_participants_eq(
+            channels.channel_participants(channel_id),
+            &[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_lost_channel_creation(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    server
+        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let channel_id = server.make_channel("x", (&client_a, cx_a), &mut []).await;
+
+    // Invite a member
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // Sanity check, B has the invitation
+    assert_channel_invitations(
+        client_b.channel_store(),
+        cx_b,
+        &[ExpectedChannel {
+            depth: 0,
+            id: channel_id,
+            name: "x".to_string(),
+            user_is_admin: false,
+        }],
+    );
+
+    // A creates a subchannel while the invite is still pending.
+    let subchannel_id = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("subchannel", Some(channel_id), cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // Make sure A sees their new channel
+    assert_channels(
+        client_a.channel_store(),
+        cx_a,
+        &[
+            ExpectedChannel {
+                depth: 0,
+                id: channel_id,
+                name: "x".to_string(),
+                user_is_admin: true,
+            },
+            ExpectedChannel {
+                depth: 1,
+                id: subchannel_id,
+                name: "subchannel".to_string(),
+                user_is_admin: true,
+            },
+        ],
+    );
+
+    // Client B accepts the invite
+    client_b
+        .channel_store()
+        .update(cx_b, |channel_store, _| {
+            channel_store.respond_to_channel_invite(channel_id, true)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // Client B should now see the channel
+    assert_channels(
+        client_b.channel_store(),
+        cx_b,
+        &[
+            ExpectedChannel {
+                depth: 0,
+                id: channel_id,
+                name: "x".to_string(),
+                user_is_admin: false,
+            },
+            ExpectedChannel {
+                depth: 1,
+                id: subchannel_id,
+                name: "subchannel".to_string(),
+                user_is_admin: false,
+            },
+        ],
+    );
+}
+
+#[derive(Debug, PartialEq)]
+struct ExpectedChannel {
+    depth: usize,
+    id: ChannelId,
+    name: String,
+    user_is_admin: bool,
+}
+
+#[track_caller]
+fn assert_channel_invitations(
+    channel_store: &ModelHandle<ChannelStore>,
+    cx: &TestAppContext,
+    expected_channels: &[ExpectedChannel],
+) {
+    let actual = channel_store.read_with(cx, |store, _| {
+        store
+            .channel_invitations()
+            .iter()
+            .map(|channel| ExpectedChannel {
+                depth: 0,
+                name: channel.name.clone(),
+                id: channel.id,
+                user_is_admin: store.is_user_admin(channel.id),
+            })
+            .collect::<Vec<_>>()
+    });
+    assert_eq!(actual, expected_channels);
+}
+
+#[track_caller]
+fn assert_channels(
+    channel_store: &ModelHandle<ChannelStore>,
+    cx: &TestAppContext,
+    expected_channels: &[ExpectedChannel],
+) {
+    let actual = channel_store.read_with(cx, |store, _| {
+        store
+            .channels()
+            .map(|(depth, channel)| ExpectedChannel {
+                depth,
+                name: channel.name.clone(),
+                id: channel.id,
+                user_is_admin: store.is_user_admin(channel.id),
+            })
+            .collect::<Vec<_>>()
+    });
+    assert_eq!(actual, expected_channels);
+}

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

@@ -1,15 +1,15 @@
 use crate::{
     rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
-    tests::{TestClient, TestServer},
+    tests::{room_participants, RoomParticipants, TestClient, TestServer},
 };
 use call::{room, ActiveCall, ParticipantLocation, Room};
 use client::{User, RECEIVE_TIMEOUT};
-use collections::HashSet;
+use collections::{HashMap, HashSet};
 use editor::{
     test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
     ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
 };
-use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions};
+use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
 use futures::StreamExt as _;
 use gpui::{
     executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
@@ -19,7 +19,7 @@ use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
     tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
-    LanguageConfig, OffsetRangeExt, Point, Rope,
+    LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
 };
 use live_kit_client::MacOSDisplay;
 use lsp::LanguageServerId;
@@ -33,7 +33,7 @@ use std::{
     path::{Path, PathBuf},
     rc::Rc,
     sync::{
-        atomic::{AtomicBool, AtomicU32, Ordering::SeqCst},
+        atomic::{self, AtomicBool, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
 };
@@ -748,7 +748,7 @@ async fn test_server_restarts(
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     client_a
-        .fs
+        .fs()
         .insert_tree("/a", json!({ "a.txt": "a-contents" }))
         .await;
 
@@ -1220,7 +1220,7 @@ async fn test_share_project(
     let active_call_c = cx_c.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -1387,7 +1387,7 @@ async fn test_unshare_project(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -1476,7 +1476,7 @@ async fn test_host_disconnect(
     cx_b.update(editor::init);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -1498,7 +1498,8 @@ async fn test_host_disconnect(
     deterministic.run_until_parked();
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
-    let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let window_b =
+        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
     let workspace_b = window_b.root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
@@ -1581,7 +1582,7 @@ async fn test_project_reconnect(
     cx_b.update(editor::init);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root-1",
             json!({
@@ -1609,7 +1610,7 @@ async fn test_project_reconnect(
         )
         .await;
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root-2",
             json!({
@@ -1618,7 +1619,7 @@ async fn test_project_reconnect(
         )
         .await;
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root-3",
             json!({
@@ -1698,7 +1699,7 @@ async fn test_project_reconnect(
 
     // While client A is disconnected, add and remove files from client A's project.
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root-1/dir1/subdir2",
             json!({
@@ -1710,7 +1711,7 @@ async fn test_project_reconnect(
         )
         .await;
     client_a
-        .fs
+        .fs()
         .remove_dir(
             "/root-1/dir1/subdir1".as_ref(),
             RemoveOptions {
@@ -1832,11 +1833,11 @@ async fn test_project_reconnect(
 
     // While client B is disconnected, add and remove files from client A's project
     client_a
-        .fs
+        .fs()
         .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into())
         .await;
     client_a
-        .fs
+        .fs()
         .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default())
         .await
         .unwrap();
@@ -1922,8 +1923,8 @@ async fn test_active_call_events(
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
-    client_a.fs.insert_tree("/a", json!({})).await;
-    client_b.fs.insert_tree("/b", json!({})).await;
+    client_a.fs().insert_tree("/a", json!({})).await;
+    client_b.fs().insert_tree("/b", json!({})).await;
 
     let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
     let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
@@ -2011,8 +2012,8 @@ async fn test_room_location(
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
-    client_a.fs.insert_tree("/a", json!({})).await;
-    client_b.fs.insert_tree("/b", json!({})).await;
+    client_a.fs().insert_tree("/a", json!({})).await;
+    client_b.fs().insert_tree("/b", json!({})).await;
 
     let active_call_a = cx_a.read(ActiveCall::global);
     let active_call_b = cx_b.read(ActiveCall::global);
@@ -2201,12 +2202,12 @@ async fn test_propagate_saves_and_fs_changes(
         Some(tree_sitter_rust::language()),
     ));
     for client in [&client_a, &client_b, &client_c] {
-        client.language_registry.add(rust.clone());
-        client.language_registry.add(javascript.clone());
+        client.language_registry().add(rust.clone());
+        client.language_registry().add(javascript.clone());
     }
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -2276,7 +2277,7 @@ async fn test_propagate_saves_and_fs_changes(
     buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
     save_b.await.unwrap();
     assert_eq!(
-        client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(),
+        client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(),
         "hi-a, i-am-c, i-am-b, i-am-a"
     );
 
@@ -2287,7 +2288,7 @@ async fn test_propagate_saves_and_fs_changes(
 
     // Make changes on host's file system, see those changes on guest worktrees.
     client_a
-        .fs
+        .fs()
         .rename(
             "/a/file1.rs".as_ref(),
             "/a/file1.js".as_ref(),
@@ -2296,11 +2297,11 @@ async fn test_propagate_saves_and_fs_changes(
         .await
         .unwrap();
     client_a
-        .fs
+        .fs()
         .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
         .await
         .unwrap();
-    client_a.fs.insert_file("/a/file4", "4".into()).await;
+    client_a.fs().insert_file("/a/file4", "4".into()).await;
     deterministic.run_until_parked();
 
     worktree_a.read_with(cx_a, |tree, _| {
@@ -2394,7 +2395,7 @@ async fn test_git_diff_base_change(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -2438,7 +2439,7 @@ async fn test_git_diff_base_change(
     "
     .unindent();
 
-    client_a.fs.as_fake().set_index_for_repo(
+    client_a.fs().set_index_for_repo(
         Path::new("/dir/.git"),
         &[(Path::new("a.txt"), diff_base.clone())],
     );
@@ -2483,7 +2484,7 @@ async fn test_git_diff_base_change(
         );
     });
 
-    client_a.fs.as_fake().set_index_for_repo(
+    client_a.fs().set_index_for_repo(
         Path::new("/dir/.git"),
         &[(Path::new("a.txt"), new_diff_base.clone())],
     );
@@ -2528,7 +2529,7 @@ async fn test_git_diff_base_change(
     "
     .unindent();
 
-    client_a.fs.as_fake().set_index_for_repo(
+    client_a.fs().set_index_for_repo(
         Path::new("/dir/sub/.git"),
         &[(Path::new("b.txt"), diff_base.clone())],
     );
@@ -2573,7 +2574,7 @@ async fn test_git_diff_base_change(
         );
     });
 
-    client_a.fs.as_fake().set_index_for_repo(
+    client_a.fs().set_index_for_repo(
         Path::new("/dir/sub/.git"),
         &[(Path::new("b.txt"), new_diff_base.clone())],
     );
@@ -2632,7 +2633,7 @@ async fn test_git_branch_name(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -2651,8 +2652,7 @@ async fn test_git_branch_name(
 
     let project_remote = client_b.build_remote_project(project_id, cx_b).await;
     client_a
-        .fs
-        .as_fake()
+        .fs()
         .set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
 
     // Wait for it to catch up to the new branch
@@ -2677,8 +2677,7 @@ async fn test_git_branch_name(
     });
 
     client_a
-        .fs
-        .as_fake()
+        .fs()
         .set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
 
     // Wait for buffer_local_a to receive it
@@ -2717,7 +2716,7 @@ async fn test_git_status_sync(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -2731,7 +2730,7 @@ async fn test_git_status_sync(
     const A_TXT: &'static str = "a.txt";
     const B_TXT: &'static str = "b.txt";
 
-    client_a.fs.as_fake().set_status_for_repo_via_git_operation(
+    client_a.fs().set_status_for_repo_via_git_operation(
         Path::new("/dir/.git"),
         &[
             (&Path::new(A_TXT), GitFileStatus::Added),
@@ -2777,16 +2776,13 @@ async fn test_git_status_sync(
         assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
     });
 
-    client_a
-        .fs
-        .as_fake()
-        .set_status_for_repo_via_working_copy_change(
-            Path::new("/dir/.git"),
-            &[
-                (&Path::new(A_TXT), GitFileStatus::Modified),
-                (&Path::new(B_TXT), GitFileStatus::Modified),
-            ],
-        );
+    client_a.fs().set_status_for_repo_via_working_copy_change(
+        Path::new("/dir/.git"),
+        &[
+            (&Path::new(A_TXT), GitFileStatus::Modified),
+            (&Path::new(B_TXT), GitFileStatus::Modified),
+        ],
+    );
 
     // Wait for buffer_local_a to receive it
     deterministic.run_until_parked();
@@ -2857,7 +2853,7 @@ async fn test_fs_operations(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -3130,7 +3126,7 @@ async fn test_local_settings(
 
     // As client A, open a project that contains some local settings files
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -3172,7 +3168,7 @@ async fn test_local_settings(
 
     // As client A, update a settings file. As Client B, see the changed settings.
     client_a
-        .fs
+        .fs()
         .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
         .await;
     deterministic.run_until_parked();
@@ -3189,17 +3185,17 @@ async fn test_local_settings(
 
     // As client A, create and remove some settings files. As client B, see the changed settings.
     client_a
-        .fs
+        .fs()
         .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
         .await
         .unwrap();
     client_a
-        .fs
+        .fs()
         .create_dir("/dir/b/.zed".as_ref())
         .await
         .unwrap();
     client_a
-        .fs
+        .fs()
         .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
         .await;
     deterministic.run_until_parked();
@@ -3220,11 +3216,11 @@ async fn test_local_settings(
 
     // As client A, change and remove settings files while client B is disconnected.
     client_a
-        .fs
+        .fs()
         .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
         .await;
     client_a
-        .fs
+        .fs()
         .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
         .await
         .unwrap();
@@ -3258,7 +3254,7 @@ async fn test_buffer_conflict_after_save(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -3320,7 +3316,7 @@ async fn test_buffer_reloading(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -3348,7 +3344,7 @@ async fn test_buffer_reloading(
 
     let new_contents = Rope::from("d\ne\nf");
     client_a
-        .fs
+        .fs()
         .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
         .await
         .unwrap();
@@ -3377,7 +3373,7 @@ async fn test_editing_while_guest_opens_buffer(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
         .await;
     let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
@@ -3426,7 +3422,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
         .await;
     let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
@@ -3520,7 +3516,7 @@ async fn test_leaving_worktree_while_opening_buffer(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
         .await;
     let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
@@ -3563,7 +3559,7 @@ async fn test_canceling_buffer_opening(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -3619,7 +3615,7 @@ async fn test_leaving_project(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -3707,9 +3703,9 @@ async fn test_leaving_project(
     cx_b.spawn(|cx| {
         Project::remote(
             project_id,
-            client_b.client.clone(),
-            client_b.user_store.clone(),
-            client_b.language_registry.clone(),
+            client_b.app_state.client.clone(),
+            client_b.user_store().clone(),
+            client_b.language_registry().clone(),
             FakeFs::new(cx.background()),
             cx,
         )
@@ -3761,11 +3757,11 @@ async fn test_collaborating_with_diagnostics(
         Some(tree_sitter_rust::language()),
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     // Share a project as client A
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -4033,11 +4029,11 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
             ..Default::default()
         }))
         .await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/test",
             json!({
@@ -4167,6 +4163,7 @@ async fn test_collaborating_with_completion(
             capabilities: lsp::ServerCapabilities {
                 completion_provider: Some(lsp::CompletionOptions {
                     trigger_characters: Some(vec![".".to_string()]),
+                    resolve_provider: Some(true),
                     ..Default::default()
                 }),
                 ..Default::default()
@@ -4174,10 +4171,10 @@ async fn test_collaborating_with_completion(
             ..Default::default()
         }))
         .await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -4335,7 +4332,7 @@ async fn test_reloading_buffer_manually(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
         .await;
     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
@@ -4366,7 +4363,7 @@ async fn test_reloading_buffer_manually(
     buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
 
     client_a
-        .fs
+        .fs()
         .save(
             "/a/a.rs".as_ref(),
             &Rope::from("let seven = 7;"),
@@ -4437,14 +4434,14 @@ async fn test_formatting_buffer(
         Some(tree_sitter_rust::language()),
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     // Here we insert a fake tree with a directory that exists on disk. This is needed
     // because later we'll invoke a command, which requires passing a working directory
     // that points to a valid location on disk.
     let directory = env::current_dir().unwrap();
     client_a
-        .fs
+        .fs()
         .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
         .await;
     let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
@@ -4546,10 +4543,10 @@ async fn test_definition(
         Some(tree_sitter_rust::language()),
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root",
             json!({
@@ -4694,10 +4691,10 @@ async fn test_references(
         Some(tree_sitter_rust::language()),
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root",
             json!({
@@ -4790,7 +4787,7 @@ async fn test_project_search(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root",
             json!({
@@ -4824,15 +4821,16 @@ async fn test_project_search(
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
 
     // Perform a search as the guest.
-    let results = project_b
-        .update(cx_b, |project, cx| {
-            project.search(
-                SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
-                cx,
-            )
-        })
-        .await
-        .unwrap();
+    let mut results = HashMap::default();
+    let mut search_rx = project_b.update(cx_b, |project, cx| {
+        project.search(
+            SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
+            cx,
+        )
+    });
+    while let Some((buffer, ranges)) = search_rx.next().await {
+        results.entry(buffer).or_insert(ranges);
+    }
 
     let mut ranges_by_path = results
         .into_iter()
@@ -4876,7 +4874,7 @@ async fn test_document_highlights(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root-1",
             json!({
@@ -4895,7 +4893,7 @@ async fn test_document_highlights(
         Some(tree_sitter_rust::language()),
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
     let project_id = active_call_a
@@ -4982,7 +4980,7 @@ async fn test_lsp_hover(
     let active_call_a = cx_a.read(ActiveCall::global);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root-1",
             json!({
@@ -5001,7 +4999,7 @@ async fn test_lsp_hover(
         Some(tree_sitter_rust::language()),
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
     let project_id = active_call_a
@@ -5100,10 +5098,10 @@ async fn test_project_symbols(
         Some(tree_sitter_rust::language()),
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/code",
             json!({
@@ -5211,10 +5209,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
         Some(tree_sitter_rust::language()),
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/root",
             json!({
@@ -5271,6 +5269,7 @@ async fn test_collaborating_with_code_actions(
     deterministic.forbid_parking();
     let mut server = TestServer::start(&deterministic).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)])
@@ -5289,10 +5288,10 @@ async fn test_collaborating_with_code_actions(
         Some(tree_sitter_rust::language()),
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -5309,7 +5308,8 @@ async fn test_collaborating_with_code_actions(
 
     // Join the project as client B.
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let window_b =
+        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
     let workspace_b = window_b.root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
@@ -5321,7 +5321,7 @@ async fn test_collaborating_with_code_actions(
         .unwrap();
 
     let mut fake_language_server = fake_language_servers.next().await.unwrap();
-    fake_language_server
+    let mut requests = fake_language_server
         .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
             assert_eq!(
                 params.text_document.uri,
@@ -5330,9 +5330,9 @@ async fn test_collaborating_with_code_actions(
             assert_eq!(params.range.start, lsp::Position::new(0, 0));
             assert_eq!(params.range.end, lsp::Position::new(0, 0));
             Ok(None)
-        })
-        .next()
-        .await;
+        });
+    deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
+    requests.next().await;
 
     // Move cursor to a location that contains code actions.
     editor_b.update(cx_b, |editor, cx| {
@@ -5342,7 +5342,7 @@ async fn test_collaborating_with_code_actions(
         cx.focus(&editor_b);
     });
 
-    fake_language_server
+    let mut requests = fake_language_server
         .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
             assert_eq!(
                 params.text_document.uri,
@@ -5394,9 +5394,9 @@ async fn test_collaborating_with_code_actions(
                     ..Default::default()
                 },
             )]))
-        })
-        .next()
-        .await;
+        });
+    deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
+    requests.next().await;
 
     // Toggle code actions and wait for them to display.
     editor_b.update(cx_b, |editor, cx| {
@@ -5515,10 +5515,10 @@ async fn test_collaborating_with_renames(
             ..Default::default()
         }))
         .await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -5534,7 +5534,8 @@ async fn test_collaborating_with_renames(
         .unwrap();
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
 
-    let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let window_b =
+        cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
     let workspace_b = window_b.root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
@@ -5702,10 +5703,10 @@ async fn test_language_server_statuses(
             ..Default::default()
         }))
         .await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/dir",
             json!({
@@ -6162,7 +6163,7 @@ async fn test_contacts(
 
     // Test removing a contact
     client_b
-        .user_store
+        .user_store()
         .update(cx_b, |store, cx| {
             store.remove_contact(client_c.user_id().unwrap(), cx)
         })
@@ -6185,7 +6186,7 @@ async fn test_contacts(
         client: &TestClient,
         cx: &TestAppContext,
     ) -> Vec<(String, &'static str, &'static str)> {
-        client.user_store.read_with(cx, |store, _| {
+        client.user_store().read_with(cx, |store, _| {
             store
                 .contacts()
                 .iter()
@@ -6228,14 +6229,14 @@ async fn test_contact_requests(
 
     // User A and User C request that user B become their contact.
     client_a
-        .user_store
+        .user_store()
         .update(cx_a, |store, cx| {
             store.request_contact(client_b.user_id().unwrap(), cx)
         })
         .await
         .unwrap();
     client_c
-        .user_store
+        .user_store()
         .update(cx_c, |store, cx| {
             store.request_contact(client_b.user_id().unwrap(), cx)
         })
@@ -6289,7 +6290,7 @@ async fn test_contact_requests(
 
     // User B accepts the request from user A.
     client_b
-        .user_store
+        .user_store()
         .update(cx_b, |store, cx| {
             store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
         })
@@ -6333,7 +6334,7 @@ async fn test_contact_requests(
 
     // User B rejects the request from user C.
     client_b
-        .user_store
+        .user_store()
         .update(cx_b, |store, cx| {
             store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
         })
@@ -6415,7 +6416,7 @@ async fn test_basic_following(
     cx_b.update(editor::init);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -6978,7 +6979,7 @@ async fn test_join_call_after_screen_was_shared(
         .await
         .unwrap();
 
-    client_b.user_store.update(cx_b, |user_store, _| {
+    client_b.user_store().update(cx_b, |user_store, _| {
         user_store.clear_cache();
     });
 
@@ -7038,7 +7039,7 @@ async fn test_following_tab_order(
     cx_b.update(editor::init);
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -7161,7 +7162,7 @@ async fn test_peers_following_each_other(
 
     // Client A shares a project.
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -7334,7 +7335,7 @@ async fn test_auto_unfollowing(
 
     // Client A shares a project.
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -7498,7 +7499,7 @@ async fn test_peers_simultaneously_following_each_other(
     cx_a.update(editor::init);
     cx_b.update(editor::init);
 
-    client_a.fs.insert_tree("/a", json!({})).await;
+    client_a.fs().insert_tree("/a", json!({})).await;
     let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     let project_id = active_call_a
@@ -7575,10 +7576,10 @@ async fn test_on_input_format_from_host_to_guest(
             ..Default::default()
         }))
         .await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -7704,10 +7705,10 @@ async fn test_on_input_format_from_guest_to_host(
             ..Default::default()
         }))
         .await;
-    client_a.language_registry.add(Arc::new(language));
+    client_a.language_registry().add(Arc::new(language));
 
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
@@ -7798,7 +7799,7 @@ async fn test_on_input_format_from_guest_to_host(
     });
 }
 
-#[gpui::test]
+#[gpui::test(iterations = 10)]
 async fn test_mutual_editor_inlay_hint_cache_update(
     deterministic: Arc<Deterministic>,
     cx_a: &mut TestAppContext,
@@ -7860,15 +7861,16 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         }))
         .await;
     let language = Arc::new(language);
-    client_a.language_registry.add(Arc::clone(&language));
-    client_b.language_registry.add(language);
+    client_a.language_registry().add(Arc::clone(&language));
+    client_b.language_registry().add(language);
 
+    // Client A opens a project.
     client_a
-        .fs
+        .fs()
         .insert_tree(
             "/a",
             json!({
-                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
                 "other.rs": "// Test file",
             }),
         )
@@ -7883,6 +7885,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .await
         .unwrap();
 
+    // Client B joins the project
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
     active_call_b
         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
@@ -7892,6 +7895,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     cx_a.foreground().start_waiting();
 
+    // The host opens a rust file.
     let _buffer_a = project_a
         .update(cx_a, |project, cx| {
             project.open_local_buffer("/a/main.rs", cx)
@@ -7899,7 +7903,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .await
         .unwrap();
     let fake_language_server = fake_language_servers.next().await.unwrap();
-    let next_call_id = Arc::new(AtomicU32::new(0));
     let editor_a = workspace_a
         .update(cx_a, |workspace, cx| {
             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -7908,53 +7911,48 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .unwrap()
         .downcast::<Editor>()
         .unwrap();
+
+    // Set up the language server to return an additional inlay hint on each request.
+    let edits_made = Arc::new(AtomicUsize::new(0));
+    let closure_edits_made = Arc::clone(&edits_made);
     fake_language_server
         .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-            let task_next_call_id = Arc::clone(&next_call_id);
+            let task_edits_made = Arc::clone(&closure_edits_made);
             async move {
                 assert_eq!(
                     params.text_document.uri,
                     lsp::Url::from_file_path("/a/main.rs").unwrap(),
                 );
-                let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
-                let mut new_hints = Vec::with_capacity(current_call_id as usize);
-                loop {
-                    new_hints.push(lsp::InlayHint {
-                        position: lsp::Position::new(0, current_call_id),
-                        label: lsp::InlayHintLabel::String(current_call_id.to_string()),
-                        kind: None,
-                        text_edits: None,
-                        tooltip: None,
-                        padding_left: None,
-                        padding_right: None,
-                        data: None,
-                    });
-                    if current_call_id == 0 {
-                        break;
-                    }
-                    current_call_id -= 1;
-                }
-                Ok(Some(new_hints))
+                let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
+                Ok(Some(vec![lsp::InlayHint {
+                    position: lsp::Position::new(0, edits_made as u32),
+                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
+                    kind: None,
+                    text_edits: None,
+                    tooltip: None,
+                    padding_left: None,
+                    padding_right: None,
+                    data: None,
+                }]))
             }
         })
         .next()
         .await
         .unwrap();
 
-    cx_a.foreground().finish_waiting();
-    cx_a.foreground().run_until_parked();
+    deterministic.run_until_parked();
 
-    let mut edits_made = 1;
+    let initial_edit = edits_made.load(atomic::Ordering::Acquire);
     editor_a.update(cx_a, |editor, _| {
         assert_eq!(
-            vec!["0".to_string()],
+            vec![initial_edit.to_string()],
             extract_hint_labels(editor),
             "Host should get its first hints when opens an editor"
         );
         let inlay_cache = editor.inlay_hint_cache();
         assert_eq!(
             inlay_cache.version(),
-            edits_made,
+            1,
             "Host editor update the cache version after every cache/view change",
         );
     });

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

@@ -6,7 +6,7 @@ use crate::{
 use anyhow::{anyhow, Result};
 use call::ActiveCall;
 use client::RECEIVE_TIMEOUT;
-use collections::BTreeMap;
+use collections::{BTreeMap, HashMap};
 use editor::Bias;
 use fs::{repository::GitFileStatus, FakeFs, Fs as _};
 use futures::StreamExt as _;
@@ -121,7 +121,9 @@ async fn test_random_collaboration(
     let mut operation_channels = Vec::new();
 
     loop {
-        let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { break };
+        let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else {
+            break;
+        };
         applied.store(true, SeqCst);
         let did_apply = apply_server_operation(
             deterministic.clone(),
@@ -224,7 +226,9 @@ async fn apply_server_operation(
             let client_ix = clients
                 .iter()
                 .position(|(client, cx)| client.current_user_id(cx) == removed_user_id);
-            let Some(client_ix) = client_ix else { return false };
+            let Some(client_ix) = client_ix else {
+                return false;
+            };
             let user_connection_ids = server
                 .connection_pool
                 .lock()
@@ -396,9 +400,9 @@ async fn apply_client_operation(
             );
 
             let root_path = Path::new("/").join(&first_root_name);
-            client.fs.create_dir(&root_path).await.unwrap();
+            client.fs().create_dir(&root_path).await.unwrap();
             client
-                .fs
+                .fs()
                 .create_file(&root_path.join("main.rs"), Default::default())
                 .await
                 .unwrap();
@@ -422,8 +426,8 @@ async fn apply_client_operation(
             );
 
             ensure_project_shared(&project, client, cx).await;
-            if !client.fs.paths(false).contains(&new_root_path) {
-                client.fs.create_dir(&new_root_path).await.unwrap();
+            if !client.fs().paths(false).contains(&new_root_path) {
+                client.fs().create_dir(&new_root_path).await.unwrap();
             }
             project
                 .update(cx, |project, cx| {
@@ -475,7 +479,7 @@ async fn apply_client_operation(
                     Some(room.update(cx, |room, cx| {
                         room.join_project(
                             project_id,
-                            client.language_registry.clone(),
+                            client.language_registry().clone(),
                             FakeFs::new(cx.background().clone()),
                             cx,
                         )
@@ -718,7 +722,7 @@ async fn apply_client_operation(
                 if detach { "detaching" } else { "awaiting" }
             );
 
-            let search = project.update(cx, |project, cx| {
+            let mut search = project.update(cx, |project, cx| {
                 project.search(
                     SearchQuery::text(query, false, false, Vec::new(), Vec::new()),
                     cx,
@@ -726,15 +730,13 @@ async fn apply_client_operation(
             });
             drop(project);
             let search = cx.background().spawn(async move {
-                search
-                    .await
-                    .map_err(|err| anyhow!("search request failed: {:?}", err))
+                let mut results = HashMap::default();
+                while let Some((buffer, ranges)) = search.next().await {
+                    results.entry(buffer).or_insert(ranges);
+                }
+                results
             });
-            if detach {
-                cx.update(|cx| search.detach_and_log_err(cx));
-            } else {
-                search.await?;
-            }
+            search.await;
         }
 
         ClientOperation::WriteFsEntry {
@@ -743,7 +745,7 @@ async fn apply_client_operation(
             content,
         } => {
             if !client
-                .fs
+                .fs()
                 .directories(false)
                 .contains(&path.parent().unwrap().to_owned())
             {
@@ -752,15 +754,15 @@ async fn apply_client_operation(
 
             if is_dir {
                 log::info!("{}: creating dir at {:?}", client.username, path);
-                client.fs.create_dir(&path).await.unwrap();
+                client.fs().create_dir(&path).await.unwrap();
             } else {
-                let exists = client.fs.metadata(&path).await?.is_some();
+                let exists = client.fs().metadata(&path).await?.is_some();
                 let verb = if exists { "updating" } else { "creating" };
                 log::info!("{}: {} file at {:?}", verb, client.username, path);
 
                 client
-                    .fs
-                    .save(&path, &content.as_str().into(), fs::LineEnding::Unix)
+                    .fs()
+                    .save(&path, &content.as_str().into(), text::LineEnding::Unix)
                     .await
                     .unwrap();
             }
@@ -771,12 +773,12 @@ async fn apply_client_operation(
                 repo_path,
                 contents,
             } => {
-                if !client.fs.directories(false).contains(&repo_path) {
+                if !client.fs().directories(false).contains(&repo_path) {
                     return Err(TestError::Inapplicable);
                 }
 
                 for (path, _) in contents.iter() {
-                    if !client.fs.files().contains(&repo_path.join(path)) {
+                    if !client.fs().files().contains(&repo_path.join(path)) {
                         return Err(TestError::Inapplicable);
                     }
                 }
@@ -793,16 +795,16 @@ async fn apply_client_operation(
                     .iter()
                     .map(|(path, contents)| (path.as_path(), contents.clone()))
                     .collect::<Vec<_>>();
-                if client.fs.metadata(&dot_git_dir).await?.is_none() {
-                    client.fs.create_dir(&dot_git_dir).await?;
+                if client.fs().metadata(&dot_git_dir).await?.is_none() {
+                    client.fs().create_dir(&dot_git_dir).await?;
                 }
-                client.fs.set_index_for_repo(&dot_git_dir, &contents);
+                client.fs().set_index_for_repo(&dot_git_dir, &contents);
             }
             GitOperation::WriteGitBranch {
                 repo_path,
                 new_branch,
             } => {
-                if !client.fs.directories(false).contains(&repo_path) {
+                if !client.fs().directories(false).contains(&repo_path) {
                     return Err(TestError::Inapplicable);
                 }
 
@@ -814,21 +816,21 @@ async fn apply_client_operation(
                 );
 
                 let dot_git_dir = repo_path.join(".git");
-                if client.fs.metadata(&dot_git_dir).await?.is_none() {
-                    client.fs.create_dir(&dot_git_dir).await?;
+                if client.fs().metadata(&dot_git_dir).await?.is_none() {
+                    client.fs().create_dir(&dot_git_dir).await?;
                 }
-                client.fs.set_branch_name(&dot_git_dir, new_branch);
+                client.fs().set_branch_name(&dot_git_dir, new_branch);
             }
             GitOperation::WriteGitStatuses {
                 repo_path,
                 statuses,
                 git_operation,
             } => {
-                if !client.fs.directories(false).contains(&repo_path) {
+                if !client.fs().directories(false).contains(&repo_path) {
                     return Err(TestError::Inapplicable);
                 }
                 for (path, _) in statuses.iter() {
-                    if !client.fs.files().contains(&repo_path.join(path)) {
+                    if !client.fs().files().contains(&repo_path.join(path)) {
                         return Err(TestError::Inapplicable);
                     }
                 }
@@ -847,16 +849,16 @@ async fn apply_client_operation(
                     .map(|(path, val)| (path.as_path(), val.clone()))
                     .collect::<Vec<_>>();
 
-                if client.fs.metadata(&dot_git_dir).await?.is_none() {
-                    client.fs.create_dir(&dot_git_dir).await?;
+                if client.fs().metadata(&dot_git_dir).await?.is_none() {
+                    client.fs().create_dir(&dot_git_dir).await?;
                 }
 
                 if git_operation {
                     client
-                        .fs
+                        .fs()
                         .set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice());
                 } else {
-                    client.fs.set_status_for_repo_via_working_copy_change(
+                    client.fs().set_status_for_repo_via_working_copy_change(
                         &dot_git_dir,
                         statuses.as_slice(),
                     );
@@ -1499,7 +1501,7 @@ impl TestPlan {
                         // Invite a contact to the current call
                         0..=70 => {
                             let available_contacts =
-                                client.user_store.read_with(cx, |user_store, _| {
+                                client.user_store().read_with(cx, |user_store, _| {
                                     user_store
                                         .contacts()
                                         .iter()
@@ -1591,12 +1593,13 @@ impl TestPlan {
                     81.. => match self.rng.gen_range(0..100_u32) {
                         // Add a worktree to a local project
                         0..=50 => {
-                            let Some(project) = client
-                                .local_projects()
-                                .choose(&mut self.rng)
-                                .cloned() else { continue };
+                            let Some(project) =
+                                client.local_projects().choose(&mut self.rng).cloned()
+                            else {
+                                continue;
+                            };
                             let project_root_name = root_name_for_project(&project, cx);
-                            let mut paths = client.fs.paths(false);
+                            let mut paths = client.fs().paths(false);
                             paths.remove(0);
                             let new_root_path = if paths.is_empty() || self.rng.gen() {
                                 Path::new("/").join(&self.next_root_dir_name(user_id))
@@ -1611,7 +1614,9 @@ impl TestPlan {
 
                         // Add an entry to a worktree
                         _ => {
-                            let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
+                            let Some(project) = choose_random_project(client, &mut self.rng) else {
+                                continue;
+                            };
                             let project_root_name = root_name_for_project(&project, cx);
                             let is_local = project.read_with(cx, |project, _| project.is_local());
                             let worktree = project.read_with(cx, |project, cx| {
@@ -1645,7 +1650,9 @@ impl TestPlan {
 
                 // Query and mutate buffers
                 60..=90 => {
-                    let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
+                    let Some(project) = choose_random_project(client, &mut self.rng) else {
+                        continue;
+                    };
                     let project_root_name = root_name_for_project(&project, cx);
                     let is_local = project.read_with(cx, |project, _| project.is_local());
 
@@ -1656,7 +1663,10 @@ impl TestPlan {
                                 .buffers_for_project(&project)
                                 .iter()
                                 .choose(&mut self.rng)
-                                .cloned() else { continue };
+                                .cloned()
+                            else {
+                                continue;
+                            };
 
                             let full_path = buffer
                                 .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
@@ -1776,7 +1786,7 @@ impl TestPlan {
                     let is_dir = self.rng.gen::<bool>();
                     let content;
                     let mut path;
-                    let dir_paths = client.fs.directories(false);
+                    let dir_paths = client.fs().directories(false);
 
                     if is_dir {
                         content = String::new();
@@ -1786,7 +1796,7 @@ impl TestPlan {
                         content = Alphanumeric.sample_string(&mut self.rng, 16);
 
                         // Create a new file or overwrite an existing file
-                        let file_paths = client.fs.files();
+                        let file_paths = client.fs().files();
                         if file_paths.is_empty() || self.rng.gen_bool(0.5) {
                             path = dir_paths.choose(&mut self.rng).unwrap().clone();
                             path.push(gen_file_name(&mut self.rng));
@@ -1812,7 +1822,7 @@ impl TestPlan {
             client: &TestClient,
         ) -> Vec<PathBuf> {
             let mut paths = client
-                .fs
+                .fs()
                 .files()
                 .into_iter()
                 .filter(|path| path.starts_with(repo_path))
@@ -1829,7 +1839,7 @@ impl TestPlan {
         }
 
         let repo_path = client
-            .fs
+            .fs()
             .directories(false)
             .choose(&mut self.rng)
             .unwrap()
@@ -1928,7 +1938,7 @@ async fn simulate_client(
             name: "the-fake-language-server",
             capabilities: lsp::LanguageServer::full_capabilities(),
             initializer: Some(Box::new({
-                let fs = client.fs.clone();
+                let fs = client.app_state.fs.clone();
                 move |fake_server: &mut FakeLanguageServer| {
                     fake_server.handle_request::<lsp::request::Completion, _, _>(
                         |_, _| async move {
@@ -1973,7 +1983,7 @@ async fn simulate_client(
                             let background = cx.background();
                             let mut rng = background.rng();
                             let count = rng.gen_range::<usize, _>(1..3);
-                            let files = fs.files();
+                            let files = fs.as_fake().files();
                             let files = (0..count)
                                 .map(|_| files.choose(&mut *rng).unwrap().clone())
                                 .collect::<Vec<_>>();
@@ -2023,10 +2033,13 @@ async fn simulate_client(
             ..Default::default()
         }))
         .await;
-    client.language_registry.add(Arc::new(language));
+    client.app_state.languages.add(Arc::new(language));
 
     while let Some(batch_id) = operation_rx.next().await {
-        let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break };
+        let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx)
+        else {
+            break;
+        };
         applied.store(true, SeqCst);
         match apply_client_operation(&client, operation, &mut cx).await {
             Ok(()) => {}

crates/collab_ui/Cargo.toml 🔗

@@ -23,8 +23,10 @@ test-support = [
 
 [dependencies]
 auto_update = { path = "../auto_update" }
+db = { path = "../db" }
 call = { path = "../call" }
 client = { path = "../client" }
+channel = { path = "../channel" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
@@ -32,11 +34,13 @@ editor = { path = "../editor" }
 feedback = { path = "../feedback" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
+language = { path = "../language" }
 menu = { path = "../menu" }
 picker = { path = "../picker" }
 project = { path = "../project" }
 recent_projects = {path = "../recent_projects"}
 settings = { path = "../settings" }
+feature_flags = {path = "../feature_flags"}
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
 vcs_menu = { path = "../vcs_menu" }
@@ -44,10 +48,10 @@ util = { path = "../util" }
 workspace = { path = "../workspace" }
 zed-actions = {path = "../zed-actions"}
 
-
 anyhow.workspace = true
 futures.workspace = true
 log.workspace = true
+schemars.workspace = true
 postage.workspace = true
 serde.workspace = true
 serde_derive.workspace = true

crates/collab_ui/src/channel_view.rs 🔗

@@ -0,0 +1,355 @@
+use anyhow::{anyhow, Result};
+use channel::{
+    channel_buffer::{self, ChannelBuffer},
+    ChannelId,
+};
+use client::proto;
+use clock::ReplicaId;
+use collections::HashMap;
+use editor::Editor;
+use gpui::{
+    actions,
+    elements::{ChildView, Label},
+    geometry::vector::Vector2F,
+    AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
+    ViewContext, ViewHandle,
+};
+use project::Project;
+use std::any::Any;
+use workspace::{
+    item::{FollowableItem, Item, ItemHandle},
+    register_followable_item,
+    searchable::SearchableItemHandle,
+    ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
+};
+
+actions!(channel_view, [Deploy]);
+
+pub(crate) fn init(cx: &mut AppContext) {
+    register_followable_item::<ChannelView>(cx)
+}
+
+pub struct ChannelView {
+    pub editor: ViewHandle<Editor>,
+    project: ModelHandle<Project>,
+    channel_buffer: ModelHandle<ChannelBuffer>,
+    remote_id: Option<ViewId>,
+    _editor_event_subscription: Subscription,
+}
+
+impl ChannelView {
+    pub fn open(
+        channel_id: ChannelId,
+        pane: ViewHandle<Pane>,
+        workspace: ViewHandle<Workspace>,
+        cx: &mut AppContext,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        let workspace = workspace.read(cx);
+        let project = workspace.project().to_owned();
+        let channel_store = workspace.app_state().channel_store.clone();
+        let markdown = workspace
+            .app_state()
+            .languages
+            .language_for_name("Markdown");
+        let channel_buffer =
+            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
+
+        cx.spawn(|mut cx| async move {
+            let channel_buffer = channel_buffer.await?;
+            let markdown = markdown.await?;
+            channel_buffer.update(&mut cx, |buffer, cx| {
+                buffer.buffer().update(cx, |buffer, cx| {
+                    buffer.set_language(Some(markdown), cx);
+                })
+            });
+
+            pane.update(&mut cx, |pane, cx| {
+                pane.items_of_type::<Self>()
+                    .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
+                    .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx)))
+            })
+            .ok_or_else(|| anyhow!("pane was dropped"))
+        })
+    }
+
+    pub fn new(
+        project: ModelHandle<Project>,
+        channel_buffer: ModelHandle<ChannelBuffer>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let buffer = channel_buffer.read(cx).buffer();
+        // buffer.update(cx, |buffer, cx| buffer.set_language(language, cx));
+        let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
+        let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
+
+        cx.subscribe(&project, Self::handle_project_event).detach();
+        cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
+            .detach();
+
+        let this = Self {
+            editor,
+            project,
+            channel_buffer,
+            remote_id: None,
+            _editor_event_subscription,
+        };
+        this.refresh_replica_id_map(cx);
+        this
+    }
+
+    fn handle_project_event(
+        &mut self,
+        _: ModelHandle<Project>,
+        event: &project::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            project::Event::RemoteIdChanged(_) => {}
+            project::Event::DisconnectedFromHost => {}
+            project::Event::Closed => {}
+            project::Event::CollaboratorUpdated { .. } => {}
+            project::Event::CollaboratorLeft(_) => {}
+            project::Event::CollaboratorJoined(_) => {}
+            _ => return,
+        }
+        self.refresh_replica_id_map(cx);
+    }
+
+    fn handle_channel_buffer_event(
+        &mut self,
+        _: ModelHandle<ChannelBuffer>,
+        event: &channel_buffer::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            channel_buffer::Event::CollaboratorsChanged => {
+                self.refresh_replica_id_map(cx);
+            }
+            channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| {
+                editor.set_read_only(true);
+                cx.notify();
+            }),
+        }
+    }
+
+    /// Build a mapping of channel buffer replica ids to the corresponding
+    /// replica ids in the current project.
+    ///
+    /// Using this mapping, a given user can be displayed with the same color
+    /// in the channel buffer as in other files in the project. Users who are
+    /// in the channel buffer but not the project will not have a color.
+    fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
+        let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
+        let project = self.project.read(cx);
+        let channel_buffer = self.channel_buffer.read(cx);
+        project_replica_ids_by_channel_buffer_replica_id
+            .insert(channel_buffer.replica_id(cx), project.replica_id());
+        project_replica_ids_by_channel_buffer_replica_id.extend(
+            channel_buffer
+                .collaborators()
+                .iter()
+                .filter_map(|channel_buffer_collaborator| {
+                    project
+                        .collaborators()
+                        .values()
+                        .find_map(|project_collaborator| {
+                            (project_collaborator.user_id == channel_buffer_collaborator.user_id)
+                                .then_some((
+                                    channel_buffer_collaborator.replica_id as ReplicaId,
+                                    project_collaborator.replica_id,
+                                ))
+                        })
+                }),
+        );
+
+        self.editor.update(cx, |editor, cx| {
+            editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
+        });
+    }
+}
+
+impl Entity for ChannelView {
+    type Event = editor::Event;
+}
+
+impl View for ChannelView {
+    fn ui_name() -> &'static str {
+        "ChannelView"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        ChildView::new(self.editor.as_any(), cx).into_any()
+    }
+
+    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if cx.is_self_focused() {
+            cx.focus(self.editor.as_any())
+        }
+    }
+}
+
+impl Item for ChannelView {
+    fn tab_content<V: 'static>(
+        &self,
+        _: Option<usize>,
+        style: &theme::Tab,
+        cx: &gpui::AppContext,
+    ) -> AnyElement<V> {
+        let channel_name = &self.channel_buffer.read(cx).channel().name;
+        let label = if self.channel_buffer.read(cx).is_connected() {
+            format!("#{}", channel_name)
+        } else {
+            format!("#{} (disconnected)", channel_name)
+        };
+        Label::new(label, style.label.to_owned()).into_any()
+    }
+
+    fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
+        Some(Self::new(
+            self.project.clone(),
+            self.channel_buffer.clone(),
+            cx,
+        ))
+    }
+
+    fn is_singleton(&self, _cx: &AppContext) -> bool {
+        false
+    }
+
+    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
+        self.editor
+            .update(cx, |editor, cx| editor.navigate(data, cx))
+    }
+
+    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |editor, cx| Item::deactivated(editor, cx))
+    }
+
+    fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
+    }
+
+    fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+        Some(Box::new(self.editor.clone()))
+    }
+
+    fn show_toolbar(&self) -> bool {
+        true
+    }
+
+    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+        self.editor.read(cx).pixel_position_of_cursor(cx)
+    }
+}
+
+impl FollowableItem for ChannelView {
+    fn remote_id(&self) -> Option<workspace::ViewId> {
+        self.remote_id
+    }
+
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+        let channel = self.channel_buffer.read(cx).channel();
+        Some(proto::view::Variant::ChannelView(
+            proto::view::ChannelView {
+                channel_id: channel.id,
+                editor: if let Some(proto::view::Variant::Editor(proto)) =
+                    self.editor.read(cx).to_state_proto(cx)
+                {
+                    Some(proto)
+                } else {
+                    None
+                },
+            },
+        ))
+    }
+
+    fn from_state_proto(
+        pane: ViewHandle<workspace::Pane>,
+        workspace: ViewHandle<workspace::Workspace>,
+        remote_id: workspace::ViewId,
+        state: &mut Option<proto::view::Variant>,
+        cx: &mut AppContext,
+    ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
+        let Some(proto::view::Variant::ChannelView(_)) = state else {
+            return None;
+        };
+        let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
+            unreachable!()
+        };
+
+        let open = ChannelView::open(state.channel_id, pane, workspace, cx);
+
+        Some(cx.spawn(|mut cx| async move {
+            let this = open.await?;
+
+            let task = this
+                .update(&mut cx, |this, cx| {
+                    this.remote_id = Some(remote_id);
+
+                    if let Some(state) = state.editor {
+                        Some(this.editor.update(cx, |editor, cx| {
+                            editor.apply_update_proto(
+                                &this.project,
+                                proto::update_view::Variant::Editor(proto::update_view::Editor {
+                                    selections: state.selections,
+                                    pending_selection: state.pending_selection,
+                                    scroll_top_anchor: state.scroll_top_anchor,
+                                    scroll_x: state.scroll_x,
+                                    scroll_y: state.scroll_y,
+                                    ..Default::default()
+                                }),
+                                cx,
+                            )
+                        }))
+                    } else {
+                        None
+                    }
+                })
+                .ok_or_else(|| anyhow!("window was closed"))?;
+
+            if let Some(task) = task {
+                task.await?;
+            }
+
+            Ok(this)
+        }))
+    }
+
+    fn add_event_to_update_proto(
+        &self,
+        event: &Self::Event,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool {
+        self.editor
+            .read(cx)
+            .add_event_to_update_proto(event, update, cx)
+    }
+
+    fn apply_update_proto(
+        &mut self,
+        project: &ModelHandle<Project>,
+        message: proto::update_view::Variant,
+        cx: &mut ViewContext<Self>,
+    ) -> gpui::Task<anyhow::Result<()>> {
+        self.editor.update(cx, |editor, cx| {
+            editor.apply_update_proto(project, message, cx)
+        })
+    }
+
+    fn set_leader_replica_id(
+        &mut self,
+        leader_replica_id: Option<u16>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.editor.update(cx, |editor, cx| {
+            editor.set_leader_replica_id(leader_replica_id, cx)
+        })
+    }
+
+    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
+        Editor::should_unfollow_on_event(event, cx)
+    }
+}

crates/collab_ui/src/collab_panel.rs 🔗

@@ -0,0 +1,2715 @@
+mod channel_modal;
+mod contact_finder;
+mod panel_settings;
+
+use anyhow::Result;
+use call::ActiveCall;
+use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+use client::{proto::PeerId, Client, Contact, User, UserStore};
+use context_menu::{ContextMenu, ContextMenuItem};
+use db::kvp::KEY_VALUE_STORE;
+use editor::{Cancel, Editor};
+
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
+use futures::StreamExt;
+use fuzzy::{match_strings, StringMatchCandidate};
+use gpui::{
+    actions,
+    elements::{
+        Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
+        MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
+        Stack, Svg,
+    },
+    fonts::TextStyle,
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    impl_actions,
+    platform::{CursorStyle, MouseButton, PromptLevel},
+    serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
+    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use menu::{Confirm, SelectNext, SelectPrev};
+use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
+use project::{Fs, Project};
+use serde_derive::{Deserialize, Serialize};
+use settings::SettingsStore;
+use std::{borrow::Cow, mem, sync::Arc};
+use theme::{components::ComponentExt, IconButton};
+use util::{iife, ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel},
+    item::ItemHandle,
+    Workspace,
+};
+
+use crate::{
+    channel_view::{self, ChannelView},
+    face_pile::FacePile,
+};
+use channel_modal::ChannelModal;
+
+use self::contact_finder::ContactFinder;
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct RemoveChannel {
+    channel_id: u64,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct ToggleCollapse {
+    channel_id: u64,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct NewChannel {
+    channel_id: u64,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct InviteMembers {
+    channel_id: u64,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct ManageMembers {
+    channel_id: u64,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct RenameChannel {
+    channel_id: u64,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct OpenChannelBuffer {
+    channel_id: u64,
+}
+
+actions!(
+    collab_panel,
+    [
+        ToggleFocus,
+        Remove,
+        Secondary,
+        CollapseSelectedChannel,
+        ExpandSelectedChannel
+    ]
+);
+
+impl_actions!(
+    collab_panel,
+    [
+        RemoveChannel,
+        NewChannel,
+        InviteMembers,
+        ManageMembers,
+        RenameChannel,
+        ToggleCollapse,
+        OpenChannelBuffer
+    ]
+);
+
+const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
+
+pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
+    settings::register::<panel_settings::CollaborationPanelSettings>(cx);
+    contact_finder::init(cx);
+    channel_modal::init(cx);
+    channel_view::init(cx);
+
+    cx.add_action(CollabPanel::cancel);
+    cx.add_action(CollabPanel::select_next);
+    cx.add_action(CollabPanel::select_prev);
+    cx.add_action(CollabPanel::confirm);
+    cx.add_action(CollabPanel::remove);
+    cx.add_action(CollabPanel::remove_selected_channel);
+    cx.add_action(CollabPanel::show_inline_context_menu);
+    cx.add_action(CollabPanel::new_subchannel);
+    cx.add_action(CollabPanel::invite_members);
+    cx.add_action(CollabPanel::manage_members);
+    cx.add_action(CollabPanel::rename_selected_channel);
+    cx.add_action(CollabPanel::rename_channel);
+    cx.add_action(CollabPanel::toggle_channel_collapsed);
+    cx.add_action(CollabPanel::collapse_selected_channel);
+    cx.add_action(CollabPanel::expand_selected_channel);
+    cx.add_action(CollabPanel::open_channel_buffer);
+}
+
+#[derive(Debug)]
+pub enum ChannelEditingState {
+    Create {
+        parent_id: Option<u64>,
+        pending_name: Option<String>,
+    },
+    Rename {
+        channel_id: u64,
+        pending_name: Option<String>,
+    },
+}
+
+impl ChannelEditingState {
+    fn pending_name(&self) -> Option<&str> {
+        match self {
+            ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
+            ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
+        }
+    }
+}
+
+pub struct CollabPanel {
+    width: Option<f32>,
+    fs: Arc<dyn Fs>,
+    has_focus: bool,
+    pending_serialization: Task<Option<()>>,
+    context_menu: ViewHandle<ContextMenu>,
+    filter_editor: ViewHandle<Editor>,
+    channel_name_editor: ViewHandle<Editor>,
+    channel_editing_state: Option<ChannelEditingState>,
+    entries: Vec<ListEntry>,
+    selection: Option<usize>,
+    user_store: ModelHandle<UserStore>,
+    client: Arc<Client>,
+    channel_store: ModelHandle<ChannelStore>,
+    project: ModelHandle<Project>,
+    match_candidates: Vec<StringMatchCandidate>,
+    list_state: ListState<Self>,
+    subscriptions: Vec<Subscription>,
+    collapsed_sections: Vec<Section>,
+    collapsed_channels: Vec<ChannelId>,
+    workspace: WeakViewHandle<Workspace>,
+    context_menu_on_selected: bool,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedCollabPanel {
+    width: Option<f32>,
+    collapsed_channels: Option<Vec<ChannelId>>,
+}
+
+#[derive(Debug)]
+pub enum Event {
+    DockPositionChanged,
+    Focus,
+    Dismissed,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+enum Section {
+    ActiveCall,
+    Channels,
+    ChannelInvites,
+    ContactRequests,
+    Contacts,
+    Online,
+    Offline,
+}
+
+#[derive(Clone, Debug)]
+enum ListEntry {
+    Header(Section, usize),
+    CallParticipant {
+        user: Arc<User>,
+        is_pending: bool,
+    },
+    ParticipantProject {
+        project_id: u64,
+        worktree_root_names: Vec<String>,
+        host_user_id: u64,
+        is_last: bool,
+    },
+    ParticipantScreen {
+        peer_id: PeerId,
+        is_last: bool,
+    },
+    IncomingRequest(Arc<User>),
+    OutgoingRequest(Arc<User>),
+    ChannelInvite(Arc<Channel>),
+    Channel {
+        channel: Arc<Channel>,
+        depth: usize,
+    },
+    ChannelNotes {
+        channel_id: ChannelId,
+    },
+    ChannelEditor {
+        depth: usize,
+    },
+    Contact {
+        contact: Arc<Contact>,
+        calling: bool,
+    },
+    ContactPlaceholder,
+}
+
+impl Entity for CollabPanel {
+    type Event = Event;
+}
+
+impl CollabPanel {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+        cx.add_view::<Self, _>(|cx| {
+            let view_id = cx.view_id();
+
+            let filter_editor = cx.add_view(|cx| {
+                let mut editor = Editor::single_line(
+                    Some(Arc::new(|theme| {
+                        theme.collab_panel.user_query_editor.clone()
+                    })),
+                    cx,
+                );
+                editor.set_placeholder_text("Filter channels, contacts", cx);
+                editor
+            });
+
+            cx.subscribe(&filter_editor, |this, _, event, cx| {
+                if let editor::Event::BufferEdited = event {
+                    let query = this.filter_editor.read(cx).text(cx);
+                    if !query.is_empty() {
+                        this.selection.take();
+                    }
+                    this.update_entries(true, cx);
+                    if !query.is_empty() {
+                        this.selection = this
+                            .entries
+                            .iter()
+                            .position(|entry| !matches!(entry, ListEntry::Header(_, _)));
+                    }
+                }
+            })
+            .detach();
+
+            let channel_name_editor = cx.add_view(|cx| {
+                Editor::single_line(
+                    Some(Arc::new(|theme| {
+                        theme.collab_panel.user_query_editor.clone()
+                    })),
+                    cx,
+                )
+            });
+
+            cx.subscribe(&channel_name_editor, |this, _, event, cx| {
+                if let editor::Event::Blurred = event {
+                    if let Some(state) = &this.channel_editing_state {
+                        if state.pending_name().is_some() {
+                            return;
+                        }
+                    }
+                    this.take_editing_state(cx);
+                    this.update_entries(false, cx);
+                    cx.notify();
+                }
+            })
+            .detach();
+
+            let list_state =
+                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
+                    let theme = theme::current(cx).clone();
+                    let is_selected = this.selection == Some(ix);
+                    let current_project_id = this.project.read(cx).remote_id();
+
+                    match &this.entries[ix] {
+                        ListEntry::Header(section, depth) => {
+                            let is_collapsed = this.collapsed_sections.contains(section);
+                            this.render_header(
+                                *section,
+                                &theme,
+                                *depth,
+                                is_selected,
+                                is_collapsed,
+                                cx,
+                            )
+                        }
+                        ListEntry::CallParticipant { user, is_pending } => {
+                            Self::render_call_participant(
+                                user,
+                                *is_pending,
+                                is_selected,
+                                &theme.collab_panel,
+                            )
+                        }
+                        ListEntry::ParticipantProject {
+                            project_id,
+                            worktree_root_names,
+                            host_user_id,
+                            is_last,
+                        } => Self::render_participant_project(
+                            *project_id,
+                            worktree_root_names,
+                            *host_user_id,
+                            Some(*project_id) == current_project_id,
+                            *is_last,
+                            is_selected,
+                            &theme.collab_panel,
+                            cx,
+                        ),
+                        ListEntry::ParticipantScreen { peer_id, is_last } => {
+                            Self::render_participant_screen(
+                                *peer_id,
+                                *is_last,
+                                is_selected,
+                                &theme.collab_panel,
+                                cx,
+                            )
+                        }
+                        ListEntry::Channel { channel, depth } => {
+                            let channel_row = this.render_channel(
+                                &*channel,
+                                *depth,
+                                &theme.collab_panel,
+                                is_selected,
+                                cx,
+                            );
+
+                            if is_selected && this.context_menu_on_selected {
+                                Stack::new()
+                                    .with_child(channel_row)
+                                    .with_child(
+                                        ChildView::new(&this.context_menu, cx)
+                                            .aligned()
+                                            .bottom()
+                                            .right(),
+                                    )
+                                    .into_any()
+                            } else {
+                                return channel_row;
+                            }
+                        }
+                        ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
+                            *channel_id,
+                            &theme.collab_panel,
+                            is_selected,
+                            cx,
+                        ),
+                        ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
+                            channel.clone(),
+                            this.channel_store.clone(),
+                            &theme.collab_panel,
+                            is_selected,
+                            cx,
+                        ),
+                        ListEntry::IncomingRequest(user) => Self::render_contact_request(
+                            user.clone(),
+                            this.user_store.clone(),
+                            &theme.collab_panel,
+                            true,
+                            is_selected,
+                            cx,
+                        ),
+                        ListEntry::OutgoingRequest(user) => Self::render_contact_request(
+                            user.clone(),
+                            this.user_store.clone(),
+                            &theme.collab_panel,
+                            false,
+                            is_selected,
+                            cx,
+                        ),
+                        ListEntry::Contact { contact, calling } => Self::render_contact(
+                            contact,
+                            *calling,
+                            &this.project,
+                            &theme.collab_panel,
+                            is_selected,
+                            cx,
+                        ),
+                        ListEntry::ChannelEditor { depth } => {
+                            this.render_channel_editor(&theme, *depth, cx)
+                        }
+                        ListEntry::ContactPlaceholder => {
+                            this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
+                        }
+                    }
+                });
+
+            let mut this = Self {
+                width: None,
+                has_focus: false,
+                fs: workspace.app_state().fs.clone(),
+                pending_serialization: Task::ready(None),
+                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
+                channel_name_editor,
+                filter_editor,
+                entries: Vec::default(),
+                channel_editing_state: None,
+                selection: None,
+                user_store: workspace.user_store().clone(),
+                channel_store: workspace.app_state().channel_store.clone(),
+                project: workspace.project().clone(),
+                subscriptions: Vec::default(),
+                match_candidates: Vec::default(),
+                collapsed_sections: vec![Section::Offline],
+                collapsed_channels: Vec::default(),
+                workspace: workspace.weak_handle(),
+                client: workspace.app_state().client.clone(),
+                context_menu_on_selected: true,
+                list_state,
+            };
+
+            this.update_entries(false, cx);
+
+            // Update the dock position when the setting changes.
+            let mut old_dock_position = this.position(cx);
+            this.subscriptions
+                .push(
+                    cx.observe_global::<SettingsStore, _>(move |this: &mut CollabPanel, cx| {
+                        let new_dock_position = this.position(cx);
+                        if new_dock_position != old_dock_position {
+                            old_dock_position = new_dock_position;
+                            cx.emit(Event::DockPositionChanged);
+                        }
+                        cx.notify();
+                    }),
+                );
+
+            let active_call = ActiveCall::global(cx);
+            this.subscriptions
+                .push(cx.observe(&this.user_store, |this, _, cx| {
+                    this.update_entries(true, cx)
+                }));
+            this.subscriptions
+                .push(cx.observe(&this.channel_store, |this, _, cx| {
+                    this.update_entries(true, cx)
+                }));
+            this.subscriptions
+                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
+            this.subscriptions
+                .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
+                    this.update_entries(true, cx)
+                }));
+            this.subscriptions.push(cx.subscribe(
+                &this.channel_store,
+                |this, _channel_store, e, cx| match e {
+                    ChannelEvent::ChannelCreated(channel_id)
+                    | ChannelEvent::ChannelRenamed(channel_id) => {
+                        if this.take_editing_state(cx) {
+                            this.update_entries(false, cx);
+                            this.selection = this.entries.iter().position(|entry| {
+                                if let ListEntry::Channel { channel, .. } = entry {
+                                    channel.id == *channel_id
+                                } else {
+                                    false
+                                }
+                            });
+                        }
+                    }
+                },
+            ));
+
+            this
+        })
+    }
+
+    pub fn load(
+        workspace: WeakViewHandle<Workspace>,
+        cx: AsyncAppContext,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        cx.spawn(|mut cx| async move {
+            let serialized_panel = if let Some(panel) = cx
+                .background()
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                Some(serde_json::from_str::<SerializedCollabPanel>(&panel)?)
+            } else {
+                None
+            };
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = CollabPanel::new(workspace, cx);
+                if let Some(serialized_panel) = serialized_panel {
+                    panel.update(cx, |panel, cx| {
+                        panel.width = serialized_panel.width;
+                        panel.collapsed_channels = serialized_panel
+                            .collapsed_channels
+                            .unwrap_or_else(|| Vec::new());
+                        cx.notify();
+                    });
+                }
+                panel
+            })
+        })
+    }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        let collapsed_channels = self.collapsed_channels.clone();
+        self.pending_serialization = cx.background().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        COLLABORATION_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedCollabPanel {
+                            width,
+                            collapsed_channels: Some(collapsed_channels),
+                        })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+
+    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.read(cx);
+        let user_store = self.user_store.read(cx);
+        let query = self.filter_editor.read(cx).text(cx);
+        let executor = cx.background().clone();
+
+        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
+        let old_entries = mem::take(&mut self.entries);
+
+        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+            self.entries.push(ListEntry::Header(Section::ActiveCall, 0));
+
+            if !self.collapsed_sections.contains(&Section::ActiveCall) {
+                let room = room.read(cx);
+
+                if let Some(channel_id) = room.channel_id() {
+                    self.entries.push(ListEntry::ChannelNotes { channel_id })
+                }
+
+                // Populate the active user.
+                if let Some(user) = user_store.current_user() {
+                    self.match_candidates.clear();
+                    self.match_candidates.push(StringMatchCandidate {
+                        id: 0,
+                        string: user.github_login.clone(),
+                        char_bag: user.github_login.chars().collect(),
+                    });
+                    let matches = executor.block(match_strings(
+                        &self.match_candidates,
+                        &query,
+                        true,
+                        usize::MAX,
+                        &Default::default(),
+                        executor.clone(),
+                    ));
+                    if !matches.is_empty() {
+                        let user_id = user.id;
+                        self.entries.push(ListEntry::CallParticipant {
+                            user,
+                            is_pending: false,
+                        });
+                        let mut projects = room.local_participant().projects.iter().peekable();
+                        while let Some(project) = projects.next() {
+                            self.entries.push(ListEntry::ParticipantProject {
+                                project_id: project.id,
+                                worktree_root_names: project.worktree_root_names.clone(),
+                                host_user_id: user_id,
+                                is_last: projects.peek().is_none(),
+                            });
+                        }
+                    }
+                }
+
+                // Populate remote participants.
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(room.remote_participants().iter().map(|(_, participant)| {
+                        StringMatchCandidate {
+                            id: participant.user.id as usize,
+                            string: participant.user.github_login.clone(),
+                            char_bag: participant.user.github_login.chars().collect(),
+                        }
+                    }));
+                let matches = executor.block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    executor.clone(),
+                ));
+                for mat in matches {
+                    let user_id = mat.candidate_id as u64;
+                    let participant = &room.remote_participants()[&user_id];
+                    self.entries.push(ListEntry::CallParticipant {
+                        user: participant.user.clone(),
+                        is_pending: false,
+                    });
+                    let mut projects = participant.projects.iter().peekable();
+                    while let Some(project) = projects.next() {
+                        self.entries.push(ListEntry::ParticipantProject {
+                            project_id: project.id,
+                            worktree_root_names: project.worktree_root_names.clone(),
+                            host_user_id: participant.user.id,
+                            is_last: projects.peek().is_none()
+                                && participant.video_tracks.is_empty(),
+                        });
+                    }
+                    if !participant.video_tracks.is_empty() {
+                        self.entries.push(ListEntry::ParticipantScreen {
+                            peer_id: participant.peer_id,
+                            is_last: true,
+                        });
+                    }
+                }
+
+                // Populate pending participants.
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(room.pending_participants().iter().enumerate().map(
+                        |(id, participant)| StringMatchCandidate {
+                            id,
+                            string: participant.github_login.clone(),
+                            char_bag: participant.github_login.chars().collect(),
+                        },
+                    ));
+                let matches = executor.block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    executor.clone(),
+                ));
+                self.entries
+                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+                        user: room.pending_participants()[mat.candidate_id].clone(),
+                        is_pending: true,
+                    }));
+            }
+        }
+
+        let mut request_entries = Vec::new();
+
+        if cx.has_flag::<ChannelsAlpha>() {
+            self.entries.push(ListEntry::Header(Section::Channels, 0));
+
+            if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(
+                        channel_store
+                            .channels()
+                            .enumerate()
+                            .map(|(ix, (_, channel))| StringMatchCandidate {
+                                id: ix,
+                                string: channel.name.clone(),
+                                char_bag: channel.name.chars().collect(),
+                            }),
+                    );
+                let matches = executor.block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    executor.clone(),
+                ));
+                if let Some(state) = &self.channel_editing_state {
+                    if matches!(
+                        state,
+                        ChannelEditingState::Create {
+                            parent_id: None,
+                            ..
+                        }
+                    ) {
+                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
+                    }
+                }
+                let mut collapse_depth = None;
+                for mat in matches {
+                    let (depth, channel) =
+                        channel_store.channel_at_index(mat.candidate_id).unwrap();
+
+                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+                        collapse_depth = Some(depth);
+                    } else if let Some(collapsed_depth) = collapse_depth {
+                        if depth > collapsed_depth {
+                            continue;
+                        }
+                        if self.is_channel_collapsed(channel.id) {
+                            collapse_depth = Some(depth);
+                        } else {
+                            collapse_depth = None;
+                        }
+                    }
+
+                    match &self.channel_editing_state {
+                        Some(ChannelEditingState::Create { parent_id, .. })
+                            if *parent_id == Some(channel.id) =>
+                        {
+                            self.entries.push(ListEntry::Channel {
+                                channel: channel.clone(),
+                                depth,
+                            });
+                            self.entries
+                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
+                        }
+                        Some(ChannelEditingState::Rename { channel_id, .. })
+                            if *channel_id == channel.id =>
+                        {
+                            self.entries.push(ListEntry::ChannelEditor { depth });
+                        }
+                        _ => {
+                            self.entries.push(ListEntry::Channel {
+                                channel: channel.clone(),
+                                depth,
+                            });
+                        }
+                    }
+                }
+            }
+
+            let channel_invites = channel_store.channel_invitations();
+            if !channel_invites.is_empty() {
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
+                        StringMatchCandidate {
+                            id: ix,
+                            string: channel.name.clone(),
+                            char_bag: channel.name.chars().collect(),
+                        }
+                    }));
+                let matches = executor.block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    executor.clone(),
+                ));
+                request_entries.extend(matches.iter().map(|mat| {
+                    ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+                }));
+
+                if !request_entries.is_empty() {
+                    self.entries
+                        .push(ListEntry::Header(Section::ChannelInvites, 1));
+                    if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+                        self.entries.append(&mut request_entries);
+                    }
+                }
+            }
+        }
+
+        self.entries.push(ListEntry::Header(Section::Contacts, 0));
+
+        request_entries.clear();
+        let incoming = user_store.incoming_contact_requests();
+        if !incoming.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    incoming
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, user)| StringMatchCandidate {
+                            id: ix,
+                            string: user.github_login.clone(),
+                            char_bag: user.github_login.chars().collect(),
+                        }),
+                );
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+            request_entries.extend(
+                matches
+                    .iter()
+                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
+            );
+        }
+
+        let outgoing = user_store.outgoing_contact_requests();
+        if !outgoing.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    outgoing
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, user)| StringMatchCandidate {
+                            id: ix,
+                            string: user.github_login.clone(),
+                            char_bag: user.github_login.chars().collect(),
+                        }),
+                );
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+            request_entries.extend(
+                matches
+                    .iter()
+                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
+            );
+        }
+
+        if !request_entries.is_empty() {
+            self.entries
+                .push(ListEntry::Header(Section::ContactRequests, 1));
+            if !self.collapsed_sections.contains(&Section::ContactRequests) {
+                self.entries.append(&mut request_entries);
+            }
+        }
+
+        let contacts = user_store.contacts();
+        if !contacts.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    contacts
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, contact)| StringMatchCandidate {
+                            id: ix,
+                            string: contact.user.github_login.clone(),
+                            char_bag: contact.user.github_login.chars().collect(),
+                        }),
+                );
+
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+
+            let (online_contacts, offline_contacts) = matches
+                .iter()
+                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
+
+            for (matches, section) in [
+                (online_contacts, Section::Online),
+                (offline_contacts, Section::Offline),
+            ] {
+                if !matches.is_empty() {
+                    self.entries.push(ListEntry::Header(section, 1));
+                    if !self.collapsed_sections.contains(&section) {
+                        let active_call = &ActiveCall::global(cx).read(cx);
+                        for mat in matches {
+                            let contact = &contacts[mat.candidate_id];
+                            self.entries.push(ListEntry::Contact {
+                                contact: contact.clone(),
+                                calling: active_call.pending_invites().contains(&contact.user.id),
+                            });
+                        }
+                    }
+                }
+            }
+        }
+
+        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
+            self.entries.push(ListEntry::ContactPlaceholder);
+        }
+
+        if select_same_item {
+            if let Some(prev_selected_entry) = prev_selected_entry {
+                self.selection.take();
+                for (ix, entry) in self.entries.iter().enumerate() {
+                    if *entry == prev_selected_entry {
+                        self.selection = Some(ix);
+                        break;
+                    }
+                }
+            }
+        } else {
+            self.selection = self.selection.and_then(|prev_selection| {
+                if self.entries.is_empty() {
+                    None
+                } else {
+                    Some(prev_selection.min(self.entries.len() - 1))
+                }
+            });
+        }
+
+        let old_scroll_top = self.list_state.logical_scroll_top();
+        self.list_state.reset(self.entries.len());
+
+        // Attempt to maintain the same scroll position.
+        if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
+            let new_scroll_top = self
+                .entries
+                .iter()
+                .position(|entry| entry == old_top_entry)
+                .map(|item_ix| ListOffset {
+                    item_ix,
+                    offset_in_item: old_scroll_top.offset_in_item,
+                })
+                .or_else(|| {
+                    let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
+                    let item_ix = self
+                        .entries
+                        .iter()
+                        .position(|entry| entry == entry_after_old_top)?;
+                    Some(ListOffset {
+                        item_ix,
+                        offset_in_item: 0.,
+                    })
+                })
+                .or_else(|| {
+                    let entry_before_old_top =
+                        old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
+                    let item_ix = self
+                        .entries
+                        .iter()
+                        .position(|entry| entry == entry_before_old_top)?;
+                    Some(ListOffset {
+                        item_ix,
+                        offset_in_item: 0.,
+                    })
+                });
+
+            self.list_state
+                .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
+        }
+
+        cx.notify();
+    }
+
+    fn render_call_participant(
+        user: &User,
+        is_pending: bool,
+        is_selected: bool,
+        theme: &theme::CollabPanel,
+    ) -> AnyElement<Self> {
+        Flex::row()
+            .with_children(user.avatar.clone().map(|avatar| {
+                Image::from_data(avatar)
+                    .with_style(theme.contact_avatar)
+                    .aligned()
+                    .left()
+            }))
+            .with_child(
+                Label::new(
+                    user.github_login.clone(),
+                    theme.contact_username.text.clone(),
+                )
+                .contained()
+                .with_style(theme.contact_username.container)
+                .aligned()
+                .left()
+                .flex(1., true),
+            )
+            .with_children(if is_pending {
+                Some(
+                    Label::new("Calling", theme.calling_indicator.text.clone())
+                        .contained()
+                        .with_style(theme.calling_indicator.container)
+                        .aligned(),
+                )
+            } else {
+                None
+            })
+            .constrained()
+            .with_height(theme.row_height)
+            .contained()
+            .with_style(
+                *theme
+                    .contact_row
+                    .in_state(is_selected)
+                    .style_for(&mut Default::default()),
+            )
+            .into_any()
+    }
+
+    fn render_participant_project(
+        project_id: u64,
+        worktree_root_names: &[String],
+        host_user_id: u64,
+        is_current: bool,
+        is_last: bool,
+        is_selected: bool,
+        theme: &theme::CollabPanel,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum JoinProject {}
+
+        let host_avatar_width = theme
+            .contact_avatar
+            .width
+            .or(theme.contact_avatar.height)
+            .unwrap_or(0.);
+        let tree_branch = theme.tree_branch;
+        let project_name = if worktree_root_names.is_empty() {
+            "untitled".to_string()
+        } else {
+            worktree_root_names.join(", ")
+        };
+
+        MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
+            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+            let row = theme
+                .project_row
+                .in_state(is_selected)
+                .style_for(mouse_state);
+
+            Flex::row()
+                .with_child(render_tree_branch(
+                    tree_branch,
+                    &row.name.text,
+                    is_last,
+                    vec2f(host_avatar_width, theme.row_height),
+                    cx.font_cache(),
+                ))
+                .with_child(
+                    Svg::new("icons/file_icons/folder.svg")
+                        .with_color(theme.channel_hash.color)
+                        .constrained()
+                        .with_width(theme.channel_hash.width)
+                        .aligned()
+                        .left(),
+                )
+                .with_child(
+                    Label::new(project_name, row.name.text.clone())
+                        .aligned()
+                        .left()
+                        .contained()
+                        .with_style(row.name.container)
+                        .flex(1., false),
+                )
+                .constrained()
+                .with_height(theme.row_height)
+                .contained()
+                .with_style(row.container)
+        })
+        .with_cursor_style(if !is_current {
+            CursorStyle::PointingHand
+        } else {
+            CursorStyle::Arrow
+        })
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            if !is_current {
+                if let Some(workspace) = this.workspace.upgrade(cx) {
+                    let app_state = workspace.read(cx).app_state().clone();
+                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+                        .detach_and_log_err(cx);
+                }
+            }
+        })
+        .into_any()
+    }
+
+    fn render_participant_screen(
+        peer_id: PeerId,
+        is_last: bool,
+        is_selected: bool,
+        theme: &theme::CollabPanel,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum OpenSharedScreen {}
+
+        let host_avatar_width = theme
+            .contact_avatar
+            .width
+            .or(theme.contact_avatar.height)
+            .unwrap_or(0.);
+        let tree_branch = theme.tree_branch;
+
+        MouseEventHandler::new::<OpenSharedScreen, _>(
+            peer_id.as_u64() as usize,
+            cx,
+            |mouse_state, cx| {
+                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+                let row = theme
+                    .project_row
+                    .in_state(is_selected)
+                    .style_for(mouse_state);
+
+                Flex::row()
+                    .with_child(render_tree_branch(
+                        tree_branch,
+                        &row.name.text,
+                        is_last,
+                        vec2f(host_avatar_width, theme.row_height),
+                        cx.font_cache(),
+                    ))
+                    .with_child(
+                        Svg::new("icons/disable_screen_sharing_12.svg")
+                            .with_color(theme.channel_hash.color)
+                            .constrained()
+                            .with_width(theme.channel_hash.width)
+                            .aligned()
+                            .left(),
+                    )
+                    .with_child(
+                        Label::new("Screen", row.name.text.clone())
+                            .aligned()
+                            .left()
+                            .contained()
+                            .with_style(row.name.container)
+                            .flex(1., false),
+                    )
+                    .constrained()
+                    .with_height(theme.row_height)
+                    .contained()
+                    .with_style(row.container)
+            },
+        )
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            if let Some(workspace) = this.workspace.upgrade(cx) {
+                workspace.update(cx, |workspace, cx| {
+                    workspace.open_shared_screen(peer_id, cx)
+                });
+            }
+        })
+        .into_any()
+    }
+
+    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        if let Some(_) = self.channel_editing_state.take() {
+            self.channel_name_editor.update(cx, |editor, cx| {
+                editor.set_text("", cx);
+            });
+            true
+        } else {
+            false
+        }
+    }
+
+    fn render_header(
+        &self,
+        section: Section,
+        theme: &theme::Theme,
+        depth: usize,
+        is_selected: bool,
+        is_collapsed: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum Header {}
+        enum LeaveCallContactList {}
+        enum AddChannel {}
+
+        let tooltip_style = &theme.tooltip;
+        let text = match section {
+            Section::ActiveCall => {
+                let channel_name = iife!({
+                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
+
+                    let name = self
+                        .channel_store
+                        .read(cx)
+                        .channel_for_id(channel_id)?
+                        .name
+                        .as_str();
+
+                    Some(name)
+                });
+
+                if let Some(name) = channel_name {
+                    Cow::Owned(format!("#{}", name))
+                } else {
+                    Cow::Borrowed("Current Call")
+                }
+            }
+            Section::ContactRequests => Cow::Borrowed("Requests"),
+            Section::Contacts => Cow::Borrowed("Contacts"),
+            Section::Channels => Cow::Borrowed("Channels"),
+            Section::ChannelInvites => Cow::Borrowed("Invites"),
+            Section::Online => Cow::Borrowed("Online"),
+            Section::Offline => Cow::Borrowed("Offline"),
+        };
+
+        enum AddContact {}
+        let button = match section {
+            Section::ActiveCall => Some(
+                MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
+                    render_icon_button(
+                        theme
+                            .collab_panel
+                            .leave_call_button
+                            .style_for(is_selected, state),
+                        "icons/exit.svg",
+                    )
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, |_, _, cx| {
+                    Self::leave_call(cx);
+                })
+                .with_tooltip::<AddContact>(
+                    0,
+                    "Leave call",
+                    None,
+                    tooltip_style.clone(),
+                    cx,
+                ),
+            ),
+            Section::Contacts => Some(
+                MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
+                    render_icon_button(
+                        theme
+                            .collab_panel
+                            .add_contact_button
+                            .style_for(is_selected, state),
+                        "icons/plus_16.svg",
+                    )
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, |_, this, cx| {
+                    this.toggle_contact_finder(cx);
+                })
+                .with_tooltip::<LeaveCallContactList>(
+                    0,
+                    "Search for new contact",
+                    None,
+                    tooltip_style.clone(),
+                    cx,
+                ),
+            ),
+            Section::Channels => Some(
+                MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
+                    render_icon_button(
+                        theme
+                            .collab_panel
+                            .add_contact_button
+                            .style_for(is_selected, state),
+                        "icons/plus.svg",
+                    )
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
+                .with_tooltip::<AddChannel>(
+                    0,
+                    "Create a channel",
+                    None,
+                    tooltip_style.clone(),
+                    cx,
+                ),
+            ),
+            _ => None,
+        };
+
+        let can_collapse = depth > 0;
+        let icon_size = (&theme.collab_panel).section_icon_size;
+        let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
+            let header_style = if can_collapse {
+                theme
+                    .collab_panel
+                    .subheader_row
+                    .in_state(is_selected)
+                    .style_for(state)
+            } else {
+                &theme.collab_panel.header_row
+            };
+
+            Flex::row()
+                .with_children(if can_collapse {
+                    Some(
+                        Svg::new(if is_collapsed {
+                            "icons/chevron_right.svg"
+                        } else {
+                            "icons/chevron_down.svg"
+                        })
+                        .with_color(header_style.text.color)
+                        .constrained()
+                        .with_max_width(icon_size)
+                        .with_max_height(icon_size)
+                        .aligned()
+                        .constrained()
+                        .with_width(icon_size)
+                        .contained()
+                        .with_margin_right(
+                            theme.collab_panel.contact_username.container.margin.left,
+                        ),
+                    )
+                } else {
+                    None
+                })
+                .with_child(
+                    Label::new(text, header_style.text.clone())
+                        .aligned()
+                        .left()
+                        .flex(1., true),
+                )
+                .with_children(button.map(|button| button.aligned().right()))
+                .constrained()
+                .with_height(theme.collab_panel.row_height)
+                .contained()
+                .with_style(header_style.container)
+        });
+
+        if can_collapse {
+            result = result
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    if can_collapse {
+                        this.toggle_section_expanded(section, cx);
+                    }
+                })
+        }
+
+        result.into_any()
+    }
+
+    fn render_contact(
+        contact: &Contact,
+        calling: bool,
+        project: &ModelHandle<Project>,
+        theme: &theme::CollabPanel,
+        is_selected: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        let online = contact.online;
+        let busy = contact.busy || calling;
+        let user_id = contact.user.id;
+        let github_login = contact.user.github_login.clone();
+        let initial_project = project.clone();
+        let mut event_handler =
+            MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
+                Flex::row()
+                    .with_children(contact.user.avatar.clone().map(|avatar| {
+                        let status_badge = if contact.online {
+                            Some(
+                                Empty::new()
+                                    .collapsed()
+                                    .contained()
+                                    .with_style(if busy {
+                                        theme.contact_status_busy
+                                    } else {
+                                        theme.contact_status_free
+                                    })
+                                    .aligned(),
+                            )
+                        } else {
+                            None
+                        };
+                        Stack::new()
+                            .with_child(
+                                Image::from_data(avatar)
+                                    .with_style(theme.contact_avatar)
+                                    .aligned()
+                                    .left(),
+                            )
+                            .with_children(status_badge)
+                    }))
+                    .with_child(
+                        Label::new(
+                            contact.user.github_login.clone(),
+                            theme.contact_username.text.clone(),
+                        )
+                        .contained()
+                        .with_style(theme.contact_username.container)
+                        .aligned()
+                        .left()
+                        .flex(1., true),
+                    )
+                    .with_child(
+                        MouseEventHandler::new::<Cancel, _>(
+                            contact.user.id as usize,
+                            cx,
+                            |mouse_state, _| {
+                                let button_style = theme.contact_button.style_for(mouse_state);
+                                render_icon_button(button_style, "icons/x.svg")
+                                    .aligned()
+                                    .flex_float()
+                            },
+                        )
+                        .with_padding(Padding::uniform(2.))
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_click(MouseButton::Left, move |_, this, cx| {
+                            this.remove_contact(user_id, &github_login, cx);
+                        })
+                        .flex_float(),
+                    )
+                    .with_children(if calling {
+                        Some(
+                            Label::new("Calling", theme.calling_indicator.text.clone())
+                                .contained()
+                                .with_style(theme.calling_indicator.container)
+                                .aligned(),
+                        )
+                    } else {
+                        None
+                    })
+                    .constrained()
+                    .with_height(theme.row_height)
+                    .contained()
+                    .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
+            })
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                if online && !busy {
+                    this.call(user_id, Some(initial_project.clone()), cx);
+                }
+            });
+
+        if online {
+            event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
+        }
+
+        event_handler.into_any()
+    }
+
+    fn render_contact_placeholder(
+        &self,
+        theme: &theme::CollabPanel,
+        is_selected: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum AddContacts {}
+        MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
+            let style = theme.list_empty_state.style_for(is_selected, state);
+            Flex::row()
+                .with_child(
+                    Svg::new("icons/plus.svg")
+                        .with_color(theme.list_empty_icon.color)
+                        .constrained()
+                        .with_width(theme.list_empty_icon.width)
+                        .aligned()
+                        .left(),
+                )
+                .with_child(
+                    Label::new("Add a contact", style.text.clone())
+                        .contained()
+                        .with_style(theme.list_empty_label_container),
+                )
+                .align_children_center()
+                .contained()
+                .with_style(style.container)
+                .into_any()
+        })
+        .on_click(MouseButton::Left, |_, this, cx| {
+            this.toggle_contact_finder(cx);
+        })
+        .into_any()
+    }
+
+    fn render_channel_editor(
+        &self,
+        theme: &theme::Theme,
+        depth: usize,
+        cx: &AppContext,
+    ) -> AnyElement<Self> {
+        Flex::row()
+            .with_child(
+                Empty::new()
+                    .constrained()
+                    .with_width(theme.collab_panel.disclosure.button_space()),
+            )
+            .with_child(
+                Svg::new("icons/hash.svg")
+                    .with_color(theme.collab_panel.channel_hash.color)
+                    .constrained()
+                    .with_width(theme.collab_panel.channel_hash.width)
+                    .aligned()
+                    .left(),
+            )
+            .with_child(
+                if let Some(pending_name) = self
+                    .channel_editing_state
+                    .as_ref()
+                    .and_then(|state| state.pending_name())
+                {
+                    Label::new(
+                        pending_name.to_string(),
+                        theme.collab_panel.contact_username.text.clone(),
+                    )
+                    .contained()
+                    .with_style(theme.collab_panel.contact_username.container)
+                    .aligned()
+                    .left()
+                    .flex(1., true)
+                    .into_any()
+                } else {
+                    ChildView::new(&self.channel_name_editor, cx)
+                        .aligned()
+                        .left()
+                        .contained()
+                        .with_style(theme.collab_panel.channel_editor)
+                        .flex(1.0, true)
+                        .into_any()
+                },
+            )
+            .align_children_center()
+            .constrained()
+            .with_height(theme.collab_panel.row_height)
+            .contained()
+            .with_style(gpui::elements::ContainerStyle {
+                background_color: Some(theme.editor.background),
+                ..*theme.collab_panel.contact_row.default_style()
+            })
+            .with_padding_left(
+                theme.collab_panel.contact_row.default_style().padding.left
+                    + theme.collab_panel.channel_indent * depth as f32,
+            )
+            .into_any()
+    }
+
+    fn render_channel(
+        &self,
+        channel: &Channel,
+        depth: usize,
+        theme: &theme::CollabPanel,
+        is_selected: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        let channel_id = channel.id;
+        let has_children = self.channel_store.read(cx).has_children(channel_id);
+        let disclosed =
+            has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
+
+        let is_active = iife!({
+            let call_channel = ActiveCall::global(cx)
+                .read(cx)
+                .room()?
+                .read(cx)
+                .channel_id()?;
+            Some(call_channel == channel_id)
+        })
+        .unwrap_or(false);
+
+        const FACEPILE_LIMIT: usize = 3;
+
+        MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
+            Flex::<Self>::row()
+                .with_child(
+                    Svg::new("icons/hash.svg")
+                        .with_color(theme.channel_hash.color)
+                        .constrained()
+                        .with_width(theme.channel_hash.width)
+                        .aligned()
+                        .left(),
+                )
+                .with_child(
+                    Label::new(channel.name.clone(), theme.channel_name.text.clone())
+                        .contained()
+                        .with_style(theme.channel_name.container)
+                        .aligned()
+                        .left()
+                        .flex(1., true),
+                )
+                .with_children({
+                    let participants = self.channel_store.read(cx).channel_participants(channel_id);
+                    if !participants.is_empty() {
+                        let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
+
+                        Some(
+                            FacePile::new(theme.face_overlap)
+                                .with_children(
+                                    participants
+                                        .iter()
+                                        .filter_map(|user| {
+                                            Some(
+                                                Image::from_data(user.avatar.clone()?)
+                                                    .with_style(theme.channel_avatar),
+                                            )
+                                        })
+                                        .take(FACEPILE_LIMIT),
+                                )
+                                .with_children((extra_count > 0).then(|| {
+                                    Label::new(
+                                        format!("+{}", extra_count),
+                                        theme.extra_participant_label.text.clone(),
+                                    )
+                                    .contained()
+                                    .with_style(theme.extra_participant_label.container)
+                                })),
+                        )
+                    } else {
+                        None
+                    }
+                })
+                .align_children_center()
+                .styleable_component()
+                .disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
+                .with_id(channel_id as usize)
+                .with_style(theme.disclosure.clone())
+                .element()
+                .constrained()
+                .with_height(theme.row_height)
+                .contained()
+                .with_style(*theme.channel_row.style_for(is_selected || is_active, state))
+                .with_padding_left(
+                    theme.channel_row.default_style().padding.left
+                        + theme.channel_indent * depth as f32,
+                )
+        })
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            this.join_channel(channel_id, cx);
+        })
+        .on_click(MouseButton::Right, move |e, this, cx| {
+            this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .into_any()
+    }
+
+    fn render_channel_notes(
+        &self,
+        channel_id: ChannelId,
+        theme: &theme::CollabPanel,
+        is_selected: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum ChannelNotes {}
+        let host_avatar_width = theme
+            .contact_avatar
+            .width
+            .or(theme.contact_avatar.height)
+            .unwrap_or(0.);
+
+        MouseEventHandler::new::<ChannelNotes, _>(channel_id as usize, cx, |state, cx| {
+            let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+            let row = theme.project_row.in_state(is_selected).style_for(state);
+
+            Flex::<Self>::row()
+                .with_child(render_tree_branch(
+                    tree_branch,
+                    &row.name.text,
+                    true,
+                    vec2f(host_avatar_width, theme.row_height),
+                    cx.font_cache(),
+                ))
+                .with_child(
+                    Svg::new("icons/radix/file.svg")
+                        .with_color(theme.channel_hash.color)
+                        .constrained()
+                        .with_width(theme.channel_hash.width)
+                        .aligned()
+                        .left(),
+                )
+                .with_child(
+                    Label::new("notes", theme.channel_name.text.clone())
+                        .contained()
+                        .with_style(theme.channel_name.container)
+                        .aligned()
+                        .left()
+                        .flex(1., true),
+                )
+                .constrained()
+                .with_height(theme.row_height)
+                .contained()
+                .with_style(*theme.channel_row.style_for(is_selected, state))
+                .with_padding_left(theme.channel_row.default_style().padding.left)
+        })
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx);
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .into_any()
+    }
+
+    fn render_channel_invite(
+        channel: Arc<Channel>,
+        channel_store: ModelHandle<ChannelStore>,
+        theme: &theme::CollabPanel,
+        is_selected: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum Decline {}
+        enum Accept {}
+
+        let channel_id = channel.id;
+        let is_invite_pending = channel_store
+            .read(cx)
+            .has_pending_channel_invite_response(&channel);
+        let button_spacing = theme.contact_button_spacing;
+
+        Flex::row()
+            .with_child(
+                Svg::new("icons/hash.svg")
+                    .with_color(theme.channel_hash.color)
+                    .constrained()
+                    .with_width(theme.channel_hash.width)
+                    .aligned()
+                    .left(),
+            )
+            .with_child(
+                Label::new(channel.name.clone(), theme.contact_username.text.clone())
+                    .contained()
+                    .with_style(theme.contact_username.container)
+                    .aligned()
+                    .left()
+                    .flex(1., true),
+            )
+            .with_child(
+                MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
+                    let button_style = if is_invite_pending {
+                        &theme.disabled_button
+                    } else {
+                        theme.contact_button.style_for(mouse_state)
+                    };
+                    render_icon_button(button_style, "icons/x.svg").aligned()
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.respond_to_channel_invite(channel_id, false, cx);
+                })
+                .contained()
+                .with_margin_right(button_spacing),
+            )
+            .with_child(
+                MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
+                    let button_style = if is_invite_pending {
+                        &theme.disabled_button
+                    } else {
+                        theme.contact_button.style_for(mouse_state)
+                    };
+                    render_icon_button(button_style, "icons/check.svg")
+                        .aligned()
+                        .flex_float()
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.respond_to_channel_invite(channel_id, true, cx);
+                }),
+            )
+            .constrained()
+            .with_height(theme.row_height)
+            .contained()
+            .with_style(
+                *theme
+                    .contact_row
+                    .in_state(is_selected)
+                    .style_for(&mut Default::default()),
+            )
+            .with_padding_left(
+                theme.contact_row.default_style().padding.left + theme.channel_indent,
+            )
+            .into_any()
+    }
+
+    fn render_contact_request(
+        user: Arc<User>,
+        user_store: ModelHandle<UserStore>,
+        theme: &theme::CollabPanel,
+        is_incoming: bool,
+        is_selected: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum Decline {}
+        enum Accept {}
+        enum Cancel {}
+
+        let mut row = Flex::row()
+            .with_children(user.avatar.clone().map(|avatar| {
+                Image::from_data(avatar)
+                    .with_style(theme.contact_avatar)
+                    .aligned()
+                    .left()
+            }))
+            .with_child(
+                Label::new(
+                    user.github_login.clone(),
+                    theme.contact_username.text.clone(),
+                )
+                .contained()
+                .with_style(theme.contact_username.container)
+                .aligned()
+                .left()
+                .flex(1., true),
+            );
+
+        let user_id = user.id;
+        let github_login = user.github_login.clone();
+        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
+        let button_spacing = theme.contact_button_spacing;
+
+        if is_incoming {
+            row.add_child(
+                MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
+                    let button_style = if is_contact_request_pending {
+                        &theme.disabled_button
+                    } else {
+                        theme.contact_button.style_for(mouse_state)
+                    };
+                    render_icon_button(button_style, "icons/x.svg").aligned()
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.respond_to_contact_request(user_id, false, cx);
+                })
+                .contained()
+                .with_margin_right(button_spacing),
+            );
+
+            row.add_child(
+                MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
+                    let button_style = if is_contact_request_pending {
+                        &theme.disabled_button
+                    } else {
+                        theme.contact_button.style_for(mouse_state)
+                    };
+                    render_icon_button(button_style, "icons/check.svg")
+                        .aligned()
+                        .flex_float()
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.respond_to_contact_request(user_id, true, cx);
+                }),
+            );
+        } else {
+            row.add_child(
+                MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
+                    let button_style = if is_contact_request_pending {
+                        &theme.disabled_button
+                    } else {
+                        theme.contact_button.style_for(mouse_state)
+                    };
+                    render_icon_button(button_style, "icons/x.svg")
+                        .aligned()
+                        .flex_float()
+                })
+                .with_padding(Padding::uniform(2.))
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.remove_contact(user_id, &github_login, cx);
+                })
+                .flex_float(),
+            );
+        }
+
+        row.constrained()
+            .with_height(theme.row_height)
+            .contained()
+            .with_style(
+                *theme
+                    .contact_row
+                    .in_state(is_selected)
+                    .style_for(&mut Default::default()),
+            )
+            .into_any()
+    }
+
+    fn deploy_channel_context_menu(
+        &mut self,
+        position: Option<Vector2F>,
+        channel_id: u64,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.context_menu_on_selected = position.is_none();
+
+        self.context_menu.update(cx, |context_menu, cx| {
+            context_menu.set_position_mode(if self.context_menu_on_selected {
+                OverlayPositionMode::Local
+            } else {
+                OverlayPositionMode::Window
+            });
+
+            let expand_action_name = if self.is_channel_collapsed(channel_id) {
+                "Expand Subchannels"
+            } else {
+                "Collapse Subchannels"
+            };
+
+            let mut items = vec![
+                ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
+                ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
+            ];
+
+            if self.channel_store.read(cx).is_user_admin(channel_id) {
+                items.extend([
+                    ContextMenuItem::Separator,
+                    ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
+                    ContextMenuItem::action("Rename", RenameChannel { channel_id }),
+                    ContextMenuItem::Separator,
+                    ContextMenuItem::action("Invite Members", InviteMembers { channel_id }),
+                    ContextMenuItem::action("Manage Members", ManageMembers { channel_id }),
+                    ContextMenuItem::Separator,
+                    ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
+                ]);
+            }
+
+            context_menu.show(
+                position.unwrap_or_default(),
+                if self.context_menu_on_selected {
+                    gpui::elements::AnchorCorner::TopRight
+                } else {
+                    gpui::elements::AnchorCorner::BottomLeft
+                },
+                items,
+                cx,
+            );
+        });
+
+        cx.notify();
+    }
+
+    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+        if self.take_editing_state(cx) {
+            cx.focus(&self.filter_editor);
+        } else {
+            self.filter_editor.update(cx, |editor, cx| {
+                if editor.buffer().read(cx).len(cx) > 0 {
+                    editor.set_text("", cx);
+                }
+            });
+        }
+
+        self.update_entries(false, cx);
+    }
+
+    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+        let ix = self.selection.map_or(0, |ix| ix + 1);
+        if ix < self.entries.len() {
+            self.selection = Some(ix);
+        }
+
+        self.list_state.reset(self.entries.len());
+        if let Some(ix) = self.selection {
+            self.list_state.scroll_to(ListOffset {
+                item_ix: ix,
+                offset_in_item: 0.,
+            });
+        }
+        cx.notify();
+    }
+
+    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+        let ix = self.selection.take().unwrap_or(0);
+        if ix > 0 {
+            self.selection = Some(ix - 1);
+        }
+
+        self.list_state.reset(self.entries.len());
+        if let Some(ix) = self.selection {
+            self.list_state.scroll_to(ListOffset {
+                item_ix: ix,
+                offset_in_item: 0.,
+            });
+        }
+        cx.notify();
+    }
+
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        if self.confirm_channel_edit(cx) {
+            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(cx),
+                        Section::Channels => self.new_root_channel(cx),
+                        Section::Contacts => self.toggle_contact_finder(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, Some(self.project.clone()), cx);
+                        }
+                    }
+                    ListEntry::ParticipantProject {
+                        project_id,
+                        host_user_id,
+                        ..
+                    } => {
+                        if let Some(workspace) = self.workspace.upgrade(cx) {
+                            let app_state = workspace.read(cx).app_state().clone();
+                            workspace::join_remote_project(
+                                *project_id,
+                                *host_user_id,
+                                app_state,
+                                cx,
+                            )
+                            .detach_and_log_err(cx);
+                        }
+                    }
+                    ListEntry::ParticipantScreen { peer_id, .. } => {
+                        if let Some(workspace) = self.workspace.upgrade(cx) {
+                            workspace.update(cx, |workspace, cx| {
+                                workspace.open_shared_screen(*peer_id, cx)
+                            });
+                        }
+                    }
+                    ListEntry::Channel { channel, .. } => {
+                        self.join_channel(channel.id, cx);
+                    }
+                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
+                    _ => {}
+                }
+            }
+        }
+    }
+
+    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
+        if let Some(editing_state) = &mut self.channel_editing_state {
+            match editing_state {
+                ChannelEditingState::Create {
+                    parent_id,
+                    pending_name,
+                    ..
+                } => {
+                    if pending_name.is_some() {
+                        return false;
+                    }
+                    let channel_name = self.channel_name_editor.read(cx).text(cx);
+
+                    *pending_name = Some(channel_name.clone());
+
+                    self.channel_store
+                        .update(cx, |channel_store, cx| {
+                            channel_store.create_channel(&channel_name, *parent_id, cx)
+                        })
+                        .detach();
+                    cx.notify();
+                }
+                ChannelEditingState::Rename {
+                    channel_id,
+                    pending_name,
+                } => {
+                    if pending_name.is_some() {
+                        return false;
+                    }
+                    let channel_name = self.channel_name_editor.read(cx).text(cx);
+                    *pending_name = Some(channel_name.clone());
+
+                    self.channel_store
+                        .update(cx, |channel_store, cx| {
+                            channel_store.rename(*channel_id, &channel_name, cx)
+                        })
+                        .detach();
+                    cx.notify();
+                }
+            }
+            cx.focus_self();
+            true
+        } else {
+            false
+        }
+    }
+
+    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
+        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
+            self.collapsed_sections.remove(ix);
+        } else {
+            self.collapsed_sections.push(section);
+        }
+        self.update_entries(false, cx);
+    }
+
+    fn collapse_selected_channel(
+        &mut self,
+        _: &CollapseSelectedChannel,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+            return;
+        };
+
+        if self.is_channel_collapsed(channel_id) {
+            return;
+        }
+
+        self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+    }
+
+    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
+        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+            return;
+        };
+
+        if !self.is_channel_collapsed(channel_id) {
+            return;
+        }
+
+        self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+    }
+
+    fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
+        let channel_id = action.channel_id;
+
+        match self.collapsed_channels.binary_search(&channel_id) {
+            Ok(ix) => {
+                self.collapsed_channels.remove(ix);
+            }
+            Err(ix) => {
+                self.collapsed_channels.insert(ix, channel_id);
+            }
+        };
+        self.serialize(cx);
+        self.update_entries(true, cx);
+        cx.notify();
+        cx.focus_self();
+    }
+
+    fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
+        self.collapsed_channels.binary_search(&channel).is_ok()
+    }
+
+    fn leave_call(cx: &mut ViewContext<Self>) {
+        ActiveCall::global(cx)
+            .update(cx, |call, cx| call.hang_up(cx))
+            .detach_and_log_err(cx);
+    }
+
+    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            workspace.update(cx, |workspace, cx| {
+                workspace.toggle_modal(cx, |_, cx| {
+                    cx.add_view(|cx| {
+                        let mut finder = ContactFinder::new(self.user_store.clone(), cx);
+                        finder.set_query(self.filter_editor.read(cx).text(cx), cx);
+                        finder
+                    })
+                });
+            });
+        }
+    }
+
+    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
+        self.channel_editing_state = Some(ChannelEditingState::Create {
+            parent_id: None,
+            pending_name: None,
+        });
+        self.update_entries(false, cx);
+        self.select_channel_editor();
+        cx.focus(self.channel_name_editor.as_any());
+        cx.notify();
+    }
+
+    fn select_channel_editor(&mut self) {
+        self.selection = self.entries.iter().position(|entry| match entry {
+            ListEntry::ChannelEditor { .. } => true,
+            _ => false,
+        });
+    }
+
+    fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
+        self.collapsed_channels
+            .retain(|&channel| channel != action.channel_id);
+        self.channel_editing_state = Some(ChannelEditingState::Create {
+            parent_id: Some(action.channel_id),
+            pending_name: None,
+        });
+        self.update_entries(false, cx);
+        self.select_channel_editor();
+        cx.focus(self.channel_name_editor.as_any());
+        cx.notify();
+    }
+
+    fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
+        self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
+    }
+
+    fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
+        self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
+    }
+
+    fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
+        if let Some(channel) = self.selected_channel() {
+            self.remove_channel(channel.id, cx)
+        }
+    }
+
+    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+        if let Some(channel) = self.selected_channel() {
+            self.rename_channel(
+                &RenameChannel {
+                    channel_id: channel.id,
+                },
+                cx,
+            );
+        }
+    }
+
+    fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.read(cx);
+        if !channel_store.is_user_admin(action.channel_id) {
+            return;
+        }
+        if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
+            self.channel_editing_state = Some(ChannelEditingState::Rename {
+                channel_id: action.channel_id,
+                pending_name: None,
+            });
+            self.channel_name_editor.update(cx, |editor, cx| {
+                editor.set_text(channel.name.clone(), cx);
+                editor.select_all(&Default::default(), cx);
+            });
+            cx.focus(self.channel_name_editor.as_any());
+            self.update_entries(false, cx);
+            self.select_channel_editor();
+        }
+    }
+
+    fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext<Self>) {
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            let pane = workspace.read(cx).active_pane().clone();
+            let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx);
+            cx.spawn(|_, mut cx| async move {
+                let channel_view = channel_view.await?;
+                pane.update(&mut cx, |pane, cx| {
+                    pane.add_item(Box::new(channel_view), true, true, None, cx)
+                });
+                anyhow::Ok(())
+            })
+            .detach();
+        }
+    }
+
+    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
+        let Some(channel) = self.selected_channel() else {
+            return;
+        };
+
+        self.deploy_channel_context_menu(None, channel.id, cx);
+    }
+
+    fn selected_channel(&self) -> Option<&Arc<Channel>> {
+        self.selection
+            .and_then(|ix| self.entries.get(ix))
+            .and_then(|entry| match entry {
+                ListEntry::Channel { channel, .. } => Some(channel),
+                _ => None,
+            })
+    }
+
+    fn show_channel_modal(
+        &mut self,
+        channel_id: ChannelId,
+        mode: channel_modal::Mode,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let workspace = self.workspace.clone();
+        let user_store = self.user_store.clone();
+        let channel_store = self.channel_store.clone();
+        let members = self.channel_store.update(cx, |channel_store, cx| {
+            channel_store.get_channel_member_details(channel_id, cx)
+        });
+
+        cx.spawn(|_, mut cx| async move {
+            let members = members.await?;
+            workspace.update(&mut cx, |workspace, cx| {
+                workspace.toggle_modal(cx, |_, cx| {
+                    cx.add_view(|cx| {
+                        ChannelModal::new(
+                            user_store.clone(),
+                            channel_store.clone(),
+                            channel_id,
+                            mode,
+                            members,
+                            cx,
+                        )
+                    })
+                });
+            })
+        })
+        .detach();
+    }
+
+    fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
+        self.remove_channel(action.channel_id, cx)
+    }
+
+    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.clone();
+        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
+            let prompt_message = format!(
+                "Are you sure you want to remove the channel \"{}\"?",
+                channel.name
+            );
+            let mut answer =
+                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+            let window = cx.window();
+            cx.spawn(|this, mut cx| async move {
+                if answer.next().await == Some(0) {
+                    if let Err(e) = channel_store
+                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
+                        .await
+                    {
+                        window.prompt(
+                            PromptLevel::Info,
+                            &format!("Failed to remove channel: {}", e),
+                            &["Ok"],
+                            &mut cx,
+                        );
+                    }
+                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
+                }
+            })
+            .detach();
+        }
+    }
+
+    // Should move to the filter editor if clicking on it
+    // Should move selection to the channel editor if activating it
+
+    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
+        let user_store = self.user_store.clone();
+        let prompt_message = format!(
+            "Are you sure you want to remove \"{}\" from your contacts?",
+            github_login
+        );
+        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+        let window = cx.window();
+        cx.spawn(|_, mut cx| async move {
+            if answer.next().await == Some(0) {
+                if let Err(e) = user_store
+                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
+                    .await
+                {
+                    window.prompt(
+                        PromptLevel::Info,
+                        &format!("Failed to remove contact: {}", e),
+                        &["Ok"],
+                        &mut cx,
+                    );
+                }
+            }
+        })
+        .detach();
+    }
+
+    fn respond_to_contact_request(
+        &mut self,
+        user_id: u64,
+        accept: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.user_store
+            .update(cx, |store, cx| {
+                store.respond_to_contact_request(user_id, accept, cx)
+            })
+            .detach();
+    }
+
+    fn respond_to_channel_invite(
+        &mut self,
+        channel_id: u64,
+        accept: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let respond = self.channel_store.update(cx, |store, _| {
+            store.respond_to_channel_invite(channel_id, accept)
+        });
+        cx.foreground().spawn(respond).detach();
+    }
+
+    fn call(
+        &mut self,
+        recipient_user_id: u64,
+        initial_project: Option<ModelHandle<Project>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        ActiveCall::global(cx)
+            .update(cx, |call, cx| {
+                call.invite(recipient_user_id, initial_project, cx)
+            })
+            .detach_and_log_err(cx);
+    }
+
+    fn join_channel(&self, channel: u64, cx: &mut ViewContext<Self>) {
+        ActiveCall::global(cx)
+            .update(cx, |call, cx| call.join_channel(channel, cx))
+            .detach_and_log_err(cx);
+    }
+}
+
+fn render_tree_branch(
+    branch_style: theme::TreeBranch,
+    row_style: &TextStyle,
+    is_last: bool,
+    size: Vector2F,
+    font_cache: &FontCache,
+) -> gpui::elements::ConstrainedBox<CollabPanel> {
+    let line_height = row_style.line_height(font_cache);
+    let cap_height = row_style.cap_height(font_cache);
+    let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
+
+    Canvas::new(move |scene, bounds, _, _, _| {
+        scene.paint_layer(None, |scene| {
+            let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
+            let end_x = bounds.max_x();
+            let start_y = bounds.min_y();
+            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
+
+            scene.push_quad(gpui::Quad {
+                bounds: RectF::from_points(
+                    vec2f(start_x, start_y),
+                    vec2f(
+                        start_x + branch_style.width,
+                        if is_last { end_y } else { bounds.max_y() },
+                    ),
+                ),
+                background: Some(branch_style.color),
+                border: gpui::Border::default(),
+                corner_radii: (0.).into(),
+            });
+            scene.push_quad(gpui::Quad {
+                bounds: RectF::from_points(
+                    vec2f(start_x, end_y),
+                    vec2f(end_x, end_y + branch_style.width),
+                ),
+                background: Some(branch_style.color),
+                border: gpui::Border::default(),
+                corner_radii: (0.).into(),
+            });
+        })
+    })
+    .constrained()
+    .with_width(size.x())
+}
+
+impl View for CollabPanel {
+    fn ui_name() -> &'static str {
+        "CollabPanel"
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if !self.has_focus {
+            self.has_focus = true;
+            if !self.context_menu.is_focused(cx) {
+                if let Some(editing_state) = &self.channel_editing_state {
+                    if editing_state.pending_name().is_none() {
+                        cx.focus(&self.channel_name_editor);
+                    } else {
+                        cx.focus(&self.filter_editor);
+                    }
+                } else {
+                    cx.focus(&self.filter_editor);
+                }
+            }
+            cx.emit(Event::Focus);
+        }
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
+
+    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+        let theme = &theme::current(cx).collab_panel;
+
+        if self.user_store.read(cx).current_user().is_none() {
+            enum LogInButton {}
+
+            return Flex::column()
+                .with_child(
+                    MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
+                        let button = theme.log_in_button.style_for(state);
+                        Label::new("Sign in to collaborate", button.text.clone())
+                            .aligned()
+                            .left()
+                            .contained()
+                            .with_style(button.container)
+                    })
+                    .on_click(MouseButton::Left, |_, this, cx| {
+                        let client = this.client.clone();
+                        cx.spawn(|_, cx| async move {
+                            client.authenticate_and_connect(true, &cx).await.log_err();
+                        })
+                        .detach();
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand),
+                )
+                .contained()
+                .with_style(theme.container)
+                .into_any();
+        }
+
+        enum PanelFocus {}
+        MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
+            Stack::new()
+                .with_child(
+                    Flex::column()
+                        .with_child(
+                            Flex::row().with_child(
+                                ChildView::new(&self.filter_editor, cx)
+                                    .contained()
+                                    .with_style(theme.user_query_editor.container)
+                                    .flex(1.0, true),
+                            ),
+                        )
+                        .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
+                        .contained()
+                        .with_style(theme.container)
+                        .into_any(),
+                )
+                .with_children(
+                    (!self.context_menu_on_selected)
+                        .then(|| ChildView::new(&self.context_menu, cx)),
+                )
+                .into_any()
+        })
+        .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
+        .into_any_named("collab panel")
+    }
+}
+
+impl Panel for CollabPanel {
+    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+        match settings::get::<CollaborationPanelSettings>(cx).dock {
+            CollaborationPanelDockPosition::Left => DockPosition::Left,
+            CollaborationPanelDockPosition::Right => DockPosition::Right,
+        }
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<CollaborationPanelSettings>(
+            self.fs.clone(),
+            cx,
+            move |settings| {
+                let dock = match position {
+                    DockPosition::Left | DockPosition::Bottom => {
+                        CollaborationPanelDockPosition::Left
+                    }
+                    DockPosition::Right => CollaborationPanelDockPosition::Right,
+                };
+                settings.dock = Some(dock);
+            },
+        );
+    }
+
+    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+        settings::get::<CollaborationPanelSettings>(cx)
+            .button
+            .then(|| "icons/conversations.svg")
+    }
+
+    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+        (
+            "Collaboration Panel".to_string(),
+            Some(Box::new(ToggleFocus)),
+        )
+    }
+
+    fn should_change_position_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::DockPositionChanged)
+    }
+
+    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+        self.has_focus
+    }
+
+    fn is_focus_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Focus)
+    }
+}
+
+impl PartialEq for ListEntry {
+    fn eq(&self, other: &Self) -> bool {
+        match self {
+            ListEntry::Header(section_1, depth_1) => {
+                if let ListEntry::Header(section_2, depth_2) = other {
+                    return section_1 == section_2 && depth_1 == depth_2;
+                }
+            }
+            ListEntry::CallParticipant { user: user_1, .. } => {
+                if let ListEntry::CallParticipant { user: user_2, .. } = other {
+                    return user_1.id == user_2.id;
+                }
+            }
+            ListEntry::ParticipantProject {
+                project_id: project_id_1,
+                ..
+            } => {
+                if let ListEntry::ParticipantProject {
+                    project_id: project_id_2,
+                    ..
+                } = other
+                {
+                    return project_id_1 == project_id_2;
+                }
+            }
+            ListEntry::ParticipantScreen {
+                peer_id: peer_id_1, ..
+            } => {
+                if let ListEntry::ParticipantScreen {
+                    peer_id: peer_id_2, ..
+                } = other
+                {
+                    return peer_id_1 == peer_id_2;
+                }
+            }
+            ListEntry::Channel {
+                channel: channel_1,
+                depth: depth_1,
+            } => {
+                if let ListEntry::Channel {
+                    channel: channel_2,
+                    depth: depth_2,
+                } = other
+                {
+                    return channel_1.id == channel_2.id && depth_1 == depth_2;
+                }
+            }
+            ListEntry::ChannelNotes { channel_id } => {
+                if let ListEntry::ChannelNotes {
+                    channel_id: other_id,
+                } = other
+                {
+                    return channel_id == other_id;
+                }
+            }
+            ListEntry::ChannelInvite(channel_1) => {
+                if let ListEntry::ChannelInvite(channel_2) = other {
+                    return channel_1.id == channel_2.id;
+                }
+            }
+            ListEntry::IncomingRequest(user_1) => {
+                if let ListEntry::IncomingRequest(user_2) = other {
+                    return user_1.id == user_2.id;
+                }
+            }
+            ListEntry::OutgoingRequest(user_1) => {
+                if let ListEntry::OutgoingRequest(user_2) = other {
+                    return user_1.id == user_2.id;
+                }
+            }
+            ListEntry::Contact {
+                contact: contact_1, ..
+            } => {
+                if let ListEntry::Contact {
+                    contact: contact_2, ..
+                } = other
+                {
+                    return contact_1.user.id == contact_2.user.id;
+                }
+            }
+            ListEntry::ChannelEditor { depth } => {
+                if let ListEntry::ChannelEditor { depth: other_depth } = other {
+                    return depth == other_depth;
+                }
+            }
+            ListEntry::ContactPlaceholder => {
+                if let ListEntry::ContactPlaceholder = other {
+                    return true;
+                }
+            }
+        }
+        false
+    }
+}
+
+fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
+    Svg::new(svg_path)
+        .with_color(style.color)
+        .constrained()
+        .with_width(style.icon_width)
+        .aligned()
+        .constrained()
+        .with_width(style.button_width)
+        .with_height(style.button_width)
+        .contained()
+        .with_style(style.container)
+}

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

@@ -0,0 +1,613 @@
+use channel::{ChannelId, ChannelMembership, ChannelStore};
+use client::{proto, User, UserId, UserStore};
+use context_menu::{ContextMenu, ContextMenuItem};
+use fuzzy::{match_strings, StringMatchCandidate};
+use gpui::{
+    actions,
+    elements::*,
+    platform::{CursorStyle, MouseButton},
+    AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use std::sync::Arc;
+use util::TryFutureExt;
+use workspace::Modal;
+
+actions!(
+    channel_modal,
+    [
+        SelectNextControl,
+        ToggleMode,
+        ToggleMemberAdmin,
+        RemoveMember
+    ]
+);
+
+pub fn init(cx: &mut AppContext) {
+    Picker::<ChannelModalDelegate>::init(cx);
+    cx.add_action(ChannelModal::toggle_mode);
+    cx.add_action(ChannelModal::toggle_member_admin);
+    cx.add_action(ChannelModal::remove_member);
+    cx.add_action(ChannelModal::dismiss);
+}
+
+pub struct ChannelModal {
+    picker: ViewHandle<Picker<ChannelModalDelegate>>,
+    channel_store: ModelHandle<ChannelStore>,
+    channel_id: ChannelId,
+    has_focus: bool,
+}
+
+impl ChannelModal {
+    pub fn new(
+        user_store: ModelHandle<UserStore>,
+        channel_store: ModelHandle<ChannelStore>,
+        channel_id: ChannelId,
+        mode: Mode,
+        members: Vec<ChannelMembership>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
+        let picker = cx.add_view(|cx| {
+            Picker::new(
+                ChannelModalDelegate {
+                    matching_users: Vec::new(),
+                    matching_member_indices: Vec::new(),
+                    selected_index: 0,
+                    user_store: user_store.clone(),
+                    channel_store: channel_store.clone(),
+                    channel_id,
+                    match_candidates: Vec::new(),
+                    members,
+                    mode,
+                    context_menu: cx.add_view(|cx| {
+                        let mut menu = ContextMenu::new(cx.view_id(), cx);
+                        menu.set_position_mode(OverlayPositionMode::Local);
+                        menu
+                    }),
+                },
+                cx,
+            )
+            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
+        });
+
+        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
+
+        let has_focus = picker.read(cx).has_focus();
+
+        Self {
+            picker,
+            channel_store,
+            channel_id,
+            has_focus,
+        }
+    }
+
+    fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
+        let mode = match self.picker.read(cx).delegate().mode {
+            Mode::ManageMembers => Mode::InviteMembers,
+            Mode::InviteMembers => Mode::ManageMembers,
+        };
+        self.set_mode(mode, cx);
+    }
+
+    fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.clone();
+        let channel_id = self.channel_id;
+        cx.spawn(|this, mut cx| async move {
+            if mode == Mode::ManageMembers {
+                let members = channel_store
+                    .update(&mut cx, |channel_store, cx| {
+                        channel_store.get_channel_member_details(channel_id, cx)
+                    })
+                    .await?;
+                this.update(&mut cx, |this, cx| {
+                    this.picker
+                        .update(cx, |picker, _| picker.delegate_mut().members = members);
+                })?;
+            }
+
+            this.update(&mut cx, |this, cx| {
+                this.picker.update(cx, |picker, cx| {
+                    let delegate = picker.delegate_mut();
+                    delegate.mode = mode;
+                    delegate.selected_index = 0;
+                    picker.set_query("", cx);
+                    picker.update_matches(picker.query(cx), cx);
+                    cx.notify()
+                });
+                cx.notify()
+            })
+        })
+        .detach();
+    }
+
+    fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
+        self.picker.update(cx, |picker, cx| {
+            picker.delegate_mut().toggle_selected_member_admin(cx);
+        })
+    }
+
+    fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
+        self.picker.update(cx, |picker, cx| {
+            picker.delegate_mut().remove_selected_member(cx);
+        });
+    }
+
+    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        cx.emit(PickerEvent::Dismiss);
+    }
+}
+
+impl Entity for ChannelModal {
+    type Event = PickerEvent;
+}
+
+impl View for ChannelModal {
+    fn ui_name() -> &'static str {
+        "ChannelModal"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let theme = &theme::current(cx).collab_panel.tabbed_modal;
+
+        let mode = self.picker.read(cx).delegate().mode;
+        let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
+            return Empty::new().into_any();
+        };
+
+        enum InviteMembers {}
+        enum ManageMembers {}
+
+        fn render_mode_button<T: 'static>(
+            mode: Mode,
+            text: &'static str,
+            current_mode: Mode,
+            theme: &theme::TabbedModal,
+            cx: &mut ViewContext<ChannelModal>,
+        ) -> AnyElement<ChannelModal> {
+            let active = mode == current_mode;
+            MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
+                let contained_text = theme.tab_button.style_for(active, state);
+                Label::new(text, contained_text.text.clone())
+                    .contained()
+                    .with_style(contained_text.container.clone())
+            })
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                if !active {
+                    this.set_mode(mode, cx);
+                }
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .into_any()
+        }
+
+        Flex::column()
+            .with_child(
+                Flex::column()
+                    .with_child(
+                        Label::new(format!("#{}", channel.name), theme.title.text.clone())
+                            .contained()
+                            .with_style(theme.title.container.clone()),
+                    )
+                    .with_child(Flex::row().with_children([
+                        render_mode_button::<InviteMembers>(
+                            Mode::InviteMembers,
+                            "Invite members",
+                            mode,
+                            theme,
+                            cx,
+                        ),
+                        render_mode_button::<ManageMembers>(
+                            Mode::ManageMembers,
+                            "Manage members",
+                            mode,
+                            theme,
+                            cx,
+                        ),
+                    ]))
+                    .expanded()
+                    .contained()
+                    .with_style(theme.header),
+            )
+            .with_child(
+                ChildView::new(&self.picker, cx)
+                    .contained()
+                    .with_style(theme.body),
+            )
+            .constrained()
+            .with_max_height(theme.max_height)
+            .with_max_width(theme.max_width)
+            .contained()
+            .with_style(theme.modal)
+            .into_any()
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
+        if cx.is_self_focused() {
+            cx.focus(&self.picker)
+        }
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
+}
+
+impl Modal for ChannelModal {
+    fn has_focus(&self) -> bool {
+        self.has_focus
+    }
+
+    fn dismiss_on_event(event: &Self::Event) -> bool {
+        match event {
+            PickerEvent::Dismiss => true,
+        }
+    }
+}
+
+#[derive(Copy, Clone, PartialEq)]
+pub enum Mode {
+    ManageMembers,
+    InviteMembers,
+}
+
+pub struct ChannelModalDelegate {
+    matching_users: Vec<Arc<User>>,
+    matching_member_indices: Vec<usize>,
+    user_store: ModelHandle<UserStore>,
+    channel_store: ModelHandle<ChannelStore>,
+    channel_id: ChannelId,
+    selected_index: usize,
+    mode: Mode,
+    match_candidates: Vec<StringMatchCandidate>,
+    members: Vec<ChannelMembership>,
+    context_menu: ViewHandle<ContextMenu>,
+}
+
+impl PickerDelegate for ChannelModalDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search collaborator by username...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        match self.mode {
+            Mode::ManageMembers => self.matching_member_indices.len(),
+            Mode::InviteMembers => self.matching_users.len(),
+        }
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+        match self.mode {
+            Mode::ManageMembers => {
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(self.members.iter().enumerate().map(|(id, member)| {
+                        StringMatchCandidate {
+                            id,
+                            string: member.user.github_login.clone(),
+                            char_bag: member.user.github_login.chars().collect(),
+                        }
+                    }));
+
+                let matches = cx.background().block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    cx.background().clone(),
+                ));
+
+                cx.spawn(|picker, mut cx| async move {
+                    picker
+                        .update(&mut cx, |picker, cx| {
+                            let delegate = picker.delegate_mut();
+                            delegate.matching_member_indices.clear();
+                            delegate
+                                .matching_member_indices
+                                .extend(matches.into_iter().map(|m| m.candidate_id));
+                            cx.notify();
+                        })
+                        .ok();
+                })
+            }
+            Mode::InviteMembers => {
+                let search_users = self
+                    .user_store
+                    .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
+                cx.spawn(|picker, mut cx| async move {
+                    async {
+                        let users = search_users.await?;
+                        picker.update(&mut cx, |picker, cx| {
+                            let delegate = picker.delegate_mut();
+                            delegate.matching_users = users;
+                            cx.notify();
+                        })?;
+                        anyhow::Ok(())
+                    }
+                    .log_err()
+                    .await;
+                })
+            }
+        }
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+        if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
+            match self.mode {
+                Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
+                Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
+                    Some(proto::channel_member::Kind::Invitee) => {
+                        self.remove_selected_member(cx);
+                    }
+                    Some(proto::channel_member::Kind::AncestorMember) | None => {
+                        self.invite_member(selected_user, cx)
+                    }
+                    Some(proto::channel_member::Kind::Member) => {}
+                },
+            }
+        }
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        cx.emit(PickerEvent::Dismiss);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        mouse_state: &mut MouseState,
+        selected: bool,
+        cx: &gpui::AppContext,
+    ) -> AnyElement<Picker<Self>> {
+        let full_theme = &theme::current(cx);
+        let theme = &full_theme.collab_panel.channel_modal;
+        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+        let (user, admin) = self.user_at_index(ix).unwrap();
+        let request_status = self.member_status(user.id, cx);
+
+        let style = tabbed_modal
+            .picker
+            .item
+            .in_state(selected)
+            .style_for(mouse_state);
+
+        let in_manage = matches!(self.mode, Mode::ManageMembers);
+
+        let mut result = Flex::row()
+            .with_children(user.avatar.clone().map(|avatar| {
+                Image::from_data(avatar)
+                    .with_style(theme.contact_avatar)
+                    .aligned()
+                    .left()
+            }))
+            .with_child(
+                Label::new(user.github_login.clone(), style.label.clone())
+                    .contained()
+                    .with_style(theme.contact_username)
+                    .aligned()
+                    .left(),
+            )
+            .with_children({
+                (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
+                    || {
+                        Label::new("Invited", theme.member_tag.text.clone())
+                            .contained()
+                            .with_style(theme.member_tag.container)
+                            .aligned()
+                            .left()
+                    },
+                )
+            })
+            .with_children(admin.and_then(|admin| {
+                (in_manage && admin).then(|| {
+                    Label::new("Admin", theme.member_tag.text.clone())
+                        .contained()
+                        .with_style(theme.member_tag.container)
+                        .aligned()
+                        .left()
+                })
+            }))
+            .with_children({
+                let svg = match self.mode {
+                    Mode::ManageMembers => Some(
+                        Svg::new("icons/ellipsis.svg")
+                            .with_color(theme.member_icon.color)
+                            .constrained()
+                            .with_width(theme.member_icon.icon_width)
+                            .aligned()
+                            .constrained()
+                            .with_width(theme.member_icon.button_width)
+                            .with_height(theme.member_icon.button_width)
+                            .contained()
+                            .with_style(theme.member_icon.container),
+                    ),
+                    Mode::InviteMembers => match request_status {
+                        Some(proto::channel_member::Kind::Member) => Some(
+                            Svg::new("icons/check.svg")
+                                .with_color(theme.member_icon.color)
+                                .constrained()
+                                .with_width(theme.member_icon.icon_width)
+                                .aligned()
+                                .constrained()
+                                .with_width(theme.member_icon.button_width)
+                                .with_height(theme.member_icon.button_width)
+                                .contained()
+                                .with_style(theme.member_icon.container),
+                        ),
+                        Some(proto::channel_member::Kind::Invitee) => Some(
+                            Svg::new("icons/check.svg")
+                                .with_color(theme.invitee_icon.color)
+                                .constrained()
+                                .with_width(theme.invitee_icon.icon_width)
+                                .aligned()
+                                .constrained()
+                                .with_width(theme.invitee_icon.button_width)
+                                .with_height(theme.invitee_icon.button_width)
+                                .contained()
+                                .with_style(theme.invitee_icon.container),
+                        ),
+                        Some(proto::channel_member::Kind::AncestorMember) | None => None,
+                    },
+                };
+
+                svg.map(|svg| svg.aligned().flex_float().into_any())
+            })
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_height(tabbed_modal.row_height)
+            .into_any();
+
+        if selected {
+            result = Stack::new()
+                .with_child(result)
+                .with_child(
+                    ChildView::new(&self.context_menu, cx)
+                        .aligned()
+                        .top()
+                        .right(),
+                )
+                .into_any();
+        }
+
+        result
+    }
+}
+
+impl ChannelModalDelegate {
+    fn member_status(
+        &self,
+        user_id: UserId,
+        cx: &AppContext,
+    ) -> Option<proto::channel_member::Kind> {
+        self.members
+            .iter()
+            .find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
+            .or_else(|| {
+                self.channel_store
+                    .read(cx)
+                    .has_pending_channel_invite(self.channel_id, user_id)
+                    .then_some(proto::channel_member::Kind::Invitee)
+            })
+    }
+
+    fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
+        match self.mode {
+            Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
+                let channel_membership = self.members.get(*ix)?;
+                Some((
+                    channel_membership.user.clone(),
+                    Some(channel_membership.admin),
+                ))
+            }),
+            Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
+        }
+    }
+
+    fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
+        let (user, admin) = self.user_at_index(self.selected_index)?;
+        let admin = !admin.unwrap_or(false);
+        let update = self.channel_store.update(cx, |store, cx| {
+            store.set_member_admin(self.channel_id, user.id, admin, cx)
+        });
+        cx.spawn(|picker, mut cx| async move {
+            update.await?;
+            picker.update(&mut cx, |picker, cx| {
+                let this = picker.delegate_mut();
+                if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
+                    member.admin = admin;
+                }
+                cx.focus_self();
+                cx.notify();
+            })
+        })
+        .detach_and_log_err(cx);
+        Some(())
+    }
+
+    fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
+        let (user, _) = self.user_at_index(self.selected_index)?;
+        let user_id = user.id;
+        let update = self.channel_store.update(cx, |store, cx| {
+            store.remove_member(self.channel_id, user_id, cx)
+        });
+        cx.spawn(|picker, mut cx| async move {
+            update.await?;
+            picker.update(&mut cx, |picker, cx| {
+                let this = picker.delegate_mut();
+                if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
+                    this.members.remove(ix);
+                    this.matching_member_indices.retain_mut(|member_ix| {
+                        if *member_ix == ix {
+                            return false;
+                        } else if *member_ix > ix {
+                            *member_ix -= 1;
+                        }
+                        true
+                    })
+                }
+
+                this.selected_index = this
+                    .selected_index
+                    .min(this.matching_member_indices.len().saturating_sub(1));
+
+                cx.focus_self();
+                cx.notify();
+            })
+        })
+        .detach_and_log_err(cx);
+        Some(())
+    }
+
+    fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
+        let invite_member = self.channel_store.update(cx, |store, cx| {
+            store.invite_member(self.channel_id, user.id, false, cx)
+        });
+
+        cx.spawn(|this, mut cx| async move {
+            invite_member.await?;
+
+            this.update(&mut cx, |this, cx| {
+                this.delegate_mut().members.push(ChannelMembership {
+                    user,
+                    kind: proto::channel_member::Kind::Invitee,
+                    admin: false,
+                });
+                cx.notify();
+            })
+        })
+        .detach_and_log_err(cx);
+    }
+
+    fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
+        self.context_menu.update(cx, |context_menu, cx| {
+            context_menu.show(
+                Default::default(),
+                AnchorCorner::TopRight,
+                vec![
+                    ContextMenuItem::action("Remove", RemoveMember),
+                    ContextMenuItem::action(
+                        if user_is_admin {
+                            "Make non-admin"
+                        } else {
+                            "Make admin"
+                        },
+                        ToggleMemberAdmin,
+                    ),
+                ],
+                cx,
+            )
+        })
+    }
+}

crates/collab_ui/src/contact_finder.rs → crates/collab_ui/src/collab_panel/contact_finder.rs 🔗

@@ -1,28 +1,132 @@
 use client::{ContactRequestStatus, User, UserStore};
-use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
+use gpui::{
+    elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+};
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::sync::Arc;
 use util::TryFutureExt;
+use workspace::Modal;
 
 pub fn init(cx: &mut AppContext) {
     Picker::<ContactFinderDelegate>::init(cx);
+    cx.add_action(ContactFinder::dismiss)
 }
 
-pub type ContactFinder = Picker<ContactFinderDelegate>;
+pub struct ContactFinder {
+    picker: ViewHandle<Picker<ContactFinderDelegate>>,
+    has_focus: bool,
+}
 
-pub fn build_contact_finder(
-    user_store: ModelHandle<UserStore>,
-    cx: &mut ViewContext<ContactFinder>,
-) -> ContactFinder {
-    Picker::new(
-        ContactFinderDelegate {
-            user_store,
-            potential_contacts: Arc::from([]),
-            selected_index: 0,
-        },
-        cx,
-    )
-    .with_theme(|theme| theme.contact_finder.picker.clone())
+impl ContactFinder {
+    pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.add_view(|cx| {
+            Picker::new(
+                ContactFinderDelegate {
+                    user_store,
+                    potential_contacts: Arc::from([]),
+                    selected_index: 0,
+                },
+                cx,
+            )
+            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
+        });
+
+        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
+
+        Self {
+            picker,
+            has_focus: false,
+        }
+    }
+
+    pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
+        self.picker.update(cx, |picker, cx| {
+            picker.set_query(query, cx);
+        });
+    }
+
+    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        cx.emit(PickerEvent::Dismiss);
+    }
+}
+
+impl Entity for ContactFinder {
+    type Event = PickerEvent;
+}
+
+impl View for ContactFinder {
+    fn ui_name() -> &'static str {
+        "ContactFinder"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let full_theme = &theme::current(cx);
+        let theme = &full_theme.collab_panel.tabbed_modal;
+
+        fn render_mode_button(
+            text: &'static str,
+            theme: &theme::TabbedModal,
+            _cx: &mut ViewContext<ContactFinder>,
+        ) -> AnyElement<ContactFinder> {
+            let contained_text = &theme.tab_button.active_state().default;
+            Label::new(text, contained_text.text.clone())
+                .contained()
+                .with_style(contained_text.container.clone())
+                .into_any()
+        }
+
+        Flex::column()
+            .with_child(
+                Flex::column()
+                    .with_child(
+                        Label::new("Contacts", theme.title.text.clone())
+                            .contained()
+                            .with_style(theme.title.container.clone()),
+                    )
+                    .with_child(Flex::row().with_children([render_mode_button(
+                        "Invite new contacts",
+                        &theme,
+                        cx,
+                    )]))
+                    .expanded()
+                    .contained()
+                    .with_style(theme.header),
+            )
+            .with_child(
+                ChildView::new(&self.picker, cx)
+                    .contained()
+                    .with_style(theme.body),
+            )
+            .constrained()
+            .with_max_height(theme.max_height)
+            .with_max_width(theme.max_width)
+            .contained()
+            .with_style(theme.modal)
+            .into_any()
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
+        if cx.is_self_focused() {
+            cx.focus(&self.picker)
+        }
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
+}
+
+impl Modal for ContactFinder {
+    fn has_focus(&self) -> bool {
+        self.has_focus
+    }
+
+    fn dismiss_on_event(event: &Self::Event) -> bool {
+        match event {
+            PickerEvent::Dismiss => true,
+        }
+    }
 }
 
 pub struct ContactFinderDelegate {
@@ -97,7 +201,9 @@ impl PickerDelegate for ContactFinderDelegate {
         selected: bool,
         cx: &gpui::AppContext,
     ) -> AnyElement<Picker<Self>> {
-        let theme = &theme::current(cx);
+        let full_theme = &theme::current(cx);
+        let theme = &full_theme.collab_panel.contact_finder;
+        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
         let user = &self.potential_contacts[ix];
         let request_status = self.user_store.read(cx).contact_request_status(user);
 
@@ -109,12 +215,11 @@ impl PickerDelegate for ContactFinderDelegate {
             ContactRequestStatus::RequestAccepted => None,
         };
         let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
-            &theme.contact_finder.disabled_contact_button
+            &theme.disabled_contact_button
         } else {
-            &theme.contact_finder.contact_button
+            &theme.contact_button
         };
-        let style = theme
-            .contact_finder
+        let style = tabbed_modal
             .picker
             .item
             .in_state(selected)
@@ -122,14 +227,14 @@ impl PickerDelegate for ContactFinderDelegate {
         Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
                 Image::from_data(avatar)
-                    .with_style(theme.contact_finder.contact_avatar)
+                    .with_style(theme.contact_avatar)
                     .aligned()
                     .left()
             }))
             .with_child(
                 Label::new(user.github_login.clone(), style.label.clone())
                     .contained()
-                    .with_style(theme.contact_finder.contact_username)
+                    .with_style(theme.contact_username)
                     .aligned()
                     .left(),
             )
@@ -150,7 +255,7 @@ impl PickerDelegate for ContactFinderDelegate {
             .contained()
             .with_style(style.container)
             .constrained()
-            .with_height(theme.contact_finder.row_height)
+            .with_height(tabbed_modal.row_height)
             .into_any()
     }
 }

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

@@ -0,0 +1,39 @@
+use anyhow;
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum CollaborationPanelDockPosition {
+    Left,
+    Right,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct CollaborationPanelSettings {
+    pub button: bool,
+    pub dock: CollaborationPanelDockPosition,
+    pub default_width: f32,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct CollaborationPanelSettingsContent {
+    pub button: Option<bool>,
+    pub dock: Option<CollaborationPanelDockPosition>,
+    pub default_width: Option<f32>,
+}
+
+impl Setting for CollaborationPanelSettings {
+    const KEY: Option<&'static str> = Some("collaboration_panel");
+
+    type FileContent = CollaborationPanelSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -1,12 +1,10 @@
 use crate::{
-    contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
-    toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
-    ToggleScreenSharing,
+    contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
+    toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
 };
 use call::{ActiveCall, ParticipantLocation, Room};
 use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
 use clock::ReplicaId;
-use contacts_popover::ContactsPopover;
 use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     actions,
@@ -33,7 +31,6 @@ const MAX_BRANCH_NAME_LENGTH: usize = 40;
 actions!(
     collab,
     [
-        ToggleContactsMenu,
         ToggleUserMenu,
         ToggleProjectMenu,
         SwitchBranch,
@@ -43,7 +40,6 @@ actions!(
 );
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
     cx.add_action(CollabTitlebarItem::share_project);
     cx.add_action(CollabTitlebarItem::unshare_project);
     cx.add_action(CollabTitlebarItem::toggle_user_menu);
@@ -56,7 +52,6 @@ pub struct CollabTitlebarItem {
     user_store: ModelHandle<UserStore>,
     client: Arc<Client>,
     workspace: WeakViewHandle<Workspace>,
-    contacts_popover: Option<ViewHandle<ContactsPopover>>,
     branch_popover: Option<ViewHandle<BranchList>>,
     project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
     user_menu: ViewHandle<ContextMenu>,
@@ -95,7 +90,7 @@ impl View for CollabTitlebarItem {
             right_container
                 .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
             right_container.add_child(self.render_leave_call(&theme, cx));
-            let muted = room.read(cx).is_muted();
+            let muted = room.read(cx).is_muted(cx);
             let speaking = room.read(cx).is_speaking();
             left_container.add_child(
                 self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
@@ -109,7 +104,6 @@ impl View for CollabTitlebarItem {
         let status = workspace.read(cx).client().status();
         let status = &*status.borrow();
         if matches!(status, client::Status::Connected { .. }) {
-            right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
             let avatar = user.as_ref().and_then(|user| user.avatar.clone());
             right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
         } else {
@@ -184,7 +178,6 @@ impl CollabTitlebarItem {
             project,
             user_store,
             client,
-            contacts_popover: None,
             user_menu: cx.add_view(|cx| {
                 let view_id = cx.view_id();
                 let mut menu = ContextMenu::new(view_id, cx);
@@ -220,7 +213,6 @@ impl CollabTitlebarItem {
             .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
         let project_style = theme.titlebar.project_menu_button.clone();
         let git_style = theme.titlebar.git_menu_button.clone();
-        let divider_style = theme.titlebar.project_name_divider.clone();
         let item_spacing = theme.titlebar.item_spacing;
 
         let mut ret = Flex::row().with_child(
@@ -255,49 +247,37 @@ impl CollabTitlebarItem {
         );
         if let Some(git_branch) = branch_prepended {
             ret = ret.with_child(
-                Flex::row()
-                    .with_child(
-                        Label::new("/", divider_style.text)
-                            .contained()
-                            .with_style(divider_style.container)
-                            .aligned()
-                            .left(),
-                    )
-                    .with_child(
-                        Stack::new()
-                            .with_child(
-                                MouseEventHandler::new::<ToggleVcsMenu, _>(
-                                    0,
-                                    cx,
-                                    |mouse_state, cx| {
-                                        enum BranchPopoverTooltip {}
-                                        let style = git_style
-                                            .in_state(self.branch_popover.is_some())
-                                            .style_for(mouse_state);
-                                        Label::new(git_branch, style.text.clone())
-                                            .contained()
-                                            .with_style(style.container.clone())
-                                            .with_margin_right(item_spacing)
-                                            .aligned()
-                                            .left()
-                                            .with_tooltip::<BranchPopoverTooltip>(
-                                                0,
-                                                "Recent branches",
-                                                Some(Box::new(ToggleVcsMenu)),
-                                                theme.tooltip.clone(),
-                                                cx,
-                                            )
-                                            .into_any_named("title-project-branch")
-                                    },
-                                )
-                                .with_cursor_style(CursorStyle::PointingHand)
-                                .on_down(MouseButton::Left, move |_, this, cx| {
-                                    this.toggle_vcs_menu(&Default::default(), cx)
-                                })
-                                .on_click(MouseButton::Left, move |_, _, _| {}),
-                            )
-                            .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
-                    ),
+                Flex::row().with_child(
+                    Stack::new()
+                        .with_child(
+                            MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
+                                enum BranchPopoverTooltip {}
+                                let style = git_style
+                                    .in_state(self.branch_popover.is_some())
+                                    .style_for(mouse_state);
+                                Label::new(git_branch, style.text.clone())
+                                    .contained()
+                                    .with_style(style.container.clone())
+                                    .with_margin_right(item_spacing)
+                                    .aligned()
+                                    .left()
+                                    .with_tooltip::<BranchPopoverTooltip>(
+                                        0,
+                                        "Recent branches",
+                                        Some(Box::new(ToggleVcsMenu)),
+                                        theme.tooltip.clone(),
+                                        cx,
+                                    )
+                                    .into_any_named("title-project-branch")
+                            })
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .on_down(MouseButton::Left, move |_, this, cx| {
+                                this.toggle_vcs_menu(&Default::default(), cx)
+                            })
+                            .on_click(MouseButton::Left, move |_, _, _| {}),
+                        )
+                        .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
+                ),
             )
         }
         ret.into_any()
@@ -315,9 +295,6 @@ impl CollabTitlebarItem {
     }
 
     fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
-        if ActiveCall::global(cx).read(cx).room().is_none() {
-            self.contacts_popover = None;
-        }
         cx.notify();
     }
 
@@ -337,32 +314,6 @@ impl CollabTitlebarItem {
             .log_err();
     }
 
-    pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
-        if self.contacts_popover.take().is_none() {
-            let view = cx.add_view(|cx| {
-                ContactsPopover::new(
-                    self.project.clone(),
-                    self.user_store.clone(),
-                    self.workspace.clone(),
-                    cx,
-                )
-            });
-            cx.subscribe(&view, |this, _, event, cx| {
-                match event {
-                    contacts_popover::Event::Dismissed => {
-                        this.contacts_popover = None;
-                    }
-                }
-
-                cx.notify();
-            })
-            .detach();
-            self.contacts_popover = Some(view);
-        }
-
-        cx.notify();
-    }
-
     pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
         self.user_menu.update(cx, |user_menu, cx| {
             let items = if let Some(_) = self.user_store.read(cx).current_user() {
@@ -390,6 +341,7 @@ impl CollabTitlebarItem {
             user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
         });
     }
+
     fn render_branches_popover_host<'a>(
         &'a self,
         _theme: &'a theme::Titlebar,
@@ -403,8 +355,8 @@ impl CollabTitlebarItem {
                     .flex(1., true)
                     .contained()
                     .constrained()
-                    .with_width(theme.contacts_popover.width)
-                    .with_height(theme.contacts_popover.height)
+                    .with_width(theme.titlebar.menu.width)
+                    .with_height(theme.titlebar.menu.height)
             })
             .on_click(MouseButton::Left, |_, _, _| {})
             .on_down_out(MouseButton::Left, move |_, this, cx| {
@@ -425,6 +377,7 @@ impl CollabTitlebarItem {
                 .into_any()
         })
     }
+
     fn render_project_popover_host<'a>(
         &'a self,
         _theme: &'a theme::Titlebar,
@@ -438,8 +391,8 @@ impl CollabTitlebarItem {
                     .flex(1., true)
                     .contained()
                     .constrained()
-                    .with_width(theme.contacts_popover.width)
-                    .with_height(theme.contacts_popover.height)
+                    .with_width(theme.titlebar.menu.width)
+                    .with_height(theme.titlebar.menu.height)
             })
             .on_click(MouseButton::Left, |_, _, _| {})
             .on_down_out(MouseButton::Left, move |_, this, cx| {
@@ -459,6 +412,7 @@ impl CollabTitlebarItem {
                 .into_any()
         })
     }
+
     pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
         if self.branch_popover.take().is_none() {
             if let Some(workspace) = self.workspace.upgrade(cx) {
@@ -519,79 +473,7 @@ impl CollabTitlebarItem {
         }
         cx.notify();
     }
-    fn render_toggle_contacts_button(
-        &self,
-        theme: &Theme,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let titlebar = &theme.titlebar;
 
-        let badge = if self
-            .user_store
-            .read(cx)
-            .incoming_contact_requests()
-            .is_empty()
-        {
-            None
-        } else {
-            Some(
-                Empty::new()
-                    .collapsed()
-                    .contained()
-                    .with_style(titlebar.toggle_contacts_badge)
-                    .contained()
-                    .with_margin_left(
-                        titlebar
-                            .toggle_contacts_button
-                            .inactive_state()
-                            .default
-                            .icon_width,
-                    )
-                    .with_margin_top(
-                        titlebar
-                            .toggle_contacts_button
-                            .inactive_state()
-                            .default
-                            .icon_width,
-                    )
-                    .aligned(),
-            )
-        };
-
-        Stack::new()
-            .with_child(
-                MouseEventHandler::new::<ToggleContactsMenu, _>(0, cx, |state, _| {
-                    let style = titlebar
-                        .toggle_contacts_button
-                        .in_state(self.contacts_popover.is_some())
-                        .style_for(state);
-                    Svg::new("icons/radix/person.svg")
-                        .with_color(style.color)
-                        .constrained()
-                        .with_width(style.icon_width)
-                        .aligned()
-                        .constrained()
-                        .with_width(style.button_width)
-                        .with_height(style.button_width)
-                        .contained()
-                        .with_style(style.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.toggle_contacts_popover(&Default::default(), cx)
-                })
-                .with_tooltip::<ToggleContactsMenu>(
-                    0,
-                    "Show contacts menu",
-                    Some(Box::new(ToggleContactsMenu)),
-                    theme.tooltip.clone(),
-                    cx,
-                ),
-            )
-            .with_children(badge)
-            .with_children(self.render_contacts_popover_host(titlebar, cx))
-            .into_any()
-    }
     fn render_toggle_screen_sharing_button(
         &self,
         theme: &Theme,
@@ -649,7 +531,7 @@ impl CollabTitlebarItem {
     ) -> AnyElement<Self> {
         let icon;
         let tooltip;
-        let is_muted = room.read(cx).is_muted();
+        let is_muted = room.read(cx).is_muted(cx);
         if is_muted {
             icon = "icons/radix/mic-mute.svg";
             tooltip = "Unmute microphone";
@@ -923,23 +805,6 @@ impl CollabTitlebarItem {
         .into_any()
     }
 
-    fn render_contacts_popover_host<'a>(
-        &'a self,
-        _theme: &'a theme::Titlebar,
-        cx: &'a ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        self.contacts_popover.as_ref().map(|popover| {
-            Overlay::new(ChildView::new(popover, cx))
-                .with_fit_mode(OverlayFitMode::SwitchAnchor)
-                .with_anchor_corner(AnchorCorner::TopLeft)
-                .with_z_index(999)
-                .aligned()
-                .bottom()
-                .right()
-                .into_any()
-        })
-    }
-
     fn render_collaborators(
         &self,
         workspace: &ViewHandle<Workspace>,
@@ -1218,7 +1083,7 @@ impl CollabTitlebarItem {
         style
     }
 
-    fn render_face<V: View>(
+    fn render_face<V: 'static>(
         avatar: Arc<ImageData>,
         avatar_style: AvatarStyle,
         background_color: Color,

crates/collab_ui/src/collab_ui.rs 🔗

@@ -1,8 +1,7 @@
+pub mod channel_view;
+pub mod collab_panel;
 mod collab_titlebar_item;
-mod contact_finder;
-mod contact_list;
 mod contact_notification;
-mod contacts_popover;
 mod face_pile;
 mod incoming_call_notification;
 mod notifications;
@@ -10,9 +9,17 @@ mod project_shared_notification;
 mod sharing_status_indicator;
 
 use call::{ActiveCall, Room};
-pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
-use gpui::{actions, AppContext, Task};
-use std::sync::Arc;
+pub use collab_titlebar_item::CollabTitlebarItem;
+use gpui::{
+    actions,
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    platform::{Screen, WindowBounds, WindowKind, WindowOptions},
+    AppContext, Task,
+};
+use std::{rc::Rc, sync::Arc};
 use util::ResultExt;
 use workspace::AppState;
 
@@ -24,9 +31,7 @@ actions!(
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     vcs_menu::init(cx);
     collab_titlebar_item::init(cx);
-    contact_list::init(cx);
-    contact_finder::init(cx);
-    contacts_popover::init(cx);
+    collab_panel::init(app_state.client.clone(), cx);
     incoming_call_notification::init(&app_state, cx);
     project_shared_notification::init(&app_state, cx);
     sharing_status_indicator::init(cx);
@@ -45,6 +50,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
                 ActiveCall::report_call_event_for_room(
                     "disable screen share",
                     room.id(),
+                    room.channel_id(),
                     &client,
                     cx,
                 );
@@ -53,6 +59,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
                 ActiveCall::report_call_event_for_room(
                     "enable screen share",
                     room.id(),
+                    room.channel_id(),
                     &client,
                     cx,
                 );
@@ -68,12 +75,19 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
     if let Some(room) = call.room().cloned() {
         let client = call.client();
         room.update(cx, |room, cx| {
-            if room.is_muted() {
-                ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx);
+            if room.is_muted(cx) {
+                ActiveCall::report_call_event_for_room(
+                    "enable microphone",
+                    room.id(),
+                    room.channel_id(),
+                    &client,
+                    cx,
+                );
             } else {
                 ActiveCall::report_call_event_for_room(
                     "disable microphone",
                     room.id(),
+                    room.channel_id(),
                     &client,
                     cx,
                 );
@@ -92,3 +106,29 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
             .log_err();
     }
 }
+
+fn notification_window_options(
+    screen: Rc<dyn Screen>,
+    window_size: Vector2F,
+) -> WindowOptions<'static> {
+    const NOTIFICATION_PADDING: f32 = 16.;
+
+    let screen_bounds = screen.content_bounds();
+    WindowOptions {
+        bounds: WindowBounds::Fixed(RectF::new(
+            screen_bounds.upper_right()
+                + vec2f(
+                    -NOTIFICATION_PADDING - window_size.x(),
+                    NOTIFICATION_PADDING,
+                ),
+            window_size,
+        )),
+        titlebar: None,
+        center: false,
+        focus: false,
+        show: true,
+        kind: WindowKind::PopUp,
+        is_movable: false,
+        screen: Some(screen),
+    }
+}

crates/collab_ui/src/contact_list.rs 🔗

@@ -1,1385 +0,0 @@
-use call::ActiveCall;
-use client::{proto::PeerId, Contact, User, UserStore};
-use editor::{Cancel, Editor};
-use futures::StreamExt;
-use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{
-    elements::*,
-    geometry::{rect::RectF, vector::vec2f},
-    impl_actions,
-    keymap_matcher::KeymapContext,
-    platform::{CursorStyle, MouseButton, PromptLevel},
-    AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
-};
-use menu::{Confirm, SelectNext, SelectPrev};
-use project::Project;
-use serde::Deserialize;
-use std::{mem, sync::Arc};
-use theme::IconButton;
-use workspace::Workspace;
-
-impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(ContactList::remove_contact);
-    cx.add_action(ContactList::respond_to_contact_request);
-    cx.add_action(ContactList::cancel);
-    cx.add_action(ContactList::select_next);
-    cx.add_action(ContactList::select_prev);
-    cx.add_action(ContactList::confirm);
-}
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
-enum Section {
-    ActiveCall,
-    Requests,
-    Online,
-    Offline,
-}
-
-#[derive(Clone)]
-enum ContactEntry {
-    Header(Section),
-    CallParticipant {
-        user: Arc<User>,
-        is_pending: bool,
-    },
-    ParticipantProject {
-        project_id: u64,
-        worktree_root_names: Vec<String>,
-        host_user_id: u64,
-        is_last: bool,
-    },
-    ParticipantScreen {
-        peer_id: PeerId,
-        is_last: bool,
-    },
-    IncomingRequest(Arc<User>),
-    OutgoingRequest(Arc<User>),
-    Contact {
-        contact: Arc<Contact>,
-        calling: bool,
-    },
-}
-
-impl PartialEq for ContactEntry {
-    fn eq(&self, other: &Self) -> bool {
-        match self {
-            ContactEntry::Header(section_1) => {
-                if let ContactEntry::Header(section_2) = other {
-                    return section_1 == section_2;
-                }
-            }
-            ContactEntry::CallParticipant { user: user_1, .. } => {
-                if let ContactEntry::CallParticipant { user: user_2, .. } = other {
-                    return user_1.id == user_2.id;
-                }
-            }
-            ContactEntry::ParticipantProject {
-                project_id: project_id_1,
-                ..
-            } => {
-                if let ContactEntry::ParticipantProject {
-                    project_id: project_id_2,
-                    ..
-                } = other
-                {
-                    return project_id_1 == project_id_2;
-                }
-            }
-            ContactEntry::ParticipantScreen {
-                peer_id: peer_id_1, ..
-            } => {
-                if let ContactEntry::ParticipantScreen {
-                    peer_id: peer_id_2, ..
-                } = other
-                {
-                    return peer_id_1 == peer_id_2;
-                }
-            }
-            ContactEntry::IncomingRequest(user_1) => {
-                if let ContactEntry::IncomingRequest(user_2) = other {
-                    return user_1.id == user_2.id;
-                }
-            }
-            ContactEntry::OutgoingRequest(user_1) => {
-                if let ContactEntry::OutgoingRequest(user_2) = other {
-                    return user_1.id == user_2.id;
-                }
-            }
-            ContactEntry::Contact {
-                contact: contact_1, ..
-            } => {
-                if let ContactEntry::Contact {
-                    contact: contact_2, ..
-                } = other
-                {
-                    return contact_1.user.id == contact_2.user.id;
-                }
-            }
-        }
-        false
-    }
-}
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct RequestContact(pub u64);
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct RemoveContact {
-    user_id: u64,
-    github_login: String,
-}
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct RespondToContactRequest {
-    pub user_id: u64,
-    pub accept: bool,
-}
-
-pub enum Event {
-    ToggleContactFinder,
-    Dismissed,
-}
-
-pub struct ContactList {
-    entries: Vec<ContactEntry>,
-    match_candidates: Vec<StringMatchCandidate>,
-    list_state: ListState<Self>,
-    project: ModelHandle<Project>,
-    workspace: WeakViewHandle<Workspace>,
-    user_store: ModelHandle<UserStore>,
-    filter_editor: ViewHandle<Editor>,
-    collapsed_sections: Vec<Section>,
-    selection: Option<usize>,
-    _subscriptions: Vec<Subscription>,
-}
-
-impl ContactList {
-    pub fn new(
-        project: ModelHandle<Project>,
-        user_store: ModelHandle<UserStore>,
-        workspace: WeakViewHandle<Workspace>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let filter_editor = cx.add_view(|cx| {
-            let mut editor = Editor::single_line(
-                Some(Arc::new(|theme| {
-                    theme.contact_list.user_query_editor.clone()
-                })),
-                cx,
-            );
-            editor.set_placeholder_text("Filter contacts", cx);
-            editor
-        });
-
-        cx.subscribe(&filter_editor, |this, _, event, cx| {
-            if let editor::Event::BufferEdited = event {
-                let query = this.filter_editor.read(cx).text(cx);
-                if !query.is_empty() {
-                    this.selection.take();
-                }
-                this.update_entries(cx);
-                if !query.is_empty() {
-                    this.selection = this
-                        .entries
-                        .iter()
-                        .position(|entry| !matches!(entry, ContactEntry::Header(_)));
-                }
-            }
-        })
-        .detach();
-
-        let list_state = ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
-            let theme = theme::current(cx).clone();
-            let is_selected = this.selection == Some(ix);
-            let current_project_id = this.project.read(cx).remote_id();
-
-            match &this.entries[ix] {
-                ContactEntry::Header(section) => {
-                    let is_collapsed = this.collapsed_sections.contains(section);
-                    Self::render_header(
-                        *section,
-                        &theme.contact_list,
-                        is_selected,
-                        is_collapsed,
-                        cx,
-                    )
-                }
-                ContactEntry::CallParticipant { user, is_pending } => {
-                    Self::render_call_participant(
-                        user,
-                        *is_pending,
-                        is_selected,
-                        &theme.contact_list,
-                    )
-                }
-                ContactEntry::ParticipantProject {
-                    project_id,
-                    worktree_root_names,
-                    host_user_id,
-                    is_last,
-                } => Self::render_participant_project(
-                    *project_id,
-                    worktree_root_names,
-                    *host_user_id,
-                    Some(*project_id) == current_project_id,
-                    *is_last,
-                    is_selected,
-                    &theme.contact_list,
-                    cx,
-                ),
-                ContactEntry::ParticipantScreen { peer_id, is_last } => {
-                    Self::render_participant_screen(
-                        *peer_id,
-                        *is_last,
-                        is_selected,
-                        &theme.contact_list,
-                        cx,
-                    )
-                }
-                ContactEntry::IncomingRequest(user) => Self::render_contact_request(
-                    user.clone(),
-                    this.user_store.clone(),
-                    &theme.contact_list,
-                    true,
-                    is_selected,
-                    cx,
-                ),
-                ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
-                    user.clone(),
-                    this.user_store.clone(),
-                    &theme.contact_list,
-                    false,
-                    is_selected,
-                    cx,
-                ),
-                ContactEntry::Contact { contact, calling } => Self::render_contact(
-                    contact,
-                    *calling,
-                    &this.project,
-                    &theme.contact_list,
-                    is_selected,
-                    cx,
-                ),
-            }
-        });
-
-        let active_call = ActiveCall::global(cx);
-        let mut subscriptions = Vec::new();
-        subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
-        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
-
-        let mut this = Self {
-            list_state,
-            selection: None,
-            collapsed_sections: Default::default(),
-            entries: Default::default(),
-            match_candidates: Default::default(),
-            filter_editor,
-            _subscriptions: subscriptions,
-            project,
-            workspace,
-            user_store,
-        };
-        this.update_entries(cx);
-        this
-    }
-
-    pub fn editor_text(&self, cx: &AppContext) -> String {
-        self.filter_editor.read(cx).text(cx)
-    }
-
-    pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
-        self.filter_editor
-            .update(cx, |picker, cx| picker.set_text(editor_text, cx));
-        self
-    }
-
-    fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
-        let user_id = request.user_id;
-        let github_login = &request.github_login;
-        let user_store = self.user_store.clone();
-        let prompt_message = format!(
-            "Are you sure you want to remove \"{}\" from your contacts?",
-            github_login
-        );
-        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-        let window = cx.window();
-        cx.spawn(|_, mut cx| async move {
-            if answer.next().await == Some(0) {
-                if let Err(e) = user_store
-                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
-                    .await
-                {
-                    window.prompt(
-                        PromptLevel::Info,
-                        &format!("Failed to remove contact: {}", e),
-                        &["Ok"],
-                        &mut cx,
-                    );
-                }
-            }
-        })
-        .detach();
-    }
-
-    fn respond_to_contact_request(
-        &mut self,
-        action: &RespondToContactRequest,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.user_store
-            .update(cx, |store, cx| {
-                store.respond_to_contact_request(action.user_id, action.accept, cx)
-            })
-            .detach();
-    }
-
-    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        let did_clear = self.filter_editor.update(cx, |editor, cx| {
-            if editor.buffer().read(cx).len(cx) > 0 {
-                editor.set_text("", cx);
-                true
-            } else {
-                false
-            }
-        });
-
-        if !did_clear {
-            cx.emit(Event::Dismissed);
-        }
-    }
-
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        if let Some(ix) = self.selection {
-            if self.entries.len() > ix + 1 {
-                self.selection = Some(ix + 1);
-            }
-        } else if !self.entries.is_empty() {
-            self.selection = Some(0);
-        }
-        self.list_state.reset(self.entries.len());
-        if let Some(ix) = self.selection {
-            self.list_state.scroll_to(ListOffset {
-                item_ix: ix,
-                offset_in_item: 0.,
-            });
-        }
-        cx.notify();
-    }
-
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        if let Some(ix) = self.selection {
-            if ix > 0 {
-                self.selection = Some(ix - 1);
-            } else {
-                self.selection = None;
-            }
-        }
-        self.list_state.reset(self.entries.len());
-        if let Some(ix) = self.selection {
-            self.list_state.scroll_to(ListOffset {
-                item_ix: ix,
-                offset_in_item: 0.,
-            });
-        }
-        cx.notify();
-    }
-
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if let Some(selection) = self.selection {
-            if let Some(entry) = self.entries.get(selection) {
-                match entry {
-                    ContactEntry::Header(section) => {
-                        self.toggle_expanded(*section, cx);
-                    }
-                    ContactEntry::Contact { contact, calling } => {
-                        if contact.online && !contact.busy && !calling {
-                            self.call(contact.user.id, Some(self.project.clone()), cx);
-                        }
-                    }
-                    ContactEntry::ParticipantProject {
-                        project_id,
-                        host_user_id,
-                        ..
-                    } => {
-                        if let Some(workspace) = self.workspace.upgrade(cx) {
-                            let app_state = workspace.read(cx).app_state().clone();
-                            workspace::join_remote_project(
-                                *project_id,
-                                *host_user_id,
-                                app_state,
-                                cx,
-                            )
-                            .detach_and_log_err(cx);
-                        }
-                    }
-                    ContactEntry::ParticipantScreen { peer_id, .. } => {
-                        if let Some(workspace) = self.workspace.upgrade(cx) {
-                            workspace.update(cx, |workspace, cx| {
-                                workspace.open_shared_screen(*peer_id, cx)
-                            });
-                        }
-                    }
-                    _ => {}
-                }
-            }
-        }
-    }
-
-    fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
-        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
-            self.collapsed_sections.remove(ix);
-        } else {
-            self.collapsed_sections.push(section);
-        }
-        self.update_entries(cx);
-    }
-
-    fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
-        let user_store = self.user_store.read(cx);
-        let query = self.filter_editor.read(cx).text(cx);
-        let executor = cx.background().clone();
-
-        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
-        let old_entries = mem::take(&mut self.entries);
-
-        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
-            let room = room.read(cx);
-            let mut participant_entries = Vec::new();
-
-            // Populate the active user.
-            if let Some(user) = user_store.current_user() {
-                self.match_candidates.clear();
-                self.match_candidates.push(StringMatchCandidate {
-                    id: 0,
-                    string: user.github_login.clone(),
-                    char_bag: user.github_login.chars().collect(),
-                });
-                let matches = executor.block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    executor.clone(),
-                ));
-                if !matches.is_empty() {
-                    let user_id = user.id;
-                    participant_entries.push(ContactEntry::CallParticipant {
-                        user,
-                        is_pending: false,
-                    });
-                    let mut projects = room.local_participant().projects.iter().peekable();
-                    while let Some(project) = projects.next() {
-                        participant_entries.push(ContactEntry::ParticipantProject {
-                            project_id: project.id,
-                            worktree_root_names: project.worktree_root_names.clone(),
-                            host_user_id: user_id,
-                            is_last: projects.peek().is_none(),
-                        });
-                    }
-                }
-            }
-
-            // Populate remote participants.
-            self.match_candidates.clear();
-            self.match_candidates
-                .extend(room.remote_participants().iter().map(|(_, participant)| {
-                    StringMatchCandidate {
-                        id: participant.user.id as usize,
-                        string: participant.user.github_login.clone(),
-                        char_bag: participant.user.github_login.chars().collect(),
-                    }
-                }));
-            let matches = executor.block(match_strings(
-                &self.match_candidates,
-                &query,
-                true,
-                usize::MAX,
-                &Default::default(),
-                executor.clone(),
-            ));
-            for mat in matches {
-                let user_id = mat.candidate_id as u64;
-                let participant = &room.remote_participants()[&user_id];
-                participant_entries.push(ContactEntry::CallParticipant {
-                    user: participant.user.clone(),
-                    is_pending: false,
-                });
-                let mut projects = participant.projects.iter().peekable();
-                while let Some(project) = projects.next() {
-                    participant_entries.push(ContactEntry::ParticipantProject {
-                        project_id: project.id,
-                        worktree_root_names: project.worktree_root_names.clone(),
-                        host_user_id: participant.user.id,
-                        is_last: projects.peek().is_none() && participant.video_tracks.is_empty(),
-                    });
-                }
-                if !participant.video_tracks.is_empty() {
-                    participant_entries.push(ContactEntry::ParticipantScreen {
-                        peer_id: participant.peer_id,
-                        is_last: true,
-                    });
-                }
-            }
-
-            // Populate pending participants.
-            self.match_candidates.clear();
-            self.match_candidates
-                .extend(
-                    room.pending_participants()
-                        .iter()
-                        .enumerate()
-                        .map(|(id, participant)| StringMatchCandidate {
-                            id,
-                            string: participant.github_login.clone(),
-                            char_bag: participant.github_login.chars().collect(),
-                        }),
-                );
-            let matches = executor.block(match_strings(
-                &self.match_candidates,
-                &query,
-                true,
-                usize::MAX,
-                &Default::default(),
-                executor.clone(),
-            ));
-            participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant {
-                user: room.pending_participants()[mat.candidate_id].clone(),
-                is_pending: true,
-            }));
-
-            if !participant_entries.is_empty() {
-                self.entries.push(ContactEntry::Header(Section::ActiveCall));
-                if !self.collapsed_sections.contains(&Section::ActiveCall) {
-                    self.entries.extend(participant_entries);
-                }
-            }
-        }
-
-        let mut request_entries = Vec::new();
-        let incoming = user_store.incoming_contact_requests();
-        if !incoming.is_empty() {
-            self.match_candidates.clear();
-            self.match_candidates
-                .extend(
-                    incoming
-                        .iter()
-                        .enumerate()
-                        .map(|(ix, user)| StringMatchCandidate {
-                            id: ix,
-                            string: user.github_login.clone(),
-                            char_bag: user.github_login.chars().collect(),
-                        }),
-                );
-            let matches = executor.block(match_strings(
-                &self.match_candidates,
-                &query,
-                true,
-                usize::MAX,
-                &Default::default(),
-                executor.clone(),
-            ));
-            request_entries.extend(
-                matches
-                    .iter()
-                    .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
-            );
-        }
-
-        let outgoing = user_store.outgoing_contact_requests();
-        if !outgoing.is_empty() {
-            self.match_candidates.clear();
-            self.match_candidates
-                .extend(
-                    outgoing
-                        .iter()
-                        .enumerate()
-                        .map(|(ix, user)| StringMatchCandidate {
-                            id: ix,
-                            string: user.github_login.clone(),
-                            char_bag: user.github_login.chars().collect(),
-                        }),
-                );
-            let matches = executor.block(match_strings(
-                &self.match_candidates,
-                &query,
-                true,
-                usize::MAX,
-                &Default::default(),
-                executor.clone(),
-            ));
-            request_entries.extend(
-                matches
-                    .iter()
-                    .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
-            );
-        }
-
-        if !request_entries.is_empty() {
-            self.entries.push(ContactEntry::Header(Section::Requests));
-            if !self.collapsed_sections.contains(&Section::Requests) {
-                self.entries.append(&mut request_entries);
-            }
-        }
-
-        let contacts = user_store.contacts();
-        if !contacts.is_empty() {
-            self.match_candidates.clear();
-            self.match_candidates
-                .extend(
-                    contacts
-                        .iter()
-                        .enumerate()
-                        .map(|(ix, contact)| StringMatchCandidate {
-                            id: ix,
-                            string: contact.user.github_login.clone(),
-                            char_bag: contact.user.github_login.chars().collect(),
-                        }),
-                );
-
-            let matches = executor.block(match_strings(
-                &self.match_candidates,
-                &query,
-                true,
-                usize::MAX,
-                &Default::default(),
-                executor.clone(),
-            ));
-
-            let (mut online_contacts, offline_contacts) = matches
-                .iter()
-                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
-            if let Some(room) = ActiveCall::global(cx).read(cx).room() {
-                let room = room.read(cx);
-                online_contacts.retain(|contact| {
-                    let contact = &contacts[contact.candidate_id];
-                    !room.contains_participant(contact.user.id)
-                });
-            }
-
-            for (matches, section) in [
-                (online_contacts, Section::Online),
-                (offline_contacts, Section::Offline),
-            ] {
-                if !matches.is_empty() {
-                    self.entries.push(ContactEntry::Header(section));
-                    if !self.collapsed_sections.contains(&section) {
-                        let active_call = &ActiveCall::global(cx).read(cx);
-                        for mat in matches {
-                            let contact = &contacts[mat.candidate_id];
-                            self.entries.push(ContactEntry::Contact {
-                                contact: contact.clone(),
-                                calling: active_call.pending_invites().contains(&contact.user.id),
-                            });
-                        }
-                    }
-                }
-            }
-        }
-
-        if let Some(prev_selected_entry) = prev_selected_entry {
-            self.selection.take();
-            for (ix, entry) in self.entries.iter().enumerate() {
-                if *entry == prev_selected_entry {
-                    self.selection = Some(ix);
-                    break;
-                }
-            }
-        }
-
-        let old_scroll_top = self.list_state.logical_scroll_top();
-        self.list_state.reset(self.entries.len());
-
-        // Attempt to maintain the same scroll position.
-        if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
-            let new_scroll_top = self
-                .entries
-                .iter()
-                .position(|entry| entry == old_top_entry)
-                .map(|item_ix| ListOffset {
-                    item_ix,
-                    offset_in_item: old_scroll_top.offset_in_item,
-                })
-                .or_else(|| {
-                    let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
-                    let item_ix = self
-                        .entries
-                        .iter()
-                        .position(|entry| entry == entry_after_old_top)?;
-                    Some(ListOffset {
-                        item_ix,
-                        offset_in_item: 0.,
-                    })
-                })
-                .or_else(|| {
-                    let entry_before_old_top =
-                        old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
-                    let item_ix = self
-                        .entries
-                        .iter()
-                        .position(|entry| entry == entry_before_old_top)?;
-                    Some(ListOffset {
-                        item_ix,
-                        offset_in_item: 0.,
-                    })
-                });
-
-            self.list_state
-                .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
-        }
-
-        cx.notify();
-    }
-
-    fn render_call_participant(
-        user: &User,
-        is_pending: bool,
-        is_selected: bool,
-        theme: &theme::ContactList,
-    ) -> AnyElement<Self> {
-        Flex::row()
-            .with_children(user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.contact_avatar)
-                    .aligned()
-                    .left()
-            }))
-            .with_child(
-                Label::new(
-                    user.github_login.clone(),
-                    theme.contact_username.text.clone(),
-                )
-                .contained()
-                .with_style(theme.contact_username.container)
-                .aligned()
-                .left()
-                .flex(1., true),
-            )
-            .with_children(if is_pending {
-                Some(
-                    Label::new("Calling", theme.calling_indicator.text.clone())
-                        .contained()
-                        .with_style(theme.calling_indicator.container)
-                        .aligned(),
-                )
-            } else {
-                None
-            })
-            .constrained()
-            .with_height(theme.row_height)
-            .contained()
-            .with_style(
-                *theme
-                    .contact_row
-                    .in_state(is_selected)
-                    .style_for(&mut Default::default()),
-            )
-            .into_any()
-    }
-
-    fn render_participant_project(
-        project_id: u64,
-        worktree_root_names: &[String],
-        host_user_id: u64,
-        is_current: bool,
-        is_last: bool,
-        is_selected: bool,
-        theme: &theme::ContactList,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum JoinProject {}
-
-        let font_cache = cx.font_cache();
-        let host_avatar_height = theme
-            .contact_avatar
-            .width
-            .or(theme.contact_avatar.height)
-            .unwrap_or(0.);
-        let row = &theme.project_row.inactive_state().default;
-        let tree_branch = theme.tree_branch;
-        let line_height = row.name.text.line_height(font_cache);
-        let cap_height = row.name.text.cap_height(font_cache);
-        let baseline_offset =
-            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
-        let project_name = if worktree_root_names.is_empty() {
-            "untitled".to_string()
-        } else {
-            worktree_root_names.join(", ")
-        };
-
-        MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, _| {
-            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-            let row = theme
-                .project_row
-                .in_state(is_selected)
-                .style_for(mouse_state);
-
-            Flex::row()
-                .with_child(
-                    Stack::new()
-                        .with_child(Canvas::new(move |scene, bounds, _, _, _| {
-                            let start_x =
-                                bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
-                            let end_x = bounds.max_x();
-                            let start_y = bounds.min_y();
-                            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
-
-                            scene.push_quad(gpui::Quad {
-                                bounds: RectF::from_points(
-                                    vec2f(start_x, start_y),
-                                    vec2f(
-                                        start_x + tree_branch.width,
-                                        if is_last { end_y } else { bounds.max_y() },
-                                    ),
-                                ),
-                                background: Some(tree_branch.color),
-                                border: gpui::Border::default(),
-                                corner_radii: Default::default(),
-                            });
-                            scene.push_quad(gpui::Quad {
-                                bounds: RectF::from_points(
-                                    vec2f(start_x, end_y),
-                                    vec2f(end_x, end_y + tree_branch.width),
-                                ),
-                                background: Some(tree_branch.color),
-                                border: gpui::Border::default(),
-                                corner_radii: Default::default(),
-                            });
-                        }))
-                        .constrained()
-                        .with_width(host_avatar_height),
-                )
-                .with_child(
-                    Label::new(project_name, row.name.text.clone())
-                        .aligned()
-                        .left()
-                        .contained()
-                        .with_style(row.name.container)
-                        .flex(1., false),
-                )
-                .constrained()
-                .with_height(theme.row_height)
-                .contained()
-                .with_style(row.container)
-        })
-        .with_cursor_style(if !is_current {
-            CursorStyle::PointingHand
-        } else {
-            CursorStyle::Arrow
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            if !is_current {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    let app_state = workspace.read(cx).app_state().clone();
-                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
-                        .detach_and_log_err(cx);
-                }
-            }
-        })
-        .into_any()
-    }
-
-    fn render_participant_screen(
-        peer_id: PeerId,
-        is_last: bool,
-        is_selected: bool,
-        theme: &theme::ContactList,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum OpenSharedScreen {}
-
-        let font_cache = cx.font_cache();
-        let host_avatar_height = theme
-            .contact_avatar
-            .width
-            .or(theme.contact_avatar.height)
-            .unwrap_or(0.);
-        let row = &theme.project_row.inactive_state().default;
-        let tree_branch = theme.tree_branch;
-        let line_height = row.name.text.line_height(font_cache);
-        let cap_height = row.name.text.cap_height(font_cache);
-        let baseline_offset =
-            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
-
-        MouseEventHandler::new::<OpenSharedScreen, _>(
-            peer_id.as_u64() as usize,
-            cx,
-            |mouse_state, _| {
-                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-                let row = theme
-                    .project_row
-                    .in_state(is_selected)
-                    .style_for(mouse_state);
-
-                Flex::row()
-                    .with_child(
-                        Stack::new()
-                            .with_child(Canvas::new(move |scene, bounds, _, _, _| {
-                                let start_x = bounds.min_x() + (bounds.width() / 2.)
-                                    - (tree_branch.width / 2.);
-                                let end_x = bounds.max_x();
-                                let start_y = bounds.min_y();
-                                let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
-
-                                scene.push_quad(gpui::Quad {
-                                    bounds: RectF::from_points(
-                                        vec2f(start_x, start_y),
-                                        vec2f(
-                                            start_x + tree_branch.width,
-                                            if is_last { end_y } else { bounds.max_y() },
-                                        ),
-                                    ),
-                                    background: Some(tree_branch.color),
-                                    border: gpui::Border::default(),
-                                    corner_radii: Default::default(),
-                                });
-                                scene.push_quad(gpui::Quad {
-                                    bounds: RectF::from_points(
-                                        vec2f(start_x, end_y),
-                                        vec2f(end_x, end_y + tree_branch.width),
-                                    ),
-                                    background: Some(tree_branch.color),
-                                    border: gpui::Border::default(),
-                                    corner_radii: Default::default(),
-                                });
-                            }))
-                            .constrained()
-                            .with_width(host_avatar_height),
-                    )
-                    .with_child(
-                        Svg::new("icons/disable_screen_sharing_12.svg")
-                            .with_color(row.icon.color)
-                            .constrained()
-                            .with_width(row.icon.width)
-                            .aligned()
-                            .left()
-                            .contained()
-                            .with_style(row.icon.container),
-                    )
-                    .with_child(
-                        Label::new("Screen", row.name.text.clone())
-                            .aligned()
-                            .left()
-                            .contained()
-                            .with_style(row.name.container)
-                            .flex(1., false),
-                    )
-                    .constrained()
-                    .with_height(theme.row_height)
-                    .contained()
-                    .with_style(row.container)
-            },
-        )
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            if let Some(workspace) = this.workspace.upgrade(cx) {
-                workspace.update(cx, |workspace, cx| {
-                    workspace.open_shared_screen(peer_id, cx)
-                });
-            }
-        })
-        .into_any()
-    }
-
-    fn render_header(
-        section: Section,
-        theme: &theme::ContactList,
-        is_selected: bool,
-        is_collapsed: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum Header {}
-        enum LeaveCallContactList {}
-
-        let header_style = theme
-            .header_row
-            .in_state(is_selected)
-            .style_for(&mut Default::default());
-        let text = match section {
-            Section::ActiveCall => "Collaborators",
-            Section::Requests => "Contact Requests",
-            Section::Online => "Online",
-            Section::Offline => "Offline",
-        };
-        let leave_call = if section == Section::ActiveCall {
-            Some(
-                MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
-                    let style = theme.leave_call.style_for(state);
-                    Label::new("Leave Call", style.text.clone())
-                        .contained()
-                        .with_style(style.container)
-                })
-                .on_click(MouseButton::Left, |_, _, cx| {
-                    ActiveCall::global(cx)
-                        .update(cx, |call, cx| call.hang_up(cx))
-                        .detach_and_log_err(cx);
-                })
-                .aligned(),
-            )
-        } else {
-            None
-        };
-
-        let icon_size = theme.section_icon_size;
-        MouseEventHandler::new::<Header, _>(section as usize, cx, |_, _| {
-            Flex::row()
-                .with_child(
-                    Svg::new(if is_collapsed {
-                        "icons/chevron_right_8.svg"
-                    } else {
-                        "icons/chevron_down_8.svg"
-                    })
-                    .with_color(header_style.text.color)
-                    .constrained()
-                    .with_max_width(icon_size)
-                    .with_max_height(icon_size)
-                    .aligned()
-                    .constrained()
-                    .with_width(icon_size),
-                )
-                .with_child(
-                    Label::new(text, header_style.text.clone())
-                        .aligned()
-                        .left()
-                        .contained()
-                        .with_margin_left(theme.contact_username.container.margin.left)
-                        .flex(1., true),
-                )
-                .with_children(leave_call)
-                .constrained()
-                .with_height(theme.row_height)
-                .contained()
-                .with_style(header_style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.toggle_expanded(section, cx);
-        })
-        .into_any()
-    }
-
-    fn render_contact(
-        contact: &Contact,
-        calling: bool,
-        project: &ModelHandle<Project>,
-        theme: &theme::ContactList,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let online = contact.online;
-        let busy = contact.busy || calling;
-        let user_id = contact.user.id;
-        let github_login = contact.user.github_login.clone();
-        let initial_project = project.clone();
-        let mut event_handler =
-            MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |_, cx| {
-                Flex::row()
-                    .with_children(contact.user.avatar.clone().map(|avatar| {
-                        let status_badge = if contact.online {
-                            Some(
-                                Empty::new()
-                                    .collapsed()
-                                    .contained()
-                                    .with_style(if busy {
-                                        theme.contact_status_busy
-                                    } else {
-                                        theme.contact_status_free
-                                    })
-                                    .aligned(),
-                            )
-                        } else {
-                            None
-                        };
-                        Stack::new()
-                            .with_child(
-                                Image::from_data(avatar)
-                                    .with_style(theme.contact_avatar)
-                                    .aligned()
-                                    .left(),
-                            )
-                            .with_children(status_badge)
-                    }))
-                    .with_child(
-                        Label::new(
-                            contact.user.github_login.clone(),
-                            theme.contact_username.text.clone(),
-                        )
-                        .contained()
-                        .with_style(theme.contact_username.container)
-                        .aligned()
-                        .left()
-                        .flex(1., true),
-                    )
-                    .with_child(
-                        MouseEventHandler::new::<Cancel, _>(
-                            contact.user.id as usize,
-                            cx,
-                            |mouse_state, _| {
-                                let button_style = theme.contact_button.style_for(mouse_state);
-                                render_icon_button(button_style, "icons/x_mark_8.svg")
-                                    .aligned()
-                                    .flex_float()
-                            },
-                        )
-                        .with_padding(Padding::uniform(2.))
-                        .with_cursor_style(CursorStyle::PointingHand)
-                        .on_click(MouseButton::Left, move |_, this, cx| {
-                            this.remove_contact(
-                                &RemoveContact {
-                                    user_id,
-                                    github_login: github_login.clone(),
-                                },
-                                cx,
-                            );
-                        })
-                        .flex_float(),
-                    )
-                    .with_children(if calling {
-                        Some(
-                            Label::new("Calling", theme.calling_indicator.text.clone())
-                                .contained()
-                                .with_style(theme.calling_indicator.container)
-                                .aligned(),
-                        )
-                    } else {
-                        None
-                    })
-                    .constrained()
-                    .with_height(theme.row_height)
-                    .contained()
-                    .with_style(
-                        *theme
-                            .contact_row
-                            .in_state(is_selected)
-                            .style_for(&mut Default::default()),
-                    )
-            })
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if online && !busy {
-                    this.call(user_id, Some(initial_project.clone()), cx);
-                }
-            });
-
-        if online {
-            event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
-        }
-
-        event_handler.into_any()
-    }
-
-    fn render_contact_request(
-        user: Arc<User>,
-        user_store: ModelHandle<UserStore>,
-        theme: &theme::ContactList,
-        is_incoming: bool,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum Decline {}
-        enum Accept {}
-        enum Cancel {}
-
-        let mut row = Flex::row()
-            .with_children(user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.contact_avatar)
-                    .aligned()
-                    .left()
-            }))
-            .with_child(
-                Label::new(
-                    user.github_login.clone(),
-                    theme.contact_username.text.clone(),
-                )
-                .contained()
-                .with_style(theme.contact_username.container)
-                .aligned()
-                .left()
-                .flex(1., true),
-            );
-
-        let user_id = user.id;
-        let github_login = user.github_login.clone();
-        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
-        let button_spacing = theme.contact_button_spacing;
-
-        if is_incoming {
-            row.add_child(
-                MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
-                    let button_style = if is_contact_request_pending {
-                        &theme.disabled_button
-                    } else {
-                        theme.contact_button.style_for(mouse_state)
-                    };
-                    render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.respond_to_contact_request(
-                        &RespondToContactRequest {
-                            user_id,
-                            accept: false,
-                        },
-                        cx,
-                    );
-                })
-                .contained()
-                .with_margin_right(button_spacing),
-            );
-
-            row.add_child(
-                MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
-                    let button_style = if is_contact_request_pending {
-                        &theme.disabled_button
-                    } else {
-                        theme.contact_button.style_for(mouse_state)
-                    };
-                    render_icon_button(button_style, "icons/check_8.svg")
-                        .aligned()
-                        .flex_float()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.respond_to_contact_request(
-                        &RespondToContactRequest {
-                            user_id,
-                            accept: true,
-                        },
-                        cx,
-                    );
-                }),
-            );
-        } else {
-            row.add_child(
-                MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
-                    let button_style = if is_contact_request_pending {
-                        &theme.disabled_button
-                    } else {
-                        theme.contact_button.style_for(mouse_state)
-                    };
-                    render_icon_button(button_style, "icons/x_mark_8.svg")
-                        .aligned()
-                        .flex_float()
-                })
-                .with_padding(Padding::uniform(2.))
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.remove_contact(
-                        &RemoveContact {
-                            user_id,
-                            github_login: github_login.clone(),
-                        },
-                        cx,
-                    );
-                })
-                .flex_float(),
-            );
-        }
-
-        row.constrained()
-            .with_height(theme.row_height)
-            .contained()
-            .with_style(
-                *theme
-                    .contact_row
-                    .in_state(is_selected)
-                    .style_for(&mut Default::default()),
-            )
-            .into_any()
-    }
-
-    fn call(
-        &mut self,
-        recipient_user_id: u64,
-        initial_project: Option<ModelHandle<Project>>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| {
-                call.invite(recipient_user_id, initial_project, cx)
-            })
-            .detach_and_log_err(cx);
-    }
-}
-
-impl Entity for ContactList {
-    type Event = Event;
-}
-
-impl View for ContactList {
-    fn ui_name() -> &'static str {
-        "ContactList"
-    }
-
-    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
-        Self::reset_to_default_keymap_context(keymap);
-        keymap.add_identifier("menu");
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        enum AddContact {}
-        let theme = theme::current(cx).clone();
-
-        Flex::column()
-            .with_child(
-                Flex::row()
-                    .with_child(
-                        ChildView::new(&self.filter_editor, cx)
-                            .contained()
-                            .with_style(theme.contact_list.user_query_editor.container)
-                            .flex(1., true),
-                    )
-                    .with_child(
-                        MouseEventHandler::new::<AddContact, _>(0, cx, |_, _| {
-                            render_icon_button(
-                                &theme.contact_list.add_contact_button,
-                                "icons/user_plus_16.svg",
-                            )
-                        })
-                        .with_cursor_style(CursorStyle::PointingHand)
-                        .on_click(MouseButton::Left, |_, _, cx| {
-                            cx.emit(Event::ToggleContactFinder)
-                        })
-                        .with_tooltip::<AddContact>(
-                            0,
-                            "Search for new contact",
-                            None,
-                            theme.tooltip.clone(),
-                            cx,
-                        ),
-                    )
-                    .constrained()
-                    .with_height(theme.contact_list.user_query_editor_height),
-            )
-            .with_child(List::new(self.list_state.clone()).flex(1., false))
-            .into_any()
-    }
-
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if !self.filter_editor.is_focused(cx) {
-            cx.focus(&self.filter_editor);
-        }
-    }
-
-    fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if !self.filter_editor.is_focused(cx) {
-            cx.emit(Event::Dismissed);
-        }
-    }
-}
-
-fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<ContactList> {
-    Svg::new(svg_path)
-        .with_color(style.color)
-        .constrained()
-        .with_width(style.icon_width)
-        .aligned()
-        .contained()
-        .with_style(style.container)
-        .constrained()
-        .with_width(style.button_width)
-        .with_height(style.button_width)
-}

crates/collab_ui/src/contacts_popover.rs 🔗

@@ -1,137 +0,0 @@
-use crate::{
-    contact_finder::{build_contact_finder, ContactFinder},
-    contact_list::ContactList,
-};
-use client::UserStore;
-use gpui::{
-    actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
-    ViewContext, ViewHandle, WeakViewHandle,
-};
-use picker::PickerEvent;
-use project::Project;
-use workspace::Workspace;
-
-actions!(contacts_popover, [ToggleContactFinder]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(ContactsPopover::toggle_contact_finder);
-}
-
-pub enum Event {
-    Dismissed,
-}
-
-enum Child {
-    ContactList(ViewHandle<ContactList>),
-    ContactFinder(ViewHandle<ContactFinder>),
-}
-
-pub struct ContactsPopover {
-    child: Child,
-    project: ModelHandle<Project>,
-    user_store: ModelHandle<UserStore>,
-    workspace: WeakViewHandle<Workspace>,
-    _subscription: Option<gpui::Subscription>,
-}
-
-impl ContactsPopover {
-    pub fn new(
-        project: ModelHandle<Project>,
-        user_store: ModelHandle<UserStore>,
-        workspace: WeakViewHandle<Workspace>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let mut this = Self {
-            child: Child::ContactList(cx.add_view(|cx| {
-                ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx)
-            })),
-            project,
-            user_store,
-            workspace,
-            _subscription: None,
-        };
-        this.show_contact_list(String::new(), cx);
-        this
-    }
-
-    fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
-        match &self.child {
-            Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
-            Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), cx),
-        }
-    }
-
-    fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
-        let child = cx.add_view(|cx| {
-            let finder = build_contact_finder(self.user_store.clone(), cx);
-            finder.set_query(editor_text, cx);
-            finder
-        });
-        cx.focus(&child);
-        self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
-            PickerEvent::Dismiss => cx.emit(Event::Dismissed),
-        }));
-        self.child = Child::ContactFinder(child);
-        cx.notify();
-    }
-
-    fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
-        let child = cx.add_view(|cx| {
-            ContactList::new(
-                self.project.clone(),
-                self.user_store.clone(),
-                self.workspace.clone(),
-                cx,
-            )
-            .with_editor_text(editor_text, cx)
-        });
-        cx.focus(&child);
-        self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event {
-            crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
-            crate::contact_list::Event::ToggleContactFinder => {
-                this.toggle_contact_finder(&Default::default(), cx)
-            }
-        }));
-        self.child = Child::ContactList(child);
-        cx.notify();
-    }
-}
-
-impl Entity for ContactsPopover {
-    type Event = Event;
-}
-
-impl View for ContactsPopover {
-    fn ui_name() -> &'static str {
-        "ContactsPopover"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = theme::current(cx).clone();
-        let child = match &self.child {
-            Child::ContactList(child) => ChildView::new(child, cx),
-            Child::ContactFinder(child) => ChildView::new(child, cx),
-        };
-
-        MouseEventHandler::new::<ContactsPopover, _>(0, cx, |_, _| {
-            Flex::column()
-                .with_child(child.flex(1., true))
-                .contained()
-                .with_style(theme.contacts_popover.container)
-                .constrained()
-                .with_width(theme.contacts_popover.width)
-                .with_height(theme.contacts_popover.height)
-        })
-        .on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed))
-        .into_any()
-    }
-
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            match &self.child {
-                Child::ContactList(child) => cx.focus(child),
-                Child::ContactFinder(child) => cx.focus(child),
-            }
-        }
-    }
-}

crates/collab_ui/src/face_pile.rs 🔗

@@ -7,44 +7,48 @@ use gpui::{
     },
     json::ToJson,
     serde_json::{self, json},
-    AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, ViewContext,
+    AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
 };
 
-use crate::CollabTitlebarItem;
-
-pub(crate) struct FacePile {
+pub(crate) struct FacePile<V: View> {
     overlap: f32,
-    faces: Vec<AnyElement<CollabTitlebarItem>>,
+    faces: Vec<AnyElement<V>>,
 }
 
-impl FacePile {
-    pub fn new(overlap: f32) -> FacePile {
-        FacePile {
+impl<V: View> FacePile<V> {
+    pub fn new(overlap: f32) -> Self {
+        Self {
             overlap,
             faces: Vec::new(),
         }
     }
 }
 
-impl Element<CollabTitlebarItem> for FacePile {
+impl<V: View> Element<V> for FacePile<V> {
     type LayoutState = ();
     type PaintState = ();
 
     fn layout(
         &mut self,
         constraint: gpui::SizeConstraint,
-        view: &mut CollabTitlebarItem,
-        cx: &mut LayoutContext<CollabTitlebarItem>,
+        view: &mut V,
+        cx: &mut LayoutContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
 
         let mut width = 0.;
+        let mut max_height = 0.;
         for face in &mut self.faces {
-            width += face.layout(constraint, view, cx).x();
+            let layout = face.layout(constraint, view, cx);
+            width += layout.x();
+            max_height = f32::max(max_height, layout.y());
         }
         width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
 
-        (Vector2F::new(width, constraint.max.y()), ())
+        (
+            Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
+            (),
+        )
     }
 
     fn paint(
@@ -53,8 +57,8 @@ impl Element<CollabTitlebarItem> for FacePile {
         bounds: RectF,
         visible_bounds: RectF,
         _layout: &mut Self::LayoutState,
-        view: &mut CollabTitlebarItem,
-        cx: &mut PaintContext<CollabTitlebarItem>,
+        view: &mut V,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
@@ -64,6 +68,7 @@ impl Element<CollabTitlebarItem> for FacePile {
         for face in self.faces.iter_mut().rev() {
             let size = face.size();
             origin_x -= size.x();
+            let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
             scene.paint_layer(None, |scene| {
                 face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx);
             });
@@ -80,8 +85,8 @@ impl Element<CollabTitlebarItem> for FacePile {
         _: RectF,
         _: &Self::LayoutState,
         _: &Self::PaintState,
-        _: &CollabTitlebarItem,
-        _: &ViewContext<CollabTitlebarItem>,
+        _: &V,
+        _: &ViewContext<V>,
     ) -> Option<RectF> {
         None
     }
@@ -91,8 +96,8 @@ impl Element<CollabTitlebarItem> for FacePile {
         bounds: RectF,
         _: &Self::LayoutState,
         _: &Self::PaintState,
-        _: &CollabTitlebarItem,
-        _: &ViewContext<CollabTitlebarItem>,
+        _: &V,
+        _: &ViewContext<V>,
     ) -> serde_json::Value {
         json!({
             "type": "FacePile",
@@ -101,8 +106,8 @@ impl Element<CollabTitlebarItem> for FacePile {
     }
 }
 
-impl Extend<AnyElement<CollabTitlebarItem>> for FacePile {
-    fn extend<T: IntoIterator<Item = AnyElement<CollabTitlebarItem>>>(&mut self, children: T) {
+impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
+    fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
         self.faces.extend(children);
     }
 }

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -1,14 +1,14 @@
-use std::sync::{Arc, Weak};
-
+use crate::notification_window_options;
 use call::{ActiveCall, IncomingCall};
 use client::proto;
 use futures::StreamExt;
 use gpui::{
     elements::*,
-    geometry::{rect::RectF, vector::vec2f},
-    platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
+    geometry::vector::vec2f,
+    platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
 };
+use std::sync::{Arc, Weak};
 use util::ResultExt;
 use workspace::AppState;
 
@@ -23,31 +23,16 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
             }
 
             if let Some(incoming_call) = incoming_call {
-                const PADDING: f32 = 16.;
                 let window_size = cx.read(|cx| {
                     let theme = &theme::current(cx).incoming_call_notification;
                     vec2f(theme.window_width, theme.window_height)
                 });
 
                 for screen in cx.platform().screens() {
-                    let screen_bounds = screen.bounds();
-                    let window = cx.add_window(
-                        WindowOptions {
-                            bounds: WindowBounds::Fixed(RectF::new(
-                                screen_bounds.upper_right()
-                                    - vec2f(PADDING + window_size.x(), PADDING),
-                                window_size,
-                            )),
-                            titlebar: None,
-                            center: false,
-                            focus: false,
-                            show: true,
-                            kind: WindowKind::PopUp,
-                            is_movable: false,
-                            screen: Some(screen),
-                        },
-                        |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
-                    );
+                    let window = cx
+                        .add_window(notification_window_options(screen, window_size), |_| {
+                            IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
+                        });
 
                     notification_windows.push(window);
                 }

crates/collab_ui/src/notifications.rs 🔗

@@ -2,14 +2,14 @@ use client::User;
 use gpui::{
     elements::*,
     platform::{CursorStyle, MouseButton},
-    AnyElement, Element, View, ViewContext,
+    AnyElement, Element, ViewContext,
 };
 use std::sync::Arc;
 
 enum Dismiss {}
 enum Button {}
 
-pub fn render_user_notification<F, V>(
+pub fn render_user_notification<F, V: 'static>(
     user: Arc<User>,
     title: &'static str,
     body: Option<&'static str>,
@@ -19,7 +19,6 @@ pub fn render_user_notification<F, V>(
 ) -> AnyElement<V>
 where
     F: 'static + Fn(&mut V, &mut ViewContext<V>),
-    V: View,
 {
     let theme = theme::current(cx).clone();
     let theme = &theme.contact_notification;

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -1,10 +1,11 @@
+use crate::notification_window_options;
 use call::{room, ActiveCall};
 use client::User;
 use collections::HashMap;
 use gpui::{
     elements::*,
-    geometry::{rect::RectF, vector::vec2f},
-    platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
+    geometry::vector::vec2f,
+    platform::{CursorStyle, MouseButton},
     AppContext, Entity, View, ViewContext,
 };
 use std::sync::{Arc, Weak};
@@ -20,35 +21,19 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
             project_id,
             worktree_root_names,
         } => {
-            const PADDING: f32 = 16.;
             let theme = &theme::current(cx).project_shared_notification;
             let window_size = vec2f(theme.window_width, theme.window_height);
 
             for screen in cx.platform().screens() {
-                let screen_bounds = screen.bounds();
-                let window = cx.add_window(
-                    WindowOptions {
-                        bounds: WindowBounds::Fixed(RectF::new(
-                            screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
-                            window_size,
-                        )),
-                        titlebar: None,
-                        center: false,
-                        focus: false,
-                        show: true,
-                        kind: WindowKind::PopUp,
-                        is_movable: false,
-                        screen: Some(screen),
-                    },
-                    |_| {
+                let window =
+                    cx.add_window(notification_window_options(screen, window_size), |_| {
                         ProjectSharedNotification::new(
                             owner.clone(),
                             *project_id,
                             worktree_root_names.clone(),
                             app_state.clone(),
                         )
-                    },
-                );
+                    });
                 notification_windows
                     .entry(*project_id)
                     .or_insert(Vec::new())

crates/component_test/Cargo.toml 🔗

@@ -0,0 +1,18 @@
+[package]
+name = "component_test"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/component_test.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+gpui = { path = "../gpui" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+project = { path = "../project" }

crates/component_test/src/component_test.rs 🔗

@@ -0,0 +1,121 @@
+use gpui::{
+    actions,
+    elements::{Component, Flex, ParentElement, SafeStylable},
+    AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use project::Project;
+use theme::components::{action_button::Button, label::Label, ComponentExt};
+use workspace::{
+    item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId,
+};
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(ComponentTest::toggle_disclosure);
+    cx.add_action(ComponentTest::toggle_toggle);
+    cx.add_action(ComponentTest::deploy);
+    register_deserializable_item::<ComponentTest>(cx);
+}
+
+actions!(
+    test,
+    [NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest]
+);
+
+struct ComponentTest {
+    disclosed: bool,
+    toggled: bool,
+}
+
+impl ComponentTest {
+    fn new() -> Self {
+        Self {
+            disclosed: false,
+            toggled: false,
+        }
+    }
+
+    fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext<Workspace>) {
+        workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx);
+    }
+
+    fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext<Self>) {
+        self.disclosed = !self.disclosed;
+        cx.notify();
+    }
+
+    fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext<Self>) {
+        self.toggled = !self.toggled;
+        cx.notify();
+    }
+}
+
+impl Entity for ComponentTest {
+    type Event = ();
+}
+
+impl View for ComponentTest {
+    fn ui_name() -> &'static str {
+        "Component Test"
+    }
+
+    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
+        let theme = theme::current(cx);
+
+        PaneBackdrop::new(
+            cx.view_id(),
+            Flex::column()
+                .with_spacing(10.)
+                .with_child(
+                    Button::action(NoAction)
+                        .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
+                        .with_contents(Label::new("Click me!"))
+                        .with_style(theme.component_test.button.clone())
+                        .element(),
+                )
+                .with_child(
+                    Button::action(ToggleToggle)
+                        .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
+                        .with_contents(Label::new("Toggle me!"))
+                        .toggleable(self.toggled)
+                        .with_style(theme.component_test.toggle.clone())
+                        .element(),
+                )
+                .with_child(
+                    Label::new("A disclosure")
+                        .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure))
+                        .with_style(theme.component_test.disclosure.clone())
+                        .element(),
+                )
+                .constrained()
+                .with_width(200.)
+                .aligned()
+                .into_any(),
+        )
+        .into_any()
+    }
+}
+
+impl Item for ComponentTest {
+    fn tab_content<V: 'static>(
+        &self,
+        _: Option<usize>,
+        style: &theme::Tab,
+        _: &AppContext,
+    ) -> gpui::AnyElement<V> {
+        gpui::elements::Label::new("Component test", style.label.clone()).into_any()
+    }
+
+    fn serialized_item_kind() -> Option<&'static str> {
+        Some("ComponentTest")
+    }
+
+    fn deserialize(
+        _project: ModelHandle<Project>,
+        _workspace: WeakViewHandle<Workspace>,
+        _workspace_id: WorkspaceId,
+        _item_id: ItemId,
+        cx: &mut ViewContext<Pane>,
+    ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+        Task::ready(Ok(cx.add_view(|_| Self::new())))
+    }
+}

crates/copilot/src/copilot.rs 🔗

@@ -980,7 +980,7 @@ mod tests {
         deterministic.forbid_parking();
         let (copilot, mut lsp) = Copilot::fake(cx);
 
-        let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx));
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello"));
         let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
         assert_eq!(
@@ -996,7 +996,7 @@ mod tests {
             }
         );
 
-        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx));
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye"));
         let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
         assert_eq!(
@@ -1188,7 +1188,7 @@ mod tests {
             _: u64,
             _: &clock::Global,
             _: language::RopeFingerprint,
-            _: ::fs::LineEnding,
+            _: language::LineEnding,
             _: std::time::SystemTime,
             _: &mut AppContext,
         ) {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -538,7 +538,7 @@ impl ProjectDiagnosticsEditor {
 }
 
 impl Item for ProjectDiagnosticsEditor {
-    fn tab_content<T: View>(
+    fn tab_content<T: 'static>(
         &self,
         _detail: Option<usize>,
         style: &theme::Tab,
@@ -735,7 +735,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
     })
 }
 
-pub(crate) fn render_summary<T: View>(
+pub(crate) fn render_summary<T: 'static>(
     summary: &DiagnosticSummary,
     text_style: &TextStyle,
     theme: &theme::ProjectDiagnostics,

crates/diagnostics/src/items.rs 🔗

@@ -105,7 +105,7 @@ impl View for DiagnosticIndicator {
                 let mut summary_row = Flex::row();
                 if self.summary.error_count > 0 {
                     summary_row.add_child(
-                        Svg::new("icons/circle_x_mark_16.svg")
+                        Svg::new("icons/error.svg")
                             .with_color(style.icon_color_error)
                             .constrained()
                             .with_width(style.icon_width)
@@ -121,7 +121,7 @@ impl View for DiagnosticIndicator {
 
                 if self.summary.warning_count > 0 {
                     summary_row.add_child(
-                        Svg::new("icons/triangle_exclamation_16.svg")
+                        Svg::new("icons/warning.svg")
                             .with_color(style.icon_color_warning)
                             .constrained()
                             .with_width(style.icon_width)
@@ -142,7 +142,7 @@ impl View for DiagnosticIndicator {
 
                 if self.summary.error_count == 0 && self.summary.warning_count == 0 {
                     summary_row.add_child(
-                        Svg::new("icons/circle_check_16.svg")
+                        Svg::new("icons/check_circle.svg")
                             .with_color(style.icon_color_ok)
                             .constrained()
                             .with_width(style.icon_width)

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -11,7 +11,7 @@ use gpui::{
 
 const DEAD_ZONE: f32 = 4.;
 
-enum State<V: View> {
+enum State<V> {
     Down {
         region_offset: Vector2F,
         region: RectF,
@@ -31,7 +31,7 @@ enum State<V: View> {
     Canceled,
 }
 
-impl<V: View> Clone for State<V> {
+impl<V> Clone for State<V> {
     fn clone(&self) -> Self {
         match self {
             &State::Down {
@@ -68,12 +68,12 @@ impl<V: View> Clone for State<V> {
     }
 }
 
-pub struct DragAndDrop<V: View> {
+pub struct DragAndDrop<V> {
     containers: HashSet<WeakViewHandle<V>>,
     currently_dragged: Option<State<V>>,
 }
 
-impl<V: View> Default for DragAndDrop<V> {
+impl<V> Default for DragAndDrop<V> {
     fn default() -> Self {
         Self {
             containers: Default::default(),
@@ -82,7 +82,7 @@ impl<V: View> Default for DragAndDrop<V> {
     }
 }
 
-impl<V: View> DragAndDrop<V> {
+impl<V: 'static> DragAndDrop<V> {
     pub fn register_container(&mut self, handle: WeakViewHandle<V>) {
         self.containers.insert(handle);
     }
@@ -291,7 +291,7 @@ impl<V: View> DragAndDrop<V> {
     }
 }
 
-pub trait Draggable<V: View> {
+pub trait Draggable<V> {
     fn as_draggable<D: View, P: Any>(
         self,
         payload: P,
@@ -301,7 +301,7 @@ pub trait Draggable<V: View> {
         Self: Sized;
 }
 
-impl<V: View> Draggable<V> for MouseEventHandler<V> {
+impl<V: 'static> Draggable<V> for MouseEventHandler<V> {
     fn as_draggable<D: View, P: Any>(
         self,
         payload: P,
@@ -37,10 +37,7 @@ impl BlinkManager {
     }
 
     pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
-        if !self.visible {
-            self.visible = true;
-            cx.notify();
-        }
+        self.show_cursor(cx);
 
         let epoch = self.next_blink_epoch();
         let interval = self.blink_interval;
@@ -82,7 +79,13 @@ impl BlinkManager {
                 })
                 .detach();
             }
-        } else if !self.visible {
+        } else {
+            self.show_cursor(cx);
+        }
+    }
+
+    pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) {
+        if !self.visible {
             self.visible = true;
             cx.notify();
         }

crates/editor/src/display_map.rs 🔗

@@ -4,7 +4,10 @@ mod inlay_map;
 mod tab_map;
 mod wrap_map;
 
-use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
+use crate::{
+    link_go_to_definition::{DocumentRange, InlayRange},
+    Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+};
 pub use block_map::{BlockMap, BlockPoint};
 use collections::{HashMap, HashSet};
 use fold_map::FoldMap;
@@ -27,7 +30,8 @@ pub use block_map::{
     BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
 };
 
-pub use self::inlay_map::Inlay;
+pub use self::fold_map::FoldPoint;
+pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum FoldStatus {
@@ -39,7 +43,7 @@ pub trait ToDisplayPoint {
     fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
 }
 
-type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
+type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<DocumentRange>)>>;
 
 pub struct DisplayMap {
     buffer: ModelHandle<MultiBuffer>,
@@ -211,11 +215,28 @@ impl DisplayMap {
         ranges: Vec<Range<Anchor>>,
         style: HighlightStyle,
     ) {
-        self.text_highlights
-            .insert(Some(type_id), Arc::new((style, ranges)));
+        self.text_highlights.insert(
+            Some(type_id),
+            Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())),
+        );
     }
 
-    pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
+    pub fn highlight_inlays(
+        &mut self,
+        type_id: TypeId,
+        ranges: Vec<InlayRange>,
+        style: HighlightStyle,
+    ) {
+        self.text_highlights.insert(
+            Some(type_id),
+            Arc::new((
+                style,
+                ranges.into_iter().map(DocumentRange::Inlay).collect(),
+            )),
+        );
+    }
+
+    pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> {
         let highlights = self.text_highlights.get(&Some(type_id))?;
         Some((highlights.0, &highlights.1))
     }
@@ -223,7 +244,7 @@ impl DisplayMap {
     pub fn clear_text_highlights(
         &mut self,
         type_id: TypeId,
-    ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
+    ) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
         self.text_highlights.remove(&Some(type_id))
     }
 
@@ -290,7 +311,7 @@ impl DisplayMap {
 
 pub struct DisplaySnapshot {
     pub buffer_snapshot: MultiBufferSnapshot,
-    fold_snapshot: fold_map::FoldSnapshot,
+    pub fold_snapshot: fold_map::FoldSnapshot,
     inlay_snapshot: inlay_map::InlaySnapshot,
     tab_snapshot: tab_map::TabSnapshot,
     wrap_snapshot: wrap_map::WrapSnapshot,
@@ -387,12 +408,49 @@ impl DisplaySnapshot {
     }
 
     fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
+        self.inlay_snapshot
+            .to_buffer_point(self.display_point_to_inlay_point(point, bias))
+    }
+
+    pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset {
+        self.inlay_snapshot
+            .to_offset(self.display_point_to_inlay_point(point, bias))
+    }
+
+    pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset {
+        self.inlay_snapshot
+            .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
+    }
+
+    pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint {
+        let inlay_point = self.inlay_snapshot.to_point(offset);
+        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+        let tab_point = self.tab_snapshot.to_tab_point(fold_point);
+        let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
+        let block_point = self.block_snapshot.to_block_point(wrap_point);
+        DisplayPoint(block_point)
+    }
+
+    fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
         let block_point = point.0;
         let wrap_point = self.block_snapshot.to_wrap_point(block_point);
         let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
         let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
-        let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
-        self.inlay_snapshot.to_buffer_point(inlay_point)
+        fold_point.to_inlay_point(&self.fold_snapshot)
+    }
+
+    pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
+        let block_point = point.0;
+        let wrap_point = self.block_snapshot.to_wrap_point(block_point);
+        let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
+        self.tab_snapshot.to_fold_point(tab_point, bias).0
+    }
+
+    pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
+        let tab_point = self.tab_snapshot.to_tab_point(fold_point);
+        let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
+        let block_point = self.block_snapshot.to_block_point(wrap_point);
+        DisplayPoint(block_point)
     }
 
     pub fn max_point(&self) -> DisplayPoint {
@@ -428,15 +486,15 @@ impl DisplaySnapshot {
         &self,
         display_rows: Range<u32>,
         language_aware: bool,
-        hint_highlights: Option<HighlightStyle>,
-        suggestion_highlights: Option<HighlightStyle>,
+        hint_highlight_style: Option<HighlightStyle>,
+        suggestion_highlight_style: Option<HighlightStyle>,
     ) -> DisplayChunks<'_> {
         self.block_snapshot.chunks(
             display_rows,
             language_aware,
             Some(&self.text_highlights),
-            hint_highlights,
-            suggestion_highlights,
+            hint_highlight_style,
+            suggestion_highlight_style,
         )
     }
 
@@ -757,7 +815,7 @@ impl DisplaySnapshot {
     #[cfg(any(test, feature = "test-support"))]
     pub fn highlight_ranges<Tag: ?Sized + 'static>(
         &self,
-    ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
+    ) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
         let type_id = TypeId::of::<Tag>();
         self.text_highlights.get(&Some(type_id)).cloned()
     }
@@ -1319,7 +1377,8 @@ pub mod tests {
 
         cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
 
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+        let buffer = cx
+            .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
         buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
@@ -1408,7 +1467,8 @@ pub mod tests {
 
         cx.update(|cx| init_test(cx, |_| {}));
 
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+        let buffer = cx
+            .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
         buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
@@ -1480,7 +1540,8 @@ pub mod tests {
 
         let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
 
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+        let buffer = cx
+            .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
         buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
 
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));

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

@@ -589,8 +589,8 @@ impl BlockSnapshot {
         rows: Range<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        hint_highlights: Option<HighlightStyle>,
-        suggestion_highlights: Option<HighlightStyle>,
+        hint_highlight_style: Option<HighlightStyle>,
+        suggestion_highlight_style: Option<HighlightStyle>,
     ) -> BlockChunks<'a> {
         let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -623,8 +623,8 @@ impl BlockSnapshot {
                 input_start..input_end,
                 language_aware,
                 text_highlights,
-                hint_highlights,
-                suggestion_highlights,
+                hint_highlight_style,
+                suggestion_highlight_style,
             ),
             input_chunk: Default::default(),
             transforms: cursor,

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

@@ -652,8 +652,8 @@ impl FoldSnapshot {
         range: Range<FoldOffset>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        hint_highlights: Option<HighlightStyle>,
-        suggestion_highlights: Option<HighlightStyle>,
+        hint_highlight_style: Option<HighlightStyle>,
+        suggestion_highlight_style: Option<HighlightStyle>,
     ) -> FoldChunks<'a> {
         let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
 
@@ -675,8 +675,8 @@ impl FoldSnapshot {
                 inlay_start..inlay_end,
                 language_aware,
                 text_highlights,
-                hint_highlights,
-                suggestion_highlights,
+                hint_highlight_style,
+                suggestion_highlight_style,
             ),
             inlay_chunk: None,
             inlay_offset: inlay_start,

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

@@ -1,4 +1,5 @@
 use crate::{
+    link_go_to_definition::DocumentRange,
     multi_buffer::{MultiBufferChunks, MultiBufferRows},
     Anchor, InlayId, MultiBufferSnapshot, ToOffset,
 };
@@ -183,7 +184,7 @@ pub struct InlayBufferRows<'a> {
     max_buffer_row: u32,
 }
 
-#[derive(Copy, Clone, Eq, PartialEq)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
 struct HighlightEndpoint {
     offset: InlayOffset,
     is_start: bool,
@@ -210,6 +211,7 @@ pub struct InlayChunks<'a> {
     buffer_chunks: MultiBufferChunks<'a>,
     buffer_chunk: Option<Chunk<'a>>,
     inlay_chunks: Option<text::Chunks<'a>>,
+    inlay_chunk: Option<&'a str>,
     output_offset: InlayOffset,
     max_output_offset: InlayOffset,
     hint_highlight_style: Option<HighlightStyle>,
@@ -297,13 +299,31 @@ impl<'a> Iterator for InlayChunks<'a> {
                         - self.transforms.start().0;
                     inlay.text.chunks_in_range(start.0..end.0)
                 });
+                let inlay_chunk = self
+                    .inlay_chunk
+                    .get_or_insert_with(|| inlay_chunks.next().unwrap());
+                let (chunk, remainder) = inlay_chunk.split_at(
+                    inlay_chunk
+                        .len()
+                        .min(next_highlight_endpoint.0 - self.output_offset.0),
+                );
+                *inlay_chunk = remainder;
+                if inlay_chunk.is_empty() {
+                    self.inlay_chunk = None;
+                }
 
-                let chunk = inlay_chunks.next().unwrap();
                 self.output_offset.0 += chunk.len();
-                let highlight_style = match inlay.id {
+                let mut highlight_style = match inlay.id {
                     InlayId::Suggestion(_) => self.suggestion_highlight_style,
                     InlayId::Hint(_) => self.hint_highlight_style,
                 };
+                if !self.active_highlights.is_empty() {
+                    for active_highlight in self.active_highlights.values() {
+                        highlight_style
+                            .get_or_insert(Default::default())
+                            .highlight(*active_highlight);
+                    }
+                }
                 Chunk {
                     text: chunk,
                     highlight_style,
@@ -973,8 +993,8 @@ impl InlaySnapshot {
         range: Range<InlayOffset>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        hint_highlights: Option<HighlightStyle>,
-        suggestion_highlights: Option<HighlightStyle>,
+        hint_highlight_style: Option<HighlightStyle>,
+        suggestion_highlight_style: Option<HighlightStyle>,
     ) -> InlayChunks<'a> {
         let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
         cursor.seek(&range.start, Bias::Right, &());
@@ -983,52 +1003,56 @@ impl InlaySnapshot {
         if let Some(text_highlights) = text_highlights {
             if !text_highlights.is_empty() {
                 while cursor.start().0 < range.end {
-                    if true {
-                        let transform_start = self.buffer.anchor_after(
-                            self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
-                        );
-
-                        let transform_end = {
-                            let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
-                            self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
-                                cursor.end(&()).0,
-                                cursor.start().0 + overshoot,
-                            )))
+                    let transform_start = self.buffer.anchor_after(
+                        self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
+                    );
+                    let transform_start =
+                        self.to_inlay_offset(transform_start.to_offset(&self.buffer));
+
+                    let transform_end = {
+                        let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
+                        self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
+                            cursor.end(&()).0,
+                            cursor.start().0 + overshoot,
+                        )))
+                    };
+                    let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer));
+
+                    for (tag, text_highlights) in text_highlights.iter() {
+                        let style = text_highlights.0;
+                        let ranges = &text_highlights.1;
+
+                        let start_ix = match ranges.binary_search_by(|probe| {
+                            let cmp = self
+                                .document_to_inlay_range(probe)
+                                .end
+                                .cmp(&transform_start);
+                            if cmp.is_gt() {
+                                cmp::Ordering::Greater
+                            } else {
+                                cmp::Ordering::Less
+                            }
+                        }) {
+                            Ok(i) | Err(i) => i,
                         };
-
-                        for (tag, highlights) in text_highlights.iter() {
-                            let style = highlights.0;
-                            let ranges = &highlights.1;
-
-                            let start_ix = match ranges.binary_search_by(|probe| {
-                                let cmp = probe.end.cmp(&transform_start, &self.buffer);
-                                if cmp.is_gt() {
-                                    cmp::Ordering::Greater
-                                } else {
-                                    cmp::Ordering::Less
-                                }
-                            }) {
-                                Ok(i) | Err(i) => i,
-                            };
-                            for range in &ranges[start_ix..] {
-                                if range.start.cmp(&transform_end, &self.buffer).is_ge() {
-                                    break;
-                                }
-
-                                highlight_endpoints.push(HighlightEndpoint {
-                                    offset: self
-                                        .to_inlay_offset(range.start.to_offset(&self.buffer)),
-                                    is_start: true,
-                                    tag: *tag,
-                                    style,
-                                });
-                                highlight_endpoints.push(HighlightEndpoint {
-                                    offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
-                                    is_start: false,
-                                    tag: *tag,
-                                    style,
-                                });
+                        for range in &ranges[start_ix..] {
+                            let range = self.document_to_inlay_range(range);
+                            if range.start.cmp(&transform_end).is_ge() {
+                                break;
                             }
+
+                            highlight_endpoints.push(HighlightEndpoint {
+                                offset: range.start,
+                                is_start: true,
+                                tag: *tag,
+                                style,
+                            });
+                            highlight_endpoints.push(HighlightEndpoint {
+                                offset: range.end,
+                                is_start: false,
+                                tag: *tag,
+                                style,
+                            });
                         }
                     }
 
@@ -1046,17 +1070,30 @@ impl InlaySnapshot {
             transforms: cursor,
             buffer_chunks,
             inlay_chunks: None,
+            inlay_chunk: None,
             buffer_chunk: None,
             output_offset: range.start,
             max_output_offset: range.end,
-            hint_highlight_style: hint_highlights,
-            suggestion_highlight_style: suggestion_highlights,
+            hint_highlight_style,
+            suggestion_highlight_style,
             highlight_endpoints: highlight_endpoints.into_iter().peekable(),
             active_highlights: Default::default(),
             snapshot: self,
         }
     }
 
+    fn document_to_inlay_range(&self, range: &DocumentRange) -> Range<InlayOffset> {
+        match range {
+            DocumentRange::Text(text_range) => {
+                self.to_inlay_offset(text_range.start.to_offset(&self.buffer))
+                    ..self.to_inlay_offset(text_range.end.to_offset(&self.buffer))
+            }
+            DocumentRange::Inlay(inlay_range) => {
+                inlay_range.highlight_start..inlay_range.highlight_end
+            }
+        }
+    }
+
     #[cfg(test)]
     pub fn text(&self) -> String {
         self.chunks(Default::default()..self.len(), false, None, None, None)
@@ -1107,13 +1144,12 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{InlayId, MultiBuffer};
+    use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer};
     use gpui::AppContext;
-    use project::{InlayHint, InlayHintLabel};
+    use project::{InlayHint, InlayHintLabel, ResolveState};
     use rand::prelude::*;
     use settings::SettingsStore;
     use std::{cmp::Reverse, env, sync::Arc};
-    use sum_tree::TreeMap;
     use text::Patch;
     use util::post_inc;
 
@@ -1125,12 +1161,12 @@ mod tests {
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String("a".to_string()),
-                    buffer_id: 0,
                     position: text::Anchor::default(),
                     padding_left: false,
                     padding_right: false,
                     tooltip: None,
                     kind: None,
+                    resolve_state: ResolveState::Resolved,
                 },
             )
             .text
@@ -1145,12 +1181,12 @@ mod tests {
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String("a".to_string()),
-                    buffer_id: 0,
                     position: text::Anchor::default(),
                     padding_left: true,
                     padding_right: true,
                     tooltip: None,
                     kind: None,
+                    resolve_state: ResolveState::Resolved,
                 },
             )
             .text
@@ -1165,12 +1201,12 @@ mod tests {
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String(" a ".to_string()),
-                    buffer_id: 0,
                     position: text::Anchor::default(),
                     padding_left: false,
                     padding_right: false,
                     tooltip: None,
                     kind: None,
+                    resolve_state: ResolveState::Resolved,
                 },
             )
             .text
@@ -1185,12 +1221,12 @@ mod tests {
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String(" a ".to_string()),
-                    buffer_id: 0,
                     position: text::Anchor::default(),
                     padding_left: true,
                     padding_right: true,
                     tooltip: None,
                     kind: None,
+                    resolve_state: ResolveState::Resolved,
                 },
             )
             .text
@@ -1542,26 +1578,6 @@ mod tests {
         let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
         let mut next_inlay_id = 0;
         log::info!("buffer text: {:?}", buffer_snapshot.text());
-
-        let mut highlights = TreeMap::default();
-        let highlight_count = rng.gen_range(0_usize..10);
-        let mut highlight_ranges = (0..highlight_count)
-            .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
-            .collect::<Vec<_>>();
-        highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
-        log::info!("highlighting ranges {:?}", highlight_ranges);
-        let highlight_ranges = highlight_ranges
-            .into_iter()
-            .map(|range| {
-                buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end)
-            })
-            .collect::<Vec<_>>();
-
-        highlights.insert(
-            Some(TypeId::of::<()>()),
-            Arc::new((HighlightStyle::default(), highlight_ranges)),
-        );
-
         let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
         for _ in 0..operations {
             let mut inlay_edits = Patch::default();
@@ -1624,6 +1640,38 @@ mod tests {
                 );
             }
 
+            let mut highlights = TextHighlights::default();
+            let highlight_count = rng.gen_range(0_usize..10);
+            let mut highlight_ranges = (0..highlight_count)
+                .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
+                .collect::<Vec<_>>();
+            highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+            log::info!("highlighting ranges {:?}", highlight_ranges);
+            let highlight_ranges = if rng.gen_bool(0.5) {
+                highlight_ranges
+                    .into_iter()
+                    .map(|range| InlayRange {
+                        inlay_position: buffer_snapshot.anchor_before(range.start),
+                        highlight_start: inlay_snapshot.to_inlay_offset(range.start),
+                        highlight_end: inlay_snapshot.to_inlay_offset(range.end),
+                    })
+                    .map(DocumentRange::Inlay)
+                    .collect::<Vec<_>>()
+            } else {
+                highlight_ranges
+                    .into_iter()
+                    .map(|range| {
+                        buffer_snapshot.anchor_before(range.start)
+                            ..buffer_snapshot.anchor_after(range.end)
+                    })
+                    .map(DocumentRange::Text)
+                    .collect::<Vec<_>>()
+            };
+            highlights.insert(
+                Some(TypeId::of::<()>()),
+                Arc::new((HighlightStyle::default(), highlight_ranges)),
+            );
+
             for _ in 0..5 {
                 let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
                 end = expected_text.clip_offset(end, Bias::Right);

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

@@ -224,8 +224,8 @@ impl TabSnapshot {
         range: Range<TabPoint>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        hint_highlights: Option<HighlightStyle>,
-        suggestion_highlights: Option<HighlightStyle>,
+        hint_highlight_style: Option<HighlightStyle>,
+        suggestion_highlight_style: Option<HighlightStyle>,
     ) -> TabChunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
             self.to_fold_point(range.start, Bias::Left);
@@ -246,8 +246,8 @@ impl TabSnapshot {
                 input_start..input_end,
                 language_aware,
                 text_highlights,
-                hint_highlights,
-                suggestion_highlights,
+                hint_highlight_style,
+                suggestion_highlight_style,
             ),
             input_column,
             column: expanded_char_column,

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

@@ -576,8 +576,8 @@ impl WrapSnapshot {
         rows: Range<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        hint_highlights: Option<HighlightStyle>,
-        suggestion_highlights: Option<HighlightStyle>,
+        hint_highlight_style: Option<HighlightStyle>,
+        suggestion_highlight_style: Option<HighlightStyle>,
     ) -> WrapChunks<'a> {
         let output_start = WrapPoint::new(rows.start, 0);
         let output_end = WrapPoint::new(rows.end, 0);
@@ -595,8 +595,8 @@ impl WrapSnapshot {
                 input_start..input_end,
                 language_aware,
                 text_highlights,
-                hint_highlights,
-                suggestion_highlights,
+                hint_highlight_style,
+                suggestion_highlight_style,
             ),
             input_chunk: Default::default(),
             output_position: output_start,

crates/editor/src/editor.rs 🔗

@@ -23,7 +23,7 @@ pub mod test;
 
 use ::git::diff::DiffHunk;
 use aho_corasick::AhoCorasick;
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
 use blink_manager::BlinkManager;
 use client::{ClickhouseEvent, TelemetrySettings};
 use clock::{Global, ReplicaId};
@@ -60,21 +60,24 @@ use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
     language_settings::{self, all_language_settings, InlayHintSettings},
-    AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
-    Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
-    OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
+    point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion,
+    CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language,
+    LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal,
+    TransactionId,
 };
 use link_go_to_definition::{
-    hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
+    hide_link_definition, show_link_definition, DocumentRange, GoToDefinitionLink, InlayRange,
+    LinkGoToDefinitionState,
 };
 use log::error;
+use lsp::LanguageServerId;
 use multi_buffer::ToOffsetUtf16;
 pub use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
     ToPoint,
 };
 use ordered_float::OrderedFloat;
-use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
+use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
 use rand::{seq::SliceRandom, thread_rng};
 use scroll::{
     autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
@@ -108,6 +111,8 @@ const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
 const MAX_SELECTION_HISTORY_LEN: usize = 1024;
 const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
+pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
+pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 
@@ -302,10 +307,11 @@ actions!(
         Hover,
         Format,
         ToggleSoftWrap,
+        ToggleInlayHints,
         RevealInFinder,
         CopyPath,
         CopyRelativePath,
-        CopyHighlightJson
+        CopyHighlightJson,
     ]
 );
 
@@ -446,6 +452,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::toggle_code_actions);
     cx.add_action(Editor::open_excerpts);
     cx.add_action(Editor::toggle_soft_wrap);
+    cx.add_action(Editor::toggle_inlay_hints);
     cx.add_action(Editor::reveal_in_finder);
     cx.add_action(Editor::copy_path);
     cx.add_action(Editor::copy_relative_path);
@@ -533,6 +540,8 @@ type CompletionId = usize;
 type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
 type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
 
+type BackgroundHighlight = (fn(&Theme) -> Color, Vec<DocumentRange>);
+
 pub struct Editor {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<MultiBuffer>,
@@ -557,12 +566,12 @@ pub struct Editor {
     blink_manager: ModelHandle<BlinkManager>,
     show_local_selections: bool,
     mode: EditorMode,
+    replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
     show_gutter: bool,
     show_wrap_guides: Option<bool>,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
-    #[allow(clippy::type_complexity)]
-    background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
+    background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
     nav_history: Option<ItemNavHistory>,
     context_menu: Option<ContextMenu>,
     mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
@@ -575,6 +584,7 @@ pub struct Editor {
     searchable: bool,
     cursor_shape: CursorShape,
     collapse_matches: bool,
+    autoindent_mode: Option<AutoindentMode>,
     workspace: Option<(WeakViewHandle<Workspace>, i64)>,
     keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
     input_enabled: bool,
@@ -1332,11 +1342,25 @@ enum GotoDefinitionKind {
 }
 
 #[derive(Debug, Clone)]
-enum InlayRefreshReason {
+enum InlayHintRefreshReason {
+    Toggle(bool),
     SettingsChange(InlayHintSettings),
     NewLinesShown,
     BufferEdited(HashSet<Arc<Language>>),
     RefreshRequested,
+    ExcerptsRemoved(Vec<ExcerptId>),
+}
+impl InlayHintRefreshReason {
+    fn description(&self) -> &'static str {
+        match self {
+            Self::Toggle(_) => "toggle",
+            Self::SettingsChange(_) => "settings change",
+            Self::NewLinesShown => "new lines shown",
+            Self::BufferEdited(_) => "buffer edited",
+            Self::RefreshRequested => "refresh requested",
+            Self::ExcerptsRemoved(_) => "excerpts removed",
+        }
+    }
 }
 
 impl Editor {
@@ -1344,7 +1368,7 @@ impl Editor {
         field_editor_style: Option<Arc<GetFieldEditorTheme>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
         Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx)
     }
@@ -1353,7 +1377,7 @@ impl Editor {
         field_editor_style: Option<Arc<GetFieldEditorTheme>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
         Self::new(EditorMode::Full, buffer, None, field_editor_style, cx)
     }
@@ -1363,7 +1387,7 @@ impl Editor {
         field_editor_style: Option<Arc<GetFieldEditorTheme>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
         Self::new(
             EditorMode::AutoHeight { max_lines },
@@ -1449,8 +1473,8 @@ impl Editor {
                     }));
                 }
                 project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
-                    if let project::Event::RefreshInlays = event {
-                        editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
+                    if let project::Event::RefreshInlayHints = event {
+                        editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
                     };
                 }));
             }
@@ -1485,6 +1509,7 @@ impl Editor {
             blink_manager: blink_manager.clone(),
             show_local_selections: true,
             mode,
+            replica_id_mapping: None,
             show_gutter: mode == EditorMode::Full,
             show_wrap_guides: None,
             placeholder_text: None,
@@ -1504,6 +1529,7 @@ impl Editor {
             searchable: true,
             override_text_style: None,
             cursor_shape: Default::default(),
+            autoindent_mode: Some(AutoindentMode::EachLine),
             collapse_matches: false,
             workspace: None,
             keymap_context_layers: Default::default(),
@@ -1523,6 +1549,16 @@ impl Editor {
                 cx.observe(&display_map, Self::on_display_map_changed),
                 cx.observe(&blink_manager, |_, _, cx| cx.notify()),
                 cx.observe_global::<SettingsStore, _>(Self::settings_changed),
+                cx.observe_window_activation(|editor, active, cx| {
+                    editor.blink_manager.update(cx, |blink_manager, cx| {
+                        if active {
+                            blink_manager.enable(cx);
+                        } else {
+                            blink_manager.show_cursor(cx);
+                            blink_manager.disable(cx);
+                        }
+                    });
+                }),
             ],
         };
 
@@ -1682,10 +1718,40 @@ impl Editor {
         self.input_enabled = input_enabled;
     }
 
+    pub fn set_autoindent(&mut self, autoindent: bool) {
+        if autoindent {
+            self.autoindent_mode = Some(AutoindentMode::EachLine);
+        } else {
+            self.autoindent_mode = None;
+        }
+    }
+
     pub fn set_read_only(&mut self, read_only: bool) {
         self.read_only = read_only;
     }
 
+    pub fn set_field_editor_style(
+        &mut self,
+        style: Option<Arc<GetFieldEditorTheme>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.get_field_editor_theme = style;
+        cx.notify();
+    }
+
+    pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
+        self.replica_id_mapping.as_ref()
+    }
+
+    pub fn set_replica_id_map(
+        &mut self,
+        mapping: Option<HashMap<ReplicaId, ReplicaId>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.replica_id_mapping = mapping;
+        cx.notify();
+    }
+
     fn selections_did_change(
         &mut self,
         local: bool,
@@ -1814,7 +1880,32 @@ impl Editor {
         }
 
         self.buffer.update(cx, |buffer, cx| {
-            buffer.edit(edits, Some(AutoindentMode::EachLine), cx)
+            buffer.edit(edits, self.autoindent_mode.clone(), cx)
+        });
+    }
+
+    pub fn edit_with_block_indent<I, S, T>(
+        &mut self,
+        edits: I,
+        original_indent_columns: Vec<u32>,
+        cx: &mut ViewContext<Self>,
+    ) where
+        I: IntoIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        if self.read_only {
+            return;
+        }
+
+        self.buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                edits,
+                Some(AutoindentMode::Block {
+                    original_indent_columns,
+                }),
+                cx,
+            )
         });
     }
 
@@ -2185,12 +2276,12 @@ impl Editor {
         for (selection, autoclose_region) in
             self.selections_with_autoclose_regions(selections, &snapshot)
         {
-            if let Some(language) = snapshot.language_scope_at(selection.head()) {
+            if let Some(scope) = snapshot.language_scope_at(selection.head()) {
                 // Determine if the inserted text matches the opening or closing
                 // bracket of any of this language's bracket pairs.
                 let mut bracket_pair = None;
                 let mut is_bracket_pair_start = false;
-                for (pair, enabled) in language.brackets() {
+                for (pair, enabled) in scope.brackets() {
                     if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
                         bracket_pair = Some(pair.clone());
                         is_bracket_pair_start = true;
@@ -2212,7 +2303,7 @@ impl Editor {
                             let following_text_allows_autoclose = snapshot
                                 .chars_at(selection.start)
                                 .next()
-                                .map_or(true, |c| language.should_autoclose_before(c));
+                                .map_or(true, |c| scope.should_autoclose_before(c));
                             let preceding_text_matches_prefix = prefix_len == 0
                                 || (selection.start.column >= (prefix_len as u32)
                                     && snapshot.contains_str_at(
@@ -2289,7 +2380,7 @@ impl Editor {
         drop(snapshot);
         self.transact(cx, |this, cx| {
             this.buffer.update(cx, |buffer, cx| {
-                buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
+                buffer.edit(edits, this.autoindent_mode.clone(), cx);
             });
 
             let new_anchor_selections = new_selections.iter().map(|e| &e.0);
@@ -2749,7 +2840,6 @@ impl Editor {
             false
         });
     }
-
     fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
         let offset = position.to_offset(buffer);
         let (word_range, kind) = buffer.surrounding_word(offset);
@@ -2764,13 +2854,42 @@ impl Editor {
         }
     }
 
-    fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
+    pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext<Self>) {
+        self.refresh_inlay_hints(
+            InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled),
+            cx,
+        );
+    }
+
+    pub fn inlay_hints_enabled(&self) -> bool {
+        self.inlay_hint_cache.enabled
+    }
+
+    fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
         if self.project.is_none() || self.mode != EditorMode::Full {
             return;
         }
 
+        let reason_description = reason.description();
         let (invalidate_cache, required_languages) = match reason {
-            InlayRefreshReason::SettingsChange(new_settings) => {
+            InlayHintRefreshReason::Toggle(enabled) => {
+                self.inlay_hint_cache.enabled = enabled;
+                if enabled {
+                    (InvalidationStrategy::RefreshRequested, None)
+                } else {
+                    self.inlay_hint_cache.clear();
+                    self.splice_inlay_hints(
+                        self.visible_inlay_hints(cx)
+                            .iter()
+                            .map(|inlay| inlay.id)
+                            .collect(),
+                        Vec::new(),
+                        cx,
+                    );
+                    return;
+                }
+            }
+            InlayHintRefreshReason::SettingsChange(new_settings) => {
                 match self.inlay_hint_cache.update_settings(
                     &self.buffer,
                     new_settings,
@@ -2788,17 +2907,30 @@ impl Editor {
                     ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
                 }
             }
-            InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
-            InlayRefreshReason::BufferEdited(buffer_languages) => {
+            InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => {
+                if let Some(InlaySplice {
+                    to_remove,
+                    to_insert,
+                }) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
+                {
+                    self.splice_inlay_hints(to_remove, to_insert, cx);
+                }
+                return;
+            }
+            InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
+            InlayHintRefreshReason::BufferEdited(buffer_languages) => {
                 (InvalidationStrategy::BufferEdited, Some(buffer_languages))
             }
-            InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
+            InlayHintRefreshReason::RefreshRequested => {
+                (InvalidationStrategy::RefreshRequested, None)
+            }
         };
 
         if let Some(InlaySplice {
             to_remove,
             to_insert,
         }) = self.inlay_hint_cache.spawn_hint_refresh(
+            reason_description,
             self.excerpt_visible_offsets(required_languages.as_ref(), cx),
             invalidate_cache,
             cx,
@@ -2869,6 +3001,7 @@ impl Editor {
         self.display_map.update(cx, |display_map, cx| {
             display_map.splice_inlays(to_remove, to_insert, cx);
         });
+        cx.notify();
     }
 
     fn trigger_on_type_formatting(
@@ -3100,7 +3233,7 @@ impl Editor {
                 this.buffer.update(cx, |buffer, cx| {
                     buffer.edit(
                         ranges.iter().map(|range| (range.clone(), text)),
-                        Some(AutoindentMode::EachLine),
+                        this.autoindent_mode.clone(),
                         cx,
                     );
                 });
@@ -3277,7 +3410,7 @@ impl Editor {
     }
 
     fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
-        let project = self.project.as_ref()?;
+        let project = self.project.clone()?;
         let buffer = self.buffer.read(cx);
         let newest_selection = self.selections.newest_anchor().clone();
         let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?;
@@ -3286,11 +3419,15 @@ impl Editor {
             return None;
         }
 
-        let actions = project.update(cx, |project, cx| {
-            project.code_actions(&start_buffer, start..end, cx)
-        });
         self.code_actions_task = Some(cx.spawn(|this, mut cx| async move {
-            let actions = actions.await;
+            cx.background().timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT).await;
+
+            let actions = project
+                .update(&mut cx, |project, cx| {
+                    project.code_actions(&start_buffer, start..end, cx)
+                })
+                .await;
+
             this.update(&mut cx, |this, cx| {
                 this.available_code_actions = actions.log_err().and_then(|actions| {
                     if actions.is_empty() {
@@ -3311,7 +3448,7 @@ impl Editor {
             return None;
         }
 
-        let project = self.project.as_ref()?;
+        let project = self.project.clone()?;
         let buffer = self.buffer.read(cx);
         let newest_selection = self.selections.newest_anchor().clone();
         let cursor_position = newest_selection.head();
@@ -3322,12 +3459,19 @@ impl Editor {
             return None;
         }
 
-        let highlights = project.update(cx, |project, cx| {
-            project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
-        });
-
         self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move {
-            if let Some(highlights) = highlights.await.log_err() {
+            cx.background()
+                .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT)
+                .await;
+
+            let highlights = project
+                .update(&mut cx, |project, cx| {
+                    project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
+                })
+                .await
+                .log_err();
+
+            if let Some(highlights) = highlights {
                 this.update(&mut cx, |this, cx| {
                     if this.pending_rename.is_some() {
                         return;
@@ -4795,6 +4939,7 @@ impl Editor {
         let mut clipboard_selections = Vec::with_capacity(selections.len());
         {
             let max_point = buffer.max_point();
+            let mut is_first = true;
             for selection in &mut selections {
                 let is_entire_line = selection.is_empty() || self.selections.line_mode;
                 if is_entire_line {
@@ -4802,6 +4947,11 @@ impl Editor {
                     selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
                     selection.goal = SelectionGoal::None;
                 }
+                if is_first {
+                    is_first = false;
+                } else {
+                    text += "\n";
+                }
                 let mut len = 0;
                 for chunk in buffer.text_for_range(selection.start..selection.end) {
                     text.push_str(chunk);
@@ -4832,6 +4982,7 @@ impl Editor {
         let mut clipboard_selections = Vec::with_capacity(selections.len());
         {
             let max_point = buffer.max_point();
+            let mut is_first = true;
             for selection in selections.iter() {
                 let mut start = selection.start;
                 let mut end = selection.end;
@@ -4840,6 +4991,11 @@ impl Editor {
                     start = Point::new(start.row, 0);
                     end = cmp::min(max_point, Point::new(end.row + 1, 0));
                 }
+                if is_first {
+                    is_first = false;
+                } else {
+                    text += "\n";
+                }
                 let mut len = 0;
                 for chunk in buffer.text_for_range(start..end) {
                     text.push_str(chunk);
@@ -4859,7 +5015,7 @@ impl Editor {
     pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
             if let Some(item) = cx.read_from_clipboard() {
-                let mut clipboard_text = Cow::Borrowed(item.text());
+                let clipboard_text = Cow::Borrowed(item.text());
                 if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
                     let old_selections = this.selections.all::<usize>(cx);
                     let all_selections_were_entire_line =
@@ -4867,18 +5023,7 @@ impl Editor {
                     let first_selection_indent_column =
                         clipboard_selections.first().map(|s| s.first_line_indent);
                     if clipboard_selections.len() != old_selections.len() {
-                        let mut newline_separated_text = String::new();
-                        let mut clipboard_selections = clipboard_selections.drain(..).peekable();
-                        let mut ix = 0;
-                        while let Some(clipboard_selection) = clipboard_selections.next() {
-                            newline_separated_text
-                                .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
-                            ix += clipboard_selection.len;
-                            if clipboard_selections.peek().is_some() {
-                                newline_separated_text.push('\n');
-                            }
-                        }
-                        clipboard_text = Cow::Owned(newline_separated_text);
+                        clipboard_selections.drain(..);
                     }
 
                     this.buffer.update(cx, |buffer, cx| {
@@ -4895,7 +5040,7 @@ impl Editor {
                                 let end_offset = start_offset + clipboard_selection.len;
                                 to_insert = &clipboard_text[start_offset..end_offset];
                                 entire_line = clipboard_selection.is_entire_line;
-                                start_offset = end_offset;
+                                start_offset = end_offset + 1;
                                 original_indent_column =
                                     Some(clipboard_selection.first_line_indent);
                             } else {
@@ -4950,6 +5095,9 @@ impl Editor {
             self.unmark_text(cx);
             self.refresh_copilot_suggestions(true, cx);
             cx.emit(Event::Edited);
+            cx.emit(Event::TransactionUndone {
+                transaction_id: tx_id,
+            });
         }
     }
 
@@ -6255,7 +6403,9 @@ impl Editor {
     ) {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_offsets_with(|snapshot, selection| {
-                let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else {
+                let Some(enclosing_bracket_ranges) =
+                    snapshot.enclosing_bracket_ranges(selection.start..selection.end)
+                else {
                     return;
                 };
 
@@ -6267,7 +6417,8 @@ impl Editor {
                     let close = close.to_inclusive();
                     let length = close.end() - open.start;
                     let inside = selection.start >= open.end && selection.end <= *close.start();
-                    let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head());
+                    let in_bracket_range = open.to_inclusive().contains(&selection.head())
+                        || close.contains(&selection.head());
 
                     // If best is next to a bracket and current isn't, skip
                     if !in_bracket_range && best_in_bracket_range {
@@ -6282,19 +6433,21 @@ impl Editor {
                     best_length = length;
                     best_inside = inside;
                     best_in_bracket_range = in_bracket_range;
-                    best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) {
-                        if inside {
-                            open.end
-                        } else {
-                            open.start
-                        }
-                    } else {
-                        if inside {
-                            *close.start()
+                    best_destination = Some(
+                        if close.contains(&selection.start) && close.contains(&selection.end) {
+                            if inside {
+                                open.end
+                            } else {
+                                open.start
+                            }
                         } else {
-                            *close.end()
-                        }
-                    });
+                            if inside {
+                                *close.start()
+                            } else {
+                                *close.end()
+                            }
+                        },
+                    );
                 }
 
                 if let Some(destination) = best_destination {
@@ -6538,7 +6691,9 @@ impl Editor {
         split: bool,
         cx: &mut ViewContext<Self>,
     ) {
-        let Some(workspace) = self.workspace(cx) else { return };
+        let Some(workspace) = self.workspace(cx) else {
+            return;
+        };
         let buffer = self.buffer.read(cx);
         let head = self.selections.newest::<usize>(cx).head();
         let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) {
@@ -6556,7 +6711,14 @@ impl Editor {
         cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move {
             let definitions = definitions.await?;
             editor.update(&mut cx, |editor, cx| {
-                editor.navigate_to_definitions(definitions, split, cx);
+                editor.navigate_to_definitions(
+                    definitions
+                        .into_iter()
+                        .map(GoToDefinitionLink::Text)
+                        .collect(),
+                    split,
+                    cx,
+                );
             })?;
             Ok::<(), anyhow::Error>(())
         })
@@ -6565,76 +6727,178 @@ impl Editor {
 
     pub fn navigate_to_definitions(
         &mut self,
-        mut definitions: Vec<LocationLink>,
+        mut definitions: Vec<GoToDefinitionLink>,
         split: bool,
         cx: &mut ViewContext<Editor>,
     ) {
-        let Some(workspace) = self.workspace(cx) else { return };
+        let Some(workspace) = self.workspace(cx) else {
+            return;
+        };
         let pane = workspace.read(cx).active_pane().clone();
         // If there is one definition, just open it directly
         if definitions.len() == 1 {
             let definition = definitions.pop().unwrap();
-            let range = definition
-                .target
-                .range
-                .to_offset(definition.target.buffer.read(cx));
-
-            let range = self.range_for_match(&range);
-            if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() {
-                self.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                    s.select_ranges([range]);
-                });
-            } else {
-                cx.window_context().defer(move |cx| {
-                    let target_editor: ViewHandle<Self> = workspace.update(cx, |workspace, cx| {
-                        if split {
-                            workspace.split_project_item(definition.target.buffer.clone(), cx)
+            let target_task = match definition {
+                GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
+                GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+                    self.compute_target_location(lsp_location, server_id, cx)
+                }
+            };
+            cx.spawn(|editor, mut cx| async move {
+                let target = target_task.await.context("target resolution task")?;
+                if let Some(target) = target {
+                    editor.update(&mut cx, |editor, cx| {
+                        let range = target.range.to_offset(target.buffer.read(cx));
+                        let range = editor.range_for_match(&range);
+                        if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() {
+                            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                                s.select_ranges([range]);
+                            });
                         } else {
-                            workspace.open_project_item(definition.target.buffer.clone(), cx)
+                            cx.window_context().defer(move |cx| {
+                                let target_editor: ViewHandle<Self> =
+                                    workspace.update(cx, |workspace, cx| {
+                                        if split {
+                                            workspace.split_project_item(target.buffer.clone(), cx)
+                                        } else {
+                                            workspace.open_project_item(target.buffer.clone(), cx)
+                                        }
+                                    });
+                                target_editor.update(cx, |target_editor, cx| {
+                                    // When selecting a definition in a different buffer, disable the nav history
+                                    // to avoid creating a history entry at the previous cursor location.
+                                    pane.update(cx, |pane, _| pane.disable_history());
+                                    target_editor.change_selections(
+                                        Some(Autoscroll::fit()),
+                                        cx,
+                                        |s| {
+                                            s.select_ranges([range]);
+                                        },
+                                    );
+                                    pane.update(cx, |pane, _| pane.enable_history());
+                                });
+                            });
                         }
-                    });
-                    target_editor.update(cx, |target_editor, cx| {
-                        // When selecting a definition in a different buffer, disable the nav history
-                        // to avoid creating a history entry at the previous cursor location.
-                        pane.update(cx, |pane, _| pane.disable_history());
-                        target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                            s.select_ranges([range]);
-                        });
-                        pane.update(cx, |pane, _| pane.enable_history());
-                    });
-                });
-            }
+                    })
+                } else {
+                    Ok(())
+                }
+            })
+            .detach_and_log_err(cx);
         } else if !definitions.is_empty() {
             let replica_id = self.replica_id(cx);
-            cx.window_context().defer(move |cx| {
-                let title = definitions
-                    .iter()
-                    .find(|definition| definition.origin.is_some())
-                    .and_then(|definition| {
-                        definition.origin.as_ref().map(|origin| {
-                            let buffer = origin.buffer.read(cx);
-                            format!(
-                                "Definitions for {}",
-                                buffer
-                                    .text_for_range(origin.range.clone())
-                                    .collect::<String>()
-                            )
-                        })
+            cx.spawn(|editor, mut cx| async move {
+                let (title, location_tasks) = editor
+                    .update(&mut cx, |editor, cx| {
+                        let title = definitions
+                            .iter()
+                            .find_map(|definition| match definition {
+                                GoToDefinitionLink::Text(link) => {
+                                    link.origin.as_ref().map(|origin| {
+                                        let buffer = origin.buffer.read(cx);
+                                        format!(
+                                            "Definitions for {}",
+                                            buffer
+                                                .text_for_range(origin.range.clone())
+                                                .collect::<String>()
+                                        )
+                                    })
+                                }
+                                GoToDefinitionLink::InlayHint(_, _) => None,
+                            })
+                            .unwrap_or("Definitions".to_string());
+                        let location_tasks = definitions
+                            .into_iter()
+                            .map(|definition| match definition {
+                                GoToDefinitionLink::Text(link) => {
+                                    Task::Ready(Some(Ok(Some(link.target))))
+                                }
+                                GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+                                    editor.compute_target_location(lsp_location, server_id, cx)
+                                }
+                            })
+                            .collect::<Vec<_>>();
+                        (title, location_tasks)
                     })
-                    .unwrap_or("Definitions".to_owned());
-                let locations = definitions
+                    .context("location tasks preparation")?;
+
+                let locations = futures::future::join_all(location_tasks)
+                    .await
                     .into_iter()
-                    .map(|definition| definition.target)
-                    .collect();
-                workspace.update(cx, |workspace, cx| {
+                    .filter_map(|location| location.transpose())
+                    .collect::<Result<_>>()
+                    .context("location tasks")?;
+                workspace.update(&mut cx, |workspace, cx| {
                     Self::open_locations_in_multibuffer(
                         workspace, locations, replica_id, title, split, cx,
                     )
                 });
-            });
+
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
         }
     }
 
+    fn compute_target_location(
+        &self,
+        lsp_location: lsp::Location,
+        server_id: LanguageServerId,
+        cx: &mut ViewContext<Editor>,
+    ) -> Task<anyhow::Result<Option<Location>>> {
+        let Some(project) = self.project.clone() else {
+            return Task::Ready(Some(Ok(None)));
+        };
+
+        cx.spawn(move |editor, mut cx| async move {
+            let location_task = editor.update(&mut cx, |editor, cx| {
+                project.update(cx, |project, cx| {
+                    let language_server_name =
+                        editor.buffer.read(cx).as_singleton().and_then(|buffer| {
+                            project
+                                .language_server_for_buffer(buffer.read(cx), server_id, cx)
+                                .map(|(_, lsp_adapter)| {
+                                    LanguageServerName(Arc::from(lsp_adapter.name()))
+                                })
+                        });
+                    language_server_name.map(|language_server_name| {
+                        project.open_local_buffer_via_lsp(
+                            lsp_location.uri.clone(),
+                            server_id,
+                            language_server_name,
+                            cx,
+                        )
+                    })
+                })
+            })?;
+            let location = match location_task {
+                Some(task) => Some({
+                    let target_buffer_handle = task.await.context("open local buffer")?;
+                    let range = {
+                        target_buffer_handle.update(&mut cx, |target_buffer, _| {
+                            let target_start = target_buffer.clip_point_utf16(
+                                point_from_lsp(lsp_location.range.start),
+                                Bias::Left,
+                            );
+                            let target_end = target_buffer.clip_point_utf16(
+                                point_from_lsp(lsp_location.range.end),
+                                Bias::Left,
+                            );
+                            target_buffer.anchor_after(target_start)
+                                ..target_buffer.anchor_before(target_end)
+                        })
+                    };
+                    Location {
+                        buffer: target_buffer_handle,
+                        range,
+                    }
+                }),
+                None => None,
+            };
+            Ok(location)
+        })
+    }
+
     pub fn find_all_references(
         workspace: &mut Workspace,
         _: &FindAllReferences,
@@ -6770,10 +7034,18 @@ impl Editor {
             let rename_range = if let Some(range) = prepare_rename.await? {
                 Some(range)
             } else {
-                this.read_with(&cx, |this, cx| {
+                this.update(&mut cx, |this, cx| {
                     let buffer = this.buffer.read(cx).snapshot(cx);
+                    let display_snapshot = this
+                        .display_map
+                        .update(cx, |display_map, cx| display_map.snapshot(cx));
                     let mut buffer_highlights = this
-                        .document_highlights_for_position(selection.head(), &buffer)
+                        .document_highlights_for_position(
+                            selection.head(),
+                            &buffer,
+                            &display_snapshot,
+                        )
+                        .filter_map(|highlight| highlight.as_text_range())
                         .filter(|highlight| {
                             highlight.start.excerpt_id() == selection.head().excerpt_id()
                                 && highlight.end.excerpt_id() == selection.head().excerpt_id()
@@ -6828,11 +7100,15 @@ impl Editor {
                     let ranges = this
                         .clear_background_highlights::<DocumentHighlightWrite>(cx)
                         .into_iter()
-                        .flat_map(|(_, ranges)| ranges)
+                        .flat_map(|(_, ranges)| {
+                            ranges.into_iter().filter_map(|range| range.as_text_range())
+                        })
                         .chain(
                             this.clear_background_highlights::<DocumentHighlightRead>(cx)
                                 .into_iter()
-                                .flat_map(|(_, ranges)| ranges),
+                                .flat_map(|(_, ranges)| {
+                                    ranges.into_iter().filter_map(|range| range.as_text_range())
+                                }),
                         )
                         .collect();
 
@@ -7189,7 +7465,7 @@ impl Editor {
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
 
-        let selections = self.selections.all::<Point>(cx);
+        let selections = self.selections.all_adjusted(cx);
         for selection in selections {
             let range = selection.range().sorted();
             let buffer_start_row = range.start.row;
@@ -7265,7 +7541,17 @@ impl Editor {
 
     pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
         let selections = self.selections.all::<Point>(cx);
-        let ranges = selections.into_iter().map(|s| s.start..s.end);
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let line_mode = self.selections.line_mode;
+        let ranges = selections.into_iter().map(|s| {
+            if line_mode {
+                let start = Point::new(s.start.row, 0);
+                let end = Point::new(s.end.row, display_map.buffer_snapshot.line_len(s.end.row));
+                start..end
+            } else {
+                s.start..s.end
+            }
+        });
         self.fold_ranges(ranges, true, cx);
     }
 
@@ -7500,16 +7786,36 @@ impl Editor {
         color_fetcher: fn(&Theme) -> Color,
         cx: &mut ViewContext<Self>,
     ) {
-        self.background_highlights
-            .insert(TypeId::of::<T>(), (color_fetcher, ranges));
+        self.background_highlights.insert(
+            TypeId::of::<T>(),
+            (
+                color_fetcher,
+                ranges.into_iter().map(DocumentRange::Text).collect(),
+            ),
+        );
+        cx.notify();
+    }
+
+    pub fn highlight_inlay_background<T: 'static>(
+        &mut self,
+        ranges: Vec<InlayRange>,
+        color_fetcher: fn(&Theme) -> Color,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.background_highlights.insert(
+            TypeId::of::<T>(),
+            (
+                color_fetcher,
+                ranges.into_iter().map(DocumentRange::Inlay).collect(),
+            ),
+        );
         cx.notify();
     }
 
-    #[allow(clippy::type_complexity)]
     pub fn clear_background_highlights<T: 'static>(
         &mut self,
         cx: &mut ViewContext<Self>,
-    ) -> Option<(fn(&Theme) -> Color, Vec<Range<Anchor>>)> {
+    ) -> Option<BackgroundHighlight> {
         let highlights = self.background_highlights.remove(&TypeId::of::<T>());
         if highlights.is_some() {
             cx.notify();
@@ -7534,7 +7840,8 @@ impl Editor {
         &'a self,
         position: Anchor,
         buffer: &'a MultiBufferSnapshot,
-    ) -> impl 'a + Iterator<Item = &Range<Anchor>> {
+        display_snapshot: &'a DisplaySnapshot,
+    ) -> impl 'a + Iterator<Item = &DocumentRange> {
         let read_highlights = self
             .background_highlights
             .get(&TypeId::of::<DocumentHighlightRead>())
@@ -7543,14 +7850,16 @@ impl Editor {
             .background_highlights
             .get(&TypeId::of::<DocumentHighlightWrite>())
             .map(|h| &h.1);
-        let left_position = position.bias_left(buffer);
-        let right_position = position.bias_right(buffer);
+        let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer));
+        let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer));
         read_highlights
             .into_iter()
             .chain(write_highlights)
             .flat_map(move |ranges| {
                 let start_ix = match ranges.binary_search_by(|probe| {
-                    let cmp = probe.end.cmp(&left_position, buffer);
+                    let cmp = document_to_inlay_range(probe, display_snapshot)
+                        .end
+                        .cmp(&left_position);
                     if cmp.is_ge() {
                         Ordering::Greater
                     } else {

crates/editor/src/editor_settings.rs 🔗

@@ -9,6 +9,7 @@ pub struct EditorSettings {
     pub show_completions_on_input: bool,
     pub use_on_type_format: bool,
     pub scrollbar: Scrollbar,
+    pub relative_line_numbers: bool,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -34,6 +35,7 @@ pub struct EditorSettingsContent {
     pub show_completions_on_input: Option<bool>,
     pub use_on_type_format: Option<bool>,
     pub scrollbar: Option<ScrollbarContent>,
+    pub relative_line_numbers: Option<bool>,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]

crates/editor/src/editor_tests.rs 🔗

@@ -43,7 +43,7 @@ fn test_edit_events(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let buffer = cx.add_model(|cx| {
-        let mut buffer = language::Buffer::new(0, "123456", cx);
+        let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456");
         buffer.set_group_interval(Duration::from_secs(1));
         buffer
     });
@@ -175,7 +175,7 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let mut now = Instant::now();
-    let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
+    let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456"));
     let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
     let editor = cx
@@ -248,7 +248,7 @@ fn test_ime_composition(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let buffer = cx.add_model(|cx| {
-        let mut buffer = language::Buffer::new(0, "abcde", cx);
+        let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde");
         // Ensure automatic grouping doesn't occur.
         buffer.set_group_interval(Duration::ZERO);
         buffer
@@ -1435,6 +1435,74 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let line_height = cx.update_editor(|editor, cx| {
+        editor.set_vertical_scroll_margin(2, cx);
+        editor.style(cx).text.line_height(cx.font_cache())
+    });
+
+    let window = cx.window;
+    window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx);
+
+    cx.set_state(
+        &r#"ˇone
+            two
+            three
+            four
+            five
+            six
+            seven
+            eight
+            nine
+            ten
+        "#,
+    );
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0));
+    });
+
+    // Add a cursor below the visible area. Since both cursors cannot fit
+    // on screen, the editor autoscrolls to reveal the newest cursor, and
+    // allows the vertical scroll margin below that cursor.
+    cx.update_editor(|editor, cx| {
+        editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+            selections.select_ranges([
+                Point::new(0, 0)..Point::new(0, 0),
+                Point::new(6, 0)..Point::new(6, 0),
+            ]);
+        })
+    });
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
+    });
+
+    // Move down. The editor cursor scrolls down to track the newest cursor.
+    cx.update_editor(|editor, cx| {
+        editor.move_down(&Default::default(), cx);
+    });
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0));
+    });
+
+    // Add a cursor above the visible area. Since both cursors fit on screen,
+    // the editor scrolls to show both.
+    cx.update_editor(|editor, cx| {
+        editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+            selections.select_ranges([
+                Point::new(1, 0)..Point::new(1, 0),
+                Point::new(6, 0)..Point::new(6, 0),
+            ]);
+        })
+    });
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0));
+    });
+}
+
 #[gpui::test]
 async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
@@ -2214,10 +2282,12 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
         None,
     ));
 
-    let toml_buffer =
-        cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx));
+    let toml_buffer = cx.add_model(|cx| {
+        Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx)
+    });
     let rust_buffer = cx.add_model(|cx| {
-        Buffer::new(0, "const c: usize = 3;\n", cx).with_language(rust_language, cx)
+        Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n")
+            .with_language(rust_language, cx)
     });
     let multibuffer = cx.add_model(|cx| {
         let mut multibuffer = MultiBuffer::new(0);
@@ -3687,7 +3757,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
     let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
@@ -3850,7 +3921,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
 
     let text = "fn a() {}";
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
     let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor
@@ -4413,7 +4485,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
     let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
@@ -4561,7 +4634,8 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
     let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor
@@ -5238,6 +5312,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
         lsp::ServerCapabilities {
             completion_provider: Some(lsp::CompletionOptions {
                 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                resolve_provider: Some(true),
                 ..Default::default()
             }),
             ..Default::default()
@@ -5766,7 +5841,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
 fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
     let multibuffer = cx.add_model(|cx| {
         let mut multibuffer = MultiBuffer::new(0);
         multibuffer.push_excerpts(
@@ -5850,7 +5925,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
             primary: None,
         }
     });
-    let buffer = cx.add_model(|cx| Buffer::new(0, initial_text, cx));
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text));
     let multibuffer = cx.add_model(|cx| {
         let mut multibuffer = MultiBuffer::new(0);
         multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
@@ -5908,7 +5983,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
 fn test_refresh_selections(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
     let mut excerpt1_id = None;
     let multibuffer = cx.add_model(|cx| {
         let mut multibuffer = MultiBuffer::new(0);
@@ -5995,7 +6070,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
 fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
     let mut excerpt1_id = None;
     let multibuffer = cx.add_model(|cx| {
         let mut multibuffer = MultiBuffer::new(0);
@@ -6092,7 +6167,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
         "{{} }\n",     //
     );
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
     let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
@@ -6384,7 +6460,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
         .update(|cx| {
             Editor::from_state_proto(
                 pane.clone(),
-                project.clone(),
+                workspace.clone(),
                 ViewId {
                     creator: Default::default(),
                     id: 0,
@@ -6479,7 +6555,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
         .update(|cx| {
             Editor::from_state_proto(
                 pane.clone(),
-                project.clone(),
+                workspace.clone(),
                 ViewId {
                     creator: Default::default(),
                     id: 0,
@@ -7092,8 +7168,8 @@ async fn test_copilot_multibuffer(
     let (copilot, copilot_lsp) = Copilot::fake(cx);
     cx.update(|cx| cx.set_global(copilot));
 
-    let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx));
-    let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx));
+    let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n"));
+    let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n"));
     let multibuffer = cx.add_model(|cx| {
         let mut multibuffer = MultiBuffer::new(0);
         multibuffer.push_excerpts(
@@ -7529,6 +7605,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
         lsp::ServerCapabilities {
             completion_provider: Some(lsp::CompletionOptions {
                 trigger_characters: Some(vec![".".to_string()]),
+                resolve_provider: Some(true),
                 ..Default::default()
             }),
             ..Default::default()

crates/editor/src/element.rs 🔗

@@ -13,6 +13,7 @@ use crate::{
     },
     link_go_to_definition::{
         go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
+        update_inlay_link_and_hover_points, GoToDefinitionTrigger,
     },
     mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
 };
@@ -62,6 +63,7 @@ struct SelectionLayout {
     head: DisplayPoint,
     cursor_shape: CursorShape,
     is_newest: bool,
+    is_local: bool,
     range: Range<DisplayPoint>,
     active_rows: Range<u32>,
 }
@@ -73,6 +75,7 @@ impl SelectionLayout {
         cursor_shape: CursorShape,
         map: &DisplaySnapshot,
         is_newest: bool,
+        is_local: bool,
     ) -> Self {
         let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
         let display_selection = point_selection.map(|p| p.to_display_point(map));
@@ -109,6 +112,7 @@ impl SelectionLayout {
             head,
             cursor_shape,
             is_newest,
+            is_local,
             range,
             active_rows,
         }
@@ -284,13 +288,13 @@ impl EditorElement {
             return false;
         }
 
-        let (position, target_position) = position_map.point_for_position(text_bounds, position);
-
+        let point_for_position = position_map.point_for_position(text_bounds, position);
+        let position = point_for_position.previous_valid;
         if shift && alt {
             editor.select(
                 SelectPhase::BeginColumnar {
                     position,
-                    goal_column: target_position.column(),
+                    goal_column: point_for_position.exact_unclipped.column(),
                 },
                 cx,
             );
@@ -326,9 +330,13 @@ impl EditorElement {
         if !text_bounds.contains_point(position) {
             return false;
         }
-
-        let (point, _) = position_map.point_for_position(text_bounds, position);
-        mouse_context_menu::deploy_context_menu(editor, position, point, cx);
+        let point_for_position = position_map.point_for_position(text_bounds, position);
+        mouse_context_menu::deploy_context_menu(
+            editor,
+            position,
+            point_for_position.previous_valid,
+            cx,
+        );
         true
     }
 
@@ -350,17 +358,15 @@ impl EditorElement {
         }
 
         if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
-            let (point, target_point) = position_map.point_for_position(text_bounds, position);
-
-            if point == target_point {
-                if shift {
-                    go_to_fetched_type_definition(editor, point, alt, cx);
-                } else {
-                    go_to_fetched_definition(editor, point, alt, cx);
-                }
-
-                return true;
+            let point = position_map.point_for_position(text_bounds, position);
+            let could_be_inlay = point.as_valid().is_none();
+            if shift || could_be_inlay {
+                go_to_fetched_type_definition(editor, point, alt, cx);
+            } else {
+                go_to_fetched_definition(editor, point, alt, cx);
             }
+
+            return true;
         }
 
         end_selection
@@ -380,17 +386,20 @@ impl EditorElement {
         // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
         // Don't trigger hover popover if mouse is hovering over context menu
         let point = if text_bounds.contains_point(position) {
-            let (point, target_point) = position_map.point_for_position(text_bounds, position);
-            if point == target_point {
-                Some(point)
-            } else {
-                None
-            }
+            position_map
+                .point_for_position(text_bounds, position)
+                .as_valid()
         } else {
             None
         };
 
-        update_go_to_definition_link(editor, point, cmd, shift, cx);
+        update_go_to_definition_link(
+            editor,
+            point.map(GoToDefinitionTrigger::Text),
+            cmd,
+            shift,
+            cx,
+        );
 
         if editor.has_pending_selection() {
             let mut scroll_delta = Vector2F::zero();
@@ -419,13 +428,12 @@ impl EditorElement {
                 ))
             }
 
-            let (position, target_position) =
-                position_map.point_for_position(text_bounds, position);
+            let point_for_position = position_map.point_for_position(text_bounds, position);
 
             editor.select(
                 SelectPhase::Update {
-                    position,
-                    goal_column: target_position.column(),
+                    position: point_for_position.previous_valid,
+                    goal_column: point_for_position.exact_unclipped.column(),
                     scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
                         .clamp(Vector2F::zero(), position_map.scroll_max),
                 },
@@ -452,10 +460,34 @@ impl EditorElement {
     ) -> bool {
         // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
         // Don't trigger hover popover if mouse is hovering over context menu
-        let point = position_to_display_point(position, text_bounds, position_map);
-
-        update_go_to_definition_link(editor, point, cmd, shift, cx);
-        hover_at(editor, point, cx);
+        if text_bounds.contains_point(position) {
+            let point_for_position = position_map.point_for_position(text_bounds, position);
+            match point_for_position.as_valid() {
+                Some(point) => {
+                    update_go_to_definition_link(
+                        editor,
+                        Some(GoToDefinitionTrigger::Text(point)),
+                        cmd,
+                        shift,
+                        cx,
+                    );
+                    hover_at(editor, Some(point), cx);
+                }
+                None => {
+                    update_inlay_link_and_hover_points(
+                        &position_map.snapshot,
+                        point_for_position,
+                        editor,
+                        cmd,
+                        shift,
+                        cx,
+                    );
+                }
+            }
+        } else {
+            update_go_to_definition_link(editor, None, cmd, shift, cx);
+            hover_at(editor, None, cx);
+        }
 
         true
     }
@@ -605,7 +637,7 @@ impl EditorElement {
         visible_bounds: RectF,
         layout: &mut LayoutState,
         editor: &mut Editor,
-        cx: &mut ViewContext<Editor>,
+        cx: &mut PaintContext<Editor>,
     ) {
         let line_height = layout.position_map.line_height;
 
@@ -760,10 +792,9 @@ impl EditorElement {
         visible_bounds: RectF,
         layout: &mut LayoutState,
         editor: &mut Editor,
-        cx: &mut ViewContext<Editor>,
+        cx: &mut PaintContext<Editor>,
     ) {
         let style = &self.style;
-        let local_replica_id = editor.replica_id(cx);
         let scroll_position = layout.position_map.snapshot.scroll_position();
         let start_row = layout.visible_display_row_range.start;
         let scroll_top = scroll_position.y() * layout.position_map.line_height;
@@ -852,15 +883,13 @@ impl EditorElement {
 
         for (replica_id, selections) in &layout.selections {
             let replica_id = *replica_id;
-            let selection_style = style.replica_selection_style(replica_id);
+            let selection_style = if let Some(replica_id) = replica_id {
+                style.replica_selection_style(replica_id)
+            } else {
+                &style.absent_selection
+            };
 
             for selection in selections {
-                if !selection.range.is_empty()
-                    && (replica_id == local_replica_id
-                        || Some(replica_id) == editor.leader_replica_id)
-                {
-                    invisible_display_ranges.push(selection.range.clone());
-                }
                 self.paint_highlighted_range(
                     scene,
                     selection.range.clone(),
@@ -874,7 +903,10 @@ impl EditorElement {
                     bounds,
                 );
 
-                if editor.show_local_cursors(cx) || replica_id != local_replica_id {
+                if selection.is_local && !selection.range.is_empty() {
+                    invisible_display_ranges.push(selection.range.clone());
+                }
+                if !selection.is_local || editor.show_local_cursors(cx) {
                     let cursor_position = selection.head;
                     if layout
                         .visible_display_row_range
@@ -906,7 +938,7 @@ impl EditorElement {
                                         &text,
                                         cursor_row_layout.font_size(),
                                         &[(
-                                            text.len(),
+                                            text.chars().count(),
                                             RunStyle {
                                                 font_id,
                                                 color: style.background,
@@ -1337,7 +1369,7 @@ impl EditorElement {
         visible_bounds: RectF,
         layout: &mut LayoutState,
         editor: &mut Editor,
-        cx: &mut ViewContext<Editor>,
+        cx: &mut PaintContext<Editor>,
     ) {
         let scroll_position = layout.position_map.snapshot.scroll_position();
         let scroll_left = scroll_position.x() * layout.position_map.em_width;
@@ -1405,10 +1437,61 @@ impl EditorElement {
             .collect()
     }
 
+    fn calculate_relative_line_numbers(
+        &self,
+        snapshot: &EditorSnapshot,
+        rows: &Range<u32>,
+        relative_to: Option<u32>,
+    ) -> HashMap<u32, u32> {
+        let mut relative_rows: HashMap<u32, u32> = Default::default();
+        let Some(relative_to) = relative_to else {
+            return relative_rows;
+        };
+
+        let start = rows.start.min(relative_to);
+        let end = rows.end.max(relative_to);
+
+        let buffer_rows = snapshot
+            .buffer_rows(start)
+            .take(1 + (end - start) as usize)
+            .collect::<Vec<_>>();
+
+        let head_idx = relative_to - start;
+        let mut delta = 1;
+        let mut i = head_idx + 1;
+        while i < buffer_rows.len() as u32 {
+            if buffer_rows[i as usize].is_some() {
+                if rows.contains(&(i + start)) {
+                    relative_rows.insert(i + start, delta);
+                }
+                delta += 1;
+            }
+            i += 1;
+        }
+        delta = 1;
+        i = head_idx.min(buffer_rows.len() as u32 - 1);
+        while i > 0 && buffer_rows[i as usize].is_none() {
+            i -= 1;
+        }
+
+        while i > 0 {
+            i -= 1;
+            if buffer_rows[i as usize].is_some() {
+                if rows.contains(&(i + start)) {
+                    relative_rows.insert(i + start, delta);
+                }
+                delta += 1;
+            }
+        }
+
+        relative_rows
+    }
+
     fn layout_line_numbers(
         &self,
         rows: Range<u32>,
         active_rows: &BTreeMap<u32, bool>,
+        newest_selection_head: DisplayPoint,
         is_singleton: bool,
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
@@ -1421,6 +1504,15 @@ impl EditorElement {
         let mut line_number_layouts = Vec::with_capacity(rows.len());
         let mut fold_statuses = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
+        let is_relative = settings::get::<EditorSettings>(cx).relative_line_numbers;
+        let relative_to = if is_relative {
+            Some(newest_selection_head.row())
+        } else {
+            None
+        };
+
+        let relative_rows = self.calculate_relative_line_numbers(&snapshot, &rows, relative_to);
+
         for (ix, row) in snapshot
             .buffer_rows(rows.start)
             .take((rows.end - rows.start) as usize)
@@ -1435,7 +1527,11 @@ impl EditorElement {
             if let Some(buffer_row) = row {
                 if include_line_numbers {
                     line_number.clear();
-                    write!(&mut line_number, "{}", buffer_row + 1).unwrap();
+                    let default_number = buffer_row + 1;
+                    let number = relative_rows
+                        .get(&(ix as u32 + rows.start))
+                        .unwrap_or(&default_number);
+                    write!(&mut line_number, "{}", number).unwrap();
                     line_number_layouts.push(Some(cx.text_layout_cache().layout_str(
                         &line_number,
                         style.text.font_size,
@@ -2079,14 +2175,11 @@ impl Element<Editor> for EditorElement {
                 scroll_height
                     .min(constraint.max_along(Axis::Vertical))
                     .max(constraint.min_along(Axis::Vertical))
+                    .max(line_height)
                     .min(line_height * max_lines as f32),
             )
         } else if let EditorMode::SingleLine = snapshot.mode {
-            size.set_y(
-                line_height
-                    .min(constraint.max_along(Axis::Vertical))
-                    .max(constraint.min_along(Axis::Vertical)),
-            )
+            size.set_y(line_height.max(constraint.min_along(Axis::Vertical)))
         } else if size.y().is_infinite() {
             size.set_y(scroll_height);
         }
@@ -2124,7 +2217,7 @@ impl Element<Editor> for EditorElement {
                 .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
         };
 
-        let mut selections: Vec<(ReplicaId, Vec<SelectionLayout>)> = Vec::new();
+        let mut selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)> = Vec::new();
         let mut active_rows = BTreeMap::new();
         let mut fold_ranges = Vec::new();
         let is_singleton = editor.is_singleton(cx);
@@ -2155,8 +2248,14 @@ impl Element<Editor> for EditorElement {
             .buffer_snapshot
             .remote_selections_in_range(&(start_anchor..end_anchor))
         {
+            let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
+                mapping.get(&replica_id).copied()
+            } else {
+                Some(replica_id)
+            };
+
             // The local selections match the leader's selections.
-            if Some(replica_id) == editor.leader_replica_id {
+            if replica_id.is_some() && replica_id == editor.leader_replica_id {
                 continue;
             }
             remote_selections
@@ -2168,6 +2267,7 @@ impl Element<Editor> for EditorElement {
                     cursor_shape,
                     &snapshot.display_snapshot,
                     false,
+                    false,
                 ));
         }
         selections.extend(remote_selections);
@@ -2191,6 +2291,7 @@ impl Element<Editor> for EditorElement {
                     editor.cursor_shape,
                     &snapshot.display_snapshot,
                     is_newest,
+                    true,
                 );
                 if is_newest {
                     newest_selection_head = Some(layout.head);
@@ -2206,11 +2307,18 @@ impl Element<Editor> for EditorElement {
             }
 
             // Render the local selections in the leader's color when following.
-            let local_replica_id = editor
-                .leader_replica_id
-                .unwrap_or_else(|| editor.replica_id(cx));
+            let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id {
+                leader_replica_id
+            } else {
+                let replica_id = editor.replica_id(cx);
+                if let Some(mapping) = &editor.replica_id_mapping {
+                    mapping.get(&replica_id).copied().unwrap_or(replica_id)
+                } else {
+                    replica_id
+                }
+            };
 
-            selections.push((local_replica_id, layouts));
+            selections.push((Some(local_replica_id), layouts));
         }
 
         let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
@@ -2244,9 +2352,23 @@ impl Element<Editor> for EditorElement {
             })
             .collect();
 
+        let head_for_relative = newest_selection_head.unwrap_or_else(|| {
+            let newest = editor.selections.newest::<Point>(cx);
+            SelectionLayout::new(
+                newest,
+                editor.selections.line_mode,
+                editor.cursor_shape,
+                &snapshot.display_snapshot,
+                true,
+                true,
+            )
+            .head
+        });
+
         let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
             start_row..end_row,
             &active_rows,
+            head_for_relative,
             is_singleton,
             &snapshot,
             cx,
@@ -2591,7 +2713,7 @@ pub struct LayoutState {
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
     fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
-    selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
+    selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)>,
     scrollbar_row_range: Range<f32>,
     show_scrollbars: bool,
     is_singleton: bool,
@@ -2614,22 +2736,42 @@ struct PositionMap {
     snapshot: EditorSnapshot,
 }
 
+#[derive(Debug, Copy, Clone)]
+pub struct PointForPosition {
+    pub previous_valid: DisplayPoint,
+    pub next_valid: DisplayPoint,
+    pub exact_unclipped: DisplayPoint,
+    pub column_overshoot_after_line_end: u32,
+}
+
+impl PointForPosition {
+    #[cfg(test)]
+    pub fn valid(valid: DisplayPoint) -> Self {
+        Self {
+            previous_valid: valid,
+            next_valid: valid,
+            exact_unclipped: valid,
+            column_overshoot_after_line_end: 0,
+        }
+    }
+
+    pub fn as_valid(&self) -> Option<DisplayPoint> {
+        if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
+            Some(self.previous_valid)
+        } else {
+            None
+        }
+    }
+}
+
 impl PositionMap {
-    /// Returns two display points:
-    /// 1. The nearest *valid* position in the editor
-    /// 2. An unclipped, potentially *invalid* position that maps directly to
-    ///    the given pixel position.
-    fn point_for_position(
-        &self,
-        text_bounds: RectF,
-        position: Vector2F,
-    ) -> (DisplayPoint, DisplayPoint) {
+    fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition {
         let scroll_position = self.snapshot.scroll_position();
         let position = position - text_bounds.origin();
         let y = position.y().max(0.0).min(self.size.y());
         let x = position.x() + (scroll_position.x() * self.em_width);
         let row = (y / self.line_height + scroll_position.y()) as u32;
-        let (column, x_overshoot) = if let Some(line) = self
+        let (column, x_overshoot_after_line_end) = if let Some(line) = self
             .line_layouts
             .get(row as usize - scroll_position.y() as usize)
             .map(|line_with_spaces| &line_with_spaces.line)
@@ -2643,11 +2785,18 @@ impl PositionMap {
             (0, x)
         };
 
-        let mut target_point = DisplayPoint::new(row, column);
-        let point = self.snapshot.clip_point(target_point, Bias::Left);
-        *target_point.column_mut() += (x_overshoot / self.em_advance) as u32;
-
-        (point, target_point)
+        let mut exact_unclipped = DisplayPoint::new(row, column);
+        let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
+        let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
+
+        let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32;
+        *exact_unclipped.column_mut() += column_overshoot_after_line_end;
+        PointForPosition {
+            previous_valid,
+            next_valid,
+            exact_unclipped,
+            column_overshoot_after_line_end,
+        }
     }
 }
 
@@ -2901,23 +3050,6 @@ impl HighlightedRange {
     }
 }
 
-fn position_to_display_point(
-    position: Vector2F,
-    text_bounds: RectF,
-    position_map: &PositionMap,
-) -> Option<DisplayPoint> {
-    if text_bounds.contains_point(position) {
-        let (point, target_point) = position_map.point_for_position(text_bounds, position);
-        if point == target_point {
-            Some(point)
-        } else {
-            None
-        }
-    } else {
-        None
-    }
-}
-
 fn range_to_bounds(
     range: &Range<DisplayPoint>,
     content_origin: Vector2F,
@@ -2995,7 +3127,6 @@ mod tests {
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut TestAppContext) {
         init_test(cx, |_| {});
-
         let editor = cx
             .add_window(|cx| {
                 let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
@@ -3007,10 +3138,50 @@ mod tests {
         let layouts = editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);
             element
-                .layout_line_numbers(0..6, &Default::default(), false, &snapshot, cx)
+                .layout_line_numbers(
+                    0..6,
+                    &Default::default(),
+                    DisplayPoint::new(0, 0),
+                    false,
+                    &snapshot,
+                    cx,
+                )
                 .0
         });
         assert_eq!(layouts.len(), 6);
+
+        let relative_rows = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3))
+        });
+        assert_eq!(relative_rows[&0], 3);
+        assert_eq!(relative_rows[&1], 2);
+        assert_eq!(relative_rows[&2], 1);
+        // current line has no relative number
+        assert_eq!(relative_rows[&4], 1);
+        assert_eq!(relative_rows[&5], 2);
+
+        // works if cursor is before screen
+        let relative_rows = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+
+            element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1))
+        });
+        assert_eq!(relative_rows.len(), 3);
+        assert_eq!(relative_rows[&3], 2);
+        assert_eq!(relative_rows[&4], 3);
+        assert_eq!(relative_rows[&5], 4);
+
+        // works if cursor is after screen
+        let relative_rows = editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(cx);
+
+            element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6))
+        });
+        assert_eq!(relative_rows.len(), 3);
+        assert_eq!(relative_rows[&0], 5);
+        assert_eq!(relative_rows[&1], 4);
+        assert_eq!(relative_rows[&2], 3);
     }
 
     #[gpui::test]

crates/editor/src/hover_popover.rs 🔗

@@ -1,6 +1,8 @@
 use crate::{
-    display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings,
-    EditorSnapshot, EditorStyle, RangeToAnchorExt,
+    display_map::{InlayOffset, ToDisplayPoint},
+    link_go_to_definition::{DocumentRange, InlayRange},
+    Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
+    ExcerptId, RangeToAnchorExt,
 };
 use futures::FutureExt;
 use gpui::{
@@ -11,7 +13,7 @@ use gpui::{
     AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
 };
 use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
-use project::{HoverBlock, HoverBlockKind, Project};
+use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
 use std::{ops::Range, sync::Arc, time::Duration};
 use util::TryFutureExt;
 
@@ -46,6 +48,106 @@ pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewC
     }
 }
 
+pub struct InlayHover {
+    pub excerpt: ExcerptId,
+    pub triggered_from: InlayOffset,
+    pub range: InlayRange,
+    pub tooltip: HoverBlock,
+}
+
+pub fn find_hovered_hint_part(
+    label_parts: Vec<InlayHintLabelPart>,
+    hint_range: Range<InlayOffset>,
+    hovered_offset: InlayOffset,
+) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
+    if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end {
+        let mut hovered_character = (hovered_offset - hint_range.start).0;
+        let mut part_start = hint_range.start;
+        for part in label_parts {
+            let part_len = part.value.chars().count();
+            if hovered_character > part_len {
+                hovered_character -= part_len;
+                part_start.0 += part_len;
+            } else {
+                let part_end = InlayOffset(part_start.0 + part_len);
+                return Some((part, part_start..part_end));
+            }
+        }
+    }
+    None
+}
+
+pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
+    if settings::get::<EditorSettings>(cx).hover_popover_enabled {
+        if editor.pending_rename.is_some() {
+            return;
+        }
+
+        let Some(project) = editor.project.clone() else {
+            return;
+        };
+
+        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
+            if let DocumentRange::Inlay(range) = symbol_range {
+                if (range.highlight_start..range.highlight_end)
+                    .contains(&inlay_hover.triggered_from)
+                {
+                    // Hover triggered from same location as last time. Don't show again.
+                    return;
+                }
+            }
+            hide_hover(editor, cx);
+        }
+
+        let snapshot = editor.snapshot(cx);
+        // 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 inlay_hover.triggered_from
+                == snapshot
+                    .display_snapshot
+                    .anchor_to_inlay_offset(triggered_from)
+            {
+                return;
+            }
+        }
+
+        let task = cx.spawn(|this, mut cx| {
+            async move {
+                cx.background()
+                    .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
+                    .await;
+                this.update(&mut cx, |this, _| {
+                    this.hover_state.diagnostic_popover = None;
+                })?;
+
+                let hover_popover = InfoPopover {
+                    project: project.clone(),
+                    symbol_range: DocumentRange::Inlay(inlay_hover.range),
+                    blocks: vec![inlay_hover.tooltip],
+                    language: None,
+                    rendered_content: None,
+                };
+
+                this.update(&mut cx, |this, cx| {
+                    // Highlight the selected symbol using a background highlight
+                    this.highlight_inlay_background::<HoverState>(
+                        vec![inlay_hover.range],
+                        |theme| theme.editor.hover_popover.highlight,
+                        cx,
+                    );
+                    this.hover_state.info_popover = Some(hover_popover);
+                    cx.notify();
+                })?;
+
+                anyhow::Ok(())
+            }
+            .log_err()
+        });
+
+        editor.hover_state.info_task = Some(task);
+    }
+}
+
 /// Hides the type information popup.
 /// Triggered by the `Hover` action when the cursor is not over a symbol or when the
 /// selections changed.
@@ -110,8 +212,13 @@ fn show_hover(
     if !ignore_timeout {
         if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
             if symbol_range
-                .to_offset(&snapshot.buffer_snapshot)
-                .contains(&multibuffer_offset)
+                .as_text_range()
+                .map(|range| {
+                    range
+                        .to_offset(&snapshot.buffer_snapshot)
+                        .contains(&multibuffer_offset)
+                })
+                .unwrap_or(false)
             {
                 // Hover triggered from same location as last time. Don't show again.
                 return;
@@ -219,7 +326,7 @@ fn show_hover(
 
                 Some(InfoPopover {
                     project: project.clone(),
-                    symbol_range: range,
+                    symbol_range: DocumentRange::Text(range),
                     blocks: hover_result.contents,
                     language: hover_result.language,
                     rendered_content: None,
@@ -227,10 +334,13 @@ fn show_hover(
             });
 
             this.update(&mut cx, |this, cx| {
-                if let Some(hover_popover) = hover_popover.as_ref() {
+                if let Some(symbol_range) = hover_popover
+                    .as_ref()
+                    .and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
+                {
                     // Highlight the selected symbol using a background highlight
                     this.highlight_background::<HoverState>(
-                        vec![hover_popover.symbol_range.clone()],
+                        vec![symbol_range],
                         |theme| theme.editor.hover_popover.highlight,
                         cx,
                     );
@@ -497,7 +607,10 @@ impl HoverState {
             .or_else(|| {
                 self.info_popover
                     .as_ref()
-                    .map(|info_popover| &info_popover.symbol_range.start)
+                    .map(|info_popover| match &info_popover.symbol_range {
+                        DocumentRange::Text(range) => &range.start,
+                        DocumentRange::Inlay(range) => &range.inlay_position,
+                    })
             })?;
         let point = anchor.to_display_point(&snapshot.display_snapshot);
 
@@ -522,7 +635,7 @@ impl HoverState {
 #[derive(Debug, Clone)]
 pub struct InfoPopover {
     pub project: ModelHandle<Project>,
-    pub symbol_range: Range<Anchor>,
+    symbol_range: DocumentRange,
     pub blocks: Vec<HoverBlock>,
     language: Option<Arc<Language>>,
     rendered_content: Option<RenderedInfo>,
@@ -692,10 +805,17 @@ impl DiagnosticPopover {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+    use crate::{
+        editor_tests::init_test,
+        element::PointForPosition,
+        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        link_go_to_definition::update_inlay_link_and_hover_points,
+        test::editor_lsp_test_context::EditorLspTestContext,
+    };
+    use collections::BTreeSet;
     use gpui::fonts::Weight;
     use indoc::indoc;
-    use language::{Diagnostic, DiagnosticSet};
+    use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
     use lsp::LanguageServerId;
     use project::{HoverBlock, HoverBlockKind};
     use smol::stream::StreamExt;
@@ -1131,4 +1251,327 @@ mod tests {
             editor
         });
     }
+
+    #[gpui::test]
+    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Right(
+                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
+                        resolve_provider: Some(true),
+                        ..Default::default()
+                    }),
+                )),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variableˇ = TestNewType(TestStruct);
+            }
+        "});
+
+        let hint_start_offset = cx.ranges(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variableˇ = TestNewType(TestStruct);
+            }
+        "})[0]
+            .start;
+        let hint_position = cx.to_lsp(hint_start_offset);
+        let new_type_target_range = cx.lsp_range(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct «TestNewType»<T>(T);
+
+            fn main() {
+                let variable = TestNewType(TestStruct);
+            }
+        "});
+        let struct_target_range = cx.lsp_range(indoc! {"
+            struct «TestStruct»;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variable = TestNewType(TestStruct);
+            }
+        "});
+
+        let uri = cx.buffer_lsp_url.clone();
+        let new_type_label = "TestNewType";
+        let struct_label = "TestStruct";
+        let entire_hint_label = ": TestNewType<TestStruct>";
+        let closure_uri = uri.clone();
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_uri = closure_uri.clone();
+                async move {
+                    assert_eq!(params.text_document.uri, task_uri);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: hint_position,
+                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+                            value: entire_hint_label.to_string(),
+                            ..Default::default()
+                        }]),
+                        kind: Some(lsp::InlayHintKind::TYPE),
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: Some(false),
+                        padding_right: Some(false),
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let expected_layers = vec![entire_hint_label.to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+        });
+
+        let inlay_range = cx
+            .ranges(indoc! {"
+                struct TestStruct;
+
+                // ==================
+
+                struct TestNewType<T>(T);
+
+                fn main() {
+                    let variable« »= TestNewType(TestStruct);
+                }
+        "})
+            .get(0)
+            .cloned()
+            .unwrap();
+        let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column()
+                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
+                        as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                new_type_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+
+        let resolve_closure_uri = uri.clone();
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
+                move |mut hint_to_resolve, _| {
+                    let mut resolved_hint_positions = BTreeSet::new();
+                    let task_uri = resolve_closure_uri.clone();
+                    async move {
+                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
+                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
+
+                        // `: TestNewType<TestStruct>`
+                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
+                            lsp::InlayHintLabelPart {
+                                value: ": ".to_string(),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: new_type_label.to_string(),
+                                location: Some(lsp::Location {
+                                    uri: task_uri.clone(),
+                                    range: new_type_target_range,
+                                }),
+                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
+                                    "A tooltip for `{new_type_label}`"
+                                ))),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: "<".to_string(),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: struct_label.to_string(),
+                                location: Some(lsp::Location {
+                                    uri: task_uri,
+                                    range: struct_target_range,
+                                }),
+                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
+                                    lsp::MarkupContent {
+                                        kind: lsp::MarkupKind::Markdown,
+                                        value: format!("A tooltip for `{struct_label}`"),
+                                    },
+                                )),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: ">".to_string(),
+                                ..Default::default()
+                            },
+                        ]);
+
+                        Ok(hint_to_resolve)
+                    }
+                },
+            )
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                new_type_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground()
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let hover_state = &editor.hover_state;
+            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
+            let popover = hover_state.info_popover.as_ref().unwrap();
+            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            let entire_inlay_start = snapshot.display_point_to_inlay_offset(
+                inlay_range.start.to_display_point(&snapshot),
+                Bias::Left,
+            );
+
+            let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len());
+            assert_eq!(
+                popover.symbol_range,
+                DocumentRange::Inlay(InlayRange {
+                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                    highlight_start: expected_new_type_label_start,
+                    highlight_end: InlayOffset(
+                        expected_new_type_label_start.0 + new_type_label.len()
+                    ),
+                }),
+                "Popover range should match the new type label part"
+            );
+            assert_eq!(
+                popover
+                    .rendered_content
+                    .as_ref()
+                    .expect("should have label text for new type hint")
+                    .text,
+                format!("A tooltip for `{new_type_label}`"),
+                "Rendered text should not anyhow alter backticks"
+            );
+        });
+
+        let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column()
+                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
+                        as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                struct_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground()
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let hover_state = &editor.hover_state;
+            assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
+            let popover = hover_state.info_popover.as_ref().unwrap();
+            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            let entire_inlay_start = snapshot.display_point_to_inlay_offset(
+                inlay_range.start.to_display_point(&snapshot),
+                Bias::Left,
+            );
+            let expected_struct_label_start =
+                InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len());
+            assert_eq!(
+                popover.symbol_range,
+                DocumentRange::Inlay(InlayRange {
+                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                    highlight_start: expected_struct_label_start,
+                    highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()),
+                }),
+                "Popover range should match the struct label part"
+            );
+            assert_eq!(
+                popover
+                    .rendered_content
+                    .as_ref()
+                    .expect("should have label text for struct hint")
+                    .text,
+                format!("A tooltip for {struct_label}"),
+                "Rendered markdown element should remove backticks from text"
+            );
+        });
+    }
 }

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -2,6 +2,7 @@ use std::{
     cmp,
     ops::{ControlFlow, Range},
     sync::Arc,
+    time::Duration,
 };
 
 use crate::{
@@ -9,23 +10,26 @@ use crate::{
 };
 use anyhow::Context;
 use clock::Global;
+use futures::future;
 use gpui::{ModelContext, ModelHandle, Task, ViewContext};
 use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
-use log::error;
 use parking_lot::RwLock;
-use project::InlayHint;
+use project::{InlayHint, ResolveState};
 
 use collections::{hash_map, HashMap, HashSet};
 use language::language_settings::InlayHintSettings;
+use smol::lock::Semaphore;
 use sum_tree::Bias;
+use text::{ToOffset, ToPoint};
 use util::post_inc;
 
 pub struct InlayHintCache {
     hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
     allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
     version: usize,
-    enabled: bool,
+    pub(super) enabled: bool,
     update_tasks: HashMap<ExcerptId, TasksForRanges>,
+    lsp_request_limiter: Arc<Semaphore>,
 }
 
 #[derive(Debug)]
@@ -60,7 +64,7 @@ struct ExcerptHintsUpdate {
     excerpt_id: ExcerptId,
     remove_from_visible: Vec<InlayId>,
     remove_from_cache: HashSet<InlayId>,
-    add_to_cache: HashSet<InlayHint>,
+    add_to_cache: Vec<InlayHint>,
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -69,6 +73,7 @@ struct ExcerptQuery {
     excerpt_id: ExcerptId,
     cache_version: usize,
     invalidate: InvalidationStrategy,
+    reason: &'static str,
 }
 
 impl InvalidationStrategy {
@@ -81,7 +86,11 @@ impl InvalidationStrategy {
 }
 
 impl TasksForRanges {
-    fn new(sorted_ranges: Vec<Range<language::Anchor>>, task: Task<()>) -> Self {
+    fn new(query_ranges: QueryRanges, task: Task<()>) -> Self {
+        let mut sorted_ranges = Vec::new();
+        sorted_ranges.extend(query_ranges.before_visible);
+        sorted_ranges.extend(query_ranges.visible);
+        sorted_ranges.extend(query_ranges.after_visible);
         Self {
             tasks: vec![task],
             sorted_ranges,
@@ -91,82 +100,138 @@ impl TasksForRanges {
     fn update_cached_tasks(
         &mut self,
         buffer_snapshot: &BufferSnapshot,
-        query_range: Range<text::Anchor>,
+        query_ranges: QueryRanges,
         invalidate: InvalidationStrategy,
-        spawn_task: impl FnOnce(Vec<Range<language::Anchor>>) -> Task<()>,
+        spawn_task: impl FnOnce(QueryRanges) -> Task<()>,
     ) {
-        let ranges_to_query = match invalidate {
-            InvalidationStrategy::None => {
-                let mut ranges_to_query = Vec::new();
-                let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
-                for cached_range in self
-                    .sorted_ranges
-                    .iter_mut()
-                    .skip_while(|cached_range| {
-                        cached_range
-                            .end
-                            .cmp(&query_range.start, buffer_snapshot)
-                            .is_lt()
-                    })
-                    .take_while(|cached_range| {
-                        cached_range
-                            .start
-                            .cmp(&query_range.end, buffer_snapshot)
-                            .is_le()
-                    })
-                {
-                    match latest_cached_range {
-                        Some(latest_cached_range) => {
-                            if latest_cached_range.end.offset.saturating_add(1)
-                                < cached_range.start.offset
-                            {
-                                ranges_to_query.push(latest_cached_range.end..cached_range.start);
-                                cached_range.start = latest_cached_range.end;
-                            }
-                        }
-                        None => {
-                            if query_range
-                                .start
-                                .cmp(&cached_range.start, buffer_snapshot)
-                                .is_lt()
-                            {
-                                ranges_to_query.push(query_range.start..cached_range.start);
-                                cached_range.start = query_range.start;
-                            }
-                        }
-                    }
-                    latest_cached_range = Some(cached_range);
-                }
+        let query_ranges = if invalidate.should_invalidate() {
+            self.tasks.clear();
+            self.sorted_ranges.clear();
+            query_ranges
+        } else {
+            let mut non_cached_query_ranges = query_ranges;
+            non_cached_query_ranges.before_visible = non_cached_query_ranges
+                .before_visible
+                .into_iter()
+                .flat_map(|query_range| {
+                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
+                })
+                .collect();
+            non_cached_query_ranges.visible = non_cached_query_ranges
+                .visible
+                .into_iter()
+                .flat_map(|query_range| {
+                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
+                })
+                .collect();
+            non_cached_query_ranges.after_visible = non_cached_query_ranges
+                .after_visible
+                .into_iter()
+                .flat_map(|query_range| {
+                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
+                })
+                .collect();
+            non_cached_query_ranges
+        };
 
-                match latest_cached_range {
-                    Some(latest_cached_range) => {
-                        if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset
-                        {
-                            ranges_to_query.push(latest_cached_range.end..query_range.end);
-                            latest_cached_range.end = query_range.end;
-                        }
+        if !query_ranges.is_empty() {
+            self.tasks.push(spawn_task(query_ranges));
+        }
+    }
+
+    fn remove_cached_ranges_from_query(
+        &mut self,
+        buffer_snapshot: &BufferSnapshot,
+        query_range: Range<language::Anchor>,
+    ) -> Vec<Range<language::Anchor>> {
+        let mut ranges_to_query = Vec::new();
+        let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
+        for cached_range in self
+            .sorted_ranges
+            .iter_mut()
+            .skip_while(|cached_range| {
+                cached_range
+                    .end
+                    .cmp(&query_range.start, buffer_snapshot)
+                    .is_lt()
+            })
+            .take_while(|cached_range| {
+                cached_range
+                    .start
+                    .cmp(&query_range.end, buffer_snapshot)
+                    .is_le()
+            })
+        {
+            match latest_cached_range {
+                Some(latest_cached_range) => {
+                    if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset
+                    {
+                        ranges_to_query.push(latest_cached_range.end..cached_range.start);
+                        cached_range.start = latest_cached_range.end;
                     }
-                    None => {
-                        ranges_to_query.push(query_range.clone());
-                        self.sorted_ranges.push(query_range);
-                        self.sorted_ranges.sort_by(|range_a, range_b| {
-                            range_a.start.cmp(&range_b.start, buffer_snapshot)
-                        });
+                }
+                None => {
+                    if query_range
+                        .start
+                        .cmp(&cached_range.start, buffer_snapshot)
+                        .is_lt()
+                    {
+                        ranges_to_query.push(query_range.start..cached_range.start);
+                        cached_range.start = query_range.start;
                     }
                 }
+            }
+            latest_cached_range = Some(cached_range);
+        }
 
-                ranges_to_query
+        match latest_cached_range {
+            Some(latest_cached_range) => {
+                if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset {
+                    ranges_to_query.push(latest_cached_range.end..query_range.end);
+                    latest_cached_range.end = query_range.end;
+                }
             }
-            InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => {
-                self.tasks.clear();
-                self.sorted_ranges.clear();
-                vec![query_range]
+            None => {
+                ranges_to_query.push(query_range.clone());
+                self.sorted_ranges.push(query_range);
+                self.sorted_ranges
+                    .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot));
             }
-        };
-
-        if !ranges_to_query.is_empty() {
-            self.tasks.push(spawn_task(ranges_to_query));
         }
+
+        ranges_to_query
+    }
+
+    fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) {
+        self.sorted_ranges = self
+            .sorted_ranges
+            .drain(..)
+            .filter_map(|mut cached_range| {
+                if cached_range.start.cmp(&range.end, buffer).is_gt()
+                    || cached_range.end.cmp(&range.start, buffer).is_lt()
+                {
+                    Some(vec![cached_range])
+                } else if cached_range.start.cmp(&range.start, buffer).is_ge()
+                    && cached_range.end.cmp(&range.end, buffer).is_le()
+                {
+                    None
+                } else if range.start.cmp(&cached_range.start, buffer).is_ge()
+                    && range.end.cmp(&cached_range.end, buffer).is_le()
+                {
+                    Some(vec![
+                        cached_range.start..range.start,
+                        range.end..cached_range.end,
+                    ])
+                } else if cached_range.start.cmp(&range.start, buffer).is_ge() {
+                    cached_range.start = range.end;
+                    Some(vec![cached_range])
+                } else {
+                    cached_range.end = range.start;
+                    Some(vec![cached_range])
+                }
+            })
+            .flatten()
+            .collect();
     }
 }
 
@@ -178,6 +243,7 @@ impl InlayHintCache {
             hints: HashMap::default(),
             update_tasks: HashMap::default(),
             version: 0,
+            lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)),
         }
     }
 
@@ -234,6 +300,7 @@ impl InlayHintCache {
 
     pub fn spawn_hint_refresh(
         &mut self,
+        reason: &'static str,
         excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
         invalidate: InvalidationStrategy,
         cx: &mut ViewContext<Editor>,
@@ -262,7 +329,14 @@ impl InlayHintCache {
         cx.spawn(|editor, mut cx| async move {
             editor
                 .update(&mut cx, |editor, cx| {
-                    spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
+                    spawn_new_update_tasks(
+                        editor,
+                        reason,
+                        excerpts_to_query,
+                        invalidate,
+                        cache_version,
+                        cx,
+                    )
                 })
                 .ok();
         })
@@ -314,7 +388,10 @@ impl InlayHintCache {
             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 { return false };
+                    .and_then(|buffer_id| multi_buffer.buffer(buffer_id))
+                else {
+                    return false;
+                };
                 let buffer_snapshot = buffer.read(cx).snapshot();
                 loop {
                     match excerpt_cache.peek() {
@@ -380,12 +457,45 @@ impl InlayHintCache {
         }
     }
 
-    fn clear(&mut self) {
-        self.version += 1;
+    pub fn remove_excerpts(&mut self, excerpts_removed: Vec<ExcerptId>) -> Option<InlaySplice> {
+        let mut to_remove = Vec::new();
+        for excerpt_to_remove in excerpts_removed {
+            self.update_tasks.remove(&excerpt_to_remove);
+            if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
+                let cached_hints = cached_hints.read();
+                to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id));
+            }
+        }
+        if to_remove.is_empty() {
+            None
+        } else {
+            self.version += 1;
+            Some(InlaySplice {
+                to_remove,
+                to_insert: Vec::new(),
+            })
+        }
+    }
+
+    pub fn clear(&mut self) {
+        if !self.update_tasks.is_empty() || !self.hints.is_empty() {
+            self.version += 1;
+        }
         self.update_tasks.clear();
         self.hints.clear();
     }
 
+    pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option<InlayHint> {
+        self.hints
+            .get(&excerpt_id)?
+            .read()
+            .hints
+            .iter()
+            .find(|&(id, _)| id == &hint_id)
+            .map(|(_, hint)| hint)
+            .cloned()
+    }
+
     pub fn hints(&self) -> Vec<InlayHint> {
         let mut hints = Vec::new();
         for excerpt_hints in self.hints.values() {
@@ -398,10 +508,80 @@ impl InlayHintCache {
     pub fn version(&self) -> usize {
         self.version
     }
+
+    pub fn spawn_hint_resolve(
+        &self,
+        buffer_id: u64,
+        excerpt_id: ExcerptId,
+        id: InlayId,
+        cx: &mut ViewContext<'_, '_, Editor>,
+    ) {
+        if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
+            let mut guard = excerpt_hints.write();
+            if let Some(cached_hint) = guard
+                .hints
+                .iter_mut()
+                .find(|(hint_id, _)| hint_id == &id)
+                .map(|(_, hint)| hint)
+            {
+                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(|editor, mut cx| async move {
+                        let resolved_hint_task = editor.update(&mut cx, |editor, cx| {
+                            editor
+                                .buffer()
+                                .read(cx)
+                                .buffer(buffer_id)
+                                .and_then(|buffer| {
+                                    let project = editor.project.as_ref()?;
+                                    Some(project.update(cx, |project, cx| {
+                                        project.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.update(&mut 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
+                                        .iter_mut()
+                                        .find(|(hint_id, _)| hint_id == &id)
+                                        .map(|(_, hint)| hint)
+                                    {
+                                        if cached_hint.resolve_state == ResolveState::Resolving {
+                                            resolved_hint.resolve_state = ResolveState::Resolved;
+                                            *cached_hint = resolved_hint;
+                                        }
+                                    }
+                                }
+                            })?;
+                        }
+
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx);
+                }
+            }
+        }
+    }
 }
 
 fn spawn_new_update_tasks(
     editor: &mut Editor,
+    reason: &'static str,
     excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
     invalidate: InvalidationStrategy,
     update_cache_version: usize,
@@ -435,11 +615,11 @@ fn spawn_new_update_tasks(
             }
         };
 
-        let (multi_buffer_snapshot, Some(query_range)) =
+        let (multi_buffer_snapshot, Some(query_ranges)) =
             editor.buffer.update(cx, |multi_buffer, cx| {
                 (
                     multi_buffer.snapshot(cx),
-                    determine_query_range(
+                    determine_query_ranges(
                         multi_buffer,
                         excerpt_id,
                         &excerpt_buffer,
@@ -447,22 +627,27 @@ fn spawn_new_update_tasks(
                         cx,
                     ),
                 )
-            }) else { return; };
+            })
+        else {
+            return;
+        };
         let query = ExcerptQuery {
             buffer_id,
             excerpt_id,
             cache_version: update_cache_version,
             invalidate,
+            reason,
         };
 
-        let new_update_task = |fetch_ranges| {
+        let new_update_task = |query_ranges| {
             new_update_task(
                 query,
-                fetch_ranges,
+                query_ranges,
                 multi_buffer_snapshot,
                 buffer_snapshot.clone(),
                 Arc::clone(&visible_hints),
                 cached_excerpt_hints,
+                Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter),
                 cx,
             )
         };
@@ -471,82 +656,184 @@ fn spawn_new_update_tasks(
             hash_map::Entry::Occupied(mut o) => {
                 o.get_mut().update_cached_tasks(
                     &buffer_snapshot,
-                    query_range,
+                    query_ranges,
                     invalidate,
                     new_update_task,
                 );
             }
             hash_map::Entry::Vacant(v) => {
                 v.insert(TasksForRanges::new(
-                    vec![query_range.clone()],
-                    new_update_task(vec![query_range]),
+                    query_ranges.clone(),
+                    new_update_task(query_ranges),
                 ));
             }
         }
     }
 }
 
-fn determine_query_range(
+#[derive(Debug, Clone)]
+struct QueryRanges {
+    before_visible: Vec<Range<language::Anchor>>,
+    visible: Vec<Range<language::Anchor>>,
+    after_visible: Vec<Range<language::Anchor>>,
+}
+
+impl QueryRanges {
+    fn is_empty(&self) -> bool {
+        self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty()
+    }
+}
+
+fn determine_query_ranges(
     multi_buffer: &mut MultiBuffer,
     excerpt_id: ExcerptId,
     excerpt_buffer: &ModelHandle<Buffer>,
     excerpt_visible_range: Range<usize>,
     cx: &mut ModelContext<'_, MultiBuffer>,
-) -> Option<Range<language::Anchor>> {
+) -> Option<QueryRanges> {
     let full_excerpt_range = multi_buffer
         .excerpts_for_buffer(excerpt_buffer, cx)
         .into_iter()
         .find(|(id, _)| id == &excerpt_id)
         .map(|(_, range)| range.context)?;
-
     let buffer = excerpt_buffer.read(cx);
+    let snapshot = buffer.snapshot();
     let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start;
-    let start_offset = excerpt_visible_range
-        .start
-        .saturating_sub(excerpt_visible_len)
-        .max(full_excerpt_range.start.offset);
-    let start = buffer.anchor_before(buffer.clip_offset(start_offset, Bias::Left));
-    let end_offset = excerpt_visible_range
+
+    let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end {
+        return None;
+    } else {
+        vec![
+            buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left))
+                ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)),
+        ]
+    };
+
+    let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot);
+    let after_visible_range_start = excerpt_visible_range
         .end
-        .saturating_add(excerpt_visible_len)
-        .min(full_excerpt_range.end.offset)
+        .saturating_add(1)
+        .min(full_excerpt_range_end_offset)
         .min(buffer.len());
-    let end = buffer.anchor_after(buffer.clip_offset(end_offset, Bias::Right));
-    if start.cmp(&end, buffer).is_eq() {
-        None
+    let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset {
+        Vec::new()
     } else {
-        Some(start..end)
-    }
+        let after_range_end_offset = after_visible_range_start
+            .saturating_add(excerpt_visible_len)
+            .min(full_excerpt_range_end_offset)
+            .min(buffer.len());
+        vec![
+            buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left))
+                ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)),
+        ]
+    };
+
+    let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot);
+    let before_visible_range_end = excerpt_visible_range
+        .start
+        .saturating_sub(1)
+        .max(full_excerpt_range_start_offset);
+    let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset {
+        Vec::new()
+    } else {
+        let before_range_start_offset = before_visible_range_end
+            .saturating_sub(excerpt_visible_len)
+            .max(full_excerpt_range_start_offset);
+        vec![
+            buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left))
+                ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)),
+        ]
+    };
+
+    Some(QueryRanges {
+        before_visible: before_visible_range,
+        visible: visible_range,
+        after_visible: after_visible_range,
+    })
 }
 
+const MAX_CONCURRENT_LSP_REQUESTS: usize = 5;
+const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400;
+
 fn new_update_task(
     query: ExcerptQuery,
-    hint_fetch_ranges: Vec<Range<language::Anchor>>,
+    query_ranges: QueryRanges,
     multi_buffer_snapshot: MultiBufferSnapshot,
     buffer_snapshot: BufferSnapshot,
     visible_hints: Arc<Vec<Inlay>>,
     cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+    lsp_request_limiter: Arc<Semaphore>,
     cx: &mut ViewContext<'_, '_, Editor>,
 ) -> Task<()> {
-    cx.spawn(|editor, cx| async move {
-        let task_update_results =
-            futures::future::join_all(hint_fetch_ranges.into_iter().map(|range| {
-                fetch_and_update_hints(
-                    editor.clone(),
-                    multi_buffer_snapshot.clone(),
-                    buffer_snapshot.clone(),
-                    Arc::clone(&visible_hints),
-                    cached_excerpt_hints.as_ref().map(Arc::clone),
-                    query,
-                    range,
-                    cx.clone(),
+    cx.spawn(|editor, mut cx| async move {
+        let closure_cx = cx.clone();
+        let fetch_and_update_hints = |invalidate, range| {
+            fetch_and_update_hints(
+                editor.clone(),
+                multi_buffer_snapshot.clone(),
+                buffer_snapshot.clone(),
+                Arc::clone(&visible_hints),
+                cached_excerpt_hints.as_ref().map(Arc::clone),
+                query,
+                invalidate,
+                range,
+                Arc::clone(&lsp_request_limiter),
+                closure_cx.clone(),
+            )
+        };
+        let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map(
+            |visible_range| async move {
+                (
+                    visible_range.clone(),
+                    fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range)
+                        .await,
                 )
-            }))
-            .await;
+            },
+        ))
+        .await;
+
+        let hint_delay = cx.background().timer(Duration::from_millis(
+            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS,
+        ));
+
+        let mut query_range_failed = |range: &Range<language::Anchor>, e: anyhow::Error| {
+            log::error!("inlay hint update task for range {range:?} failed: {e:#}");
+            editor
+                .update(&mut cx, |editor, _| {
+                    if let Some(task_ranges) = editor
+                        .inlay_hint_cache
+                        .update_tasks
+                        .get_mut(&query.excerpt_id)
+                    {
+                        task_ranges.invalidate_range(&buffer_snapshot, &range);
+                    }
+                })
+                .ok()
+        };
+
+        for (range, result) in visible_range_update_results {
+            if let Err(e) = result {
+                query_range_failed(&range, e);
+            }
+        }
 
-        for result in task_update_results {
+        hint_delay.await;
+        let invisible_range_update_results = future::join_all(
+            query_ranges
+                .before_visible
+                .into_iter()
+                .chain(query_ranges.after_visible.into_iter())
+                .map(|invisible_range| async move {
+                    (
+                        invisible_range.clone(),
+                        fetch_and_update_hints(false, invisible_range).await,
+                    )
+                }),
+        )
+        .await;
+        for (range, result) in invisible_range_update_results {
             if let Err(e) = result {
-                error!("inlay hint update task failed: {e:#}");
+                query_range_failed(&range, e);
             }
         }
     })
@@ -559,11 +846,53 @@ async fn fetch_and_update_hints(
     visible_hints: Arc<Vec<Inlay>>,
     cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
     query: ExcerptQuery,
+    invalidate: bool,
     fetch_range: Range<language::Anchor>,
+    lsp_request_limiter: Arc<Semaphore>,
     mut cx: gpui::AsyncAppContext,
 ) -> anyhow::Result<()> {
+    let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
+        (None, false)
+    } else {
+        match lsp_request_limiter.try_acquire() {
+            Some(guard) => (Some(guard), false),
+            None => (Some(lsp_request_limiter.acquire().await), true),
+        }
+    };
+    let fetch_range_to_log =
+        fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
     let inlay_hints_fetch_task = editor
         .update(&mut cx, |editor, cx| {
+            if got_throttled {
+                let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) {
+                    Some((_, _, current_visible_range)) => {
+                        let visible_offset_length = current_visible_range.len();
+                        let double_visible_range = current_visible_range
+                            .start
+                            .saturating_sub(visible_offset_length)
+                            ..current_visible_range
+                                .end
+                                .saturating_add(visible_offset_length)
+                                .min(buffer_snapshot.len());
+                        !double_visible_range
+                            .contains(&fetch_range.start.to_offset(&buffer_snapshot))
+                            && !double_visible_range
+                                .contains(&fetch_range.end.to_offset(&buffer_snapshot))
+                    },
+                    None => true,
+                };
+                if query_not_around_visible_range {
+                    log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
+                    if let Some(task_ranges) = editor
+                        .inlay_hint_cache
+                        .update_tasks
+                        .get_mut(&query.excerpt_id)
+                    {
+                        task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
+                    }
+                    return None;
+                }
+            }
             editor
                 .buffer()
                 .read(cx)
@@ -578,16 +907,34 @@ async fn fetch_and_update_hints(
         .ok()
         .flatten();
     let new_hints = match inlay_hints_fetch_task {
-        Some(task) => task.await.context("inlay hint fetch task")?,
+        Some(fetch_task) => {
+            log::debug!(
+                "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
+                query_reason = query.reason,
+            );
+            log::trace!(
+                "Currently visible hints: {visible_hints:?}, cached hints present: {}",
+                cached_excerpt_hints.is_some(),
+            );
+            fetch_task.await.context("inlay hint fetch task")?
+        }
         None => return Ok(()),
     };
+    drop(lsp_request_guard);
+    log::debug!(
+        "Fetched {} hints for range {fetch_range_to_log:?}",
+        new_hints.len()
+    );
+    log::trace!("Fetched hints: {new_hints:?}");
+
     let background_task_buffer_snapshot = buffer_snapshot.clone();
     let backround_fetch_range = fetch_range.clone();
     let new_update = cx
         .background()
         .spawn(async move {
             calculate_hint_updates(
-                query,
+                query.excerpt_id,
+                invalidate,
                 backround_fetch_range,
                 new_hints,
                 &background_task_buffer_snapshot,
@@ -597,12 +944,20 @@ async fn fetch_and_update_hints(
         })
         .await;
     if let Some(new_update) = new_update {
+        log::info!(
+            "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
+            new_update.remove_from_visible.len(),
+            new_update.remove_from_cache.len(),
+            new_update.add_to_cache.len()
+        );
+        log::trace!("New update: {new_update:?}");
         editor
             .update(&mut cx, |editor, cx| {
                 apply_hint_update(
                     editor,
                     new_update,
                     query,
+                    invalidate,
                     buffer_snapshot,
                     multi_buffer_snapshot,
                     cx,
@@ -614,14 +969,15 @@ async fn fetch_and_update_hints(
 }
 
 fn calculate_hint_updates(
-    query: ExcerptQuery,
+    excerpt_id: ExcerptId,
+    invalidate: bool,
     fetch_range: Range<language::Anchor>,
     new_excerpt_hints: Vec<InlayHint>,
     buffer_snapshot: &BufferSnapshot,
     cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
     visible_hints: &[Inlay],
 ) -> Option<ExcerptHintsUpdate> {
-    let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
+    let mut add_to_cache = Vec::<InlayHint>::new();
     let mut excerpt_hints_to_persist = HashMap::default();
     for new_hint in new_excerpt_hints {
         if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
@@ -634,13 +990,21 @@ fn calculate_hint_updates(
                     probe.1.position.cmp(&new_hint.position, buffer_snapshot)
                 }) {
                     Ok(ix) => {
-                        let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
-                        if cached_hint == &new_hint {
-                            excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
-                            false
-                        } else {
-                            true
+                        let mut missing_from_cache = true;
+                        for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] {
+                            if new_hint
+                                .position
+                                .cmp(&cached_hint.position, buffer_snapshot)
+                                .is_gt()
+                            {
+                                break;
+                            }
+                            if cached_hint == &new_hint {
+                                excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
+                                missing_from_cache = false;
+                            }
                         }
+                        missing_from_cache
                     }
                     Err(_) => true,
                 }
@@ -648,17 +1012,17 @@ fn calculate_hint_updates(
             None => true,
         };
         if missing_from_cache {
-            add_to_cache.insert(new_hint);
+            add_to_cache.push(new_hint);
         }
     }
 
     let mut remove_from_visible = Vec::new();
     let mut remove_from_cache = HashSet::default();
-    if query.invalidate.should_invalidate() {
+    if invalidate {
         remove_from_visible.extend(
             visible_hints
                 .iter()
-                .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
+                .filter(|hint| hint.position.excerpt_id == excerpt_id)
                 .map(|inlay_hint| inlay_hint.id)
                 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
         );
@@ -681,7 +1045,7 @@ fn calculate_hint_updates(
         None
     } else {
         Some(ExcerptHintsUpdate {
-            excerpt_id: query.excerpt_id,
+            excerpt_id,
             remove_from_visible,
             remove_from_cache,
             add_to_cache,
@@ -702,6 +1066,7 @@ fn apply_hint_update(
     editor: &mut Editor,
     new_update: ExcerptHintsUpdate,
     query: ExcerptQuery,
+    invalidate: bool,
     buffer_snapshot: BufferSnapshot,
     multi_buffer_snapshot: MultiBufferSnapshot,
     cx: &mut ViewContext<'_, '_, Editor>,
@@ -740,11 +1105,21 @@ fn apply_hint_update(
             .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot))
         {
             Ok(i) => {
-                if cached_hints[i].1.text() == new_hint.text() {
-                    None
-                } else {
-                    Some(i)
+                let mut insert_position = Some(i);
+                for (_, cached_hint) in &cached_hints[i..] {
+                    if new_hint
+                        .position
+                        .cmp(&cached_hint.position, &buffer_snapshot)
+                        .is_gt()
+                    {
+                        break;
+                    }
+                    if cached_hint.text() == new_hint.text() {
+                        insert_position = None;
+                        break;
+                    }
                 }
+                insert_position
             }
             Err(i) => Some(i),
         };
@@ -769,7 +1144,7 @@ fn apply_hint_update(
     cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
     drop(cached_excerpt_hints);
 
-    if query.invalidate.should_invalidate() {
+    if invalidate {
         let mut outdated_excerpt_caches = HashSet::default();
         for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
             let excerpt_hints = excerpt_hints.read();
@@ -806,8 +1181,8 @@ fn apply_hint_update(
 }
 
 #[cfg(test)]
-mod tests {
-    use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+pub mod tests {
+    use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
 
     use crate::{
         scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
@@ -885,13 +1260,13 @@ mod tests {
 
         let mut edits_made = 1;
         editor.update(cx, |editor, cx| {
-            let expected_layers = vec!["0".to_string()];
+            let expected_hints = vec!["0".to_string()];
             assert_eq!(
-                expected_layers,
+                expected_hints,
                 cached_hint_labels(editor),
                 "Should get its first hints when opening the editor"
             );
-            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
                 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
@@ -910,13 +1285,13 @@ mod tests {
         });
         cx.foreground().run_until_parked();
         editor.update(cx, |editor, cx| {
-            let expected_layers = vec!["0".to_string(), "1".to_string()];
+            let expected_hints = vec!["0".to_string(), "1".to_string()];
             assert_eq!(
-                expected_layers,
+                expected_hints,
                 cached_hint_labels(editor),
                 "Should get new hints after an edit"
             );
-            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
                 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
@@ -935,13 +1310,13 @@ mod tests {
         edits_made += 1;
         cx.foreground().run_until_parked();
         editor.update(cx, |editor, cx| {
-            let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
+            let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()];
             assert_eq!(
-                expected_layers,
+                expected_hints,
                 cached_hint_labels(editor),
                 "Should get new hints after hint refresh/ request"
             );
-            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
             let inlay_cache = editor.inlay_hint_cache();
             assert_eq!(
                 inlay_cache.allowed_hint_kinds, allowed_hint_kinds,

crates/editor/src/items.rs 🔗

@@ -49,13 +49,18 @@ impl FollowableItem for Editor {
 
     fn from_state_proto(
         pane: ViewHandle<workspace::Pane>,
-        project: ModelHandle<Project>,
+        workspace: ViewHandle<Workspace>,
         remote_id: ViewId,
         state: &mut Option<proto::view::Variant>,
         cx: &mut AppContext,
     ) -> Option<Task<Result<ViewHandle<Self>>>> {
-        let Some(proto::view::Variant::Editor(_)) = state else { return None };
-        let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
+        let project = workspace.read(cx).project().to_owned();
+        let Some(proto::view::Variant::Editor(_)) = state else {
+            return None;
+        };
+        let Some(proto::view::Variant::Editor(state)) = state.take() else {
+            unreachable!()
+        };
 
         let client = project.read(cx).client();
         let replica_id = project.read(cx).replica_id();
@@ -340,10 +345,16 @@ async fn update_editor_from_message(
 
             let mut insertions = message.inserted_excerpts.into_iter().peekable();
             while let Some(insertion) = insertions.next() {
-                let Some(excerpt) = insertion.excerpt else { continue };
-                let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
+                let Some(excerpt) = insertion.excerpt else {
+                    continue;
+                };
+                let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
+                    continue;
+                };
                 let buffer_id = excerpt.buffer_id;
-                let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
+                let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
+                    continue;
+                };
 
                 let adjacent_excerpts = iter::from_fn(|| {
                     let insertion = insertions.peek()?;
@@ -561,7 +572,7 @@ impl Item for Editor {
         }
     }
 
-    fn tab_content<T: View>(
+    fn tab_content<T: 'static>(
         &self,
         detail: Option<usize>,
         style: &theme::Tab,
@@ -614,7 +625,7 @@ impl Item for Editor {
 
     fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
         hide_link_definition(self, cx);
-        self.link_go_to_definition_state.last_mouse_location = None;
+        self.link_go_to_definition_state.last_trigger_point = None;
     }
 
     fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -753,7 +764,7 @@ impl Item for Editor {
         Some(Box::new(handle.clone()))
     }
 
-    fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
+    fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
         self.pixel_position_of_newest_cursor
     }
 
@@ -1,22 +1,108 @@
-use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
+use crate::{
+    display_map::{DisplaySnapshot, InlayOffset},
+    element::PointForPosition,
+    hover_popover::{self, InlayHover},
+    Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase,
+};
 use gpui::{Task, ViewContext};
 use language::{Bias, ToOffset};
-use project::LocationLink;
+use lsp::LanguageServerId;
+use project::{
+    HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
+    ResolveState,
+};
 use std::ops::Range;
 use util::TryFutureExt;
 
 #[derive(Debug, Default)]
 pub struct LinkGoToDefinitionState {
-    pub last_mouse_location: Option<Anchor>,
-    pub symbol_range: Option<Range<Anchor>>,
+    pub last_trigger_point: Option<TriggerPoint>,
+    pub symbol_range: Option<DocumentRange>,
     pub kind: Option<LinkDefinitionKind>,
-    pub definitions: Vec<LocationLink>,
+    pub definitions: Vec<GoToDefinitionLink>,
     pub task: Option<Task<Option<()>>>,
 }
 
+#[derive(Debug)]
+pub enum GoToDefinitionTrigger {
+    Text(DisplayPoint),
+    InlayHint(InlayRange, lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone)]
+pub enum GoToDefinitionLink {
+    Text(LocationLink),
+    InlayHint(lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct InlayRange {
+    pub inlay_position: Anchor,
+    pub highlight_start: InlayOffset,
+    pub highlight_end: InlayOffset,
+}
+
+#[derive(Debug, Clone)]
+pub enum TriggerPoint {
+    Text(Anchor),
+    InlayHint(InlayRange, lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum DocumentRange {
+    Text(Range<Anchor>),
+    Inlay(InlayRange),
+}
+
+impl DocumentRange {
+    pub fn as_text_range(&self) -> Option<Range<Anchor>> {
+        match self {
+            Self::Text(range) => Some(range.clone()),
+            Self::Inlay(_) => None,
+        }
+    }
+
+    fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
+        match (self, trigger_point) {
+            (DocumentRange::Text(range), TriggerPoint::Text(point)) => {
+                let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
+                point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
+            }
+            (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _, _)) => {
+                range.highlight_start.cmp(&point.highlight_end).is_le()
+                    && range.highlight_end.cmp(&point.highlight_end).is_ge()
+            }
+            (DocumentRange::Inlay(_), TriggerPoint::Text(_))
+            | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
+        }
+    }
+}
+
+impl TriggerPoint {
+    fn anchor(&self) -> &Anchor {
+        match self {
+            TriggerPoint::Text(anchor) => anchor,
+            TriggerPoint::InlayHint(range, _, _) => &range.inlay_position,
+        }
+    }
+
+    pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
+        match self {
+            TriggerPoint::Text(_) => {
+                if shift {
+                    LinkDefinitionKind::Type
+                } else {
+                    LinkDefinitionKind::Symbol
+                }
+            }
+            TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
+        }
+    }
+}
+
 pub fn update_go_to_definition_link(
     editor: &mut Editor,
-    point: Option<DisplayPoint>,
+    origin: Option<GoToDefinitionTrigger>,
     cmd_held: bool,
     shift_held: bool,
     cx: &mut ViewContext<Editor>,
@@ -25,23 +111,43 @@ pub fn update_go_to_definition_link(
 
     // Store new mouse point as an anchor
     let snapshot = editor.snapshot(cx);
-    let point = point.map(|point| {
-        snapshot
-            .buffer_snapshot
-            .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left))
-    });
+    let trigger_point = match origin {
+        Some(GoToDefinitionTrigger::Text(p)) => {
+            Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before(
+                p.to_offset(&snapshot.display_snapshot, Bias::Left),
+            )))
+        }
+        Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => {
+            Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id))
+        }
+        None => None,
+    };
 
     // If the new point is the same as the previously stored one, return early
     if let (Some(a), Some(b)) = (
-        &point,
-        &editor.link_go_to_definition_state.last_mouse_location,
+        &trigger_point,
+        &editor.link_go_to_definition_state.last_trigger_point,
     ) {
-        if a.cmp(b, &snapshot.buffer_snapshot).is_eq() {
-            return;
+        match (a, b) {
+            (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => {
+                if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() {
+                    return;
+                }
+            }
+            (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
+                if range_a
+                    .inlay_position
+                    .cmp(&range_b.inlay_position, &snapshot.buffer_snapshot)
+                    .is_eq()
+                {
+                    return;
+                }
+            }
+            _ => {}
         }
     }
 
-    editor.link_go_to_definition_state.last_mouse_location = point.clone();
+    editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone();
 
     if pending_nonempty_selection {
         hide_link_definition(editor, cx);
@@ -49,14 +155,9 @@ pub fn update_go_to_definition_link(
     }
 
     if cmd_held {
-        if let Some(point) = point {
-            let kind = if shift_held {
-                LinkDefinitionKind::Type
-            } else {
-                LinkDefinitionKind::Symbol
-            };
-
-            show_link_definition(kind, editor, point, snapshot, cx);
+        if let Some(trigger_point) = trigger_point {
+            let kind = trigger_point.definition_kind(shift_held);
+            show_link_definition(kind, editor, trigger_point, snapshot, cx);
             return;
         }
     }
@@ -64,6 +165,182 @@ pub fn update_go_to_definition_link(
     hide_link_definition(editor, cx);
 }
 
+pub fn update_inlay_link_and_hover_points(
+    snapshot: &DisplaySnapshot,
+    point_for_position: PointForPosition,
+    editor: &mut Editor,
+    cmd_held: bool,
+    shift_held: bool,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) {
+    let hint_start_offset =
+        snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left);
+    let hint_end_offset =
+        snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right);
+    let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
+        Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
+    } else {
+        None
+    };
+    let mut go_to_definition_updated = false;
+    let mut hover_updated = false;
+    if let Some(hovered_offset) = hovered_offset {
+        let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
+        let previous_valid_anchor = buffer_snapshot.anchor_at(
+            point_for_position.previous_valid.to_point(snapshot),
+            Bias::Left,
+        );
+        let next_valid_anchor = buffer_snapshot.anchor_at(
+            point_for_position.next_valid.to_point(snapshot),
+            Bias::Right,
+        );
+        if let Some(hovered_hint) = editor
+            .visible_inlay_hints(cx)
+            .into_iter()
+            .skip_while(|hint| {
+                hint.position
+                    .cmp(&previous_valid_anchor, &buffer_snapshot)
+                    .is_lt()
+            })
+            .take_while(|hint| {
+                hint.position
+                    .cmp(&next_valid_anchor, &buffer_snapshot)
+                    .is_le()
+            })
+            .max_by_key(|hint| hint.id)
+        {
+            let inlay_hint_cache = editor.inlay_hint_cache();
+            let excerpt_id = previous_valid_anchor.excerpt_id;
+            if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
+                match cached_hint.resolve_state {
+                    ResolveState::CanResolve(_, _) => {
+                        if let Some(buffer_id) = previous_valid_anchor.buffer_id {
+                            inlay_hint_cache.spawn_hint_resolve(
+                                buffer_id,
+                                excerpt_id,
+                                hovered_hint.id,
+                                cx,
+                            );
+                        }
+                    }
+                    ResolveState::Resolved => {
+                        let mut actual_hint_start = hint_start_offset;
+                        let mut actual_hint_end = hint_end_offset;
+                        if cached_hint.padding_left {
+                            actual_hint_start.0 += 1;
+                            actual_hint_end.0 += 1;
+                        }
+                        if cached_hint.padding_right {
+                            actual_hint_start.0 += 1;
+                            actual_hint_end.0 += 1;
+                        }
+                        match cached_hint.label {
+                            project::InlayHintLabel::String(_) => {
+                                if let Some(tooltip) = cached_hint.tooltip {
+                                    hover_popover::hover_at_inlay(
+                                        editor,
+                                        InlayHover {
+                                            excerpt: excerpt_id,
+                                            tooltip: match tooltip {
+                                                InlayHintTooltip::String(text) => HoverBlock {
+                                                    text,
+                                                    kind: HoverBlockKind::PlainText,
+                                                },
+                                                InlayHintTooltip::MarkupContent(content) => {
+                                                    HoverBlock {
+                                                        text: content.value,
+                                                        kind: content.kind,
+                                                    }
+                                                }
+                                            },
+                                            triggered_from: hovered_offset,
+                                            range: InlayRange {
+                                                inlay_position: hovered_hint.position,
+                                                highlight_start: actual_hint_start,
+                                                highlight_end: actual_hint_end,
+                                            },
+                                        },
+                                        cx,
+                                    );
+                                    hover_updated = true;
+                                }
+                            }
+                            project::InlayHintLabel::LabelParts(label_parts) => {
+                                if let Some((hovered_hint_part, part_range)) =
+                                    hover_popover::find_hovered_hint_part(
+                                        label_parts,
+                                        actual_hint_start..actual_hint_end,
+                                        hovered_offset,
+                                    )
+                                {
+                                    if let Some(tooltip) = hovered_hint_part.tooltip {
+                                        hover_popover::hover_at_inlay(
+                                            editor,
+                                            InlayHover {
+                                                excerpt: excerpt_id,
+                                                tooltip: match tooltip {
+                                                    InlayHintLabelPartTooltip::String(text) => {
+                                                        HoverBlock {
+                                                            text,
+                                                            kind: HoverBlockKind::PlainText,
+                                                        }
+                                                    }
+                                                    InlayHintLabelPartTooltip::MarkupContent(
+                                                        content,
+                                                    ) => HoverBlock {
+                                                        text: content.value,
+                                                        kind: content.kind,
+                                                    },
+                                                },
+                                                triggered_from: hovered_offset,
+                                                range: InlayRange {
+                                                    inlay_position: hovered_hint.position,
+                                                    highlight_start: part_range.start,
+                                                    highlight_end: part_range.end,
+                                                },
+                                            },
+                                            cx,
+                                        );
+                                        hover_updated = true;
+                                    }
+                                    if let Some((language_server_id, location)) =
+                                        hovered_hint_part.location
+                                    {
+                                        go_to_definition_updated = true;
+                                        update_go_to_definition_link(
+                                            editor,
+                                            Some(GoToDefinitionTrigger::InlayHint(
+                                                InlayRange {
+                                                    inlay_position: hovered_hint.position,
+                                                    highlight_start: part_range.start,
+                                                    highlight_end: part_range.end,
+                                                },
+                                                location,
+                                                language_server_id,
+                                            )),
+                                            cmd_held,
+                                            shift_held,
+                                            cx,
+                                        );
+                                    }
+                                }
+                            }
+                        };
+                    }
+                    ResolveState::Resolving => {}
+                }
+            }
+        }
+    }
+
+    if !go_to_definition_updated {
+        update_go_to_definition_link(editor, None, cmd_held, shift_held, cx);
+    }
+    if !hover_updated {
+        hover_popover::hover_at(editor, None, cx);
+    }
+}
+
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub enum LinkDefinitionKind {
     Symbol,
@@ -73,7 +350,7 @@ pub enum LinkDefinitionKind {
 pub fn show_link_definition(
     definition_kind: LinkDefinitionKind,
     editor: &mut Editor,
-    trigger_point: Anchor,
+    trigger_point: TriggerPoint,
     snapshot: EditorSnapshot,
     cx: &mut ViewContext<Editor>,
 ) {
@@ -86,10 +363,11 @@ pub fn show_link_definition(
         return;
     }
 
+    let trigger_anchor = trigger_point.anchor();
     let (buffer, buffer_position) = if let Some(output) = editor
         .buffer
         .read(cx)
-        .text_anchor_for_position(trigger_point.clone(), cx)
+        .text_anchor_for_position(trigger_anchor.clone(), cx)
     {
         output
     } else {
@@ -99,7 +377,7 @@ pub fn show_link_definition(
     let excerpt_id = if let Some((excerpt_id, _, _)) = editor
         .buffer()
         .read(cx)
-        .excerpt_containing(trigger_point.clone(), cx)
+        .excerpt_containing(trigger_anchor.clone(), cx)
     {
         excerpt_id
     } else {
@@ -114,52 +392,57 @@ pub fn show_link_definition(
 
     // Don't request again if the location is within the symbol region of a previous request with the same kind
     if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
-        let point_after_start = symbol_range
-            .start
-            .cmp(&trigger_point, &snapshot.buffer_snapshot)
-            .is_le();
-
-        let point_before_end = symbol_range
-            .end
-            .cmp(&trigger_point, &snapshot.buffer_snapshot)
-            .is_ge();
-
-        let point_within_range = point_after_start && point_before_end;
-        if point_within_range && same_kind {
+        if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) {
             return;
         }
     }
 
     let task = cx.spawn(|this, mut cx| {
         async move {
-            // query the LSP for definition info
-            let definition_request = cx.update(|cx| {
-                project.update(cx, |project, cx| match definition_kind {
-                    LinkDefinitionKind::Symbol => project.definition(&buffer, buffer_position, cx),
-
-                    LinkDefinitionKind::Type => {
-                        project.type_definition(&buffer, buffer_position, cx)
-                    }
-                })
-            });
+            let result = match &trigger_point {
+                TriggerPoint::Text(_) => {
+                    // query the LSP for definition info
+                    cx.update(|cx| {
+                        project.update(cx, |project, cx| match definition_kind {
+                            LinkDefinitionKind::Symbol => {
+                                project.definition(&buffer, buffer_position, cx)
+                            }
 
-            let result = definition_request.await.ok().map(|definition_result| {
-                (
-                    definition_result.iter().find_map(|link| {
-                        link.origin.as_ref().map(|origin| {
-                            let start = snapshot
-                                .buffer_snapshot
-                                .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
-                            let end = snapshot
-                                .buffer_snapshot
-                                .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
-
-                            start..end
+                            LinkDefinitionKind::Type => {
+                                project.type_definition(&buffer, buffer_position, cx)
+                            }
                         })
-                    }),
-                    definition_result,
-                )
-            });
+                    })
+                    .await
+                    .ok()
+                    .map(|definition_result| {
+                        (
+                            definition_result.iter().find_map(|link| {
+                                link.origin.as_ref().map(|origin| {
+                                    let start = snapshot
+                                        .buffer_snapshot
+                                        .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
+                                    let end = snapshot
+                                        .buffer_snapshot
+                                        .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
+                                    DocumentRange::Text(start..end)
+                                })
+                            }),
+                            definition_result
+                                .into_iter()
+                                .map(GoToDefinitionLink::Text)
+                                .collect(),
+                        )
+                    })
+                }
+                TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => Some((
+                    Some(DocumentRange::Inlay(*trigger_source)),
+                    vec![GoToDefinitionLink::InlayHint(
+                        lsp_location.clone(),
+                        *server_id,
+                    )],
+                )),
+            };
 
             this.update(&mut cx, |this, cx| {
                 // Clear any existing highlights
@@ -178,43 +461,67 @@ pub fn show_link_definition(
                     // the current location.
                     let any_definition_does_not_contain_current_location =
                         definitions.iter().any(|definition| {
-                            let target = &definition.target;
-                            if target.buffer == buffer {
-                                let range = &target.range;
-                                // Expand range by one character as lsp definition ranges include positions adjacent
-                                // but not contained by the symbol range
-                                let start = buffer_snapshot.clip_offset(
-                                    range.start.to_offset(&buffer_snapshot).saturating_sub(1),
-                                    Bias::Left,
-                                );
-                                let end = buffer_snapshot.clip_offset(
-                                    range.end.to_offset(&buffer_snapshot) + 1,
-                                    Bias::Right,
-                                );
-                                let offset = buffer_position.to_offset(&buffer_snapshot);
-                                !(start <= offset && end >= offset)
-                            } else {
-                                true
+                            match &definition {
+                                GoToDefinitionLink::Text(link) => {
+                                    if link.target.buffer == buffer {
+                                        let range = &link.target.range;
+                                        // Expand range by one character as lsp definition ranges include positions adjacent
+                                        // but not contained by the symbol range
+                                        let start = buffer_snapshot.clip_offset(
+                                            range
+                                                .start
+                                                .to_offset(&buffer_snapshot)
+                                                .saturating_sub(1),
+                                            Bias::Left,
+                                        );
+                                        let end = buffer_snapshot.clip_offset(
+                                            range.end.to_offset(&buffer_snapshot) + 1,
+                                            Bias::Right,
+                                        );
+                                        let offset = buffer_position.to_offset(&buffer_snapshot);
+                                        !(start <= offset && end >= offset)
+                                    } else {
+                                        true
+                                    }
+                                }
+                                GoToDefinitionLink::InlayHint(_, _) => true,
                             }
                         });
 
                     if any_definition_does_not_contain_current_location {
-                        // If no symbol range returned from language server, use the surrounding word.
-                        let highlight_range = symbol_range.unwrap_or_else(|| {
-                            let snapshot = &snapshot.buffer_snapshot;
-                            let (offset_range, _) = snapshot.surrounding_word(trigger_point);
-
-                            snapshot.anchor_before(offset_range.start)
-                                ..snapshot.anchor_after(offset_range.end)
-                        });
-
                         // Highlight symbol using theme link definition highlight style
                         let style = theme::current(cx).editor.link_definition;
-                        this.highlight_text::<LinkGoToDefinitionState>(
-                            vec![highlight_range],
-                            style,
-                            cx,
-                        );
+                        let highlight_range =
+                            symbol_range.unwrap_or_else(|| match &trigger_point {
+                                TriggerPoint::Text(trigger_anchor) => {
+                                    let snapshot = &snapshot.buffer_snapshot;
+                                    // If no symbol range returned from language server, use the surrounding word.
+                                    let (offset_range, _) =
+                                        snapshot.surrounding_word(*trigger_anchor);
+                                    DocumentRange::Text(
+                                        snapshot.anchor_before(offset_range.start)
+                                            ..snapshot.anchor_after(offset_range.end),
+                                    )
+                                }
+                                TriggerPoint::InlayHint(inlay_coordinates, _, _) => {
+                                    DocumentRange::Inlay(*inlay_coordinates)
+                                }
+                            });
+
+                        match highlight_range {
+                            DocumentRange::Text(text_range) => this
+                                .highlight_text::<LinkGoToDefinitionState>(
+                                    vec![text_range],
+                                    style,
+                                    cx,
+                                ),
+                            DocumentRange::Inlay(inlay_coordinates) => this
+                                .highlight_inlays::<LinkGoToDefinitionState>(
+                                    vec![inlay_coordinates],
+                                    style,
+                                    cx,
+                                ),
+                        }
                     } else {
                         hide_link_definition(this, cx);
                     }
@@ -245,7 +552,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
 
 pub fn go_to_fetched_definition(
     editor: &mut Editor,
-    point: DisplayPoint,
+    point: PointForPosition,
     split: bool,
     cx: &mut ViewContext<Editor>,
 ) {
@@ -254,7 +561,7 @@ pub fn go_to_fetched_definition(
 
 pub fn go_to_fetched_type_definition(
     editor: &mut Editor,
-    point: DisplayPoint,
+    point: PointForPosition,
     split: bool,
     cx: &mut ViewContext<Editor>,
 ) {
@@ -264,7 +571,7 @@ pub fn go_to_fetched_type_definition(
 fn go_to_fetched_definition_of_kind(
     kind: LinkDefinitionKind,
     editor: &mut Editor,
-    point: DisplayPoint,
+    point: PointForPosition,
     split: bool,
     cx: &mut ViewContext<Editor>,
 ) {
@@ -282,16 +589,18 @@ fn go_to_fetched_definition_of_kind(
     } else {
         editor.select(
             SelectPhase::Begin {
-                position: point,
+                position: point.next_valid,
                 add: false,
                 click_count: 1,
             },
             cx,
         );
 
-        match kind {
-            LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
-            LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
+        if point.as_valid().is_some() {
+            match kind {
+                LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
+                LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
+            }
         }
     }
 }
@@ -299,14 +608,21 @@ fn go_to_fetched_definition_of_kind(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+    use crate::{
+        display_map::ToDisplayPoint,
+        editor_tests::init_test,
+        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        test::editor_lsp_test_context::EditorLspTestContext,
+    };
     use futures::StreamExt;
     use gpui::{
         platform::{self, Modifiers, ModifiersChangedEvent},
         View,
     };
     use indoc::indoc;
+    use language::language_settings::InlayHintSettings;
     use lsp::request::{GotoDefinition, GotoTypeDefinition};
+    use util::assert_set_eq;
 
     #[gpui::test]
     async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
@@ -355,7 +671,13 @@ mod tests {
 
         // Press cmd+shift to trigger highlight
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                true,
+                cx,
+            );
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -406,7 +728,7 @@ mod tests {
             });
 
         cx.update_editor(|editor, cx| {
-            go_to_fetched_type_definition(editor, hover_point, false, cx);
+            go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -461,7 +783,13 @@ mod tests {
         });
 
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -482,7 +810,7 @@ mod tests {
         "});
 
         // Response without source range still highlights word
-        cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
+        cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
         let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
             Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
                 lsp::LocationLink {
@@ -495,7 +823,13 @@ mod tests {
             ])))
         });
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -517,7 +851,13 @@ mod tests {
                 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
             });
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -534,7 +874,13 @@ mod tests {
             fn do_work() { teˇst(); }
         "});
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(editor, Some(hover_point), false, false, cx);
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                false,
+                false,
+                cx,
+            );
         });
         cx.foreground().run_until_parked();
 
@@ -593,7 +939,13 @@ mod tests {
 
         // Moving the mouse restores the highlights.
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
         });
         cx.foreground().run_until_parked();
         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
@@ -607,7 +959,13 @@ mod tests {
             fn do_work() { tesˇt(); }
         "});
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
         });
         cx.foreground().run_until_parked();
         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
@@ -617,7 +975,7 @@ mod tests {
 
         // Cmd click with existing definition doesn't re-request and dismisses highlight
         cx.update_editor(|editor, cx| {
-            go_to_fetched_definition(editor, hover_point, false, cx);
+            go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
         });
         // Assert selection moved to to definition
         cx.lsp
@@ -626,6 +984,7 @@ mod tests {
                 // the cached location instead
                 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
             });
+        cx.foreground().run_until_parked();
         cx.assert_editor_state(indoc! {"
             fn «testˇ»() { do_work(); }
             fn do_work() { test(); }
@@ -658,7 +1017,7 @@ mod tests {
             ])))
         });
         cx.update_editor(|editor, cx| {
-            go_to_fetched_definition(editor, hover_point, false, cx);
+            go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -703,7 +1062,13 @@ mod tests {
             });
         });
         cx.update_editor(|editor, cx| {
-            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
         });
         cx.foreground().run_until_parked();
         assert!(requests.try_next().is_err());
@@ -713,4 +1078,217 @@ mod tests {
         "});
         cx.foreground().run_until_parked();
     }
+
+    #[gpui::test]
+    async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+        cx.set_state(indoc! {"
+            struct TestStruct;
+
+            fn main() {
+                let variableˇ = TestStruct;
+            }
+        "});
+        let hint_start_offset = cx.ranges(indoc! {"
+            struct TestStruct;
+
+            fn main() {
+                let variableˇ = TestStruct;
+            }
+        "})[0]
+            .start;
+        let hint_position = cx.to_lsp(hint_start_offset);
+        let target_range = cx.lsp_range(indoc! {"
+            struct «TestStruct»;
+
+            fn main() {
+                let variable = TestStruct;
+            }
+        "});
+
+        let expected_uri = cx.buffer_lsp_url.clone();
+        let hint_label = ": TestStruct";
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let expected_uri = expected_uri.clone();
+                async move {
+                    assert_eq!(params.text_document.uri, expected_uri);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: hint_position,
+                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+                            value: hint_label.to_string(),
+                            location: Some(lsp::Location {
+                                uri: params.text_document.uri,
+                                range: target_range,
+                            }),
+                            ..Default::default()
+                        }]),
+                        kind: Some(lsp::InlayHintKind::TYPE),
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: Some(false),
+                        padding_right: Some(false),
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let expected_layers = vec![hint_label.to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+        });
+
+        let inlay_range = cx
+            .ranges(indoc! {"
+            struct TestStruct;
+
+            fn main() {
+                let variable« »= TestStruct;
+            }
+        "})
+            .get(0)
+            .cloned()
+            .unwrap();
+        let hint_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column() + (hint_label.len() / 2) as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        // Press cmd to trigger highlight
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                hint_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let actual_ranges = snapshot
+                .highlight_ranges::<LinkGoToDefinitionState>()
+                .map(|ranges| ranges.as_ref().clone().1)
+                .unwrap_or_default()
+                .into_iter()
+                .map(|range| match range {
+                    DocumentRange::Text(range) => {
+                        panic!("Unexpected regular text selection range {range:?}")
+                    }
+                    DocumentRange::Inlay(inlay_range) => inlay_range,
+                })
+                .collect::<Vec<_>>();
+
+            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            let expected_highlight_start = snapshot.display_point_to_inlay_offset(
+                inlay_range.start.to_display_point(&snapshot),
+                Bias::Left,
+            );
+            let expected_ranges = vec![InlayRange {
+                inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                highlight_start: expected_highlight_start,
+                highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()),
+            }];
+            assert_set_eq!(actual_ranges, expected_ranges);
+        });
+
+        // Unpress cmd causes highlight to go away
+        cx.update_editor(|editor, cx| {
+            editor.modifiers_changed(
+                &platform::ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        cmd: false,
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                cx,
+            );
+        });
+        // Assert no link highlights
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let actual_ranges = snapshot
+                .highlight_ranges::<LinkGoToDefinitionState>()
+                .map(|ranges| ranges.as_ref().clone().1)
+                .unwrap_or_default()
+                .into_iter()
+                .map(|range| match range {
+                    DocumentRange::Text(range) => {
+                        panic!("Unexpected regular text selection range {range:?}")
+                    }
+                    DocumentRange::Inlay(inlay_range) => inlay_range,
+                })
+                .collect::<Vec<_>>();
+
+            assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
+        });
+
+        // Cmd+click without existing definition requests and jumps
+        cx.update_editor(|editor, cx| {
+            editor.modifiers_changed(
+                &platform::ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        cmd: true,
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                cx,
+            );
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                hint_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.foreground().run_until_parked();
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
+        });
+        cx.foreground().run_until_parked();
+        cx.assert_editor_state(indoc! {"
+            struct «TestStructˇ»;
+
+            fn main() {
+                let variable = TestStruct;
+            }
+        "});
+    }
 }

crates/editor/src/movement.rs 🔗

@@ -61,10 +61,10 @@ pub fn up_by_rows(
     goal: SelectionGoal,
     preserve_column_at_start: bool,
 ) -> (DisplayPoint, SelectionGoal) {
-    let mut goal_column = if let SelectionGoal::Column(column) = goal {
-        column
-    } else {
-        map.column_to_chars(start.row(), start.column())
+    let mut goal_column = match goal {
+        SelectionGoal::Column(column) => column,
+        SelectionGoal::ColumnRange { end, .. } => end,
+        _ => map.column_to_chars(start.row(), start.column()),
     };
 
     let prev_row = start.row().saturating_sub(row_count);
@@ -95,10 +95,10 @@ pub fn down_by_rows(
     goal: SelectionGoal,
     preserve_column_at_end: bool,
 ) -> (DisplayPoint, SelectionGoal) {
-    let mut goal_column = if let SelectionGoal::Column(column) = goal {
-        column
-    } else {
-        map.column_to_chars(start.row(), start.column())
+    let mut goal_column = match goal {
+        SelectionGoal::Column(column) => column,
+        SelectionGoal::ColumnRange { end, .. } => end,
+        _ => map.column_to_chars(start.row(), start.column()),
     };
 
     let new_row = start.row() + row_count;
@@ -756,7 +756,8 @@ mod tests {
             .select_font(family_id, &Default::default())
             .unwrap();
 
-        let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
+        let buffer =
+            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
         let multibuffer = cx.add_model(|cx| {
             let mut multibuffer = MultiBuffer::new(0);
             multibuffer.push_excerpts(

crates/editor/src/multi_buffer.rs 🔗

@@ -6,7 +6,7 @@ use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet};
 use futures::{channel::mpsc, SinkExt};
 use git::diff::DiffHunk;
-use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
+use gpui::{AppContext, Entity, ModelContext, ModelHandle};
 pub use language::Completion;
 use language::{
     char_kind,
@@ -67,7 +67,9 @@ pub enum Event {
     ExcerptsEdited {
         ids: Vec<ExcerptId>,
     },
-    Edited,
+    Edited {
+        sigleton_buffer_edited: bool,
+    },
     Reloaded,
     DiffBaseChanged,
     LanguageChanged,
@@ -615,6 +617,42 @@ impl MultiBuffer {
         }
     }
 
+    pub fn merge_transactions(
+        &mut self,
+        transaction: TransactionId,
+        destination: TransactionId,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(buffer) = self.as_singleton() {
+            buffer.update(cx, |buffer, _| {
+                buffer.merge_transactions(transaction, destination)
+            });
+        } else {
+            if let Some(transaction) = self.history.forget(transaction) {
+                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);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext<Self>) {
         self.history.finalize_last_transaction();
         for BufferState { buffer, .. } in self.buffers.borrow().values() {
@@ -786,61 +824,75 @@ impl MultiBuffer {
         None
     }
 
+    pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext<Self>) {
+        if let Some(buffer) = self.as_singleton() {
+            buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
+        } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) {
+            for (buffer_id, transaction_id) in &transaction.buffer_transactions {
+                if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) {
+                    buffer.update(cx, |buffer, cx| {
+                        buffer.undo_transaction(*transaction_id, cx)
+                    });
+                }
+            }
+        }
+    }
+
     pub fn stream_excerpts_with_context_lines(
         &mut self,
-        excerpts: Vec<(ModelHandle<Buffer>, Vec<Range<text::Anchor>>)>,
+        buffer: ModelHandle<Buffer>,
+        ranges: Vec<Range<text::Anchor>>,
         context_line_count: u32,
         cx: &mut ModelContext<Self>,
-    ) -> (Task<()>, mpsc::Receiver<Range<Anchor>>) {
+    ) -> mpsc::Receiver<Range<Anchor>> {
         let (mut tx, rx) = mpsc::channel(256);
-        let task = cx.spawn(|this, mut cx| async move {
-            for (buffer, ranges) in excerpts {
-                let (buffer_id, buffer_snapshot) =
-                    buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot()));
-
-                let mut excerpt_ranges = Vec::new();
-                let mut range_counts = Vec::new();
-                cx.background()
-                    .scoped(|scope| {
-                        scope.spawn(async {
-                            let (ranges, counts) =
-                                build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
-                            excerpt_ranges = ranges;
-                            range_counts = counts;
-                        });
-                    })
-                    .await;
-
-                let mut ranges = ranges.into_iter();
-                let mut range_counts = range_counts.into_iter();
-                for excerpt_ranges in excerpt_ranges.chunks(100) {
-                    let excerpt_ids = this.update(&mut cx, |this, cx| {
-                        this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx)
+        cx.spawn(|this, mut cx| async move {
+            let (buffer_id, buffer_snapshot) =
+                buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot()));
+
+            let mut excerpt_ranges = Vec::new();
+            let mut range_counts = Vec::new();
+            cx.background()
+                .scoped(|scope| {
+                    scope.spawn(async {
+                        let (ranges, counts) =
+                            build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
+                        excerpt_ranges = ranges;
+                        range_counts = counts;
                     });
+                })
+                .await;
 
-                    for (excerpt_id, range_count) in
-                        excerpt_ids.into_iter().zip(range_counts.by_ref())
-                    {
-                        for range in ranges.by_ref().take(range_count) {
-                            let start = Anchor {
-                                buffer_id: Some(buffer_id),
-                                excerpt_id: excerpt_id.clone(),
-                                text_anchor: range.start,
-                            };
-                            let end = Anchor {
-                                buffer_id: Some(buffer_id),
-                                excerpt_id: excerpt_id.clone(),
-                                text_anchor: range.end,
-                            };
-                            if tx.send(start..end).await.is_err() {
-                                break;
-                            }
+            let mut ranges = ranges.into_iter();
+            let mut range_counts = range_counts.into_iter();
+            for excerpt_ranges in excerpt_ranges.chunks(100) {
+                let excerpt_ids = this.update(&mut cx, |this, cx| {
+                    this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx)
+                });
+
+                for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.by_ref())
+                {
+                    for range in ranges.by_ref().take(range_count) {
+                        let start = Anchor {
+                            buffer_id: Some(buffer_id),
+                            excerpt_id: excerpt_id.clone(),
+                            text_anchor: range.start,
+                        };
+                        let end = Anchor {
+                            buffer_id: Some(buffer_id),
+                            excerpt_id: excerpt_id.clone(),
+                            text_anchor: range.end,
+                        };
+                        if tx.send(start..end).await.is_err() {
+                            break;
                         }
                     }
                 }
             }
-        });
-        (task, rx)
+        })
+        .detach();
+
+        rx
     }
 
     pub fn push_excerpts<O>(
@@ -1022,7 +1074,9 @@ impl MultiBuffer {
             old: edit_start..edit_start,
             new: edit_start..edit_end,
         }]);
-        cx.emit(Event::Edited);
+        cx.emit(Event::Edited {
+            sigleton_buffer_edited: false,
+        });
         cx.emit(Event::ExcerptsAdded {
             buffer,
             predecessor: prev_excerpt_id,
@@ -1046,7 +1100,9 @@ impl MultiBuffer {
             old: 0..prev_len,
             new: 0..0,
         }]);
-        cx.emit(Event::Edited);
+        cx.emit(Event::Edited {
+            sigleton_buffer_edited: false,
+        });
         cx.emit(Event::ExcerptsRemoved { ids });
         cx.notify();
     }
@@ -1254,7 +1310,9 @@ impl MultiBuffer {
         }
 
         self.subscriptions.publish_mut(edits);
-        cx.emit(Event::Edited);
+        cx.emit(Event::Edited {
+            sigleton_buffer_edited: false,
+        });
         cx.emit(Event::ExcerptsRemoved { ids });
         cx.notify();
     }
@@ -1315,7 +1373,9 @@ impl MultiBuffer {
         cx: &mut ModelContext<Self>,
     ) {
         cx.emit(match event {
-            language::Event::Edited => Event::Edited,
+            language::Event::Edited => Event::Edited {
+                sigleton_buffer_edited: true,
+            },
             language::Event::DirtyChanged => Event::DirtyChanged,
             language::Event::Saved => Event::Saved,
             language::Event::FileHandleChanged => Event::FileHandleChanged,
@@ -1346,10 +1406,7 @@ impl MultiBuffer {
             .map(|state| state.buffer.clone())
     }
 
-    pub fn is_completion_trigger<T>(&self, position: T, text: &str, cx: &AppContext) -> bool
-    where
-        T: ToOffset,
-    {
+    pub fn is_completion_trigger(&self, position: Anchor, text: &str, cx: &AppContext) -> bool {
         let mut chars = text.chars();
         let char = if let Some(char) = chars.next() {
             char
@@ -1563,7 +1620,7 @@ impl MultiBuffer {
 #[cfg(any(test, feature = "test-support"))]
 impl MultiBuffer {
     pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> ModelHandle<Self> {
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text));
         cx.add_model(|cx| Self::singleton(buffer, cx))
     }
 
@@ -1573,7 +1630,7 @@ impl MultiBuffer {
     ) -> ModelHandle<Self> {
         let multi = cx.add_model(|_| Self::new(0));
         for (text, ranges) in excerpts {
-            let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+            let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text));
             let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange {
                 context: range,
                 primary: None,
@@ -1665,7 +1722,7 @@ impl MultiBuffer {
             if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) {
                 let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
                     let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
-                    buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx)));
+                    buffers.push(cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)));
                     let buffer = buffers.last().unwrap().read(cx);
                     log::info!(
                         "Creating new buffer {} with text: {:?}",
@@ -2309,6 +2366,16 @@ impl MultiBufferSnapshot {
         }
     }
 
+    pub fn prev_non_blank_row(&self, mut row: u32) -> Option<u32> {
+        while row > 0 {
+            row -= 1;
+            if !self.is_line_blank(row) {
+                return Some(row);
+            }
+        }
+        None
+    }
+
     pub fn line_len(&self, row: u32) -> u32 {
         if let Some((_, range)) = self.buffer_line_for_row(row) {
             range.end.column - range.start.column
@@ -2759,7 +2826,9 @@ impl MultiBufferSnapshot {
         // Get the ranges of the innermost pair of brackets.
         let mut result: Option<(Range<usize>, Range<usize>)> = None;
 
-        let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; };
+        let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else {
+            return None;
+        };
 
         for (open, close) in enclosing_bracket_ranges {
             let len = close.end - open.start;
@@ -3338,6 +3407,35 @@ impl History {
         }
     }
 
+    fn forget(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
+        if let Some(ix) = self
+            .undo_stack
+            .iter()
+            .rposition(|transaction| transaction.id == transaction_id)
+        {
+            Some(self.undo_stack.remove(ix))
+        } else if let Some(ix) = self
+            .redo_stack
+            .iter()
+            .rposition(|transaction| transaction.id == transaction_id)
+        {
+            Some(self.redo_stack.remove(ix))
+        } else {
+            None
+        }
+    }
+
+    fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
+        self.undo_stack
+            .iter_mut()
+            .find(|transaction| transaction.id == transaction_id)
+            .or_else(|| {
+                self.redo_stack
+                    .iter_mut()
+                    .find(|transaction| transaction.id == transaction_id)
+            })
+    }
+
     fn pop_undo(&mut self) -> Option<&mut Transaction> {
         assert_eq!(self.transaction_depth, 0);
         if let Some(transaction) = self.undo_stack.pop() {
@@ -3358,6 +3456,16 @@ impl History {
         }
     }
 
+    fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
+        let ix = self
+            .undo_stack
+            .iter()
+            .rposition(|transaction| transaction.id == transaction_id)?;
+        let transaction = self.undo_stack.remove(ix);
+        self.redo_stack.push(transaction);
+        self.redo_stack.last()
+    }
+
     fn group(&mut self) -> Option<TransactionId> {
         let mut count = 0;
         let mut transactions = self.undo_stack.iter();
@@ -4013,7 +4121,8 @@ mod tests {
 
     #[gpui::test]
     fn test_singleton(cx: &mut AppContext) {
-        let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
+        let buffer =
+            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a')));
         let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
 
         let snapshot = multibuffer.read(cx).snapshot(cx);
@@ -4040,7 +4149,7 @@ mod tests {
 
     #[gpui::test]
     fn test_remote(cx: &mut AppContext) {
-        let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx));
+        let host_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a"));
         let guest_buffer = cx.add_model(|cx| {
             let state = host_buffer.read(cx).to_proto();
             let ops = cx
@@ -4071,15 +4180,17 @@ mod tests {
 
     #[gpui::test]
     fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) {
-        let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
-        let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
+        let buffer_1 =
+            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a')));
+        let buffer_2 =
+            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'g')));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
 
         let events = Rc::new(RefCell::new(Vec::<Event>::new()));
         multibuffer.update(cx, |_, cx| {
             let events = events.clone();
             cx.subscribe(&multibuffer, move |_, _, event, _| {
-                if let Event::Edited = event {
+                if let Event::Edited { .. } = event {
                     events.borrow_mut().push(event.clone())
                 }
             })
@@ -4134,7 +4245,17 @@ mod tests {
         // Adding excerpts emits an edited event.
         assert_eq!(
             events.borrow().as_slice(),
-            &[Event::Edited, Event::Edited, Event::Edited]
+            &[
+                Event::Edited {
+                    sigleton_buffer_edited: false
+                },
+                Event::Edited {
+                    sigleton_buffer_edited: false
+                },
+                Event::Edited {
+                    sigleton_buffer_edited: false
+                }
+            ]
         );
 
         let snapshot = multibuffer.read(cx).snapshot(cx);
@@ -4295,8 +4416,10 @@ mod tests {
 
     #[gpui::test]
     fn test_excerpt_events(cx: &mut AppContext) {
-        let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx));
-        let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx));
+        let buffer_1 =
+            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'a')));
+        let buffer_2 =
+            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'm')));
 
         let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
         let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -4313,7 +4436,7 @@ mod tests {
                         excerpts,
                     } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
                     Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
-                    Event::Edited => {
+                    Event::Edited { .. } => {
                         *follower_edit_event_count.borrow_mut() += 1;
                     }
                     _ => {}
@@ -4401,7 +4524,8 @@ mod tests {
 
     #[gpui::test]
     fn test_push_excerpts_with_context_lines(cx: &mut AppContext) {
-        let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
+        let buffer =
+            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a')));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
         let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
             multibuffer.push_excerpts_with_context_lines(
@@ -4437,9 +4561,10 @@ mod tests {
 
     #[gpui::test]
     async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) {
-        let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
+        let buffer =
+            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a')));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
-        let (task, anchor_ranges) = multibuffer.update(cx, |multibuffer, cx| {
+        let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
             let snapshot = buffer.read(cx);
             let ranges = vec![
                 snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)),
@@ -4447,12 +4572,10 @@ mod tests {
                 snapshot.anchor_before(Point::new(15, 0))
                     ..snapshot.anchor_before(Point::new(15, 0)),
             ];
-            multibuffer.stream_excerpts_with_context_lines(vec![(buffer.clone(), ranges)], 2, cx)
+            multibuffer.stream_excerpts_with_context_lines(buffer.clone(), ranges, 2, cx)
         });
 
         let anchor_ranges = anchor_ranges.collect::<Vec<_>>().await;
-        // Ensure task is finished when stream completes.
-        task.await;
 
         let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
         assert_eq!(
@@ -4485,7 +4608,7 @@ mod tests {
 
     #[gpui::test]
     fn test_singleton_multibuffer_anchors(cx: &mut AppContext) {
-        let buffer = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd"));
         let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
         let old_snapshot = multibuffer.read(cx).snapshot(cx);
         buffer.update(cx, |buffer, cx| {
@@ -4505,8 +4628,8 @@ mod tests {
 
     #[gpui::test]
     fn test_multibuffer_anchors(cx: &mut AppContext) {
-        let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
-        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx));
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd"));
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "efghi"));
         let multibuffer = cx.add_model(|cx| {
             let mut multibuffer = MultiBuffer::new(0);
             multibuffer.push_excerpts(
@@ -4563,8 +4686,8 @@ mod tests {
 
     #[gpui::test]
     fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) {
-        let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
-        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx));
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd"));
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "ABCDEFGHIJKLMNOP"));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
 
         // Create an insertion id in buffer 1 that doesn't exist in buffer 2.
@@ -4959,7 +5082,9 @@ mod tests {
                         let base_text = util::RandomCharIter::new(&mut rng)
                             .take(10)
                             .collect::<String>();
-                        buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx)));
+                        buffers.push(
+                            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text)),
+                        );
                         buffers.last().unwrap()
                     } else {
                         buffers.choose(&mut rng).unwrap()
@@ -5300,8 +5425,8 @@ mod tests {
     fn test_history(cx: &mut AppContext) {
         cx.set_global(SettingsStore::test(cx));
 
-        let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
-        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "1234"));
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "5678"));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
         let group_interval = multibuffer.read(cx).history.group_interval;
         multibuffer.update(cx, |multibuffer, cx| {

crates/editor/src/scroll.rs 🔗

@@ -19,7 +19,7 @@ use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     hover_popover::hide_hover,
     persistence::DB,
-    Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
+    Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
     ToPoint,
 };
 
@@ -29,6 +29,7 @@ use self::{
 };
 
 pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
+pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 
 #[derive(Default)]
@@ -136,7 +137,7 @@ pub struct ScrollManager {
 impl ScrollManager {
     pub fn new() -> Self {
         ScrollManager {
-            vertical_scroll_margin: 3.0,
+            vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
             anchor: ScrollAnchor::new(),
             ongoing: OngoingScroll::new(),
             autoscroll_request: None,
@@ -301,7 +302,7 @@ impl Editor {
             cx.spawn(|editor, mut cx| async move {
                 editor
                     .update(&mut cx, |editor, cx| {
-                        editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
+                        editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
                     })
                     .ok()
             })
@@ -333,7 +334,7 @@ impl Editor {
             cx,
         );
 
-        self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
+        self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
     }
 
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {

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

@@ -65,47 +65,52 @@ impl Editor {
             self.set_scroll_position(scroll_position, cx);
         }
 
-        let (autoscroll, local) =
-            if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() {
-                autoscroll
-            } else {
-                return false;
-            };
-
-        let first_cursor_top;
-        let last_cursor_bottom;
+        let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
+            return false;
+        };
+
+        let mut target_top;
+        let mut target_bottom;
         if let Some(highlighted_rows) = &self.highlighted_rows {
-            first_cursor_top = highlighted_rows.start as f32;
-            last_cursor_bottom = first_cursor_top + 1.;
-        } else if autoscroll == Autoscroll::newest() {
-            let newest_selection = self.selections.newest::<Point>(cx);
-            first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
-            last_cursor_bottom = first_cursor_top + 1.;
+            target_top = highlighted_rows.start as f32;
+            target_bottom = target_top + 1.;
         } else {
             let selections = self.selections.all::<Point>(cx);
-            first_cursor_top = selections
+            target_top = selections
                 .first()
                 .unwrap()
                 .head()
                 .to_display_point(&display_map)
                 .row() as f32;
-            last_cursor_bottom = selections
+            target_bottom = selections
                 .last()
                 .unwrap()
                 .head()
                 .to_display_point(&display_map)
                 .row() as f32
                 + 1.0;
+
+            // If the selections can't all fit on screen, scroll to the newest.
+            if autoscroll == Autoscroll::newest()
+                || autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines
+            {
+                let newest_selection_top = selections
+                    .iter()
+                    .max_by_key(|s| s.id)
+                    .unwrap()
+                    .head()
+                    .to_display_point(&display_map)
+                    .row() as f32;
+                target_top = newest_selection_top;
+                target_bottom = newest_selection_top + 1.;
+            }
         }
 
         let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
             0.
         } else {
-            ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor()
+            ((visible_lines - (target_bottom - target_top)) / 2.0).floor()
         };
-        if margin < 0.0 {
-            return false;
-        }
 
         let strategy = match autoscroll {
             Autoscroll::Strategy(strategy) => strategy,
@@ -113,8 +118,8 @@ impl Editor {
                 let last_autoscroll = &self.scroll_manager.last_autoscroll;
                 if let Some(last_autoscroll) = last_autoscroll {
                     if self.scroll_manager.anchor.offset == last_autoscroll.0
-                        && first_cursor_top == last_autoscroll.1
-                        && last_cursor_bottom == last_autoscroll.2
+                        && target_top == last_autoscroll.1
+                        && target_bottom == last_autoscroll.2
                     {
                         last_autoscroll.3.next()
                     } else {
@@ -129,37 +134,41 @@ impl Editor {
         match strategy {
             AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
                 let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
-                let target_top = (first_cursor_top - margin).max(0.0);
-                let target_bottom = last_cursor_bottom + margin;
+                let target_top = (target_top - margin).max(0.0);
+                let target_bottom = target_bottom + margin;
                 let start_row = scroll_position.y();
                 let end_row = start_row + visible_lines;
 
-                if target_top < start_row {
+                let needs_scroll_up = target_top < start_row;
+                let needs_scroll_down = target_bottom >= end_row;
+
+                if needs_scroll_up && !needs_scroll_down {
                     scroll_position.set_y(target_top);
                     self.set_scroll_position_internal(scroll_position, local, true, cx);
-                } else if target_bottom >= end_row {
+                }
+                if !needs_scroll_up && needs_scroll_down {
                     scroll_position.set_y(target_bottom - visible_lines);
                     self.set_scroll_position_internal(scroll_position, local, true, cx);
                 }
             }
             AutoscrollStrategy::Center => {
-                scroll_position.set_y((first_cursor_top - margin).max(0.0));
+                scroll_position.set_y((target_top - margin).max(0.0));
                 self.set_scroll_position_internal(scroll_position, local, true, cx);
             }
             AutoscrollStrategy::Top => {
-                scroll_position.set_y((first_cursor_top).max(0.0));
+                scroll_position.set_y((target_top).max(0.0));
                 self.set_scroll_position_internal(scroll_position, local, true, cx);
             }
             AutoscrollStrategy::Bottom => {
-                scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
+                scroll_position.set_y((target_bottom - visible_lines).max(0.0));
                 self.set_scroll_position_internal(scroll_position, local, true, cx);
             }
         }
 
         self.scroll_manager.last_autoscroll = Some((
             self.scroll_manager.anchor.offset,
-            first_cursor_top,
-            last_cursor_bottom,
+            target_top,
+            target_bottom,
             strategy,
         ));
 

crates/editor/src/selections_collection.rs 🔗

@@ -1,7 +1,7 @@
 use std::{
     cell::Ref,
     cmp, iter, mem,
-    ops::{Deref, Range, Sub},
+    ops::{Deref, DerefMut, Range, Sub},
     sync::Arc,
 };
 
@@ -53,7 +53,7 @@ impl SelectionsCollection {
         }
     }
 
-    fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
+    pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
         self.display_map.update(cx, |map, cx| map.snapshot(cx))
     }
 
@@ -250,6 +250,10 @@ impl SelectionsCollection {
         resolve(self.oldest_anchor(), &self.buffer(cx))
     }
 
+    pub fn first_anchor(&self) -> Selection<Anchor> {
+        self.disjoint[0].clone()
+    }
+
     pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
         &self,
         cx: &AppContext,
@@ -352,7 +356,7 @@ pub struct MutableSelectionsCollection<'a> {
 }
 
 impl<'a> MutableSelectionsCollection<'a> {
-    fn display_map(&mut self) -> DisplaySnapshot {
+    pub fn display_map(&mut self) -> DisplaySnapshot {
         self.collection.display_map(self.cx)
     }
 
@@ -607,6 +611,10 @@ impl<'a> MutableSelectionsCollection<'a> {
         self.select_anchors(selections)
     }
 
+    pub fn new_selection_id(&mut self) -> usize {
+        post_inc(&mut self.next_selection_id)
+    }
+
     pub fn select_display_ranges<T>(&mut self, ranges: T)
     where
         T: IntoIterator<Item = Range<DisplayPoint>>,
@@ -831,6 +839,12 @@ impl<'a> Deref for MutableSelectionsCollection<'a> {
     }
 }
 
+impl<'a> DerefMut for MutableSelectionsCollection<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.collection
+    }
+}
+
 // Panics if passed selections are not in order
 pub fn resolve_multiple<'a, D, I>(
     selections: I,

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

@@ -6,6 +6,7 @@ use std::{
 
 use anyhow::Result;
 
+use collections::HashSet;
 use futures::Future;
 use gpui::{json, ViewContext, ViewHandle};
 use indoc::indoc;
@@ -154,10 +155,23 @@ impl<'a> EditorLspTestContext<'a> {
         capabilities: lsp::ServerCapabilities,
         cx: &'a mut gpui::TestAppContext,
     ) -> EditorLspTestContext<'a> {
+        let mut word_characters: HashSet<char> = Default::default();
+        word_characters.insert('$');
+        word_characters.insert('#');
         let language = Language::new(
             LanguageConfig {
                 name: "Typescript".into(),
                 path_suffixes: vec!["ts".to_string()],
+                brackets: language::BracketPairConfig {
+                    pairs: vec![language::BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    }],
+                    disabled_scopes_by_bracket_ix: Default::default(),
+                },
+                word_characters,
                 ..Default::default()
             },
             Some(tree_sitter_typescript::language_typescript()),
@@ -169,6 +183,23 @@ impl<'a> EditorLspTestContext<'a> {
                 ("{" @open "}" @close)
                 ("<" @open ">" @close)
                 ("\"" @open "\"" @close)"#})),
+            indents: Some(Cow::from(indoc! {r#"
+                [
+                    (call_expression)
+                    (assignment_expression)
+                    (member_expression)
+                    (lexical_declaration)
+                    (variable_declaration)
+                    (assignment_expression)
+                    (if_statement)
+                    (for_statement)
+                ] @indent
+
+                (_ "[" "]" @end) @indent
+                (_ "<" ">" @end) @indent
+                (_ "{" "}" @end) @indent
+                (_ "(" ")" @end) @indent
+                "#})),
             ..Default::default()
         })
         .expect("Could not parse queries");

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

@@ -225,6 +225,7 @@ impl<'a> EditorTestContext<'a> {
                 .map(|h| h.1.clone())
                 .unwrap_or_default()
                 .into_iter()
+                .filter_map(|range| range.as_text_range())
                 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
                 .collect()
         });
@@ -240,6 +241,7 @@ impl<'a> EditorTestContext<'a> {
             .map(|ranges| ranges.as_ref().clone().1)
             .unwrap_or_default()
             .into_iter()
+            .filter_map(|range| range.as_text_range())
             .map(|range| range.to_offset(&snapshot.buffer_snapshot))
             .collect();
         assert_set_eq!(actual_ranges, expected_ranges);

crates/staff_mode/Cargo.toml → crates/feature_flags/Cargo.toml 🔗

@@ -1,11 +1,11 @@
 [package]
-name = "staff_mode"
+name = "feature_flags"
 version = "0.1.0"
 edition = "2021"
 publish = false
 
 [lib]
-path = "src/staff_mode.rs"
+path = "src/feature_flags.rs"
 
 [dependencies]
 gpui = { path = "../gpui" }

crates/feature_flags/src/feature_flags.rs 🔗

@@ -0,0 +1,79 @@
+use gpui::{AppContext, Subscription, ViewContext};
+
+#[derive(Default)]
+struct FeatureFlags {
+    flags: Vec<String>,
+    staff: bool,
+}
+
+impl FeatureFlags {
+    fn has_flag(&self, flag: &str) -> bool {
+        self.staff || self.flags.iter().find(|f| f.as_str() == flag).is_some()
+    }
+}
+
+pub trait FeatureFlag {
+    const NAME: &'static str;
+}
+
+pub enum ChannelsAlpha {}
+
+impl FeatureFlag for ChannelsAlpha {
+    const NAME: &'static str = "channels_alpha";
+}
+
+pub trait FeatureFlagViewExt<V: 'static> {
+    fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
+    where
+        F: Fn(bool, &mut V, &mut ViewContext<V>) + 'static;
+}
+
+impl<V: 'static> FeatureFlagViewExt<V> for ViewContext<'_, '_, V> {
+    fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
+    where
+        F: Fn(bool, &mut V, &mut ViewContext<V>) + 'static,
+    {
+        self.observe_global::<FeatureFlags, _>(move |v, cx| {
+            let feature_flags = cx.global::<FeatureFlags>();
+            callback(feature_flags.has_flag(<T as FeatureFlag>::NAME), v, cx);
+        })
+    }
+}
+
+pub trait FeatureFlagAppExt {
+    fn update_flags(&mut self, staff: bool, flags: Vec<String>);
+    fn set_staff(&mut self, staff: bool);
+    fn has_flag<T: FeatureFlag>(&self) -> bool;
+    fn is_staff(&self) -> bool;
+}
+
+impl FeatureFlagAppExt for AppContext {
+    fn update_flags(&mut self, staff: bool, flags: Vec<String>) {
+        self.update_default_global::<FeatureFlags, _, _>(|feature_flags, _| {
+            feature_flags.staff = staff;
+            feature_flags.flags = flags;
+        })
+    }
+
+    fn set_staff(&mut self, staff: bool) {
+        self.update_default_global::<FeatureFlags, _, _>(|feature_flags, _| {
+            feature_flags.staff = staff;
+        })
+    }
+
+    fn has_flag<T: FeatureFlag>(&self) -> bool {
+        if self.has_global::<FeatureFlags>() {
+            self.global::<FeatureFlags>().has_flag(T::NAME)
+        } else {
+            false
+        }
+    }
+
+    fn is_staff(&self) -> bool {
+        if self.has_global::<FeatureFlags>() {
+            return self.global::<FeatureFlags>().staff;
+        } else {
+            false
+        }
+    }
+}

crates/feedback/src/deploy_feedback_button.rs 🔗

@@ -44,7 +44,7 @@ impl View for DeployFeedbackButton {
                         .in_state(active)
                         .style_for(state);
 
-                    Svg::new("icons/feedback_16.svg")
+                    Svg::new("icons/feedback.svg")
                         .with_color(style.icon_color)
                         .constrained()
                         .with_width(style.icon_size)

crates/feedback/src/feedback_editor.rs 🔗

@@ -268,7 +268,7 @@ impl Item for FeedbackEditor {
         Some("Send Feedback".into())
     }
 
-    fn tab_content<T: View>(
+    fn tab_content<T: 'static>(
         &self,
         _: Option<usize>,
         style: &theme::Tab,

crates/fs/Cargo.toml 🔗

@@ -12,6 +12,7 @@ collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 lsp = { path = "../lsp" }
 rope = { path = "../rope" }
+text = { path = "../text" }
 util = { path = "../util" }
 sum_tree = { path = "../sum_tree" }
 rpc = { path = "../rpc" }

crates/fs/src/fs.rs 🔗

@@ -4,14 +4,10 @@ use anyhow::{anyhow, Result};
 use fsevent::EventStream;
 use futures::{future::BoxFuture, Stream, StreamExt};
 use git2::Repository as LibGitRepository;
-use lazy_static::lazy_static;
 use parking_lot::Mutex;
-use regex::Regex;
 use repository::GitRepository;
 use rope::Rope;
 use smol::io::{AsyncReadExt, AsyncWriteExt};
-use std::borrow::Cow;
-use std::cmp;
 use std::io::Write;
 use std::sync::Arc;
 use std::{
@@ -22,6 +18,7 @@ use std::{
     time::{Duration, SystemTime},
 };
 use tempfile::NamedTempFile;
+use text::LineEnding;
 use util::ResultExt;
 
 #[cfg(any(test, feature = "test-support"))]
@@ -33,66 +30,6 @@ use std::ffi::OsStr;
 #[cfg(any(test, feature = "test-support"))]
 use std::sync::Weak;
 
-lazy_static! {
-    static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
-}
-
-#[derive(Clone, Copy, Debug, PartialEq)]
-pub enum LineEnding {
-    Unix,
-    Windows,
-}
-
-impl Default for LineEnding {
-    fn default() -> Self {
-        #[cfg(unix)]
-        return Self::Unix;
-
-        #[cfg(not(unix))]
-        return Self::CRLF;
-    }
-}
-
-impl LineEnding {
-    pub fn as_str(&self) -> &'static str {
-        match self {
-            LineEnding::Unix => "\n",
-            LineEnding::Windows => "\r\n",
-        }
-    }
-
-    pub fn detect(text: &str) -> Self {
-        let mut max_ix = cmp::min(text.len(), 1000);
-        while !text.is_char_boundary(max_ix) {
-            max_ix -= 1;
-        }
-
-        if let Some(ix) = text[..max_ix].find(&['\n']) {
-            if ix > 0 && text.as_bytes()[ix - 1] == b'\r' {
-                Self::Windows
-            } else {
-                Self::Unix
-            }
-        } else {
-            Self::default()
-        }
-    }
-
-    pub fn normalize(text: &mut String) {
-        if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") {
-            *text = replaced;
-        }
-    }
-
-    pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
-        if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
-            replaced.into()
-        } else {
-            text
-        }
-    }
-}
-
 #[async_trait::async_trait]
 pub trait Fs: Send + Sync {
     async fn create_dir(&self, path: &Path) -> Result<()>;
@@ -520,7 +457,7 @@ impl FakeFsState {
 }
 
 #[cfg(any(test, feature = "test-support"))]
-lazy_static! {
+lazy_static::lazy_static! {
     pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
 }
 

crates/gpui/Cargo.toml 🔗

@@ -39,6 +39,7 @@ pathfinder_color = "0.5"
 pathfinder_geometry = "0.5"
 postage.workspace = true
 rand.workspace = true
+refineable.workspace = true
 resvg = "0.14"
 schemars = "0.8"
 seahash = "4.1"
@@ -47,6 +48,7 @@ serde_derive.workspace = true
 serde_json.workspace = true
 smallvec.workspace = true
 smol.workspace = true
+taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] }
 time.workspace = true
 tiny-skia = "0.5"
 usvg = { version = "0.14", features = [] }

crates/gpui/examples/components.rs 🔗

@@ -1,9 +1,8 @@
 use button_component::Button;
 
-use component::AdaptComponent;
 use gpui::{
     color::Color,
-    elements::{ContainerStyle, Flex, Label, ParentElement},
+    elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent},
     fonts::{self, TextStyle},
     platform::WindowOptions,
     AnyElement, App, Element, Entity, View, ViewContext,
@@ -14,6 +13,8 @@ use simplelog::SimpleLogger;
 use theme::Toggleable;
 use toggleable_button::ToggleableButton;
 
+// cargo run -p gpui --example components
+
 fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
@@ -71,7 +72,7 @@ impl View for TestView {
                         TextStyle::for_color(Color::blue()),
                     )
                     .with_style(ButtonStyle::fill(Color::yellow()))
-                    .into_element(),
+                    .element(),
                 )
                 .with_child(
                     ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| {
@@ -83,7 +84,7 @@ impl View for TestView {
                         inactive: ButtonStyle::fill(Color::red()),
                         active: ButtonStyle::fill(Color::green()),
                     })
-                    .into_element(),
+                    .element(),
                 )
                 .expanded()
                 .contained()
@@ -113,12 +114,12 @@ mod theme {
 // Component creation:
 mod toggleable_button {
     use gpui::{
-        elements::{ContainerStyle, LabelStyle},
+        elements::{ContainerStyle, LabelStyle, StatefulComponent},
         scene::MouseClick,
         EventContext, View,
     };
 
-    use crate::{button_component::Button, component::Component, theme::Toggleable};
+    use crate::{button_component::Button, theme::Toggleable};
 
     pub struct ToggleableButton<V: View> {
         active: bool,
@@ -155,14 +156,8 @@ mod toggleable_button {
         }
     }
 
-    impl<V: View> Component for ToggleableButton<V> {
-        type View = V;
-
-        fn render(
-            self,
-            v: &mut Self::View,
-            cx: &mut gpui::ViewContext<Self::View>,
-        ) -> gpui::AnyElement<V> {
+    impl<V: View> StatefulComponent<V> for ToggleableButton<V> {
+        fn render(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
             let button = if let Some(style) = self.style {
                 self.button.with_style(*style.style_for(self.active))
             } else {
@@ -176,14 +171,12 @@ mod toggleable_button {
 mod button_component {
 
     use gpui::{
-        elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler},
+        elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent},
         platform::MouseButton,
         scene::MouseClick,
         AnyElement, Element, EventContext, TypeTag, View, ViewContext,
     };
 
-    use crate::component::Component;
-
     type ClickHandler<V> = Box<dyn Fn(MouseClick, &mut V, &mut EventContext<V>)>;
 
     pub struct Button<V: View> {
@@ -219,10 +212,8 @@ mod button_component {
         }
     }
 
-    impl<V: View> Component for Button<V> {
-        type View = V;
-
-        fn render(self, _: &mut Self::View, cx: &mut ViewContext<V>) -> AnyElement<Self::View> {
+    impl<V: View> StatefulComponent<V> for Button<V> {
+        fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
             let click_handler = self.click_handler;
 
             let result = MouseEventHandler::new_dynamic(self.tag, 0, cx, |_, _| {
@@ -244,92 +235,3 @@ mod button_component {
         }
     }
 }
-
-mod component {
-
-    use gpui::{AnyElement, Element, View, ViewContext};
-    use pathfinder_geometry::vector::Vector2F;
-
-    // Public API:
-    pub trait Component {
-        type View: View;
-
-        fn render(
-            self,
-            v: &mut Self::View,
-            cx: &mut ViewContext<Self::View>,
-        ) -> AnyElement<Self::View>;
-    }
-
-    pub struct ComponentAdapter<E> {
-        component: Option<E>,
-    }
-
-    impl<E> ComponentAdapter<E> {
-        pub fn new(e: E) -> Self {
-            Self { component: Some(e) }
-        }
-    }
-
-    pub trait AdaptComponent<C: Component>: Sized {
-        fn into_element(self) -> ComponentAdapter<Self> {
-            ComponentAdapter::new(self)
-        }
-    }
-
-    impl<C: Component> AdaptComponent<C> for C {}
-
-    impl<C: Component + 'static> Element<C::View> for ComponentAdapter<C> {
-        type LayoutState = AnyElement<C::View>;
-
-        type PaintState = ();
-
-        fn layout(
-            &mut self,
-            constraint: gpui::SizeConstraint,
-            view: &mut C::View,
-            cx: &mut gpui::LayoutContext<C::View>,
-        ) -> (Vector2F, Self::LayoutState) {
-            let component = self.component.take().unwrap();
-            let mut element = component.render(view, cx.view_context());
-            let constraint = element.layout(constraint, view, cx);
-            (constraint, element)
-        }
-
-        fn paint(
-            &mut self,
-            scene: &mut gpui::SceneBuilder,
-            bounds: gpui::geometry::rect::RectF,
-            visible_bounds: gpui::geometry::rect::RectF,
-            layout: &mut Self::LayoutState,
-            view: &mut C::View,
-            cx: &mut gpui::PaintContext<C::View>,
-        ) -> Self::PaintState {
-            layout.paint(scene, bounds.origin(), visible_bounds, view, cx)
-        }
-
-        fn rect_for_text_range(
-            &self,
-            _: std::ops::Range<usize>,
-            _: gpui::geometry::rect::RectF,
-            _: gpui::geometry::rect::RectF,
-            _: &Self::LayoutState,
-            _: &Self::PaintState,
-            _: &C::View,
-            _: &ViewContext<C::View>,
-        ) -> Option<gpui::geometry::rect::RectF> {
-            todo!()
-        }
-
-        fn debug(
-            &self,
-            _: gpui::geometry::rect::RectF,
-            _: &Self::LayoutState,
-            _: &Self::PaintState,
-            _: &C::View,
-            _: &ViewContext<C::View>,
-        ) -> serde_json::Value {
-            todo!()
-        }
-    }
-}

crates/gpui/examples/text.rs 🔗

@@ -58,6 +58,7 @@ impl gpui::View for TextView {
                 font_family_id: family,
                 underline: Default::default(),
                 font_properties: Default::default(),
+                soft_wrap: false,
             },
         )
         .with_highlights(vec![(17..26, underline), (34..40, underline)])

crates/gpui/playground/Cargo.lock 🔗

@@ -0,0 +1,2919 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
+name = "aho-corasick"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+
+[[package]]
+name = "arrayref"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb"
+dependencies = [
+ "async-lock",
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite",
+ "log",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "socket2",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-net"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f"
+dependencies = [
+ "async-io",
+ "autocfg",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "autocfg",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+ "signal-hook",
+ "windows-sys",
+]
+
+[[package]]
+name = "async-task"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae"
+
+[[package]]
+name = "atomic"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide 0.7.1",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "bindgen"
+version = "0.65.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 2.0.25",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65"
+dependencies = [
+ "async-channel",
+ "async-lock",
+ "async-task",
+ "atomic-waker",
+ "fastrand",
+ "futures-lite",
+ "log",
+]
+
+[[package]]
+name = "bstr"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bytemuck"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+
+[[package]]
+name = "castaway"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clang-sys"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading 0.7.4",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cocoa"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
+dependencies = [
+ "bitflags",
+ "block",
+ "cocoa-foundation",
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6"
+dependencies = [
+ "bitflags",
+ "block",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "collections"
+version = "0.1.0"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-cstr"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+ "uuid 0.5.1",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
+name = "core-graphics"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "core-text"
+version = "19.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25"
+dependencies = [
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.63+curl-8.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "data-url"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "deflate"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
+dependencies = [
+ "adler32",
+ "byteorder",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dlib"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+dependencies = [
+ "libloading 0.8.0",
+]
+
+[[package]]
+name = "dwrote"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "winapi",
+ "wio",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "erased-serde"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "etagere"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644"
+dependencies = [
+ "euclid",
+ "svg_fmt",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide 0.7.1",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e"
+
+[[package]]
+name = "float-ord"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "font-kit"
+version = "0.11.0"
+source = "git+https://github.com/zed-industries/font-kit?rev=b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18#b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18"
+dependencies = [
+ "bitflags",
+ "byteorder",
+ "core-foundation",
+ "core-graphics",
+ "core-text",
+ "dirs-next",
+ "dwrote",
+ "float-ord",
+ "freetype",
+ "lazy_static",
+ "libc",
+ "log",
+ "pathfinder_geometry",
+ "pathfinder_simd",
+ "walkdir",
+ "winapi",
+ "yeslogic-fontconfig-sys",
+]
+
+[[package]]
+name = "fontdb"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e58903f4f8d5b58c7d300908e4ebe5289c1bfdf5587964330f12023b8ff17fd1"
+dependencies = [
+ "log",
+ "memmap2",
+ "ttf-parser 0.12.3",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "freetype"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6"
+dependencies = [
+ "freetype-sys",
+ "libc",
+]
+
+[[package]]
+name = "freetype-sys"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a"
+dependencies = [
+ "cmake",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gif"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "gimli"
+version = "0.27.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "globset"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "gpui"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-task",
+ "bindgen",
+ "block",
+ "cc",
+ "cocoa",
+ "collections",
+ "core-foundation",
+ "core-graphics",
+ "core-text",
+ "ctor",
+ "etagere",
+ "font-kit",
+ "foreign-types",
+ "futures",
+ "gpui_macros",
+ "image",
+ "itertools",
+ "lazy_static",
+ "log",
+ "media",
+ "metal",
+ "num_cpus",
+ "objc",
+ "ordered-float",
+ "parking",
+ "parking_lot 0.11.2",
+ "pathfinder_color",
+ "pathfinder_geometry",
+ "postage",
+ "rand",
+ "resvg",
+ "schemars",
+ "seahash",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smallvec",
+ "smol",
+ "sqlez",
+ "sum_tree",
+ "time",
+ "tiny-skia",
+ "usvg",
+ "util",
+ "uuid 1.4.0",
+ "waker-fn",
+]
+
+[[package]]
+name = "gpui_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+
+[[package]]
+name = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "idna"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "image"
+version = "0.23.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "gif",
+ "jpeg-decoder",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+ "png",
+ "scoped_threadpool",
+ "tiff",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "indoc"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "isahc"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
+dependencies = [
+ "async-channel",
+ "castaway",
+ "crossbeam-utils",
+ "curl",
+ "curl-sys",
+ "encoding_rs",
+ "event-listener",
+ "futures-lite",
+ "http",
+ "log",
+ "mime",
+ "once_cell",
+ "polling",
+ "slab",
+ "sluice",
+ "tracing",
+ "tracing-futures",
+ "url",
+ "waker-fn",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
+
+[[package]]
+name = "jpeg-decoder"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+dependencies = [
+ "rayon",
+]
+
+[[package]]
+name = "kurbo"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
+dependencies = [
+ "arrayvec 0.7.4",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libloading"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
+dependencies = [
+ "cfg-if",
+ "windows-sys",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+dependencies = [
+ "serde",
+ "value-bag",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "media"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bindgen",
+ "block",
+ "bytes",
+ "core-foundation",
+ "foreign-types",
+ "metal",
+ "objc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memmap2"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "metal"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4598d719460ade24c7d91f335daf055bf2a7eec030728ce751814c50cdd6a26c"
+dependencies = [
+ "bitflags",
+ "block",
+ "cocoa-foundation",
+ "foreign-types",
+ "log",
+ "objc",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
+dependencies = [
+ "adler32",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+ "objc_exception",
+]
+
+[[package]]
+name = "objc_exception"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "object"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "ordered-float"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "parking"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.8",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.3.5",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "pathfinder_color"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69bdc0d277d559e35e1b374de56df9262a6b71e091ca04a8831a239f8c7f0c62"
+dependencies = [
+ "pathfinder_simd",
+]
+
+[[package]]
+name = "pathfinder_geometry"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3"
+dependencies = [
+ "log",
+ "pathfinder_simd",
+]
+
+[[package]]
+name = "pathfinder_simd"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff"
+dependencies = [
+ "rustc_version",
+]
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[package]]
+name = "pest"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9"
+dependencies = [
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pico-args"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
+
+[[package]]
+name = "pin-project"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+
+[[package]]
+name = "playground"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+]
+
+[[package]]
+name = "png"
+version = "0.16.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "deflate",
+ "miniz_oxide 0.3.7",
+]
+
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite",
+ "windows-sys",
+]
+
+[[package]]
+name = "pollster"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
+
+[[package]]
+name = "postage"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1"
+dependencies = [
+ "atomic",
+ "crossbeam-queue",
+ "futures",
+ "log",
+ "parking_lot 0.12.1",
+ "pin-project",
+ "pollster",
+ "static_assertions",
+ "thiserror",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92139198957b410250d43fad93e630d956499a625c527eda65175c8680f83387"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rayon"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
+[[package]]
+name = "rctree"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be9e29cb19c8fe84169fcb07f8f11e66bc9e6e0280efd4715c54818296f8a4a8"
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall 0.2.16",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
+
+[[package]]
+name = "resvg"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09697862c5c3f940cbaffef91969c62188b5c8ed385b0aef43a5ff01ddc8000f"
+dependencies = [
+ "jpeg-decoder",
+ "log",
+ "pico-args",
+ "png",
+ "rgb",
+ "svgfilters",
+ "tiny-skia",
+ "usvg",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "roxmltree"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b"
+dependencies = [
+ "xmlparser",
+]
+
+[[package]]
+name = "rust-embed"
+version = "6.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "6.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.25",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "7.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
+dependencies = [
+ "globset",
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc_version"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustybuzz"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ab463a295d00f3692e0974a0bfd83c7a9bcd119e27e07c2beecdb1b44a09d10"
+dependencies = [
+ "bitflags",
+ "bytemuck",
+ "smallvec",
+ "ttf-parser 0.9.0",
+ "unicode-bidi-mirroring",
+ "unicode-ccc",
+ "unicode-general-category",
+ "unicode-script",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
+
+[[package]]
+name = "safe_arch"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "scoped_threadpool"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "serde_fmt"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
+
+[[package]]
+name = "signal-hook"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simplecss"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "sluice"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
+dependencies = [
+ "async-channel",
+ "futures-core",
+ "futures-io",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+
+[[package]]
+name = "smol"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "async-net",
+ "async-process",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "sqlez"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "futures",
+ "indoc",
+ "lazy_static",
+ "libsqlite3-sys",
+ "parking_lot 0.11.2",
+ "smol",
+ "thread_local",
+ "uuid 1.4.0",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "sum_tree"
+version = "0.1.0"
+dependencies = [
+ "arrayvec 0.7.4",
+ "log",
+]
+
+[[package]]
+name = "sval"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1"
+
+[[package]]
+name = "sval_buffer"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028"
+dependencies = [
+ "sval",
+ "sval_ref",
+]
+
+[[package]]
+name = "sval_dynamic"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf"
+dependencies = [
+ "sval",
+]
+
+[[package]]
+name = "sval_fmt"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326"
+dependencies = [
+ "itoa",
+ "ryu",
+ "sval",
+]
+
+[[package]]
+name = "sval_json"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d"
+dependencies = [
+ "itoa",
+ "ryu",
+ "sval",
+]
+
+[[package]]
+name = "sval_ref"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c"
+dependencies = [
+ "sval",
+]
+
+[[package]]
+name = "sval_serde"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046"
+dependencies = [
+ "serde",
+ "sval",
+ "sval_buffer",
+ "sval_fmt",
+]
+
+[[package]]
+name = "svg_fmt"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
+
+[[package]]
+name = "svgfilters"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb0dce2fee79ac40c21dafba48565ff7a5fa275e23ffe9ce047a40c9574ba34e"
+dependencies = [
+ "float-cmp",
+ "rgb",
+]
+
+[[package]]
+name = "svgtypes"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
+dependencies = [
+ "float-cmp",
+ "siphasher",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "take-until"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb"
+
+[[package]]
+name = "thiserror"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tiff"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
+dependencies = [
+ "jpeg-decoder",
+ "miniz_oxide 0.4.4",
+ "weezl",
+]
+
+[[package]]
+name = "time"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
+dependencies = [
+ "itoa",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
+
+[[package]]
+name = "time-macros"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
+name = "tiny-skia"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf81f2900d2e235220e6f31ec9f63ade6a7f59090c556d74fe949bb3b15e9fe"
+dependencies = [
+ "arrayref",
+ "arrayvec 0.5.2",
+ "bytemuck",
+ "cfg-if",
+ "png",
+ "safe_arch",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tracing-futures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
+dependencies = [
+ "pin-project",
+ "tracing",
+]
+
+[[package]]
+name = "ttf-parser"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ddb402ac6c2af6f7a2844243887631c4e94b51585b229fcfddb43958cd55ca"
+
+[[package]]
+name = "ttf-parser"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6"
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
+
+[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694"
+
+[[package]]
+name = "unicode-ccc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1"
+
+[[package]]
+name = "unicode-general-category"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-script"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc"
+
+[[package]]
+name = "unicode-vo"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
+
+[[package]]
+name = "url"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "usvg"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef8352f317d8f9a918ba5154797fb2a93e2730244041cf7d5be35148266adfa5"
+dependencies = [
+ "base64",
+ "data-url",
+ "flate2",
+ "fontdb",
+ "kurbo",
+ "log",
+ "memmap2",
+ "pico-args",
+ "rctree",
+ "roxmltree",
+ "rustybuzz",
+ "simplecss",
+ "siphasher",
+ "svgtypes",
+ "ttf-parser 0.12.3",
+ "unicode-bidi",
+ "unicode-script",
+ "unicode-vo",
+ "xmlwriter",
+]
+
+[[package]]
+name = "util"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "backtrace",
+ "dirs",
+ "futures",
+ "isahc",
+ "lazy_static",
+ "log",
+ "rand",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "smol",
+ "take-until",
+ "url",
+]
+
+[[package]]
+name = "uuid"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
+
+[[package]]
+name = "uuid"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "value-bag"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3"
+dependencies = [
+ "value-bag-serde1",
+ "value-bag-sval2",
+]
+
+[[package]]
+name = "value-bag-serde1"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_fmt",
+]
+
+[[package]]
+name = "value-bag-sval2"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d"
+dependencies = [
+ "sval",
+ "sval_buffer",
+ "sval_dynamic",
+ "sval_fmt",
+ "sval_json",
+ "sval_ref",
+ "sval_serde",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "walkdir"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "weezl"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
+
+[[package]]
+name = "which"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "wio"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "xmlparser"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
+
+[[package]]
+name = "xmlwriter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
+
+[[package]]
+name = "yeslogic-fontconfig-sys"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386"
+dependencies = [
+ "const-cstr",
+ "dlib",
+ "once_cell",
+ "pkg-config",
+]

crates/gpui/playground/Cargo.toml 🔗

@@ -0,0 +1,26 @@
+[package]
+name = "playground"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "playground"
+path = "src/playground.rs"
+
+[dependencies]
+anyhow.workspace = true
+derive_more.workspace = true
+gpui = { path = ".." }
+log.workspace = true
+playground_macros = { path = "../playground_macros" }
+parking_lot.workspace = true
+refineable.workspace = true
+serde.workspace = true
+simplelog = "0.9"
+smallvec.workspace = true
+taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "dab541d6104d58e2e10ce90c4a1dad0b703160cd", features = ["flexbox"] }
+util = { path = "../../util" }
+
+[dev-dependencies]
+gpui = { path = "..", features = ["test-support"] }

crates/gpui/playground/docs/thoughts.md 🔗

@@ -0,0 +1,72 @@
+Much of element styling is now handled by an external engine.
+
+
+How do I make an element hover.
+
+There's a hover style.
+
+Hoverable needs to wrap another element. That element can be styled.
+
+```rs
+struct Hoverable<E: Element> {
+
+}
+
+impl<V> Element<V> for Hoverable {
+
+}
+
+```
+
+
+
+```rs
+#[derive(Styled, Interactive)]
+pub struct Div {
+    declared_style: StyleRefinement,
+    interactions: Interactions
+}
+
+pub trait Styled {
+    fn declared_style(&mut self) -> &mut StyleRefinement;
+    fn compute_style(&mut self) -> Style {
+        Style::default().refine(self.declared_style())
+    }
+
+    // All the tailwind classes, modifying self.declared_style()
+}
+
+impl Style {
+    pub fn paint_background<V>(layout: Layout, cx: &mut PaintContext<V>);
+    pub fn paint_foreground<V>(layout: Layout, cx: &mut PaintContext<V>);
+}
+
+pub trait Interactive<V> {
+    fn interactions(&mut self) -> &mut Interactions<V>;
+
+    fn on_click(self, )
+}
+
+struct Interactions<V> {
+    click: SmallVec<[<Rc<dyn Fn(&mut V, &dyn Any, )>; 1]>,
+}
+
+
+```
+
+
+```rs
+
+
+trait Stylable {
+    type Style;
+
+    fn with_style(self, style: Self::Style) -> Self;
+}
+
+
+
+
+
+
+```

crates/gpui/playground/src/adapter.rs 🔗

@@ -0,0 +1,78 @@
+use crate::{layout_context::LayoutContext, paint_context::PaintContext};
+use gpui::{geometry::rect::RectF, LayoutEngine, LayoutId};
+use util::ResultExt;
+
+/// Makes a new, playground-style element into a legacy element.
+pub struct AdapterElement<V>(pub(crate) crate::element::AnyElement<V>);
+
+impl<V: 'static> gpui::Element<V> for AdapterElement<V> {
+    type LayoutState = Option<(LayoutEngine, LayoutId)>;
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        view: &mut V,
+        cx: &mut gpui::LayoutContext<V>,
+    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+        cx.push_layout_engine(LayoutEngine::new());
+
+        let size = constraint.max;
+        let mut cx = LayoutContext::new(cx);
+        let layout_id = self.0.layout(view, &mut cx).log_err();
+        if let Some(layout_id) = layout_id {
+            cx.layout_engine()
+                .unwrap()
+                .compute_layout(layout_id, constraint.max)
+                .log_err();
+        }
+
+        let layout_engine = cx.pop_layout_engine();
+        debug_assert!(layout_engine.is_some(),
+            "unexpected layout stack state. is there an unmatched pop_layout_engine in the called code?"
+        );
+
+        (constraint.max, layout_engine.zip(layout_id))
+    }
+
+    fn paint(
+        &mut self,
+        scene: &mut gpui::SceneBuilder,
+        bounds: RectF,
+        visible_bounds: RectF,
+        layout_data: &mut Option<(LayoutEngine, LayoutId)>,
+        view: &mut V,
+        legacy_cx: &mut gpui::PaintContext<V>,
+    ) -> Self::PaintState {
+        let (layout_engine, layout_id) = layout_data.take().unwrap();
+        legacy_cx.push_layout_engine(layout_engine);
+        let mut cx = PaintContext::new(legacy_cx, scene);
+        self.0.paint(view, &mut cx);
+        *layout_data = legacy_cx.pop_layout_engine().zip(Some(layout_id));
+        debug_assert!(layout_data.is_some());
+    }
+
+    fn rect_for_text_range(
+        &self,
+        range_utf16: std::ops::Range<usize>,
+        bounds: RectF,
+        visible_bounds: RectF,
+        layout: &Self::LayoutState,
+        paint: &Self::PaintState,
+        view: &V,
+        cx: &gpui::ViewContext<V>,
+    ) -> Option<RectF> {
+        todo!("implement before merging to main")
+    }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        layout: &Self::LayoutState,
+        paint: &Self::PaintState,
+        view: &V,
+        cx: &gpui::ViewContext<V>,
+    ) -> gpui::serde_json::Value {
+        todo!("implement before merging to main")
+    }
+}

crates/gpui/playground/src/color.rs 🔗

@@ -0,0 +1,276 @@
+#![allow(dead_code)]
+
+use std::{num::ParseIntError, ops::Range};
+
+use smallvec::SmallVec;
+
+pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
+    let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
+    let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
+    let b = (hex & 0xFF) as f32 / 255.0;
+    Rgba { r, g, b, a: 1.0 }.into()
+}
+
+#[derive(Clone, Copy, Default, Debug)]
+pub struct Rgba {
+    pub r: f32,
+    pub g: f32,
+    pub b: f32,
+    pub a: f32,
+}
+
+pub trait Lerp {
+    fn lerp(&self, level: f32) -> Hsla;
+}
+
+impl Lerp for Range<Hsla> {
+    fn lerp(&self, level: f32) -> Hsla {
+        let level = level.clamp(0., 1.);
+        Hsla {
+            h: self.start.h + (level * (self.end.h - self.start.h)),
+            s: self.start.s + (level * (self.end.s - self.start.s)),
+            l: self.start.l + (level * (self.end.l - self.start.l)),
+            a: self.start.a + (level * (self.end.a - self.start.a)),
+        }
+    }
+}
+
+impl From<gpui::color::Color> for Rgba {
+    fn from(value: gpui::color::Color) -> Self {
+        Self {
+            r: value.0.r as f32 / 255.0,
+            g: value.0.g as f32 / 255.0,
+            b: value.0.b as f32 / 255.0,
+            a: value.0.a as f32 / 255.0,
+        }
+    }
+}
+
+impl From<Hsla> for Rgba {
+    fn from(color: Hsla) -> Self {
+        let h = color.h;
+        let s = color.s;
+        let l = color.l;
+
+        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
+        let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
+        let m = l - c / 2.0;
+        let cm = c + m;
+        let xm = x + m;
+
+        let (r, g, b) = match (h * 6.0).floor() as i32 {
+            0 | 6 => (cm, xm, m),
+            1 => (xm, cm, m),
+            2 => (m, cm, xm),
+            3 => (m, xm, cm),
+            4 => (xm, m, cm),
+            _ => (cm, m, xm),
+        };
+
+        Rgba {
+            r,
+            g,
+            b,
+            a: color.a,
+        }
+    }
+}
+
+impl TryFrom<&'_ str> for Rgba {
+    type Error = ParseIntError;
+
+    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
+        let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0;
+        let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0;
+        let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0;
+        let a = if value.len() > 7 {
+            u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0
+        } else {
+            1.0
+        };
+
+        Ok(Rgba { r, g, b, a })
+    }
+}
+
+impl Into<gpui::color::Color> for Rgba {
+    fn into(self) -> gpui::color::Color {
+        gpui::color::rgba(self.r, self.g, self.b, self.a)
+    }
+}
+
+#[derive(Default, Copy, Clone, Debug, PartialEq)]
+pub struct Hsla {
+    pub h: f32,
+    pub s: f32,
+    pub l: f32,
+    pub a: f32,
+}
+
+pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
+    Hsla {
+        h: h.clamp(0., 1.),
+        s: s.clamp(0., 1.),
+        l: l.clamp(0., 1.),
+        a: a.clamp(0., 1.),
+    }
+}
+
+pub fn black() -> Hsla {
+    Hsla {
+        h: 0.,
+        s: 0.,
+        l: 0.,
+        a: 1.,
+    }
+}
+
+impl From<Rgba> for Hsla {
+    fn from(color: Rgba) -> Self {
+        let r = color.r;
+        let g = color.g;
+        let b = color.b;
+
+        let max = r.max(g.max(b));
+        let min = r.min(g.min(b));
+        let delta = max - min;
+
+        let l = (max + min) / 2.0;
+        let s = if l == 0.0 || l == 1.0 {
+            0.0
+        } else if l < 0.5 {
+            delta / (2.0 * l)
+        } else {
+            delta / (2.0 - 2.0 * l)
+        };
+
+        let h = if delta == 0.0 {
+            0.0
+        } else if max == r {
+            ((g - b) / delta).rem_euclid(6.0) / 6.0
+        } else if max == g {
+            ((b - r) / delta + 2.0) / 6.0
+        } else {
+            ((r - g) / delta + 4.0) / 6.0
+        };
+
+        Hsla {
+            h,
+            s,
+            l,
+            a: color.a,
+        }
+    }
+}
+
+impl Hsla {
+    /// Scales the saturation and lightness by the given values, clamping at 1.0.
+    pub fn scale_sl(mut self, s: f32, l: f32) -> Self {
+        self.s = (self.s * s).clamp(0., 1.);
+        self.l = (self.l * l).clamp(0., 1.);
+        self
+    }
+
+    /// Increases the saturation of the color by a certain amount, with a max
+    /// value of 1.0.
+    pub fn saturate(mut self, amount: f32) -> Self {
+        self.s += amount;
+        self.s = self.s.clamp(0.0, 1.0);
+        self
+    }
+
+    /// Decreases the saturation of the color by a certain amount, with a min
+    /// value of 0.0.
+    pub fn desaturate(mut self, amount: f32) -> Self {
+        self.s -= amount;
+        self.s = self.s.max(0.0);
+        if self.s < 0.0 {
+            self.s = 0.0;
+        }
+        self
+    }
+
+    /// Brightens the color by increasing the lightness by a certain amount,
+    /// with a max value of 1.0.
+    pub fn brighten(mut self, amount: f32) -> Self {
+        self.l += amount;
+        self.l = self.l.clamp(0.0, 1.0);
+        self
+    }
+
+    /// Darkens the color by decreasing the lightness by a certain amount,
+    /// with a max value of 0.0.
+    pub fn darken(mut self, amount: f32) -> Self {
+        self.l -= amount;
+        self.l = self.l.clamp(0.0, 1.0);
+        self
+    }
+}
+
+impl From<gpui::color::Color> for Hsla {
+    fn from(value: gpui::color::Color) -> Self {
+        Rgba::from(value).into()
+    }
+}
+
+impl Into<gpui::color::Color> for Hsla {
+    fn into(self) -> gpui::color::Color {
+        Rgba::from(self).into()
+    }
+}
+
+pub struct ColorScale {
+    colors: SmallVec<[Hsla; 2]>,
+    positions: SmallVec<[f32; 2]>,
+}
+
+pub fn scale<I, C>(colors: I) -> ColorScale
+where
+    I: IntoIterator<Item = C>,
+    C: Into<Hsla>,
+{
+    let mut scale = ColorScale {
+        colors: colors.into_iter().map(Into::into).collect(),
+        positions: SmallVec::new(),
+    };
+    let num_colors: f32 = scale.colors.len() as f32 - 1.0;
+    scale.positions = (0..scale.colors.len())
+        .map(|i| i as f32 / num_colors)
+        .collect();
+    scale
+}
+
+impl ColorScale {
+    fn at(&self, t: f32) -> Hsla {
+        // Ensure that the input is within [0.0, 1.0]
+        debug_assert!(
+            0.0 <= t && t <= 1.0,
+            "t value {} is out of range. Expected value in range 0.0 to 1.0",
+            t
+        );
+
+        let position = match self
+            .positions
+            .binary_search_by(|a| a.partial_cmp(&t).unwrap())
+        {
+            Ok(index) | Err(index) => index,
+        };
+        let lower_bound = position.saturating_sub(1);
+        let upper_bound = position.min(self.colors.len() - 1);
+        let lower_color = &self.colors[lower_bound];
+        let upper_color = &self.colors[upper_bound];
+
+        match upper_bound.checked_sub(lower_bound) {
+            Some(0) | None => *lower_color,
+            Some(_) => {
+                let interval_t = (t - self.positions[lower_bound])
+                    / (self.positions[upper_bound] - self.positions[lower_bound]);
+                let h = lower_color.h + interval_t * (upper_color.h - lower_color.h);
+                let s = lower_color.s + interval_t * (upper_color.s - lower_color.s);
+                let l = lower_color.l + interval_t * (upper_color.l - lower_color.l);
+                let a = lower_color.a + interval_t * (upper_color.a - lower_color.a);
+                Hsla { h, s, l, a }
+            }
+        }
+    }
+}

crates/gpui/playground/src/components.rs 🔗

@@ -0,0 +1,100 @@
+use crate::{
+    div::div,
+    element::{Element, ParentElement},
+    style::StyleHelpers,
+    text::ArcCow,
+    themes::rose_pine,
+};
+use gpui::ViewContext;
+use playground_macros::Element;
+use std::{marker::PhantomData, rc::Rc};
+
+struct ButtonHandlers<V, D> {
+    click: Option<Rc<dyn Fn(&mut V, &D, &mut ViewContext<V>)>>,
+}
+
+impl<V, D> Default for ButtonHandlers<V, D> {
+    fn default() -> Self {
+        Self { click: None }
+    }
+}
+
+use crate as playground;
+#[derive(Element)]
+pub struct Button<V: 'static, D: 'static> {
+    handlers: ButtonHandlers<V, D>,
+    label: Option<ArcCow<'static, str>>,
+    icon: Option<ArcCow<'static, str>>,
+    data: Rc<D>,
+    view_type: PhantomData<V>,
+}
+
+// Impl block for buttons without data.
+// See below for an impl block for any button.
+impl<V: 'static> Button<V, ()> {
+    fn new() -> Self {
+        Self {
+            handlers: ButtonHandlers::default(),
+            label: None,
+            icon: None,
+            data: Rc::new(()),
+            view_type: PhantomData,
+        }
+    }
+
+    pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
+        Button {
+            handlers: ButtonHandlers::default(),
+            label: self.label,
+            icon: self.icon,
+            data: Rc::new(data),
+            view_type: PhantomData,
+        }
+    }
+}
+
+// Impl block for *any* button.
+impl<V: 'static, D: 'static> Button<V, D> {
+    pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
+        self.label = Some(label.into());
+        self
+    }
+
+    pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
+        self.icon = Some(icon.into());
+        self
+    }
+
+    // pub fn click(self, handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static) -> Self {
+    //     let data = self.data.clone();
+    //     Self::click(self, MouseButton::Left, move |view, _, cx| {
+    //         handler(view, data.as_ref(), cx);
+    //     })
+    // }
+}
+
+pub fn button<V>() -> Button<V, ()> {
+    Button::new()
+}
+
+impl<V: 'static, D: 'static> Button<V, D> {
+    fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
+        // TODO: Drive theme from the context
+        let button = div()
+            .fill(rose_pine::dawn().error(0.5))
+            .h_4()
+            .children(self.label.clone());
+
+        button
+
+        // TODO: Event handling
+        // if let Some(handler) = self.handlers.click.clone() {
+        //     let data = self.data.clone();
+        //     // button.mouse_down(MouseButton::Left, move |view, event, cx| {
+        //     //     handler(view, data.as_ref(), cx)
+        //     // })
+        // } else {
+        //     button
+        // }
+    }
+}

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

@@ -0,0 +1,108 @@
+use crate::{
+    element::{AnyElement, Element, Layout, ParentElement},
+    interactive::{InteractionHandlers, Interactive},
+    layout_context::LayoutContext,
+    paint_context::PaintContext,
+    style::{Style, StyleHelpers, StyleRefinement, Styleable},
+};
+use anyhow::Result;
+use gpui::LayoutId;
+use smallvec::SmallVec;
+
+pub struct Div<V: 'static> {
+    style: StyleRefinement,
+    handlers: InteractionHandlers<V>,
+    children: SmallVec<[AnyElement<V>; 2]>,
+}
+
+pub fn div<V>() -> Div<V> {
+    Div {
+        style: Default::default(),
+        handlers: Default::default(),
+        children: Default::default(),
+    }
+}
+
+impl<V: 'static> Element<V> for Div<V> {
+    type Layout = ();
+
+    fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<Layout<V, ()>>
+    where
+        Self: Sized,
+    {
+        let children = self
+            .children
+            .iter_mut()
+            .map(|child| child.layout(view, cx))
+            .collect::<Result<Vec<LayoutId>>>()?;
+
+        cx.add_layout_node(self.style(), (), children)
+    }
+
+    fn paint(&mut self, view: &mut V, layout: &mut Layout<V, ()>, cx: &mut PaintContext<V>)
+    where
+        Self: Sized,
+    {
+        let style = self.style();
+
+        style.paint_background::<V, Self>(layout, cx);
+        for child in &mut self.children {
+            child.paint(view, cx);
+        }
+    }
+}
+
+impl<V> Styleable for Div<V> {
+    type Style = Style;
+
+    fn declared_style(&mut self) -> &mut StyleRefinement {
+        &mut self.style
+    }
+}
+
+impl<V> StyleHelpers for Div<V> {}
+
+impl<V> Interactive<V> for Div<V> {
+    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
+        &mut self.handlers
+    }
+}
+
+impl<V: 'static> ParentElement<V> for Div<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
+}
+
+#[test]
+fn test() {
+    // let elt = div().w_auto();
+}
+
+// trait Element<V: 'static> {
+//     type Style;
+
+//     fn layout()
+// }
+
+// trait Stylable<V: 'static>: Element<V> {
+//     type Style;
+
+//     fn with_style(self, style: Self::Style) -> Self;
+// }
+
+// pub struct HoverStyle<S> {
+//     default: S,
+//     hovered: S,
+// }
+
+// struct Hover<V: 'static, C: Stylable<V>> {
+//     child: C,
+//     style: HoverStyle<C::Style>,
+// }
+
+// impl<V: 'static, C: Stylable<V>> Hover<V, C> {
+//     fn new(child: C, style: HoverStyle<C::Style>) -> Self {
+//         Self { child, style }
+//     }
+// }

crates/gpui/playground/src/element.rs 🔗

@@ -0,0 +1,158 @@
+use anyhow::Result;
+use derive_more::{Deref, DerefMut};
+use gpui::{geometry::rect::RectF, EngineLayout};
+use smallvec::SmallVec;
+use std::marker::PhantomData;
+use util::ResultExt;
+
+pub use crate::layout_context::LayoutContext;
+pub use crate::paint_context::PaintContext;
+
+type LayoutId = gpui::LayoutId;
+
+pub trait Element<V: 'static>: 'static {
+    type Layout;
+
+    fn layout(
+        &mut self,
+        view: &mut V,
+        cx: &mut LayoutContext<V>,
+    ) -> Result<Layout<V, Self::Layout>>
+    where
+        Self: Sized;
+
+    fn paint(
+        &mut self,
+        view: &mut V,
+        layout: &mut Layout<V, Self::Layout>,
+        cx: &mut PaintContext<V>,
+    ) where
+        Self: Sized;
+
+    fn into_any(self) -> AnyElement<V>
+    where
+        Self: 'static + Sized,
+    {
+        AnyElement(Box::new(ElementState {
+            element: self,
+            layout: None,
+        }))
+    }
+}
+
+/// Used to make ElementState<V, E> into a trait object, so we can wrap it in AnyElement<V>.
+trait ElementStateObject<V> {
+    fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId>;
+    fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>);
+}
+
+/// A wrapper around an element that stores its layout state.
+struct ElementState<V: 'static, E: Element<V>> {
+    element: E,
+    layout: Option<Layout<V, E::Layout>>,
+}
+
+/// We blanket-implement the object-safe ElementStateObject interface to make ElementStates into trait objects
+impl<V, E: Element<V>> ElementStateObject<V> for ElementState<V, E> {
+    fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId> {
+        let layout = self.element.layout(view, cx)?;
+        let layout_id = layout.id;
+        self.layout = Some(layout);
+        Ok(layout_id)
+    }
+
+    fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
+        let layout = self.layout.as_mut().expect("paint called before layout");
+        if layout.engine_layout.is_none() {
+            layout.engine_layout = cx.computed_layout(layout.id).log_err()
+        }
+        self.element.paint(view, layout, cx)
+    }
+}
+
+/// A dynamic element.
+pub struct AnyElement<V>(Box<dyn ElementStateObject<V>>);
+
+impl<V> AnyElement<V> {
+    pub fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<LayoutId> {
+        self.0.layout(view, cx)
+    }
+
+    pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
+        self.0.paint(view, cx)
+    }
+}
+
+#[derive(Deref, DerefMut)]
+pub struct Layout<V, D> {
+    id: LayoutId,
+    engine_layout: Option<EngineLayout>,
+    #[deref]
+    #[deref_mut]
+    element_data: D,
+    view_type: PhantomData<V>,
+}
+
+impl<V: 'static, D> Layout<V, D> {
+    pub fn new(id: LayoutId, element_data: D) -> Self {
+        Self {
+            id,
+            engine_layout: None,
+            element_data: element_data,
+            view_type: PhantomData,
+        }
+    }
+
+    pub fn bounds(&mut self, cx: &mut PaintContext<V>) -> RectF {
+        self.engine_layout(cx).bounds
+    }
+
+    pub fn order(&mut self, cx: &mut PaintContext<V>) -> u32 {
+        self.engine_layout(cx).order
+    }
+
+    fn engine_layout(&mut self, cx: &mut PaintContext<'_, '_, '_, '_, V>) -> &mut EngineLayout {
+        self.engine_layout
+            .get_or_insert_with(|| cx.computed_layout(self.id).log_err().unwrap_or_default())
+    }
+}
+
+impl<V: 'static> Layout<V, Option<AnyElement<V>>> {
+    pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) {
+        let mut element = self.element_data.take().unwrap();
+        element.paint(view, cx);
+        self.element_data = Some(element);
+    }
+}
+
+pub trait ParentElement<V: 'static> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
+
+    fn child(mut self, child: impl IntoElement<V>) -> Self
+    where
+        Self: Sized,
+    {
+        self.children_mut().push(child.into_element().into_any());
+        self
+    }
+
+    fn children<I, E>(mut self, children: I) -> Self
+    where
+        I: IntoIterator<Item = E>,
+        E: IntoElement<V>,
+        Self: Sized,
+    {
+        self.children_mut().extend(
+            children
+                .into_iter()
+                .map(|child| child.into_element().into_any()),
+        );
+        self
+    }
+}
+
+pub trait IntoElement<V: 'static> {
+    type Element: Element<V>;
+
+    fn into_element(self) -> Self::Element;
+}

crates/gpui/playground/src/hoverable.rs 🔗

@@ -0,0 +1,76 @@
+use crate::{
+    element::{Element, Layout},
+    layout_context::LayoutContext,
+    paint_context::PaintContext,
+    style::{StyleRefinement, Styleable},
+};
+use anyhow::Result;
+use gpui::platform::MouseMovedEvent;
+use refineable::Refineable;
+use std::{cell::Cell, marker::PhantomData};
+
+pub struct Hoverable<V: 'static, E: Element<V> + Styleable> {
+    hovered: Cell<bool>,
+    child_style: StyleRefinement,
+    hovered_style: StyleRefinement,
+    child: E,
+    view_type: PhantomData<V>,
+}
+
+pub fn hoverable<V, E: Element<V> + Styleable>(mut child: E) -> Hoverable<V, E> {
+    Hoverable {
+        hovered: Cell::new(false),
+        child_style: child.declared_style().clone(),
+        hovered_style: Default::default(),
+        child,
+        view_type: PhantomData,
+    }
+}
+
+impl<V, E: Element<V> + Styleable> Styleable for Hoverable<V, E> {
+    type Style = E::Style;
+
+    fn declared_style(&mut self) -> &mut crate::style::StyleRefinement {
+        self.child.declared_style()
+    }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> Element<V> for Hoverable<V, E> {
+    type Layout = E::Layout;
+
+    fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<Layout<V, Self::Layout>>
+    where
+        Self: Sized,
+    {
+        self.child.layout(view, cx)
+    }
+
+    fn paint(
+        &mut self,
+        view: &mut V,
+        layout: &mut Layout<V, Self::Layout>,
+        cx: &mut PaintContext<V>,
+    ) where
+        Self: Sized,
+    {
+        if self.hovered.get() {
+            // If hovered, refine the child's style with this element's style.
+            self.child.declared_style().refine(&self.hovered_style);
+        } else {
+            // Otherwise, set the child's style back to its original style.
+            *self.child.declared_style() = self.child_style.clone();
+        }
+
+        let bounds = layout.bounds(cx);
+        let order = layout.order(cx);
+        self.hovered.set(bounds.contains_point(cx.mouse_position()));
+        let was_hovered = self.hovered.clone();
+        cx.on_event(order, move |view, event: &MouseMovedEvent, cx| {
+            let is_hovered = bounds.contains_point(event.position);
+            if is_hovered != was_hovered.get() {
+                was_hovered.set(is_hovered);
+                cx.repaint();
+            }
+        });
+    }
+}

crates/gpui/playground/src/interactive.rs 🔗

@@ -0,0 +1,34 @@
+use gpui::{platform::MouseMovedEvent, EventContext};
+use smallvec::SmallVec;
+use std::rc::Rc;
+
+pub trait Interactive<V: 'static> {
+    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V>;
+
+    fn on_mouse_move<H>(mut self, handler: H) -> Self
+    where
+        H: 'static + Fn(&mut V, &MouseMovedEvent, bool, &mut EventContext<V>),
+        Self: Sized,
+    {
+        self.interaction_handlers()
+            .mouse_moved
+            .push(Rc::new(move |view, event, hit_test, cx| {
+                handler(view, event, hit_test, cx);
+                cx.bubble
+            }));
+        self
+    }
+}
+
+pub struct InteractionHandlers<V: 'static> {
+    mouse_moved:
+        SmallVec<[Rc<dyn Fn(&mut V, &MouseMovedEvent, bool, &mut EventContext<V>) -> bool>; 2]>,
+}
+
+impl<V> Default for InteractionHandlers<V> {
+    fn default() -> Self {
+        Self {
+            mouse_moved: Default::default(),
+        }
+    }
+}

crates/gpui/playground/src/layout_context.rs 🔗

@@ -0,0 +1,54 @@
+use anyhow::{anyhow, Result};
+use derive_more::{Deref, DerefMut};
+pub use gpui::LayoutContext as LegacyLayoutContext;
+use gpui::{RenderContext, ViewContext};
+pub use taffy::tree::NodeId;
+
+use crate::{element::Layout, style::Style};
+
+#[derive(Deref, DerefMut)]
+pub struct LayoutContext<'a, 'b, 'c, 'd, V> {
+    #[deref]
+    #[deref_mut]
+    pub(crate) legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>,
+}
+
+impl<'a, 'b, V> RenderContext<'a, 'b, V> for LayoutContext<'a, 'b, '_, '_, V> {
+    fn text_style(&self) -> gpui::fonts::TextStyle {
+        self.legacy_cx.text_style()
+    }
+
+    fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
+        self.legacy_cx.push_text_style(style)
+    }
+
+    fn pop_text_style(&mut self) {
+        self.legacy_cx.pop_text_style()
+    }
+
+    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
+        &mut self.view_context
+    }
+}
+
+impl<'a, 'b, 'c, 'd, V: 'static> LayoutContext<'a, 'b, 'c, 'd, V> {
+    pub fn new(legacy_cx: &'d mut LegacyLayoutContext<'a, 'b, 'c, V>) -> Self {
+        Self { legacy_cx }
+    }
+
+    pub fn add_layout_node<D>(
+        &mut self,
+        style: Style,
+        element_data: D,
+        children: impl IntoIterator<Item = NodeId>,
+    ) -> Result<Layout<V, D>> {
+        let rem_size = self.rem_pixels();
+        let id = self
+            .legacy_cx
+            .layout_engine()
+            .ok_or_else(|| anyhow!("no layout engine"))?
+            .add_node(style.to_taffy(rem_size), children)?;
+
+        Ok(Layout::new(id, element_data))
+    }
+}

crates/gpui/playground/src/paint_context.rs 🔗

@@ -0,0 +1,71 @@
+use anyhow::{anyhow, Result};
+use derive_more::{Deref, DerefMut};
+use gpui::{scene::EventHandler, EngineLayout, EventContext, LayoutId, RenderContext, ViewContext};
+pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext};
+use std::{any::TypeId, rc::Rc};
+pub use taffy::tree::NodeId;
+
+#[derive(Deref, DerefMut)]
+pub struct PaintContext<'a, 'b, 'c, 'd, V> {
+    #[deref]
+    #[deref_mut]
+    pub(crate) legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
+    pub(crate) scene: &'d mut gpui::SceneBuilder,
+}
+
+impl<'a, 'b, V> RenderContext<'a, 'b, V> for PaintContext<'a, 'b, '_, '_, V> {
+    fn text_style(&self) -> gpui::fonts::TextStyle {
+        self.legacy_cx.text_style()
+    }
+
+    fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
+        self.legacy_cx.push_text_style(style)
+    }
+
+    fn pop_text_style(&mut self) {
+        self.legacy_cx.pop_text_style()
+    }
+
+    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
+        &mut self.view_context
+    }
+}
+
+impl<'a, 'b, 'c, 'd, V: 'static> PaintContext<'a, 'b, 'c, 'd, V> {
+    pub fn new(
+        legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
+        scene: &'d mut gpui::SceneBuilder,
+    ) -> Self {
+        Self { legacy_cx, scene }
+    }
+
+    pub fn on_event<E: 'static>(
+        &mut self,
+        order: u32,
+        handler: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
+    ) {
+        let view = self.weak_handle();
+
+        self.scene.event_handlers.push(EventHandler {
+            order,
+            handler: Rc::new(move |event, window_cx| {
+                if let Some(view) = view.upgrade(window_cx) {
+                    view.update(window_cx, |view, view_cx| {
+                        let mut event_cx = EventContext::new(view_cx);
+                        handler(view, event.downcast_ref().unwrap(), &mut event_cx);
+                        event_cx.bubble
+                    })
+                } else {
+                    true
+                }
+            }),
+            event_type: TypeId::of::<E>(),
+        })
+    }
+
+    pub(crate) fn computed_layout(&mut self, layout_id: LayoutId) -> Result<EngineLayout> {
+        self.layout_engine()
+            .ok_or_else(|| anyhow!("no layout engine present"))?
+            .computed_layout(layout_id)
+    }
+}

crates/gpui/playground/src/playground.rs 🔗

@@ -0,0 +1,83 @@
+#![allow(dead_code, unused_variables)]
+use crate::{color::black, style::StyleHelpers};
+use element::Element;
+use gpui::{
+    geometry::{rect::RectF, vector::vec2f},
+    platform::WindowOptions,
+};
+use log::LevelFilter;
+use simplelog::SimpleLogger;
+use themes::{rose_pine, ThemeColors};
+use view::view;
+
+mod adapter;
+mod color;
+mod components;
+mod div;
+mod element;
+mod hoverable;
+mod interactive;
+mod layout_context;
+mod paint_context;
+mod style;
+mod text;
+mod themes;
+mod view;
+
+fn main() {
+    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+
+    gpui::App::new(()).unwrap().run(|cx| {
+        cx.add_window(
+            WindowOptions {
+                bounds: gpui::platform::WindowBounds::Fixed(RectF::new(
+                    vec2f(0., 0.),
+                    vec2f(400., 300.),
+                )),
+                center: true,
+                ..Default::default()
+            },
+            |_| view(|_| playground(&rose_pine::moon())),
+        );
+        cx.platform().activate(true);
+    });
+}
+
+fn playground<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+    use div::div;
+
+    div()
+        .text_color(black())
+        .h_full()
+        .w_1_2()
+        .fill(theme.success(0.5))
+    // .hover()
+    // .fill(theme.error(0.5))
+    // .child(button().label("Hello").click(|_, _, _| println!("click!")))
+}
+
+//     todo!()
+//     // column()
+//     // .size(auto())
+//     // .fill(theme.base(0.5))
+//     // .text_color(theme.text(0.5))
+//     // .child(title_bar(theme))
+//     // .child(stage(theme))
+//     // .child(status_bar(theme))
+// }
+
+// fn title_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+//     row()
+//         .fill(theme.base(0.2))
+//         .justify(0.)
+//         .width(auto())
+//         .child(text("Zed Playground"))
+// }
+
+// fn stage<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+//     row().fill(theme.surface(0.9))
+// }
+
+// fn status_bar<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+//     row().fill(theme.surface(0.1))
+// }

crates/gpui/playground/src/style.rs 🔗

@@ -0,0 +1,286 @@
+use crate::{
+    color::Hsla,
+    element::{Element, Layout},
+    paint_context::PaintContext,
+};
+use gpui::{
+    fonts::TextStyleRefinement,
+    geometry::{
+        AbsoluteLength, DefiniteLength, Edges, EdgesRefinement, Length, Point, PointRefinement,
+        Size, SizeRefinement,
+    },
+};
+use playground_macros::styleable_helpers;
+use refineable::Refineable;
+pub use taffy::style::{
+    AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
+    Overflow, Position,
+};
+
+#[derive(Clone, Refineable)]
+pub struct Style {
+    /// What layout strategy should be used?
+    pub display: Display,
+
+    // Overflow properties
+    /// How children overflowing their container should affect layout
+    #[refineable]
+    pub overflow: Point<Overflow>,
+    /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
+    pub scrollbar_width: f32,
+
+    // Position properties
+    /// What should the `position` value of this struct use as a base offset?
+    pub position: Position,
+    /// How should the position of this element be tweaked relative to the layout defined?
+    #[refineable]
+    pub inset: Edges<Length>,
+
+    // Size properies
+    /// Sets the initial size of the item
+    #[refineable]
+    pub size: Size<Length>,
+    /// Controls the minimum size of the item
+    #[refineable]
+    pub min_size: Size<Length>,
+    /// Controls the maximum size of the item
+    #[refineable]
+    pub max_size: Size<Length>,
+    /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
+    pub aspect_ratio: Option<f32>,
+
+    // Spacing Properties
+    /// How large should the margin be on each side?
+    #[refineable]
+    pub margin: Edges<Length>,
+    /// How large should the padding be on each side?
+    #[refineable]
+    pub padding: Edges<DefiniteLength>,
+    /// How large should the border be on each side?
+    #[refineable]
+    pub border: Edges<DefiniteLength>,
+
+    // Alignment properties
+    /// How this node's children aligned in the cross/block axis?
+    pub align_items: Option<AlignItems>,
+    /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
+    pub align_self: Option<AlignSelf>,
+    /// How should content contained within this item be aligned in the cross/block axis
+    pub align_content: Option<AlignContent>,
+    /// How should contained within this item be aligned in the main/inline axis
+    pub justify_content: Option<JustifyContent>,
+    /// How large should the gaps between items in a flex container be?
+    #[refineable]
+    pub gap: Size<DefiniteLength>,
+
+    // Flexbox properies
+    /// Which direction does the main axis flow in?
+    pub flex_direction: FlexDirection,
+    /// Should elements wrap, or stay in a single line?
+    pub flex_wrap: FlexWrap,
+    /// Sets the initial main axis size of the item
+    pub flex_basis: Length,
+    /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive.
+    pub flex_grow: f32,
+    /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive.
+    pub flex_shrink: f32,
+
+    /// The fill color of this element
+    pub fill: Option<Fill>,
+    /// The radius of the corners of this element
+    #[refineable]
+    pub corner_radii: CornerRadii,
+    /// The color of text within this element. Cascades to children unless overridden.
+    pub text_color: Option<Hsla>,
+}
+
+impl Style {
+    pub fn to_taffy(&self, rem_size: f32) -> taffy::style::Style {
+        taffy::style::Style {
+            display: self.display,
+            overflow: self.overflow.clone().into(),
+            scrollbar_width: self.scrollbar_width,
+            position: self.position,
+            inset: self.inset.to_taffy(rem_size),
+            size: self.size.to_taffy(rem_size),
+            min_size: self.min_size.to_taffy(rem_size),
+            max_size: self.max_size.to_taffy(rem_size),
+            aspect_ratio: self.aspect_ratio,
+            margin: self.margin.to_taffy(rem_size),
+            padding: self.padding.to_taffy(rem_size),
+            border: self.border.to_taffy(rem_size),
+            align_items: self.align_items,
+            align_self: self.align_self,
+            align_content: self.align_content,
+            justify_content: self.justify_content,
+            gap: self.gap.to_taffy(rem_size),
+            flex_direction: self.flex_direction,
+            flex_wrap: self.flex_wrap,
+            flex_basis: self.flex_basis.to_taffy(rem_size).into(),
+            flex_grow: self.flex_grow,
+            flex_shrink: self.flex_shrink,
+            ..Default::default() // Ignore grid properties for now
+        }
+    }
+
+    /// Paints the background of an element styled with this style.
+    /// Return the bounds in which to paint the content.
+    pub fn paint_background<V: 'static, E: Element<V>>(
+        &self,
+        layout: &mut Layout<V, E::Layout>,
+        cx: &mut PaintContext<V>,
+    ) {
+        let bounds = layout.bounds(cx);
+        let rem_size = cx.rem_pixels();
+        if let Some(color) = self.fill.as_ref().and_then(Fill::color) {
+            cx.scene.push_quad(gpui::Quad {
+                bounds,
+                background: Some(color.into()),
+                corner_radii: self.corner_radii.to_gpui(rem_size),
+                border: Default::default(),
+            });
+        }
+    }
+}
+
+impl Default for Style {
+    fn default() -> Self {
+        Style {
+            display: Display::DEFAULT,
+            overflow: Point {
+                x: Overflow::Visible,
+                y: Overflow::Visible,
+            },
+            scrollbar_width: 0.0,
+            position: Position::Relative,
+            inset: Edges::auto(),
+            margin: Edges::<Length>::zero(),
+            padding: Edges::<DefiniteLength>::zero(),
+            border: Edges::<DefiniteLength>::zero(),
+            size: Size::auto(),
+            min_size: Size::auto(),
+            max_size: Size::auto(),
+            aspect_ratio: None,
+            gap: Size::zero(),
+            // Aligment
+            align_items: None,
+            align_self: None,
+            align_content: None,
+            justify_content: None,
+            // Flexbox
+            flex_direction: FlexDirection::Row,
+            flex_wrap: FlexWrap::NoWrap,
+            flex_grow: 0.0,
+            flex_shrink: 1.0,
+            flex_basis: Length::Auto,
+            fill: None,
+            text_color: None,
+            corner_radii: CornerRadii::default(),
+        }
+    }
+}
+
+impl StyleRefinement {
+    pub fn text_style(&self) -> Option<TextStyleRefinement> {
+        self.text_color.map(|color| TextStyleRefinement {
+            color: Some(color.into()),
+            ..Default::default()
+        })
+    }
+}
+
+pub struct OptionalTextStyle {
+    color: Option<Hsla>,
+}
+
+impl OptionalTextStyle {
+    pub fn apply(&self, style: &mut gpui::fonts::TextStyle) {
+        if let Some(color) = self.color {
+            style.color = color.into();
+        }
+    }
+}
+
+#[derive(Clone)]
+pub enum Fill {
+    Color(Hsla),
+}
+
+impl Fill {
+    pub fn color(&self) -> Option<Hsla> {
+        match self {
+            Fill::Color(color) => Some(*color),
+        }
+    }
+}
+
+impl Default for Fill {
+    fn default() -> Self {
+        Self::Color(Hsla::default())
+    }
+}
+
+impl From<Hsla> for Fill {
+    fn from(color: Hsla) -> Self {
+        Self::Color(color)
+    }
+}
+
+#[derive(Clone, Refineable, Default)]
+pub struct CornerRadii {
+    top_left: AbsoluteLength,
+    top_right: AbsoluteLength,
+    bottom_left: AbsoluteLength,
+    bottom_right: AbsoluteLength,
+}
+
+impl CornerRadii {
+    pub fn to_gpui(&self, rem_size: f32) -> gpui::scene::CornerRadii {
+        gpui::scene::CornerRadii {
+            top_left: self.top_left.to_pixels(rem_size),
+            top_right: self.top_right.to_pixels(rem_size),
+            bottom_left: self.bottom_left.to_pixels(rem_size),
+            bottom_right: self.bottom_right.to_pixels(rem_size),
+        }
+    }
+}
+
+pub trait Styleable {
+    type Style: refineable::Refineable;
+
+    fn declared_style(&mut self) -> &mut playground::style::StyleRefinement;
+
+    fn style(&mut self) -> playground::style::Style {
+        let mut style = playground::style::Style::default();
+        style.refine(self.declared_style());
+        style
+    }
+}
+
+// Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
+//
+// Example:
+// // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
+// fn p_2(mut self) -> Self where Self: Sized;
+use crate as playground; // Macro invocation references this crate as playground.
+pub trait StyleHelpers: Styleable<Style = Style> {
+    styleable_helpers!();
+
+    fn fill<F>(mut self, fill: F) -> Self
+    where
+        F: Into<Fill>,
+        Self: Sized,
+    {
+        self.declared_style().fill = Some(fill.into());
+        self
+    }
+
+    fn text_color<C>(mut self, color: C) -> Self
+    where
+        C: Into<Hsla>,
+        Self: Sized,
+    {
+        self.declared_style().text_color = Some(color.into());
+        self
+    }
+}

crates/gpui/playground/src/text.rs 🔗

@@ -0,0 +1,151 @@
+use crate::{
+    element::{Element, IntoElement, Layout},
+    layout_context::LayoutContext,
+    paint_context::PaintContext,
+};
+use anyhow::Result;
+use gpui::text_layout::LineLayout;
+use parking_lot::Mutex;
+use std::sync::Arc;
+
+impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
+    type Element = Text;
+
+    fn into_element(self) -> Self::Element {
+        Text { text: self.into() }
+    }
+}
+
+pub struct Text {
+    text: ArcCow<'static, str>,
+}
+
+impl<V: 'static> Element<V> for Text {
+    type Layout = Arc<Mutex<Option<TextLayout>>>;
+
+    fn layout(
+        &mut self,
+        view: &mut V,
+        cx: &mut LayoutContext<V>,
+    ) -> Result<Layout<V, Self::Layout>> {
+        // let rem_size = cx.rem_pixels();
+        // let fonts = cx.platform().fonts();
+        // let text_style = cx.text_style();
+        // let line_height = cx.font_cache().line_height(text_style.font_size);
+        // let layout_engine = cx.layout_engine().expect("no layout engine present");
+        // let text = self.text.clone();
+        // let layout = Arc::new(Mutex::new(None));
+
+        // let style: Style = Style::default().refined(&self.metadata.style);
+        // let node_id = layout_engine.add_measured_node(style.to_taffy(rem_size), {
+        //     let layout = layout.clone();
+        //     move |params| {
+        //         let line_layout = fonts.layout_line(
+        //             text.as_ref(),
+        //             text_style.font_size,
+        //             &[(text.len(), text_style.to_run())],
+        //         );
+
+        //         let size = Size {
+        //             width: line_layout.width,
+        //             height: line_height,
+        //         };
+
+        //         layout.lock().replace(TextLayout {
+        //             line_layout: Arc::new(line_layout),
+        //             line_height,
+        //         });
+
+        //         size
+        //     }
+        // })?;
+
+        // Ok((node_id, layout))
+        todo!()
+    }
+
+    fn paint<'a>(
+        &mut self,
+        view: &mut V,
+        layout: &mut Layout<V, Self::Layout>,
+        cx: &mut PaintContext<V>,
+    ) {
+        // ) {
+        //     let element_layout_lock = layout.from_element.lock();
+        //     let element_layout = element_layout_lock
+        //         .as_ref()
+        //         .expect("layout has not been performed");
+        //     let line_layout = element_layout.line_layout.clone();
+        //     let line_height = element_layout.line_height;
+        //     drop(element_layout_lock);
+
+        //     let text_style = cx.text_style();
+        //     let line =
+        //         gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
+        //     line.paint(
+        //         cx.scene,
+        //         layout.from_engine.bounds.origin(),
+        //         layout.from_engine.bounds,
+        //         line_height,
+        //         cx.legacy_cx,
+        //     );
+        todo!()
+    }
+}
+
+pub struct TextLayout {
+    line_layout: Arc<LineLayout>,
+    line_height: f32,
+}
+
+pub enum ArcCow<'a, T: ?Sized> {
+    Borrowed(&'a T),
+    Owned(Arc<T>),
+}
+
+impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
+    fn clone(&self) -> Self {
+        match self {
+            Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
+            Self::Owned(owned) => Self::Owned(owned.clone()),
+        }
+    }
+}
+
+impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
+    fn from(s: &'a T) -> Self {
+        Self::Borrowed(s)
+    }
+}
+
+impl<T> From<Arc<T>> for ArcCow<'_, T> {
+    fn from(s: Arc<T>) -> Self {
+        Self::Owned(s)
+    }
+}
+
+impl From<String> for ArcCow<'_, str> {
+    fn from(value: String) -> Self {
+        Self::Owned(value.into())
+    }
+}
+
+impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        match self {
+            ArcCow::Borrowed(s) => s,
+            ArcCow::Owned(s) => s.as_ref(),
+        }
+    }
+}
+
+impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
+    fn as_ref(&self) -> &T {
+        match self {
+            ArcCow::Borrowed(borrowed) => borrowed,
+            ArcCow::Owned(owned) => owned.as_ref(),
+        }
+    }
+}

crates/gpui/playground/src/themes.rs 🔗

@@ -0,0 +1,84 @@
+use crate::color::{Hsla, Lerp};
+use std::ops::Range;
+
+pub mod rose_pine;
+
+pub struct ThemeColors {
+    pub base: Range<Hsla>,
+    pub surface: Range<Hsla>,
+    pub overlay: Range<Hsla>,
+    pub muted: Range<Hsla>,
+    pub subtle: Range<Hsla>,
+    pub text: Range<Hsla>,
+    pub highlight_low: Range<Hsla>,
+    pub highlight_med: Range<Hsla>,
+    pub highlight_high: Range<Hsla>,
+    pub success: Range<Hsla>,
+    pub warning: Range<Hsla>,
+    pub error: Range<Hsla>,
+    pub inserted: Range<Hsla>,
+    pub deleted: Range<Hsla>,
+    pub modified: Range<Hsla>,
+}
+
+impl ThemeColors {
+    pub fn base(&self, level: f32) -> Hsla {
+        self.base.lerp(level)
+    }
+
+    pub fn surface(&self, level: f32) -> Hsla {
+        self.surface.lerp(level)
+    }
+
+    pub fn overlay(&self, level: f32) -> Hsla {
+        self.overlay.lerp(level)
+    }
+
+    pub fn muted(&self, level: f32) -> Hsla {
+        self.muted.lerp(level)
+    }
+
+    pub fn subtle(&self, level: f32) -> Hsla {
+        self.subtle.lerp(level)
+    }
+
+    pub fn text(&self, level: f32) -> Hsla {
+        self.text.lerp(level)
+    }
+
+    pub fn highlight_low(&self, level: f32) -> Hsla {
+        self.highlight_low.lerp(level)
+    }
+
+    pub fn highlight_med(&self, level: f32) -> Hsla {
+        self.highlight_med.lerp(level)
+    }
+
+    pub fn highlight_high(&self, level: f32) -> Hsla {
+        self.highlight_high.lerp(level)
+    }
+
+    pub fn success(&self, level: f32) -> Hsla {
+        self.success.lerp(level)
+    }
+
+    pub fn warning(&self, level: f32) -> Hsla {
+        self.warning.lerp(level)
+    }
+
+    pub fn error(&self, level: f32) -> Hsla {
+        self.error.lerp(level)
+    }
+
+    pub fn inserted(&self, level: f32) -> Hsla {
+        self.inserted.lerp(level)
+    }
+
+    pub fn deleted(&self, level: f32) -> Hsla {
+        self.deleted.lerp(level)
+    }
+
+    pub fn modified(&self, level: f32) -> Hsla {
+        self.modified.lerp(level)
+    }
+}

crates/gpui/playground/src/themes/rose_pine.rs 🔗

@@ -0,0 +1,133 @@
+use std::ops::Range;
+
+use crate::{
+    color::{hsla, rgb, Hsla},
+    ThemeColors,
+};
+
+pub struct RosePineThemes {
+    pub default: RosePinePalette,
+    pub dawn: RosePinePalette,
+    pub moon: RosePinePalette,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct RosePinePalette {
+    pub base: Hsla,
+    pub surface: Hsla,
+    pub overlay: Hsla,
+    pub muted: Hsla,
+    pub subtle: Hsla,
+    pub text: Hsla,
+    pub love: Hsla,
+    pub gold: Hsla,
+    pub rose: Hsla,
+    pub pine: Hsla,
+    pub foam: Hsla,
+    pub iris: Hsla,
+    pub highlight_low: Hsla,
+    pub highlight_med: Hsla,
+    pub highlight_high: Hsla,
+}
+
+impl RosePinePalette {
+    pub fn default() -> RosePinePalette {
+        RosePinePalette {
+            base: rgb(0x191724),
+            surface: rgb(0x1f1d2e),
+            overlay: rgb(0x26233a),
+            muted: rgb(0x6e6a86),
+            subtle: rgb(0x908caa),
+            text: rgb(0xe0def4),
+            love: rgb(0xeb6f92),
+            gold: rgb(0xf6c177),
+            rose: rgb(0xebbcba),
+            pine: rgb(0x31748f),
+            foam: rgb(0x9ccfd8),
+            iris: rgb(0xc4a7e7),
+            highlight_low: rgb(0x21202e),
+            highlight_med: rgb(0x403d52),
+            highlight_high: rgb(0x524f67),
+        }
+    }
+
+    pub fn moon() -> RosePinePalette {
+        RosePinePalette {
+            base: rgb(0x232136),
+            surface: rgb(0x2a273f),
+            overlay: rgb(0x393552),
+            muted: rgb(0x6e6a86),
+            subtle: rgb(0x908caa),
+            text: rgb(0xe0def4),
+            love: rgb(0xeb6f92),
+            gold: rgb(0xf6c177),
+            rose: rgb(0xea9a97),
+            pine: rgb(0x3e8fb0),
+            foam: rgb(0x9ccfd8),
+            iris: rgb(0xc4a7e7),
+            highlight_low: rgb(0x2a283e),
+            highlight_med: rgb(0x44415a),
+            highlight_high: rgb(0x56526e),
+        }
+    }
+
+    pub fn dawn() -> RosePinePalette {
+        RosePinePalette {
+            base: rgb(0xfaf4ed),
+            surface: rgb(0xfffaf3),
+            overlay: rgb(0xf2e9e1),
+            muted: rgb(0x9893a5),
+            subtle: rgb(0x797593),
+            text: rgb(0x575279),
+            love: rgb(0xb4637a),
+            gold: rgb(0xea9d34),
+            rose: rgb(0xd7827e),
+            pine: rgb(0x286983),
+            foam: rgb(0x56949f),
+            iris: rgb(0x907aa9),
+            highlight_low: rgb(0xf4ede8),
+            highlight_med: rgb(0xdfdad9),
+            highlight_high: rgb(0xcecacd),
+        }
+    }
+}
+
+pub fn default() -> ThemeColors {
+    theme_colors(&RosePinePalette::default())
+}
+
+pub fn moon() -> ThemeColors {
+    theme_colors(&RosePinePalette::moon())
+}
+
+pub fn dawn() -> ThemeColors {
+    theme_colors(&RosePinePalette::dawn())
+}
+
+fn theme_colors(p: &RosePinePalette) -> ThemeColors {
+    ThemeColors {
+        base: scale_sl(p.base, (0.8, 0.8), (1.2, 1.2)),
+        surface: scale_sl(p.surface, (0.8, 0.8), (1.2, 1.2)),
+        overlay: scale_sl(p.overlay, (0.8, 0.8), (1.2, 1.2)),
+        muted: scale_sl(p.muted, (0.8, 0.8), (1.2, 1.2)),
+        subtle: scale_sl(p.subtle, (0.8, 0.8), (1.2, 1.2)),
+        text: scale_sl(p.text, (0.8, 0.8), (1.2, 1.2)),
+        highlight_low: scale_sl(p.highlight_low, (0.8, 0.8), (1.2, 1.2)),
+        highlight_med: scale_sl(p.highlight_med, (0.8, 0.8), (1.2, 1.2)),
+        highlight_high: scale_sl(p.highlight_high, (0.8, 0.8), (1.2, 1.2)),
+        success: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
+        warning: scale_sl(p.gold, (0.8, 0.8), (1.2, 1.2)),
+        error: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
+        inserted: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
+        deleted: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
+        modified: scale_sl(p.rose, (0.8, 0.8), (1.2, 1.2)),
+    }
+}
+
+/// Produces a range by multiplying the saturation and lightness of the base color by the given
+/// start and end factors.
+fn scale_sl(base: Hsla, (start_s, start_l): (f32, f32), (end_s, end_l): (f32, f32)) -> Range<Hsla> {
+    let start = hsla(base.h, base.s * start_s, base.l * start_l, base.a);
+    let end = hsla(base.h, base.s * end_s, base.l * end_l, base.a);
+    Range { start, end }
+}

crates/gpui/playground/src/view.rs 🔗

@@ -0,0 +1,26 @@
+use crate::{
+    adapter::AdapterElement,
+    element::{AnyElement, Element},
+};
+use gpui::ViewContext;
+
+pub fn view<F, E>(mut render: F) -> ViewFn
+where
+    F: 'static + FnMut(&mut ViewContext<ViewFn>) -> E,
+    E: Element<ViewFn>,
+{
+    ViewFn(Box::new(move |cx| (render)(cx).into_any()))
+}
+
+pub struct ViewFn(Box<dyn FnMut(&mut ViewContext<ViewFn>) -> AnyElement<ViewFn>>);
+
+impl gpui::Entity for ViewFn {
+    type Event = ();
+}
+
+impl gpui::View for ViewFn {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
+        use gpui::Element as _;
+        AdapterElement((self.0)(cx)).into_any()
+    }
+}

crates/gpui/playground_macros/Cargo.toml 🔗

@@ -0,0 +1,14 @@
+[package]
+name = "playground_macros"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/playground_macros.rs"
+proc-macro = true
+
+[dependencies]
+syn = "1.0.72"
+quote = "1.0.9"
+proc-macro2 = "1.0.66"

crates/gpui/playground_macros/src/derive_element.rs 🔗

@@ -0,0 +1,91 @@
+use proc_macro::TokenStream;
+use proc_macro2::Ident;
+use quote::quote;
+use syn::{parse_macro_input, parse_quote, DeriveInput, GenericParam, Generics};
+
+use crate::derive_into_element::impl_into_element;
+
+pub fn derive_element(input: TokenStream) -> TokenStream {
+    let ast = parse_macro_input!(input as DeriveInput);
+    let type_name = ast.ident;
+    let placeholder_view_generics: Generics = parse_quote! { <V: 'static> };
+
+    let (impl_generics, type_generics, where_clause, view_type_name, lifetimes) =
+        if let Some(first_type_param) = ast.generics.params.iter().find_map(|param| {
+            if let GenericParam::Type(type_param) = param {
+                Some(type_param.ident.clone())
+            } else {
+                None
+            }
+        }) {
+            let mut lifetimes = vec![];
+            for param in ast.generics.params.iter() {
+                if let GenericParam::Lifetime(lifetime_def) = param {
+                    lifetimes.push(lifetime_def.lifetime.clone());
+                }
+            }
+            let generics = ast.generics.split_for_impl();
+            (
+                generics.0,
+                Some(generics.1),
+                generics.2,
+                first_type_param,
+                lifetimes,
+            )
+        } else {
+            let generics = placeholder_view_generics.split_for_impl();
+            let placeholder_view_type_name: Ident = parse_quote! { V };
+            (
+                generics.0,
+                None,
+                generics.2,
+                placeholder_view_type_name,
+                vec![],
+            )
+        };
+
+    let lifetimes = if !lifetimes.is_empty() {
+        quote! { <#(#lifetimes),*> }
+    } else {
+        quote! {}
+    };
+
+    let impl_into_element = impl_into_element(
+        &impl_generics,
+        &view_type_name,
+        &type_name,
+        &type_generics,
+        &where_clause,
+    );
+
+    let gen = quote! {
+        impl #impl_generics playground::element::Element<#view_type_name> for #type_name #type_generics
+        #where_clause
+        {
+            type Layout = Option<playground::element::AnyElement<#view_type_name #lifetimes>>;
+
+            fn layout(
+                &mut self,
+                view: &mut V,
+                cx: &mut playground::element::LayoutContext<V>,
+            ) -> anyhow::Result<playground::element::Layout<V, Self::Layout>> {
+                let mut element = self.render(view, cx).into_any();
+                let layout_id = element.layout(view, cx)?;
+                Ok(playground::element::Layout::new(layout_id, Some(element)))
+            }
+
+            fn paint(
+                &mut self,
+                view: &mut V,
+                layout: &mut playground::element::Layout<V, Self::Layout>,
+                cx: &mut playground::element::PaintContext<V>,
+            ) {
+                layout.paint(view, cx);
+            }
+        }
+
+        #impl_into_element
+    };
+
+    gen.into()
+}

crates/gpui/playground_macros/src/derive_into_element.rs 🔗

@@ -0,0 +1,69 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{
+    parse_macro_input, parse_quote, DeriveInput, GenericParam, Generics, Ident, WhereClause,
+};
+
+pub fn derive_into_element(input: TokenStream) -> TokenStream {
+    let ast = parse_macro_input!(input as DeriveInput);
+    let type_name = ast.ident;
+
+    let placeholder_view_generics: Generics = parse_quote! { <V: 'static> };
+    let placeholder_view_type_name: Ident = parse_quote! { V };
+    let view_type_name: Ident;
+    let impl_generics: syn::ImplGenerics<'_>;
+    let type_generics: Option<syn::TypeGenerics<'_>>;
+    let where_clause: Option<&'_ WhereClause>;
+
+    match ast.generics.params.iter().find_map(|param| {
+        if let GenericParam::Type(type_param) = param {
+            Some(type_param.ident.clone())
+        } else {
+            None
+        }
+    }) {
+        Some(type_name) => {
+            view_type_name = type_name;
+            let generics = ast.generics.split_for_impl();
+            impl_generics = generics.0;
+            type_generics = Some(generics.1);
+            where_clause = generics.2;
+        }
+        _ => {
+            view_type_name = placeholder_view_type_name;
+            let generics = placeholder_view_generics.split_for_impl();
+            impl_generics = generics.0;
+            type_generics = None;
+            where_clause = generics.2;
+        }
+    }
+
+    impl_into_element(
+        &impl_generics,
+        &view_type_name,
+        &type_name,
+        &type_generics,
+        &where_clause,
+    )
+    .into()
+}
+
+pub fn impl_into_element(
+    impl_generics: &syn::ImplGenerics<'_>,
+    view_type_name: &Ident,
+    type_name: &Ident,
+    type_generics: &Option<syn::TypeGenerics<'_>>,
+    where_clause: &Option<&WhereClause>,
+) -> proc_macro2::TokenStream {
+    quote! {
+        impl #impl_generics playground::element::IntoElement<#view_type_name> for #type_name #type_generics
+        #where_clause
+        {
+            type Element = Self;
+
+            fn into_element(self) -> Self {
+                self
+            }
+        }
+    }
+}

crates/gpui/playground_macros/src/playground_macros.rs 🔗

@@ -0,0 +1,26 @@
+use proc_macro::TokenStream;
+
+mod derive_element;
+mod derive_into_element;
+mod styleable_helpers;
+mod tailwind_lengths;
+
+#[proc_macro]
+pub fn styleable_helpers(args: TokenStream) -> TokenStream {
+    styleable_helpers::styleable_helpers(args)
+}
+
+#[proc_macro_derive(Element, attributes(element_crate))]
+pub fn derive_element(input: TokenStream) -> TokenStream {
+    derive_element::derive_element(input)
+}
+
+#[proc_macro_derive(IntoElement, attributes(element_crate))]
+pub fn derive_into_element(input: TokenStream) -> TokenStream {
+    derive_into_element::derive_into_element(input)
+}
+
+#[proc_macro_attribute]
+pub fn tailwind_lengths(attr: TokenStream, item: TokenStream) -> TokenStream {
+    tailwind_lengths::tailwind_lengths(attr, item)
+}

crates/gpui/playground_macros/src/styleable_helpers.rs 🔗

@@ -0,0 +1,147 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{
+    parse::{Parse, ParseStream, Result},
+    parse_macro_input,
+};
+
+struct StyleableMacroInput;
+
+impl Parse for StyleableMacroInput {
+    fn parse(_input: ParseStream) -> Result<Self> {
+        Ok(StyleableMacroInput)
+    }
+}
+
+pub fn styleable_helpers(input: TokenStream) -> TokenStream {
+    let _ = parse_macro_input!(input as StyleableMacroInput);
+    let methods = generate_methods();
+    let output = quote! {
+        #(#methods)*
+    };
+    output.into()
+}
+
+fn generate_methods() -> Vec<TokenStream2> {
+    let mut methods = Vec::new();
+
+    for (prefix, auto_allowed, fields) in tailwind_prefixes() {
+        for (suffix, length_tokens) in tailwind_lengths() {
+            if !auto_allowed && suffix == "auto" {
+                // Conditional to skip "auto"
+                continue;
+            }
+
+            let method_name = format_ident!("{}_{}", prefix, suffix);
+            let field_assignments = fields
+                .iter()
+                .map(|field_tokens| {
+                    quote! {
+                        style.#field_tokens = Some(gpui::geometry::#length_tokens);
+                    }
+                })
+                .collect::<Vec<_>>();
+
+            let method = quote! {
+                fn #method_name(mut self) -> Self where Self: std::marker::Sized {
+                    let mut style = self.declared_style();
+                    #(#field_assignments)*
+                    self
+                }
+            };
+
+            methods.push(method);
+        }
+    }
+
+    methods
+}
+
+fn tailwind_lengths() -> Vec<(&'static str, TokenStream2)> {
+    vec![
+        ("0", quote! { pixels(0.) }),
+        ("1", quote! { rems(0.25) }),
+        ("2", quote! { rems(0.5) }),
+        ("3", quote! { rems(0.75) }),
+        ("4", quote! { rems(1.) }),
+        ("5", quote! { rems(1.25) }),
+        ("6", quote! { rems(1.5) }),
+        ("8", quote! { rems(2.0) }),
+        ("10", quote! { rems(2.5) }),
+        ("12", quote! { rems(3.) }),
+        ("16", quote! { rems(4.) }),
+        ("20", quote! { rems(5.) }),
+        ("24", quote! { rems(6.) }),
+        ("32", quote! { rems(8.) }),
+        ("40", quote! { rems(10.) }),
+        ("48", quote! { rems(12.) }),
+        ("56", quote! { rems(14.) }),
+        ("64", quote! { rems(16.) }),
+        ("72", quote! { rems(18.) }),
+        ("80", quote! { rems(20.) }),
+        ("96", quote! { rems(24.) }),
+        ("auto", quote! { auto() }),
+        ("px", quote! { pixels(1.) }),
+        ("full", quote! { relative(1.) }),
+        ("1_2", quote! { relative(0.5) }),
+        ("1_3", quote! { relative(1./3.) }),
+        ("2_3", quote! { relative(2./3.) }),
+        ("1_4", quote! { relative(0.25) }),
+        ("2_4", quote! { relative(0.5) }),
+        ("3_4", quote! { relative(0.75) }),
+        ("1_5", quote! { relative(0.2) }),
+        ("2_5", quote! { relative(0.4) }),
+        ("3_5", quote! { relative(0.6) }),
+        ("4_5", quote! { relative(0.8) }),
+        ("1_6", quote! { relative(1./6.) }),
+        ("5_6", quote! { relative(5./6.) }),
+        ("1_12", quote! { relative(1./12.) }),
+        // ("screen_50", quote! { DefiniteLength::Vh(50.0) }),
+        // ("screen_75", quote! { DefiniteLength::Vh(75.0) }),
+        // ("screen", quote! { DefiniteLength::Vh(100.0) }),
+    ]
+}
+
+fn tailwind_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
+    vec![
+        ("w", true, vec![quote! { size.width }]),
+        ("h", true, vec![quote! { size.height }]),
+        ("min_w", false, vec![quote! { min_size.width }]),
+        ("min_h", false, vec![quote! { min_size.height }]),
+        ("max_w", false, vec![quote! { max_size.width }]),
+        ("max_h", false, vec![quote! { max_size.height }]),
+        (
+            "m",
+            true,
+            vec![quote! { margin.top }, quote! { margin.bottom }],
+        ),
+        ("mt", true, vec![quote! { margin.top }]),
+        ("mb", true, vec![quote! { margin.bottom }]),
+        (
+            "mx",
+            true,
+            vec![quote! { margin.left }, quote! { margin.right }],
+        ),
+        ("ml", true, vec![quote! { margin.left }]),
+        ("mr", true, vec![quote! { margin.right }]),
+        (
+            "p",
+            false,
+            vec![quote! { padding.top }, quote! { padding.bottom }],
+        ),
+        ("pt", false, vec![quote! { padding.top }]),
+        ("pb", false, vec![quote! { padding.bottom }]),
+        (
+            "px",
+            false,
+            vec![quote! { padding.left }, quote! { padding.right }],
+        ),
+        ("pl", false, vec![quote! { padding.left }]),
+        ("pr", false, vec![quote! { padding.right }]),
+        ("top", true, vec![quote! { inset.top }]),
+        ("bottom", true, vec![quote! { inset.bottom }]),
+        ("left", true, vec![quote! { inset.left }]),
+        ("right", true, vec![quote! { inset.right }]),
+    ]
+}

crates/gpui/playground_macros/src/tailwind_lengths.rs 🔗

@@ -0,0 +1,99 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{parse_macro_input, FnArg, ItemFn, PatType};
+
+pub fn tailwind_lengths(_attr: TokenStream, item: TokenStream) -> TokenStream {
+    let input_function = parse_macro_input!(item as ItemFn);
+
+    let visibility = &input_function.vis;
+    let function_signature = input_function.sig.clone();
+    let function_body = input_function.block;
+    let where_clause = &function_signature.generics.where_clause;
+
+    let argument_name = match function_signature.inputs.iter().nth(1) {
+        Some(FnArg::Typed(PatType { pat, .. })) => pat,
+        _ => panic!("Couldn't find the second argument in the function signature"),
+    };
+
+    let mut output_functions = TokenStream2::new();
+
+    for (length, value) in fixed_lengths() {
+        let function_name = format_ident!("{}{}", function_signature.ident, length);
+        output_functions.extend(quote! {
+            #visibility fn #function_name(mut self) -> Self #where_clause {
+                let #argument_name = #value.into();
+                #function_body
+            }
+        });
+    }
+
+    output_functions.into()
+}
+
+fn fixed_lengths() -> Vec<(&'static str, TokenStream2)> {
+    vec![
+        ("0", quote! { DefinedLength::Pixels(0.) }),
+        ("px", quote! { DefinedLength::Pixels(1.) }),
+        ("0_5", quote! { DefinedLength::Rems(0.125) }),
+        ("1", quote! { DefinedLength::Rems(0.25) }),
+        ("1_5", quote! { DefinedLength::Rems(0.375) }),
+        ("2", quote! { DefinedLength::Rems(0.5) }),
+        ("2_5", quote! { DefinedLength::Rems(0.625) }),
+        ("3", quote! { DefinedLength::Rems(0.75) }),
+        ("3_5", quote! { DefinedLength::Rems(0.875) }),
+        ("4", quote! { DefinedLength::Rems(1.) }),
+        ("5", quote! { DefinedLength::Rems(1.25) }),
+        ("6", quote! { DefinedLength::Rems(1.5) }),
+        ("7", quote! { DefinedLength::Rems(1.75) }),
+        ("8", quote! { DefinedLength::Rems(2.) }),
+        ("9", quote! { DefinedLength::Rems(2.25) }),
+        ("10", quote! { DefinedLength::Rems(2.5) }),
+        ("11", quote! { DefinedLength::Rems(2.75) }),
+        ("12", quote! { DefinedLength::Rems(3.) }),
+        ("14", quote! { DefinedLength::Rems(3.5) }),
+        ("16", quote! { DefinedLength::Rems(4.) }),
+        ("20", quote! { DefinedLength::Rems(5.) }),
+        ("24", quote! { DefinedLength::Rems(6.) }),
+        ("28", quote! { DefinedLength::Rems(7.) }),
+        ("32", quote! { DefinedLength::Rems(8.) }),
+        ("36", quote! { DefinedLength::Rems(9.) }),
+        ("40", quote! { DefinedLength::Rems(10.) }),
+        ("44", quote! { DefinedLength::Rems(11.) }),
+        ("48", quote! { DefinedLength::Rems(12.) }),
+        ("52", quote! { DefinedLength::Rems(13.) }),
+        ("56", quote! { DefinedLength::Rems(14.) }),
+        ("60", quote! { DefinedLength::Rems(15.) }),
+        ("64", quote! { DefinedLength::Rems(16.) }),
+        ("72", quote! { DefinedLength::Rems(18.) }),
+        ("80", quote! { DefinedLength::Rems(20.) }),
+        ("96", quote! { DefinedLength::Rems(24.) }),
+        ("half", quote! { DefinedLength::Percent(50.) }),
+        ("1_3rd", quote! { DefinedLength::Percent(33.333333) }),
+        ("2_3rd", quote! { DefinedLength::Percent(66.666667) }),
+        ("1_4th", quote! { DefinedLength::Percent(25.) }),
+        ("2_4th", quote! { DefinedLength::Percent(50.) }),
+        ("3_4th", quote! { DefinedLength::Percent(75.) }),
+        ("1_5th", quote! { DefinedLength::Percent(20.) }),
+        ("2_5th", quote! { DefinedLength::Percent(40.) }),
+        ("3_5th", quote! { DefinedLength::Percent(60.) }),
+        ("4_5th", quote! { DefinedLength::Percent(80.) }),
+        ("1_6th", quote! { DefinedLength::Percent(16.666667) }),
+        ("2_6th", quote! { DefinedLength::Percent(33.333333) }),
+        ("3_6th", quote! { DefinedLength::Percent(50.) }),
+        ("4_6th", quote! { DefinedLength::Percent(66.666667) }),
+        ("5_6th", quote! { DefinedLength::Percent(83.333333) }),
+        ("1_12th", quote! { DefinedLength::Percent(8.333333) }),
+        ("2_12th", quote! { DefinedLength::Percent(16.666667) }),
+        ("3_12th", quote! { DefinedLength::Percent(25.) }),
+        ("4_12th", quote! { DefinedLength::Percent(33.333333) }),
+        ("5_12th", quote! { DefinedLength::Percent(41.666667) }),
+        ("6_12th", quote! { DefinedLength::Percent(50.) }),
+        ("7_12th", quote! { DefinedLength::Percent(58.333333) }),
+        ("8_12th", quote! { DefinedLength::Percent(66.666667) }),
+        ("9_12th", quote! { DefinedLength::Percent(75.) }),
+        ("10_12th", quote! { DefinedLength::Percent(83.333333) }),
+        ("11_12th", quote! { DefinedLength::Percent(91.666667) }),
+        ("full", quote! { DefinedLength::Percent(100.) }),
+    ]
+}

crates/gpui/src/app.rs 🔗

@@ -7,6 +7,34 @@ pub mod test_app_context;
 pub(crate) mod window;
 mod window_input_handler;
 
+use crate::{
+    elements::{AnyElement, AnyRootElement, RootElement},
+    executor::{self, Task},
+    fonts::TextStyle,
+    json,
+    keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
+    platform::{
+        self, FontSystem, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton,
+        PathPromptOptions, Platform, PromptLevel, WindowBounds, WindowOptions,
+    },
+    util::post_inc,
+    window::{Window, WindowContext},
+    AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId,
+};
+pub use action::*;
+use anyhow::{anyhow, Context, Result};
+use callback_collection::CallbackCollection;
+use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
+use derive_more::Deref;
+pub use menu::*;
+use parking_lot::Mutex;
+use platform::Event;
+use postage::oneshot;
+#[cfg(any(test, feature = "test-support"))]
+use ref_counts::LeakDetector;
+use ref_counts::RefCounts;
+use smallvec::SmallVec;
+use smol::prelude::*;
 use std::{
     any::{type_name, Any, TypeId},
     cell::RefCell,
@@ -21,45 +49,12 @@ use std::{
     sync::{Arc, Weak},
     time::Duration,
 };
-
-use anyhow::{anyhow, Context, Result};
-
-use derive_more::Deref;
-use parking_lot::Mutex;
-use postage::oneshot;
-use smallvec::SmallVec;
-use smol::prelude::*;
-use util::ResultExt;
-use uuid::Uuid;
-
-pub use action::*;
-use callback_collection::CallbackCollection;
-use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
-pub use menu::*;
-use platform::Event;
-#[cfg(any(test, feature = "test-support"))]
-use ref_counts::LeakDetector;
 #[cfg(any(test, feature = "test-support"))]
 pub use test_app_context::{ContextHandle, TestAppContext};
+use util::ResultExt;
+use uuid::Uuid;
 use window_input_handler::WindowInputHandler;
 
-use crate::{
-    elements::{AnyElement, AnyRootElement, RootElement},
-    executor::{self, Task},
-    fonts::TextStyle,
-    json,
-    keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
-    platform::{
-        self, FontSystem, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton,
-        PathPromptOptions, Platform, PromptLevel, WindowBounds, WindowOptions,
-    },
-    util::post_inc,
-    window::{Window, WindowContext},
-    AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId,
-};
-
-use self::ref_counts::RefCounts;
-
 pub trait Entity: 'static {
     type Event;
 
@@ -73,10 +68,12 @@ pub trait Entity: 'static {
 }
 
 pub trait View: Entity + Sized {
-    fn ui_name() -> &'static str;
     fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self>;
     fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
+    fn ui_name() -> &'static str {
+        type_name::<Self>()
+    }
     fn key_down(&mut self, _: &KeyDownEvent, _: &mut ViewContext<Self>) -> bool {
         false
     }
@@ -577,6 +574,14 @@ impl AppContext {
         }
     }
 
+    pub fn optional_global<T: 'static>(&self) -> Option<&T> {
+        if let Some(global) = self.globals.get(&TypeId::of::<T>()) {
+            Some(global.downcast_ref().unwrap())
+        } else {
+            None
+        }
+    }
+
     pub fn upgrade(&self) -> App {
         App(self.weak_self.as_ref().unwrap().upgrade().unwrap())
     }
@@ -632,7 +637,7 @@ impl AppContext {
     pub fn add_action<A, V, F, R>(&mut self, handler: F)
     where
         A: Action,
-        V: View,
+        V: 'static,
         F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
     {
         self.add_action_internal(handler, false)
@@ -641,7 +646,7 @@ impl AppContext {
     pub fn capture_action<A, V, F>(&mut self, handler: F)
     where
         A: Action,
-        V: View,
+        V: 'static,
         F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
     {
         self.add_action_internal(handler, true)
@@ -650,7 +655,7 @@ impl AppContext {
     fn add_action_internal<A, V, F, R>(&mut self, mut handler: F, capture: bool)
     where
         A: Action,
-        V: View,
+        V: 'static,
         F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
     {
         let handler = Box::new(
@@ -691,7 +696,7 @@ impl AppContext {
     pub fn add_async_action<A, V, F>(&mut self, mut handler: F)
     where
         A: Action,
-        V: View,
+        V: 'static,
         F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> Option<Task<Result<()>>>,
     {
         self.add_action(move |view, action, cx| {
@@ -890,8 +895,8 @@ impl AppContext {
 
     fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
     where
+        V: 'static,
         F: 'static + FnMut(ViewHandle<V>, bool, &mut WindowContext) -> bool,
-        V: View,
     {
         let subscription_id = post_inc(&mut self.next_subscription_id);
         let observed = handle.downgrade();
@@ -1374,15 +1379,15 @@ impl AppContext {
         self.windows.keys().copied()
     }
 
-    pub fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
+    pub fn read_view<V: 'static>(&self, handle: &ViewHandle<V>) -> &V {
         if let Some(view) = self.views.get(&(handle.window, handle.view_id)) {
             view.as_any().downcast_ref().expect("downcast is type safe")
         } else {
-            panic!("circular view reference for type {}", type_name::<T>());
+            panic!("circular view reference for type {}", type_name::<V>());
         }
     }
 
-    fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
+    fn upgrade_view_handle<V: 'static>(&self, handle: &WeakViewHandle<V>) -> Option<ViewHandle<V>> {
         if self.ref_counts.lock().is_entity_alive(handle.view_id) {
             Some(ViewHandle::new(
                 handle.window,
@@ -1651,6 +1656,9 @@ impl AppContext {
                             subscription_id,
                             callback,
                         ),
+                        Effect::RepaintWindow { window } => {
+                            self.handle_repaint_window_effect(window)
+                        }
                     }
                     self.pending_notifications.clear();
                 } else {
@@ -1888,6 +1896,15 @@ impl AppContext {
         });
     }
 
+    fn handle_repaint_window_effect(&mut self, window: AnyWindowHandle) {
+        self.update_window(window, |cx| {
+            cx.layout(false).log_err();
+            if let Some(scene) = cx.paint().log_err() {
+                cx.window.platform_window.present_scene(scene);
+            }
+        });
+    }
+
     fn handle_window_activation_effect(&mut self, window: AnyWindowHandle, active: bool) -> bool {
         self.update_window(window, |cx| {
             if cx.window.is_active == active {
@@ -2143,7 +2160,7 @@ struct ViewMetadata {
     keymap_context: KeymapContext,
 }
 
-#[derive(Default, Clone)]
+#[derive(Default, Clone, Debug)]
 pub struct WindowInvalidation {
     pub updated: HashSet<usize>,
     pub removed: Vec<usize>,
@@ -2247,6 +2264,9 @@ pub enum Effect {
         window: AnyWindowHandle,
         is_active: bool,
     },
+    RepaintWindow {
+        window: AnyWindowHandle,
+    },
     WindowActivationObservation {
         window: AnyWindowHandle,
         subscription_id: usize,
@@ -2440,6 +2460,10 @@ impl Debug for Effect {
                 .debug_struct("Effect::ActiveLabeledTasksObservation")
                 .field("subscription_id", subscription_id)
                 .finish(),
+            Effect::RepaintWindow { window } => f
+                .debug_struct("Effect::RepaintWindow")
+                .field("window_id", &window.id())
+                .finish(),
         }
     }
 }
@@ -2535,10 +2559,7 @@ pub trait AnyView {
     }
 }
 
-impl<V> AnyView for V
-where
-    V: View,
-{
+impl<V: View> AnyView for V {
     fn as_any(&self) -> &dyn Any {
         self
     }
@@ -2870,7 +2891,7 @@ pub struct ViewContext<'a, 'b, T: ?Sized> {
     view_type: PhantomData<T>,
 }
 
-impl<'a, 'b, T: View> Deref for ViewContext<'a, 'b, T> {
+impl<'a, 'b, V> Deref for ViewContext<'a, 'b, V> {
     type Target = WindowContext<'a>;
 
     fn deref(&self) -> &Self::Target {
@@ -2878,14 +2899,14 @@ impl<'a, 'b, T: View> Deref for ViewContext<'a, 'b, T> {
     }
 }
 
-impl<T: View> DerefMut for ViewContext<'_, '_, T> {
+impl<'a, 'b, V> DerefMut for ViewContext<'a, 'b, V> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.window_context
     }
 }
 
-impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
-    pub(crate) fn mutable(window_context: &'b mut WindowContext<'a>, view_id: usize) -> Self {
+impl<'a, 'b, V: 'static> ViewContext<'a, 'b, V> {
+    pub fn mutable(window_context: &'b mut WindowContext<'a>, view_id: usize) -> Self {
         Self {
             window_context: Reference::Mutable(window_context),
             view_id,
@@ -2893,7 +2914,7 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         }
     }
 
-    pub(crate) fn immutable(window_context: &'b WindowContext<'a>, view_id: usize) -> Self {
+    pub fn immutable(window_context: &'b WindowContext<'a>, view_id: usize) -> Self {
         Self {
             window_context: Reference::Immutable(window_context),
             view_id,
@@ -2905,6 +2926,12 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         &mut self.window_context
     }
 
+    pub fn notify(&mut self) {
+        let window = self.window_handle;
+        let view_id = self.view_id;
+        self.window_context.notify_view(window, view_id);
+    }
+
     pub fn handle(&self) -> ViewHandle<V> {
         ViewHandle::new(
             self.window_handle,
@@ -3218,21 +3245,6 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         })
     }
 
-    pub fn emit(&mut self, payload: V::Event) {
-        self.window_context
-            .pending_effects
-            .push_back(Effect::Event {
-                entity_id: self.view_id,
-                payload: Box::new(payload),
-            });
-    }
-
-    pub fn notify(&mut self) {
-        let window = self.window_handle;
-        let view_id = self.view_id;
-        self.window_context.notify_view(window, view_id);
-    }
-
     pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext<V>)) {
         let handle = self.handle();
         self.window_context
@@ -3287,15 +3299,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         let region_id = MouseRegionId::new(tag, self.view_id, region_id);
         MouseState {
             hovered: self.window.hovered_region_ids.contains(&region_id),
-            clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
-                if region_id == clicked_region_id {
-                    Some(button)
-                } else {
-                    None
-                }
-            } else {
-                None
-            },
+            mouse_down: !self.window.clicked_region_ids.is_empty(),
+            clicked: self
+                .window
+                .clicked_region_ids
+                .iter()
+                .find(|click_region_id| **click_region_id == region_id)
+                // If we've gotten here, there should always be a clicked region.
+                // But let's be defensive and return None if there isn't.
+                .and_then(|_| self.window.clicked_region.map(|(_, button)| button)),
             accessed_hovered: false,
             accessed_clicked: false,
         }
@@ -3305,11 +3317,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         &mut self,
         element_id: usize,
         initial: T,
+    ) -> ElementStateHandle<T> {
+        self.element_state_dynamic(TypeTag::new::<Tag>(), element_id, initial)
+    }
+
+    pub fn element_state_dynamic<T: 'static>(
+        &mut self,
+        tag: TypeTag,
+        element_id: usize,
+        initial: T,
     ) -> ElementStateHandle<T> {
         let id = ElementStateId {
             view_id: self.view_id(),
             element_id,
-            tag: TypeId::of::<Tag>(),
+            tag,
         };
         self.element_states
             .entry(id)
@@ -3323,11 +3344,35 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
     ) -> ElementStateHandle<T> {
         self.element_state::<Tag, T>(element_id, T::default())
     }
+
+    pub fn rem_pixels(&self) -> f32 {
+        16.
+    }
+
+    pub fn default_element_state_dynamic<T: 'static + Default>(
+        &mut self,
+        tag: TypeTag,
+        element_id: usize,
+    ) -> ElementStateHandle<T> {
+        self.element_state_dynamic::<T>(tag, element_id, T::default())
+    }
+}
+
+impl<V: View> ViewContext<'_, '_, V> {
+    pub fn emit(&mut self, event: V::Event) {
+        self.window_context
+            .pending_effects
+            .push_back(Effect::Event {
+                entity_id: self.view_id,
+                payload: Box::new(event),
+            });
+    }
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub struct TypeTag {
     tag: TypeId,
+    composed: Option<TypeId>,
     #[cfg(debug_assertions)]
     tag_type_name: &'static str,
 }
@@ -3336,6 +3381,7 @@ impl TypeTag {
     pub fn new<Tag: 'static>() -> Self {
         Self {
             tag: TypeId::of::<Tag>(),
+            composed: None,
             #[cfg(debug_assertions)]
             tag_type_name: std::any::type_name::<Tag>(),
         }
@@ -3344,11 +3390,17 @@ impl TypeTag {
     pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self {
         Self {
             tag,
+            composed: None,
             #[cfg(debug_assertions)]
             tag_type_name: type_name,
         }
     }
 
+    pub fn compose(mut self, other: TypeTag) -> Self {
+        self.composed = Some(other.tag);
+        self
+    }
+
     #[cfg(debug_assertions)]
     pub(crate) fn type_name(&self) -> &'static str {
         self.tag_type_name
@@ -3395,15 +3447,27 @@ impl<V> BorrowWindowContext for ViewContext<'_, '_, V> {
     }
 }
 
-pub struct LayoutContext<'a, 'b, 'c, V: View> {
-    view_context: &'c mut ViewContext<'a, 'b, V>,
+/// Methods shared by both LayoutContext and PaintContext
+///
+/// It's that PaintContext should be implemented in terms of layout context and
+/// deref to it, in which case we wouldn't need this.
+pub trait RenderContext<'a, 'b, V> {
+    fn text_style(&self) -> TextStyle;
+    fn push_text_style(&mut self, style: TextStyle);
+    fn pop_text_style(&mut self);
+    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V>;
+}
+
+pub struct LayoutContext<'a, 'b, 'c, V> {
+    // Nathan: Making this is public while I work on playground.
+    pub view_context: &'c mut ViewContext<'a, 'b, V>,
     new_parents: &'c mut HashMap<usize, usize>,
     views_to_notify_if_ancestors_change: &'c mut HashMap<usize, SmallVec<[usize; 2]>>,
-    text_style_stack: Vec<Arc<TextStyle>>,
+    text_style_stack: Vec<TextStyle>,
     pub refreshing: bool,
 }
 
-impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> LayoutContext<'a, 'b, 'c, V> {
     pub fn new(
         view_context: &'c mut ViewContext<'a, 'b, V>,
         new_parents: &'c mut HashMap<usize, usize>,
@@ -3467,26 +3531,39 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
             .push(self_view_id);
     }
 
-    pub fn text_style(&self) -> Arc<TextStyle> {
+    pub fn with_text_style<F, T>(&mut self, style: TextStyle, f: F) -> T
+    where
+        F: FnOnce(&mut Self) -> T,
+    {
+        self.push_text_style(style);
+        let result = f(self);
+        self.pop_text_style();
+        result
+    }
+}
+
+impl<'a, 'b, 'c, V> RenderContext<'a, 'b, V> for LayoutContext<'a, 'b, 'c, V> {
+    fn text_style(&self) -> TextStyle {
         self.text_style_stack
             .last()
             .cloned()
-            .unwrap_or(Default::default())
+            .unwrap_or(TextStyle::default(&self.font_cache))
     }
 
-    pub fn with_text_style<S, F, T>(&mut self, style: S, f: F) -> T
-    where
-        S: Into<Arc<TextStyle>>,
-        F: FnOnce(&mut Self) -> T,
-    {
-        self.text_style_stack.push(style.into());
-        let result = f(self);
+    fn push_text_style(&mut self, style: TextStyle) {
+        self.text_style_stack.push(style);
+    }
+
+    fn pop_text_style(&mut self) {
         self.text_style_stack.pop();
-        result
+    }
+
+    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
+        &mut self.view_context
     }
 }
 
-impl<'a, 'b, 'c, V: View> Deref for LayoutContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> Deref for LayoutContext<'a, 'b, 'c, V> {
     type Target = ViewContext<'a, 'b, V>;
 
     fn deref(&self) -> &Self::Target {
@@ -3494,13 +3571,13 @@ impl<'a, 'b, 'c, V: View> Deref for LayoutContext<'a, 'b, 'c, V> {
     }
 }
 
-impl<V: View> DerefMut for LayoutContext<'_, '_, '_, V> {
+impl<V> DerefMut for LayoutContext<'_, '_, '_, V> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.view_context
     }
 }
 
-impl<V: View> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
+impl<V> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
     fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
         BorrowAppContext::read_with(&*self.view_context, f)
     }
@@ -3510,7 +3587,7 @@ impl<V: View> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
     }
 }
 
-impl<V: View> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
+impl<V> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
     type Result<T> = T;
 
     fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
@@ -3540,39 +3617,42 @@ impl<V: View> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
     }
 }
 
-pub struct PaintContext<'a, 'b, 'c, V: View> {
-    view_context: &'c mut ViewContext<'a, 'b, V>,
-    text_style_stack: Vec<Arc<TextStyle>>,
+pub struct PaintContext<'a, 'b, 'c, V> {
+    pub view_context: &'c mut ViewContext<'a, 'b, V>,
+    text_style_stack: Vec<TextStyle>,
 }
 
-impl<'a, 'b, 'c, V: View> PaintContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> PaintContext<'a, 'b, 'c, V> {
     pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
         Self {
             view_context,
             text_style_stack: Vec::new(),
         }
     }
+}
 
-    pub fn text_style(&self) -> Arc<TextStyle> {
+impl<'a, 'b, 'c, V> RenderContext<'a, 'b, V> for PaintContext<'a, 'b, 'c, V> {
+    fn text_style(&self) -> TextStyle {
         self.text_style_stack
             .last()
             .cloned()
-            .unwrap_or(Default::default())
+            .unwrap_or(TextStyle::default(&self.font_cache))
     }
 
-    pub fn with_text_style<S, F, T>(&mut self, style: S, f: F) -> T
-    where
-        S: Into<Arc<TextStyle>>,
-        F: FnOnce(&mut Self) -> T,
-    {
-        self.text_style_stack.push(style.into());
-        let result = f(self);
+    fn push_text_style(&mut self, style: TextStyle) {
+        self.text_style_stack.push(style);
+    }
+
+    fn pop_text_style(&mut self) {
         self.text_style_stack.pop();
-        result
+    }
+
+    fn as_view_context(&mut self) -> &mut ViewContext<'a, 'b, V> {
+        &mut self.view_context
     }
 }
 
-impl<'a, 'b, 'c, V: View> Deref for PaintContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> Deref for PaintContext<'a, 'b, 'c, V> {
     type Target = ViewContext<'a, 'b, V>;
 
     fn deref(&self) -> &Self::Target {
@@ -3580,13 +3660,13 @@ impl<'a, 'b, 'c, V: View> Deref for PaintContext<'a, 'b, 'c, V> {
     }
 }
 
-impl<V: View> DerefMut for PaintContext<'_, '_, '_, V> {
+impl<V> DerefMut for PaintContext<'_, '_, '_, V> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.view_context
     }
 }
 
-impl<V: View> BorrowAppContext for PaintContext<'_, '_, '_, V> {
+impl<V> BorrowAppContext for PaintContext<'_, '_, '_, V> {
     fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
         BorrowAppContext::read_with(&*self.view_context, f)
     }
@@ -3596,7 +3676,7 @@ impl<V: View> BorrowAppContext for PaintContext<'_, '_, '_, V> {
     }
 }
 
-impl<V: View> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
+impl<V> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
     type Result<T> = T;
 
     fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
@@ -3628,25 +3708,37 @@ impl<V: View> BorrowWindowContext for PaintContext<'_, '_, '_, V> {
     }
 }
 
-pub struct EventContext<'a, 'b, 'c, V: View> {
+pub struct EventContext<'a, 'b, 'c, V> {
     view_context: &'c mut ViewContext<'a, 'b, V>,
     pub(crate) handled: bool,
+    // I would like to replace handled with this.
+    // Being additive for now.
+    pub bubble: bool,
 }
 
-impl<'a, 'b, 'c, V: View> EventContext<'a, 'b, 'c, V> {
-    pub(crate) fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
+impl<'a, 'b, 'c, V: 'static> EventContext<'a, 'b, 'c, V> {
+    pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self {
         EventContext {
             view_context,
             handled: true,
+            bubble: false,
         }
     }
 
     pub fn propagate_event(&mut self) {
         self.handled = false;
     }
+
+    pub fn bubble_event(&mut self) {
+        self.bubble = true;
+    }
+
+    pub fn event_bubbled(&self) -> bool {
+        self.bubble
+    }
 }
 
-impl<'a, 'b, 'c, V: View> Deref for EventContext<'a, 'b, 'c, V> {
+impl<'a, 'b, 'c, V> Deref for EventContext<'a, 'b, 'c, V> {
     type Target = ViewContext<'a, 'b, V>;
 
     fn deref(&self) -> &Self::Target {
@@ -3654,13 +3746,13 @@ impl<'a, 'b, 'c, V: View> Deref for EventContext<'a, 'b, 'c, V> {
     }
 }
 
-impl<V: View> DerefMut for EventContext<'_, '_, '_, V> {
+impl<V> DerefMut for EventContext<'_, '_, '_, V> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.view_context
     }
 }
 
-impl<V: View> BorrowAppContext for EventContext<'_, '_, '_, V> {
+impl<V> BorrowAppContext for EventContext<'_, '_, '_, V> {
     fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
         BorrowAppContext::read_with(&*self.view_context, f)
     }
@@ -3670,7 +3762,7 @@ impl<V: View> BorrowAppContext for EventContext<'_, '_, '_, V> {
     }
 }
 
-impl<V: View> BorrowWindowContext for EventContext<'_, '_, '_, V> {
+impl<V> BorrowWindowContext for EventContext<'_, '_, '_, V> {
     type Result<T> = T;
 
     fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
@@ -3731,14 +3823,20 @@ impl<'a, T> DerefMut for Reference<'a, T> {
 pub struct MouseState {
     pub(crate) hovered: bool,
     pub(crate) clicked: Option<MouseButton>,
+    pub(crate) mouse_down: bool,
     pub(crate) accessed_hovered: bool,
     pub(crate) accessed_clicked: bool,
 }
 
 impl MouseState {
+    pub fn dragging(&mut self) -> bool {
+        self.accessed_hovered = true;
+        self.hovered && self.mouse_down
+    }
+
     pub fn hovered(&mut self) -> bool {
         self.accessed_hovered = true;
-        self.hovered
+        self.hovered && (!self.mouse_down || self.clicked.is_some())
     }
 
     pub fn clicked(&mut self) -> Option<MouseButton> {
@@ -3998,7 +4096,7 @@ impl<V> Clone for WindowHandle<V> {
 
 impl<V> Copy for WindowHandle<V> {}
 
-impl<V: View> WindowHandle<V> {
+impl<V: 'static> WindowHandle<V> {
     fn new(window_id: usize) -> Self {
         WindowHandle {
             any_handle: AnyWindowHandle::new(window_id, TypeId::of::<V>()),
@@ -4036,7 +4134,9 @@ impl<V: View> WindowHandle<V> {
                 .update(cx, update)
         })
     }
+}
 
+impl<V: View> WindowHandle<V> {
     pub fn replace_root<C, F>(&self, cx: &mut C, build_root: F) -> C::Result<ViewHandle<V>>
     where
         C: BorrowWindowContext,
@@ -4116,7 +4216,7 @@ impl AnyWindowHandle {
         self.update(cx, |cx| cx.add_view(build_view))
     }
 
-    pub fn downcast<V: View>(self) -> Option<WindowHandle<V>> {
+    pub fn downcast<V: 'static>(self) -> Option<WindowHandle<V>> {
         if self.root_view_type == TypeId::of::<V>() {
             Some(WindowHandle {
                 any_handle: self,
@@ -4127,7 +4227,7 @@ impl AnyWindowHandle {
         }
     }
 
-    pub fn root_is<V: View>(&self) -> bool {
+    pub fn root_is<V: 'static>(&self) -> bool {
         self.root_view_type == TypeId::of::<V>()
     }
 
@@ -4205,9 +4305,9 @@ impl AnyWindowHandle {
 }
 
 #[repr(transparent)]
-pub struct ViewHandle<T> {
+pub struct ViewHandle<V> {
     any_handle: AnyViewHandle,
-    view_type: PhantomData<T>,
+    view_type: PhantomData<V>,
 }
 
 impl<T> Deref for ViewHandle<T> {
@@ -4218,15 +4318,15 @@ impl<T> Deref for ViewHandle<T> {
     }
 }
 
-impl<T: View> ViewHandle<T> {
+impl<V: 'static> ViewHandle<V> {
     fn new(window: AnyWindowHandle, view_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
         Self {
-            any_handle: AnyViewHandle::new(window, view_id, TypeId::of::<T>(), ref_counts.clone()),
+            any_handle: AnyViewHandle::new(window, view_id, TypeId::of::<V>(), ref_counts.clone()),
             view_type: PhantomData,
         }
     }
 
-    pub fn downgrade(&self) -> WeakViewHandle<T> {
+    pub fn downgrade(&self) -> WeakViewHandle<V> {
         WeakViewHandle::new(self.window, self.view_id)
     }
 
@@ -4242,14 +4342,14 @@ impl<T: View> ViewHandle<T> {
         self.view_id
     }
 
-    pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
+    pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
         cx.read_view(self)
     }
 
     pub fn read_with<C, F, S>(&self, cx: &C, read: F) -> C::Result<S>
     where
         C: BorrowWindowContext,
-        F: FnOnce(&T, &ViewContext<T>) -> S,
+        F: FnOnce(&V, &ViewContext<V>) -> S,
     {
         cx.read_window(self.window, |cx| {
             let cx = ViewContext::immutable(cx, self.view_id);
@@ -4260,7 +4360,7 @@ impl<T: View> ViewHandle<T> {
     pub fn update<C, F, S>(&self, cx: &mut C, update: F) -> C::Result<S>
     where
         C: BorrowWindowContext,
-        F: FnOnce(&mut T, &mut ViewContext<T>) -> S,
+        F: FnOnce(&mut V, &mut ViewContext<V>) -> S,
     {
         let mut update = Some(update);
 
@@ -4396,8 +4496,8 @@ impl AnyViewHandle {
         TypeId::of::<T>() == self.view_type
     }
 
-    pub fn downcast<T: View>(self) -> Option<ViewHandle<T>> {
-        if self.is::<T>() {
+    pub fn downcast<V: 'static>(self) -> Option<ViewHandle<V>> {
+        if self.is::<V>() {
             Some(ViewHandle {
                 any_handle: self,
                 view_type: PhantomData,
@@ -4407,8 +4507,8 @@ impl AnyViewHandle {
         }
     }
 
-    pub fn downcast_ref<T: View>(&self) -> Option<&ViewHandle<T>> {
-        if self.is::<T>() {
+    pub fn downcast_ref<V: 'static>(&self) -> Option<&ViewHandle<V>> {
+        if self.is::<V>() {
             Some(unsafe { mem::transmute(self) })
         } else {
             None
@@ -4587,12 +4687,13 @@ impl AnyWeakModelHandle {
     }
 }
 
-#[derive(Copy)]
 pub struct WeakViewHandle<T> {
     any_handle: AnyWeakViewHandle,
     view_type: PhantomData<T>,
 }
 
+impl<T> Copy for WeakViewHandle<T> {}
+
 impl<T> Debug for WeakViewHandle<T> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct(&format!("WeakViewHandle<{}>", type_name::<T>()))
@@ -4607,7 +4708,7 @@ impl<T> WeakHandle for WeakViewHandle<T> {
     }
 }
 
-impl<V: View> WeakViewHandle<V> {
+impl<V: 'static> WeakViewHandle<V> {
     fn new(window: AnyWindowHandle, view_id: usize) -> Self {
         Self {
             any_handle: AnyWeakViewHandle {
@@ -4647,28 +4748,47 @@ impl<V: View> WeakViewHandle<V> {
         cx.read(|cx| {
             let handle = cx
                 .upgrade_view_handle(self)
-                .ok_or_else(|| anyhow!("view {} was dropped", V::ui_name()))?;
+                .ok_or_else(|| anyhow!("view was dropped"))?;
             cx.read_window(self.window, |cx| handle.read_with(cx, read))
                 .ok_or_else(|| anyhow!("window was removed"))
         })
     }
 
-    pub fn update<T>(
+    pub fn update<T, B>(
         &self,
-        cx: &mut AsyncAppContext,
+        cx: &mut B,
         update: impl FnOnce(&mut V, &mut ViewContext<V>) -> T,
-    ) -> Result<T> {
-        cx.update(|cx| {
-            let handle = cx
-                .upgrade_view_handle(self)
-                .ok_or_else(|| anyhow!("view {} was dropped", V::ui_name()))?;
-            cx.update_window(self.window, |cx| handle.update(cx, update))
-                .ok_or_else(|| anyhow!("window was removed"))
+    ) -> Result<T>
+    where
+        B: BorrowWindowContext,
+        B::Result<Option<T>>: Flatten<T>,
+    {
+        cx.update_window(self.window(), |cx| {
+            cx.upgrade_view_handle(self)
+                .map(|handle| handle.update(cx, update))
         })
+        .flatten()
+        .ok_or_else(|| anyhow!("window was removed"))
+    }
+}
+
+pub trait Flatten<T> {
+    fn flatten(self) -> Option<T>;
+}
+
+impl<T> Flatten<T> for Option<Option<T>> {
+    fn flatten(self) -> Option<T> {
+        self.flatten()
+    }
+}
+
+impl<T> Flatten<T> for Option<T> {
+    fn flatten(self) -> Option<T> {
+        self
     }
 }
 
-impl<T> Deref for WeakViewHandle<T> {
+impl<V> Deref for WeakViewHandle<V> {
     type Target = AnyWeakViewHandle;
 
     fn deref(&self) -> &Self::Target {
@@ -4676,7 +4796,7 @@ impl<T> Deref for WeakViewHandle<T> {
     }
 }
 
-impl<T> Clone for WeakViewHandle<T> {
+impl<V> Clone for WeakViewHandle<V> {
     fn clone(&self) -> Self {
         Self {
             any_handle: self.any_handle.clone(),
@@ -4743,7 +4863,7 @@ impl Hash for AnyWeakViewHandle {
 pub struct ElementStateId {
     view_id: usize,
     element_id: usize,
-    tag: TypeId,
+    tag: TypeTag,
 }
 
 pub struct ElementStateHandle<T> {
@@ -5230,6 +5350,7 @@ mod tests {
                     button: MouseButton::Left,
                     modifiers: Default::default(),
                     click_count: 1,
+                    is_down: true,
                 }),
                 false,
             );

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

@@ -1,10 +1,13 @@
 use std::any::{Any, TypeId};
 
+use crate::TypeTag;
+
 pub trait Action: 'static {
     fn id(&self) -> TypeId;
     fn namespace(&self) -> &'static str;
     fn name(&self) -> &'static str;
     fn as_any(&self) -> &dyn Any;
+    fn type_tag(&self) -> TypeTag;
     fn boxed_clone(&self) -> Box<dyn Action>;
     fn eq(&self, other: &dyn Action) -> bool;
 
@@ -107,6 +110,10 @@ macro_rules! __impl_action {
                 }
             }
 
+            fn type_tag(&self) -> $crate::TypeTag {
+                $crate::TypeTag::new::<Self>()
+            }
+
             $from_json_fn
         }
     };

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

@@ -1,6 +1,6 @@
 use crate::{
     elements::AnyRootElement,
-    geometry::rect::RectF,
+    geometry::{rect::RectF, Size},
     json::ToJson,
     keymap_matcher::{Binding, KeymapContext, Keystroke, MatchResult},
     platform::{
@@ -8,8 +8,9 @@ use crate::{
         MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
     },
     scene::{
-        CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent,
-        MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
+        CursorRegion, EventHandler, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
+        MouseEvent, MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
+        Scene,
     },
     text_layout::TextLayoutCache,
     util::post_inc,
@@ -31,7 +32,11 @@ use sqlez::{
 use std::{
     any::TypeId,
     mem,
-    ops::{Deref, DerefMut, Range},
+    ops::{Deref, DerefMut, Range, Sub},
+};
+use taffy::{
+    tree::{Measurable, MeasureFunc},
+    Taffy,
 };
 use util::ResultExt;
 use uuid::Uuid;
@@ -39,6 +44,7 @@ use uuid::Uuid;
 use super::{Reference, ViewMetadata};
 
 pub struct Window {
+    layout_engines: Vec<LayoutEngine>,
     pub(crate) root_view: Option<AnyViewHandle>,
     pub(crate) focused_view_id: Option<usize>,
     pub(crate) parents: HashMap<usize, usize>,
@@ -51,6 +57,7 @@ pub struct Window {
     appearance: Appearance,
     cursor_regions: Vec<CursorRegion>,
     mouse_regions: Vec<(MouseRegion, usize)>,
+    event_handlers: Vec<EventHandler>,
     last_mouse_moved_event: Option<Event>,
     pub(crate) hovered_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region_ids: Vec<MouseRegionId>,
@@ -67,12 +74,13 @@ impl Window {
         build_view: F,
     ) -> Self
     where
-        F: FnOnce(&mut ViewContext<V>) -> V,
         V: View,
+        F: FnOnce(&mut ViewContext<V>) -> V,
     {
         let titlebar_height = platform_window.titlebar_height();
         let appearance = platform_window.appearance();
         let mut window = Self {
+            layout_engines: Vec::new(),
             root_view: None,
             focused_view_id: None,
             parents: Default::default(),
@@ -83,6 +91,7 @@ impl Window {
             rendered_views: Default::default(),
             cursor_regions: Default::default(),
             mouse_regions: Default::default(),
+            event_handlers: Default::default(),
             text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
             last_mouse_moved_event: None,
             hovered_region_ids: Default::default(),
@@ -109,6 +118,10 @@ impl Window {
             .as_ref()
             .expect("root_view called during window construction")
     }
+
+    pub fn take_event_handlers(&mut self) -> Vec<EventHandler> {
+        mem::take(&mut self.event_handlers)
+    }
 }
 
 pub struct WindowContext<'a> {
@@ -207,6 +220,24 @@ impl<'a> WindowContext<'a> {
         }
     }
 
+    pub fn repaint(&mut self) {
+        let window = self.window();
+        self.pending_effects
+            .push_back(Effect::RepaintWindow { window });
+    }
+
+    pub fn layout_engine(&mut self) -> Option<&mut LayoutEngine> {
+        self.window.layout_engines.last_mut()
+    }
+
+    pub fn push_layout_engine(&mut self, engine: LayoutEngine) {
+        self.window.layout_engines.push(engine);
+    }
+
+    pub fn pop_layout_engine(&mut self) -> Option<LayoutEngine> {
+        self.window.layout_engines.pop()
+    }
+
     pub fn remove_window(&mut self) {
         self.removed = true;
     }
@@ -227,6 +258,10 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.content_size()
     }
 
+    pub fn mouse_position(&self) -> Vector2F {
+        self.window.mouse_position
+    }
+
     pub fn text_layout_cache(&self) -> &TextLayoutCache {
         &self.window.text_layout_cache
     }
@@ -242,14 +277,11 @@ impl<'a> WindowContext<'a> {
         Some(result)
     }
 
-    pub(crate) fn update_view<T, S>(
+    pub(crate) fn update_view<V: 'static, S>(
         &mut self,
-        handle: &ViewHandle<T>,
-        update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
-    ) -> S
-    where
-        T: View,
-    {
+        handle: &ViewHandle<V>,
+        update: &mut dyn FnMut(&mut V, &mut ViewContext<V>) -> S,
+    ) -> S {
         self.update_any_view(handle.view_id, |view, cx| {
             let mut cx = ViewContext::mutable(cx, handle.view_id);
             update(
@@ -475,6 +507,8 @@ impl<'a> WindowContext<'a> {
     }
 
     pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
+        self.dispatch_to_new_event_handlers(&event);
+
         let mut mouse_events = SmallVec::<[_; 2]>::new();
         let mut notified_views: HashSet<usize> = Default::default();
         let handle = self.window_handle;
@@ -583,10 +617,11 @@ impl<'a> WindowContext<'a> {
                     }
                 }
 
-                if self
-                    .window
-                    .platform_window
-                    .is_topmost_for_position(*position)
+                if pressed_button.is_none()
+                    && self
+                        .window
+                        .platform_window
+                        .is_topmost_for_position(*position)
                 {
                     self.platform().set_cursor_style(style_to_assign);
                 }
@@ -753,6 +788,11 @@ impl<'a> WindowContext<'a> {
                                     .contains_point(self.window.mouse_position)
                                 {
                                     valid_regions.push(mouse_region.clone());
+                                } else {
+                                    // Let the view know that it hasn't been clicked anymore
+                                    if mouse_region.notify_on_click {
+                                        notified_views.insert(mouse_region.id().view_id());
+                                    }
                                 }
                             }
                         }
@@ -852,6 +892,18 @@ impl<'a> WindowContext<'a> {
         any_event_handled
     }
 
+    fn dispatch_to_new_event_handlers(&mut self, event: &Event) {
+        if let Some(mouse_event) = event.mouse_event() {
+            let event_handlers = self.window.take_event_handlers();
+            for event_handler in event_handlers.iter().rev() {
+                if event_handler.event_type == mouse_event.type_id() {
+                    (event_handler.handler)(mouse_event, self);
+                }
+            }
+            self.window.event_handlers = event_handlers;
+        }
+    }
+
     pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
         let handle = self.window_handle;
         if let Some(focused_view_id) = self.window.focused_view_id {
@@ -942,14 +994,16 @@ impl<'a> WindowContext<'a> {
         Ok(element)
     }
 
-    pub(crate) fn layout(&mut self, refreshing: bool) -> Result<HashMap<usize, usize>> {
+    pub fn layout(&mut self, refreshing: bool) -> Result<HashMap<usize, usize>> {
         let window_size = self.window.platform_window.content_size();
         let root_view_id = self.window.root_view().id();
+
         let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
+
         let mut new_parents = HashMap::default();
         let mut views_to_notify_if_ancestors_change = HashMap::default();
         rendered_root.layout(
-            SizeConstraint::strict(window_size),
+            SizeConstraint::new(window_size, window_size),
             &mut new_parents,
             &mut views_to_notify_if_ancestors_change,
             refreshing,
@@ -982,7 +1036,7 @@ impl<'a> WindowContext<'a> {
         Ok(old_parents)
     }
 
-    pub(crate) fn paint(&mut self) -> Result<Scene> {
+    pub fn paint(&mut self) -> Result<Scene> {
         let window_size = self.window.platform_window.content_size();
         let scale_factor = self.window.platform_window.scale_factor();
 
@@ -1001,9 +1055,10 @@ impl<'a> WindowContext<'a> {
             .insert(root_view_id, rendered_root);
 
         self.window.text_layout_cache.finish_frame();
-        let scene = scene_builder.build();
+        let mut scene = scene_builder.build();
         self.window.cursor_regions = scene.cursor_regions();
         self.window.mouse_regions = scene.mouse_regions();
+        self.window.event_handlers = scene.take_event_handlers();
 
         if self.window_is_active() {
             if let Some(event) = self.window.last_mouse_moved_event.clone() {
@@ -1014,6 +1069,11 @@ impl<'a> WindowContext<'a> {
         Ok(scene)
     }
 
+    pub fn root_element(&self) -> &Box<dyn AnyRootElement> {
+        let view_id = self.window.root_view().id();
+        self.window.rendered_views.get(&view_id).unwrap()
+    }
+
     pub fn rect_for_text_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
         let focused_view_id = self.window.focused_view_id?;
         self.window
@@ -1216,6 +1276,119 @@ impl<'a> WindowContext<'a> {
     }
 }
 
+#[derive(Default)]
+pub struct LayoutEngine(Taffy);
+pub use taffy::style::Style as LayoutStyle;
+
+impl LayoutEngine {
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    pub fn add_node<C>(&mut self, style: LayoutStyle, children: C) -> Result<LayoutId>
+    where
+        C: IntoIterator<Item = LayoutId>,
+    {
+        Ok(self
+            .0
+            .new_with_children(style, &children.into_iter().collect::<Vec<_>>())?)
+    }
+
+    pub fn add_measured_node<F>(&mut self, style: LayoutStyle, measure: F) -> Result<LayoutId>
+    where
+        F: Fn(MeasureParams) -> Size<f32> + Sync + Send + 'static,
+    {
+        Ok(self
+            .0
+            .new_leaf_with_measure(style, MeasureFunc::Boxed(Box::new(MeasureFn(measure))))?)
+    }
+
+    pub fn compute_layout(&mut self, root: LayoutId, available_space: Vector2F) -> Result<()> {
+        self.0.compute_layout(
+            root,
+            taffy::geometry::Size {
+                width: available_space.x().into(),
+                height: available_space.y().into(),
+            },
+        )?;
+        Ok(())
+    }
+
+    pub fn computed_layout(&mut self, node: LayoutId) -> Result<EngineLayout> {
+        Ok(self.0.layout(node)?.into())
+    }
+}
+
+pub struct MeasureFn<F>(F);
+
+impl<F: Send + Sync> Measurable for MeasureFn<F>
+where
+    F: Fn(MeasureParams) -> Size<f32>,
+{
+    fn measure(
+        &self,
+        known_dimensions: taffy::prelude::Size<Option<f32>>,
+        available_space: taffy::prelude::Size<taffy::style::AvailableSpace>,
+    ) -> taffy::prelude::Size<f32> {
+        (self.0)(MeasureParams {
+            known_dimensions: known_dimensions.into(),
+            available_space: available_space.into(),
+        })
+        .into()
+    }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct EngineLayout {
+    pub bounds: RectF,
+    pub order: u32,
+}
+
+pub struct MeasureParams {
+    pub known_dimensions: Size<Option<f32>>,
+    pub available_space: Size<AvailableSpace>,
+}
+
+#[derive(Clone)]
+pub enum AvailableSpace {
+    /// The amount of space available is the specified number of pixels
+    Pixels(f32),
+    /// The amount of space available is indefinite and the node should be laid out under a min-content constraint
+    MinContent,
+    /// The amount of space available is indefinite and the node should be laid out under a max-content constraint
+    MaxContent,
+}
+
+impl Default for AvailableSpace {
+    fn default() -> Self {
+        Self::Pixels(0.)
+    }
+}
+
+impl From<taffy::prelude::AvailableSpace> for AvailableSpace {
+    fn from(value: taffy::prelude::AvailableSpace) -> Self {
+        match value {
+            taffy::prelude::AvailableSpace::Definite(pixels) => Self::Pixels(pixels),
+            taffy::prelude::AvailableSpace::MinContent => Self::MinContent,
+            taffy::prelude::AvailableSpace::MaxContent => Self::MaxContent,
+        }
+    }
+}
+
+impl From<&taffy::tree::Layout> for EngineLayout {
+    fn from(value: &taffy::tree::Layout) -> Self {
+        Self {
+            bounds: RectF::new(
+                vec2f(value.location.x, value.location.y),
+                vec2f(value.size.width, value.size.height),
+            ),
+            order: value.order,
+        }
+    }
+}
+
+pub type LayoutId = taffy::prelude::NodeId;
+
 pub struct RenderParams {
     pub view_id: usize,
     pub titlebar_height: f32,
@@ -1324,6 +1497,12 @@ impl SizeConstraint {
             max: size,
         }
     }
+    pub fn loose(max: Vector2F) -> Self {
+        Self {
+            min: Vector2F::zero(),
+            max,
+        }
+    }
 
     pub fn strict_along(axis: Axis, max: f32) -> Self {
         match axis {
@@ -1360,6 +1539,17 @@ impl SizeConstraint {
     }
 }
 
+impl Sub<Vector2F> for SizeConstraint {
+    type Output = SizeConstraint;
+
+    fn sub(self, rhs: Vector2F) -> SizeConstraint {
+        SizeConstraint {
+            min: self.min - rhs,
+            max: self.max - rhs,
+        }
+    }
+}
+
 impl Default for SizeConstraint {
     fn default() -> Self {
         SizeConstraint {
@@ -1378,6 +1568,7 @@ impl ToJson for SizeConstraint {
     }
 }
 
+#[derive(Clone)]
 pub struct ChildView {
     view_id: usize,
     view_name: &'static str,
@@ -1393,7 +1584,7 @@ impl ChildView {
     }
 }
 
-impl<V: View> Element<V> for ChildView {
+impl<V: 'static> Element<V> for ChildView {
     type LayoutState = ();
     type PaintState = ();
 

crates/gpui/src/color.rs 🔗

@@ -15,35 +15,75 @@ use serde_json::json;
 
 #[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
 #[repr(transparent)]
-pub struct Color(#[schemars(with = "String")] ColorU);
+pub struct Color(#[schemars(with = "String")] pub ColorU);
+
+pub fn color(rgba: u32) -> Color {
+    Color::from_u32(rgba)
+}
+
+pub fn rgb(r: f32, g: f32, b: f32) -> Color {
+    Color(ColorF::new(r, g, b, 1.).to_u8())
+}
+
+pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
+    Color(ColorF::new(r, g, b, a).to_u8())
+}
+
+pub fn transparent_black() -> Color {
+    Color(ColorU::transparent_black())
+}
+
+pub fn black() -> Color {
+    Color(ColorU::black())
+}
+
+pub fn white() -> Color {
+    Color(ColorU::white())
+}
+
+pub fn red() -> Color {
+    color(0xff0000ff)
+}
+
+pub fn green() -> Color {
+    color(0x00ff00ff)
+}
+
+pub fn blue() -> Color {
+    color(0x0000ffff)
+}
+
+pub fn yellow() -> Color {
+    color(0xffff00ff)
+}
 
 impl Color {
     pub fn transparent_black() -> Self {
-        Self(ColorU::transparent_black())
+        transparent_black()
     }
 
     pub fn black() -> Self {
-        Self(ColorU::black())
+        black()
     }
 
     pub fn white() -> Self {
-        Self(ColorU::white())
+        white()
     }
 
     pub fn red() -> Self {
-        Self(ColorU::from_u32(0xff0000ff))
+        Color::from_u32(0xff0000ff)
     }
 
     pub fn green() -> Self {
-        Self(ColorU::from_u32(0x00ff00ff))
+        Color::from_u32(0x00ff00ff)
     }
 
     pub fn blue() -> Self {
-        Self(ColorU::from_u32(0x0000ffff))
+        Color::from_u32(0x0000ffff)
     }
 
     pub fn yellow() -> Self {
-        Self(ColorU::from_u32(0xffff00ff))
+        Color::from_u32(0xffff00ff)
     }
 
     pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
@@ -101,6 +141,12 @@ impl<'de> Deserialize<'de> for Color {
     }
 }
 
+impl From<u32> for Color {
+    fn from(value: u32) -> Self {
+        Self(ColorU::from_u32(value))
+    }
+}
+
 impl ToJson for Color {
     fn to_json(&self) -> serde_json::Value {
         json!(format!(

crates/gpui/src/elements.rs 🔗

@@ -1,6 +1,7 @@
 mod align;
 mod canvas;
 mod clipped;
+mod component;
 mod constrained_box;
 mod container;
 mod empty;
@@ -21,9 +22,9 @@ mod tooltip;
 mod uniform_list;
 
 pub use self::{
-    align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*,
-    keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*,
-    stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
+    align::*, canvas::*, component::*, constrained_box::*, container::*, empty::*, flex::*,
+    hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
+    resizable::*, stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
 };
 pub use crate::window::ChildView;
 
@@ -33,20 +34,29 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
-    WeakViewHandle, WindowContext,
+    json, Action, Entity, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
+    ViewContext, WeakViewHandle, WindowContext,
 };
 use anyhow::{anyhow, Result};
 use collections::HashMap;
 use core::panic;
 use json::ToJson;
 use smallvec::SmallVec;
-use std::{any::Any, borrow::Cow, mem, ops::Range};
+use std::{
+    any::{type_name, Any},
+    borrow::Cow,
+    mem,
+    ops::Range,
+};
 
-pub trait Element<V: View>: 'static {
+pub trait Element<V: 'static>: 'static {
     type LayoutState;
     type PaintState;
 
+    fn view_name(&self) -> &'static str {
+        type_name::<V>()
+    }
+
     fn layout(
         &mut self,
         constraint: SizeConstraint,
@@ -167,6 +177,20 @@ pub trait Element<V: View>: 'static {
         FlexItem::new(self.into_any()).float()
     }
 
+    fn with_dynamic_tooltip(
+        self,
+        tag: TypeTag,
+        id: usize,
+        text: impl Into<Cow<'static, str>>,
+        action: Option<Box<dyn Action>>,
+        style: TooltipStyle,
+        cx: &mut ViewContext<V>,
+    ) -> Tooltip<V>
+    where
+        Self: 'static + Sized,
+    {
+        Tooltip::new_dynamic(tag, id, text, action, style, self.into_any(), cx)
+    }
     fn with_tooltip<Tag: 'static>(
         self,
         id: usize,
@@ -181,16 +205,27 @@ pub trait Element<V: View>: 'static {
         Tooltip::new::<Tag>(id, text, action, style, self.into_any(), cx)
     }
 
-    fn resizable(
+    /// Uses the the given element to calculate resizes for the given tag
+    fn provide_resize_bounds<Tag: 'static>(self) -> BoundsProvider<V, Tag>
+    where
+        Self: 'static + Sized,
+    {
+        BoundsProvider::<_, Tag>::new(self.into_any())
+    }
+
+    /// Calls the given closure with the new size of the element whenever the
+    /// handle is dragged. This will be calculated in relation to the bounds
+    /// provided by the given tag
+    fn resizable<Tag: 'static>(
         self,
         side: HandleSide,
         size: f32,
-        on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
+        on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
     ) -> Resizable<V>
     where
         Self: 'static + Sized,
     {
-        Resizable::new(self.into_any(), side, size, on_resize)
+        Resizable::new::<Tag>(self.into_any(), side, size, on_resize)
     }
 
     fn mouse<Tag: 'static>(self, region_id: usize) -> MouseEventHandler<V>
@@ -199,13 +234,30 @@ pub trait Element<V: View>: 'static {
     {
         MouseEventHandler::for_child::<Tag>(self.into_any(), region_id)
     }
-}
 
-pub trait RenderElement {
-    fn render<V: View>(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
+    fn component(self) -> StatelessElementAdapter
+    where
+        Self: Sized,
+    {
+        StatelessElementAdapter::new(self.into_any())
+    }
+
+    fn stateful_component(self) -> StatefulElementAdapter<V>
+    where
+        Self: Sized,
+    {
+        StatefulElementAdapter::new(self.into_any())
+    }
+
+    fn styleable_component(self) -> StylableAdapter<StatelessElementAdapter>
+    where
+        Self: Sized,
+    {
+        StatelessElementAdapter::new(self.into_any()).stylable()
+    }
 }
 
-trait AnyElementState<V: View> {
+trait AnyElementState<V> {
     fn layout(
         &mut self,
         constraint: SizeConstraint,
@@ -219,7 +271,7 @@ trait AnyElementState<V: View> {
         origin: Vector2F,
         visible_bounds: RectF,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     );
 
     fn rect_for_text_range(
@@ -236,7 +288,7 @@ trait AnyElementState<V: View> {
     fn metadata(&self) -> Option<&dyn Any>;
 }
 
-enum ElementState<V: View, E: Element<V>> {
+enum ElementState<V: 'static, E: Element<V>> {
     Empty,
     Init {
         element: E,
@@ -257,7 +309,7 @@ enum ElementState<V: View, E: Element<V>> {
     },
 }
 
-impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
+impl<V, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
     fn layout(
         &mut self,
         constraint: SizeConstraint,
@@ -271,8 +323,16 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
             | ElementState::PostLayout { mut element, .. }
             | ElementState::PostPaint { mut element, .. } => {
                 let (size, layout) = element.layout(constraint, view, cx);
-                debug_assert!(size.x().is_finite());
-                debug_assert!(size.y().is_finite());
+                debug_assert!(
+                    size.x().is_finite(),
+                    "Element for {:?} had infinite x size after layout",
+                    element.view_name()
+                );
+                debug_assert!(
+                    size.y().is_finite(),
+                    "Element for {:?} had infinite y size after layout",
+                    element.view_name()
+                );
 
                 result = size;
                 ElementState::PostLayout {
@@ -292,7 +352,7 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
         origin: Vector2F,
         visible_bounds: RectF,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         *self = match mem::take(self) {
             ElementState::PostLayout {
@@ -431,18 +491,18 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
     }
 }
 
-impl<V: View, E: Element<V>> Default for ElementState<V, E> {
+impl<V, E: Element<V>> Default for ElementState<V, E> {
     fn default() -> Self {
         Self::Empty
     }
 }
 
-pub struct AnyElement<V: View> {
+pub struct AnyElement<V> {
     state: Box<dyn AnyElementState<V>>,
     name: Option<Cow<'static, str>>,
 }
 
-impl<V: View> AnyElement<V> {
+impl<V> AnyElement<V> {
     pub fn name(&self) -> Option<&str> {
         self.name.as_deref()
     }
@@ -468,7 +528,7 @@ impl<V: View> AnyElement<V> {
         origin: Vector2F,
         visible_bounds: RectF,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         self.state.paint(scene, origin, visible_bounds, view, cx);
     }
@@ -510,7 +570,7 @@ impl<V: View> AnyElement<V> {
     }
 }
 
-impl<V: View> Element<V> for AnyElement<V> {
+impl<V: 'static> Element<V> for AnyElement<V> {
     type LayoutState = ();
     type PaintState = ();
 
@@ -568,12 +628,18 @@ impl<V: View> Element<V> for AnyElement<V> {
     }
 }
 
-pub struct RootElement<V: View> {
+impl Entity for AnyElement<()> {
+    type Event = ();
+}
+
+// impl View for AnyElement<()> {}
+
+pub struct RootElement<V> {
     element: AnyElement<V>,
     view: WeakViewHandle<V>,
 }
 
-impl<V: View> RootElement<V> {
+impl<V> RootElement<V> {
     pub fn new(element: AnyElement<V>, view: WeakViewHandle<V>) -> Self {
         Self { element, view }
     }
@@ -641,7 +707,9 @@ impl<V: View> AnyRootElement for RootElement<V> {
             .ok_or_else(|| anyhow!("paint called on a root element for a dropped view"))?;
 
         view.update(cx, |view, cx| {
-            self.element.paint(scene, origin, visible_bounds, view, cx);
+            let mut cx = PaintContext::new(cx);
+            self.element
+                .paint(scene, origin, visible_bounds, view, &mut cx);
             Ok(())
         })
     }
@@ -681,7 +749,7 @@ impl<V: View> AnyRootElement for RootElement<V> {
     }
 }
 
-pub trait ParentElement<'a, V: View>: Extend<AnyElement<V>> + Sized {
+pub trait ParentElement<'a, V: 'static>: Extend<AnyElement<V>> + Sized {
     fn add_children<E: Element<V>>(&mut self, children: impl IntoIterator<Item = E>) {
         self.extend(children.into_iter().map(|child| child.into_any()));
     }
@@ -701,7 +769,12 @@ pub trait ParentElement<'a, V: View>: Extend<AnyElement<V>> + Sized {
     }
 }
 
-impl<'a, V: View, T> ParentElement<'a, V> for T where T: Extend<AnyElement<V>> {}
+impl<'a, V, T> ParentElement<'a, V> for T
+where
+    V: 'static,
+    T: Extend<AnyElement<V>>,
+{
+}
 
 pub fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
     if max_size.x().is_infinite() && max_size.y().is_infinite() {

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

@@ -1,18 +1,18 @@
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
     ViewContext,
 };
 use json::ToJson;
 
 use serde_json::json;
 
-pub struct Align<V: View> {
+pub struct Align<V> {
     child: AnyElement<V>,
     alignment: Vector2F,
 }
 
-impl<V: View> Align<V> {
+impl<V> Align<V> {
     pub fn new(child: AnyElement<V>) -> Self {
         Self {
             child,
@@ -41,7 +41,7 @@ impl<V: View> Align<V> {
     }
 }
 
-impl<V: View> Element<V> for Align<V> {
+impl<V: 'static> Element<V> for Align<V> {
     type LayoutState = ();
     type PaintState = ();
 

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

@@ -3,7 +3,7 @@ use std::marker::PhantomData;
 use super::Element;
 use crate::{
     json::{self, json},
-    PaintContext, SceneBuilder, View, ViewContext,
+    PaintContext, SceneBuilder, ViewContext,
 };
 use json::ToJson;
 use pathfinder_geometry::{
@@ -15,7 +15,6 @@ pub struct Canvas<V, F>(F, PhantomData<V>);
 
 impl<V, F> Canvas<V, F>
 where
-    V: View,
     F: FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
 {
     pub fn new(f: F) -> Self {
@@ -23,7 +22,7 @@ where
     }
 }
 
-impl<V: View, F> Element<V> for Canvas<V, F>
+impl<V: 'static, F> Element<V> for Canvas<V, F>
 where
     F: 'static + FnMut(&mut SceneBuilder, RectF, RectF, &mut V, &mut ViewContext<V>),
 {

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

@@ -4,21 +4,21 @@ use pathfinder_geometry::{rect::RectF, vector::Vector2F};
 use serde_json::json;
 
 use crate::{
-    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
     ViewContext,
 };
 
-pub struct Clipped<V: View> {
+pub struct Clipped<V> {
     child: AnyElement<V>,
 }
 
-impl<V: View> Clipped<V> {
+impl<V> Clipped<V> {
     pub fn new(child: AnyElement<V>) -> Self {
         Self { child }
     }
 }
 
-impl<V: View> Element<V> for Clipped<V> {
+impl<V: 'static> Element<V> for Clipped<V> {
     type LayoutState = ();
     type PaintState = ();
 

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

@@ -0,0 +1,345 @@
+use std::{any::Any, marker::PhantomData};
+
+use pathfinder_geometry::{rect::RectF, vector::Vector2F};
+
+use crate::{
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
+};
+
+use super::Empty;
+
+/// The core stateless component trait, simply rendering an element tree
+pub trait Component {
+    fn render<V: 'static>(self, cx: &mut ViewContext<V>) -> AnyElement<V>;
+
+    fn element<V: 'static>(self) -> ComponentAdapter<V, Self>
+    where
+        Self: Sized,
+    {
+        ComponentAdapter::new(self)
+    }
+
+    fn stylable(self) -> StylableAdapter<Self>
+    where
+        Self: Sized,
+    {
+        StylableAdapter::new(self)
+    }
+
+    fn stateful<V: 'static>(self) -> StatefulAdapter<Self, V>
+    where
+        Self: Sized,
+    {
+        StatefulAdapter::new(self)
+    }
+}
+
+/// Allows a a component's styles to be rebound in a simple way.
+pub trait Stylable: Component {
+    type Style: Clone;
+
+    fn with_style(self, style: Self::Style) -> Self;
+}
+
+/// This trait models the typestate pattern for a component's style,
+/// enforcing at compile time that a component is only usable after
+/// it has been styled while still allowing for late binding of the
+/// styling information
+pub trait SafeStylable {
+    type Style: Clone;
+    type Output: Component;
+
+    fn with_style(self, style: Self::Style) -> Self::Output;
+}
+
+/// All stylable components can trivially implement SafeStylable
+impl<C: Stylable> SafeStylable for C {
+    type Style = C::Style;
+
+    type Output = C;
+
+    fn with_style(self, style: Self::Style) -> Self::Output {
+        self.with_style(style)
+    }
+}
+
+/// Allows converting an unstylable component into a stylable one
+/// by using `()` as the style type
+pub struct StylableAdapter<C: Component> {
+    component: C,
+}
+
+impl<C: Component> StylableAdapter<C> {
+    pub fn new(component: C) -> Self {
+        Self { component }
+    }
+}
+
+impl<C: Component> SafeStylable for StylableAdapter<C> {
+    type Style = ();
+
+    type Output = C;
+
+    fn with_style(self, _: Self::Style) -> Self::Output {
+        self.component
+    }
+}
+
+/// This is a secondary trait for components that can be styled
+/// which rely on their view's state. This is useful for components that, for example,
+/// want to take click handler callbacks Unfortunately, the generic bound on the
+/// Component trait makes it incompatible with the stateless components above.
+// So let's just replicate them for now
+pub trait StatefulComponent<V: 'static> {
+    fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
+
+    fn element(self) -> ComponentAdapter<V, Self>
+    where
+        Self: Sized,
+    {
+        ComponentAdapter::new(self)
+    }
+
+    fn styleable(self) -> StatefulStylableAdapter<Self, V>
+    where
+        Self: Sized,
+    {
+        StatefulStylableAdapter::new(self)
+    }
+
+    fn stateless(self) -> StatelessElementAdapter
+    where
+        Self: Sized + 'static,
+    {
+        StatelessElementAdapter::new(self.element().into_any())
+    }
+}
+
+/// It is trivial to convert stateless components to stateful components, so lets
+/// do so en masse. Note that the reverse is impossible without a helper.
+impl<V: 'static, C: Component> StatefulComponent<V> for C {
+    fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
+        self.render(cx)
+    }
+}
+
+/// Same as stylable, but generic over a view type
+pub trait StatefulStylable<V: 'static>: StatefulComponent<V> {
+    type Style: Clone;
+
+    fn with_style(self, style: Self::Style) -> Self;
+}
+
+/// Same as SafeStylable, but generic over a view type
+pub trait StatefulSafeStylable<V: 'static> {
+    type Style: Clone;
+    type Output: StatefulComponent<V>;
+
+    fn with_style(self, style: Self::Style) -> Self::Output;
+}
+
+/// Converting from stateless to stateful
+impl<V: 'static, C: SafeStylable> StatefulSafeStylable<V> for C {
+    type Style = C::Style;
+
+    type Output = C::Output;
+
+    fn with_style(self, style: Self::Style) -> Self::Output {
+        self.with_style(style)
+    }
+}
+
+// A helper for converting stateless components into stateful ones
+pub struct StatefulAdapter<C, V> {
+    component: C,
+    phantom: std::marker::PhantomData<V>,
+}
+
+impl<C: Component, V: 'static> StatefulAdapter<C, V> {
+    pub fn new(component: C) -> Self {
+        Self {
+            component,
+            phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<C: Component, V: 'static> StatefulComponent<V> for StatefulAdapter<C, V> {
+    fn render(self, _: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
+        self.component.render(cx)
+    }
+}
+
+// A helper for converting stateful but style-less components into stylable ones
+// by using `()` as the style type
+pub struct StatefulStylableAdapter<C: StatefulComponent<V>, V: 'static> {
+    component: C,
+    phantom: std::marker::PhantomData<V>,
+}
+
+impl<C: StatefulComponent<V>, V: 'static> StatefulStylableAdapter<C, V> {
+    pub fn new(component: C) -> Self {
+        Self {
+            component,
+            phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<C: StatefulComponent<V>, V: 'static> StatefulSafeStylable<V>
+    for StatefulStylableAdapter<C, V>
+{
+    type Style = ();
+
+    type Output = C;
+
+    fn with_style(self, _: Self::Style) -> Self::Output {
+        self.component
+    }
+}
+
+/// A way of erasing the view generic from an element, useful
+/// for wrapping up an explicit element tree into stateless
+/// components
+pub struct StatelessElementAdapter {
+    element: Box<dyn Any>,
+}
+
+impl StatelessElementAdapter {
+    pub fn new<V: 'static>(element: AnyElement<V>) -> Self {
+        StatelessElementAdapter {
+            element: Box::new(element) as Box<dyn Any>,
+        }
+    }
+}
+
+impl Component for StatelessElementAdapter {
+    fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
+        *self
+            .element
+            .downcast::<AnyElement<V>>()
+            .expect("Don't move elements out of their view :(")
+    }
+}
+
+// For converting elements into stateful components
+pub struct StatefulElementAdapter<V: 'static> {
+    element: AnyElement<V>,
+    _phantom: std::marker::PhantomData<V>,
+}
+
+impl<V: 'static> StatefulElementAdapter<V> {
+    pub fn new(element: AnyElement<V>) -> Self {
+        Self {
+            element,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<V: 'static> StatefulComponent<V> for StatefulElementAdapter<V> {
+    fn render(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
+        self.element
+    }
+}
+
+/// A convenient shorthand for creating an empty component.
+impl Component for () {
+    fn render<V: 'static>(self, _: &mut ViewContext<V>) -> AnyElement<V> {
+        Empty::new().into_any()
+    }
+}
+
+impl Stylable for () {
+    type Style = ();
+
+    fn with_style(self, _: Self::Style) -> Self {
+        ()
+    }
+}
+
+// For converting components back into Elements
+pub struct ComponentAdapter<V: 'static, E> {
+    component: Option<E>,
+    element: Option<AnyElement<V>>,
+    phantom: PhantomData<V>,
+}
+
+impl<E, V: 'static> ComponentAdapter<V, E> {
+    pub fn new(e: E) -> Self {
+        Self {
+            component: Some(e),
+            element: None,
+            phantom: PhantomData,
+        }
+    }
+}
+
+impl<V: 'static, C: StatefulComponent<V> + 'static> Element<V> for ComponentAdapter<V, C> {
+    type LayoutState = ();
+
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: SizeConstraint,
+        view: &mut V,
+        cx: &mut LayoutContext<V>,
+    ) -> (Vector2F, Self::LayoutState) {
+        if self.element.is_none() {
+            let element = self
+                .component
+                .take()
+                .expect("Component can only be rendered once")
+                .render(view, cx.view_context());
+            self.element = Some(element);
+        }
+        let constraint = self.element.as_mut().unwrap().layout(constraint, view, cx);
+        (constraint, ())
+    }
+
+    fn paint(
+        &mut self,
+        scene: &mut SceneBuilder,
+        bounds: RectF,
+        visible_bounds: RectF,
+        _: &mut Self::LayoutState,
+        view: &mut V,
+        cx: &mut PaintContext<V>,
+    ) -> Self::PaintState {
+        self.element
+            .as_mut()
+            .expect("Layout should always be called before paint")
+            .paint(scene, bounds.origin(), visible_bounds, view, cx)
+    }
+
+    fn rect_for_text_range(
+        &self,
+        range_utf16: std::ops::Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        view: &V,
+        cx: &ViewContext<V>,
+    ) -> Option<RectF> {
+        self.element
+            .as_ref()
+            .and_then(|el| el.rect_for_text_range(range_utf16, view, cx))
+    }
+
+    fn debug(
+        &self,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        view: &V,
+        cx: &ViewContext<V>,
+    ) -> serde_json::Value {
+        serde_json::json!({
+            "type": "ComponentAdapter",
+            "component": std::any::type_name::<C>(),
+            "child": self.element.as_ref().map(|el| el.debug(view, cx)),
+        })
+    }
+}

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

@@ -5,21 +5,21 @@ use serde_json::json;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
     ViewContext,
 };
 
-pub struct ConstrainedBox<V: View> {
+pub struct ConstrainedBox<V> {
     child: AnyElement<V>,
     constraint: Constraint<V>,
 }
 
-pub enum Constraint<V: View> {
+pub enum Constraint<V> {
     Static(SizeConstraint),
     Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint>),
 }
 
-impl<V: View> ToJson for Constraint<V> {
+impl<V> ToJson for Constraint<V> {
     fn to_json(&self) -> serde_json::Value {
         match self {
             Constraint::Static(constraint) => constraint.to_json(),
@@ -28,7 +28,7 @@ impl<V: View> ToJson for Constraint<V> {
     }
 }
 
-impl<V: View> ConstrainedBox<V> {
+impl<V: 'static> ConstrainedBox<V> {
     pub fn new(child: impl Element<V>) -> Self {
         Self {
             child: child.into_any(),
@@ -132,7 +132,7 @@ impl<V: View> ConstrainedBox<V> {
     }
 }
 
-impl<V: View> Element<V> for ConstrainedBox<V> {
+impl<V: 'static> Element<V> for ConstrainedBox<V> {
     type LayoutState = ();
     type PaintState = ();
 

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

@@ -10,8 +10,7 @@ use crate::{
     json::ToJson,
     platform::CursorStyle,
     scene::{self, Border, CornerRadii, CursorRegion, Quad},
-    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -45,14 +44,22 @@ impl ContainerStyle {
             ..Default::default()
         }
     }
+
+    pub fn additional_length(&self) -> f32 {
+        self.padding.left
+            + self.padding.right
+            + self.border.width * 2.
+            + self.margin.left
+            + self.margin.right
+    }
 }
 
-pub struct Container<V: View> {
+pub struct Container<V> {
     child: AnyElement<V>,
     style: ContainerStyle,
 }
 
-impl<V: View> Container<V> {
+impl<V> Container<V> {
     pub fn new(child: AnyElement<V>) -> Self {
         Self {
             child,
@@ -199,7 +206,7 @@ impl<V: View> Container<V> {
     }
 }
 
-impl<V: View> Element<V> for Container<V> {
+impl<V: 'static> Element<V> for Container<V> {
     type LayoutState = ();
     type PaintState = ();
 
@@ -350,8 +357,8 @@ impl ToJson for ContainerStyle {
 #[derive(Clone, Copy, Debug, Default, JsonSchema)]
 pub struct Margin {
     pub top: f32,
-    pub left: f32,
     pub bottom: f32,
+    pub left: f32,
     pub right: f32,
 }
 

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

@@ -6,7 +6,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
+    LayoutContext, PaintContext, SceneBuilder, ViewContext,
 };
 use crate::{Element, SizeConstraint};
 
@@ -26,7 +26,7 @@ impl Empty {
     }
 }
 
-impl<V: View> Element<V> for Empty {
+impl<V: 'static> Element<V> for Empty {
     type LayoutState = ();
     type PaintState = ();
 

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

@@ -2,18 +2,18 @@ use std::ops::Range;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
     ViewContext,
 };
 use serde_json::json;
 
-pub struct Expanded<V: View> {
+pub struct Expanded<V> {
     child: AnyElement<V>,
     full_width: bool,
     full_height: bool,
 }
 
-impl<V: View> Expanded<V> {
+impl<V: 'static> Expanded<V> {
     pub fn new(child: impl Element<V>) -> Self {
         Self {
             child: child.into_any(),
@@ -35,7 +35,7 @@ impl<V: View> Expanded<V> {
     }
 }
 
-impl<V: View> Element<V> for Expanded<V> {
+impl<V: 'static> Element<V> for Expanded<V> {
     type LayoutState = ();
     type PaintState = ();
 

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

@@ -3,7 +3,7 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
 use crate::{
     json::{self, ToJson, Value},
     AnyElement, Axis, Element, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder,
-    SizeConstraint, Vector2FExt, View, ViewContext,
+    SizeConstraint, Vector2FExt, ViewContext,
 };
 use pathfinder_geometry::{
     rect::RectF,
@@ -17,20 +17,22 @@ struct ScrollState {
     scroll_position: Cell<f32>,
 }
 
-pub struct Flex<V: View> {
+pub struct Flex<V> {
     axis: Axis,
     children: Vec<AnyElement<V>>,
     scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
     child_alignment: f32,
+    spacing: f32,
 }
 
-impl<V: View> Flex<V> {
+impl<V: 'static> Flex<V> {
     pub fn new(axis: Axis) -> Self {
         Self {
             axis,
             children: Default::default(),
             scroll_state: None,
             child_alignment: -1.,
+            spacing: 0.,
         }
     }
 
@@ -51,6 +53,11 @@ impl<V: View> Flex<V> {
         self
     }
 
+    pub fn with_spacing(mut self, spacing: f32) -> Self {
+        self.spacing = spacing;
+        self
+    }
+
     pub fn scrollable<Tag>(
         mut self,
         element_id: usize,
@@ -81,7 +88,7 @@ impl<V: View> Flex<V> {
         cx: &mut LayoutContext<V>,
     ) {
         let cross_axis = self.axis.invert();
-        for child in &mut self.children {
+        for child in self.children.iter_mut() {
             if let Some(metadata) = child.metadata::<FlexParentData>() {
                 if let Some((flex, expanded)) = metadata.flex {
                     if expanded != layout_expanded {
@@ -115,13 +122,13 @@ impl<V: View> Flex<V> {
     }
 }
 
-impl<V: View> Extend<AnyElement<V>> for Flex<V> {
+impl<V> Extend<AnyElement<V>> for Flex<V> {
     fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
         self.children.extend(children);
     }
 }
 
-impl<V: View> Element<V> for Flex<V> {
+impl<V: 'static> Element<V> for Flex<V> {
     type LayoutState = f32;
     type PaintState = ();
 
@@ -132,12 +139,12 @@ impl<V: View> Element<V> for Flex<V> {
         cx: &mut LayoutContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         let mut total_flex = None;
-        let mut fixed_space = 0.0;
+        let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing;
         let mut contains_float = false;
 
         let cross_axis = self.axis.invert();
         let mut cross_axis_max: f32 = 0.0;
-        for child in &mut self.children {
+        for child in self.children.iter_mut() {
             let metadata = child.metadata::<FlexParentData>();
             contains_float |= metadata.map_or(false, |metadata| metadata.float);
 
@@ -315,7 +322,7 @@ impl<V: View> Element<V> for Flex<V> {
             }
         }
 
-        for child in &mut self.children {
+        for child in self.children.iter_mut() {
             if remaining_space > 0. {
                 if let Some(metadata) = child.metadata::<FlexParentData>() {
                     if metadata.float {
@@ -354,8 +361,8 @@ impl<V: View> Element<V> for Flex<V> {
             child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
 
             match self.axis {
-                Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
-                Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
+                Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0),
+                Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing),
             }
         }
 
@@ -401,12 +408,12 @@ struct FlexParentData {
     float: bool,
 }
 
-pub struct FlexItem<V: View> {
+pub struct FlexItem<V> {
     metadata: FlexParentData,
     child: AnyElement<V>,
 }
 
-impl<V: View> FlexItem<V> {
+impl<V: 'static> FlexItem<V> {
     pub fn new(child: impl Element<V>) -> Self {
         FlexItem {
             metadata: FlexParentData {
@@ -428,7 +435,7 @@ impl<V: View> FlexItem<V> {
     }
 }
 
-impl<V: View> Element<V> for FlexItem<V> {
+impl<V: 'static> Element<V> for FlexItem<V> {
     type LayoutState = ();
     type PaintState = ();
 

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

@@ -3,16 +3,15 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
-    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
 };
 
-pub struct Hook<V: View> {
+pub struct Hook<V> {
     child: AnyElement<V>,
     after_layout: Option<Box<dyn FnMut(Vector2F, &mut ViewContext<V>)>>,
 }
 
-impl<V: View> Hook<V> {
+impl<V: 'static> Hook<V> {
     pub fn new(child: impl Element<V>) -> Self {
         Self {
             child: child.into_any(),
@@ -29,7 +28,7 @@ impl<V: View> Hook<V> {
     }
 }
 
-impl<V: View> Element<V> for Hook<V> {
+impl<V: 'static> Element<V> for Hook<V> {
     type LayoutState = ();
     type PaintState = ();
 

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

@@ -6,7 +6,7 @@ use crate::{
     },
     json::{json, ToJson},
     scene, Border, Element, ImageData, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    View, ViewContext,
+    ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -57,7 +57,7 @@ impl Image {
     }
 }
 
-impl<V: View> Element<V> for Image {
+impl<V: 'static> Element<V> for Image {
     type LayoutState = Option<Arc<ImageData>>;
     type PaintState = ();
 

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

@@ -31,7 +31,7 @@ impl KeystrokeLabel {
     }
 }
 
-impl<V: View> Element<V> for KeystrokeLabel {
+impl<V: 'static> Element<V> for KeystrokeLabel {
     type LayoutState = AnyElement<V>;
     type PaintState = ();
 

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

@@ -8,7 +8,7 @@ use crate::{
     },
     json::{ToJson, Value},
     text_layout::{Line, RunStyle},
-    Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -128,7 +128,7 @@ impl Label {
     }
 }
 
-impl<V: View> Element<V> for Label {
+impl<V: 'static> Element<V> for Label {
     type LayoutState = Line;
     type PaintState = ();
 

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

@@ -5,16 +5,16 @@ use crate::{
     },
     json::json,
     AnyElement, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, SizeConstraint,
-    View, ViewContext,
+    ViewContext,
 };
 use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc};
 use sum_tree::{Bias, SumTree};
 
-pub struct List<V: View> {
+pub struct List<V> {
     state: ListState<V>,
 }
 
-pub struct ListState<V: View>(Rc<RefCell<StateInner<V>>>);
+pub struct ListState<V>(Rc<RefCell<StateInner<V>>>);
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum Orientation {
@@ -22,7 +22,7 @@ pub enum Orientation {
     Bottom,
 }
 
-struct StateInner<V: View> {
+struct StateInner<V> {
     last_layout_width: Option<f32>,
     render_item: Box<dyn FnMut(&mut V, usize, &mut ViewContext<V>) -> AnyElement<V>>,
     rendered_range: Range<usize>,
@@ -40,13 +40,13 @@ pub struct ListOffset {
     pub offset_in_item: f32,
 }
 
-enum ListItem<V: View> {
+enum ListItem<V> {
     Unrendered,
     Rendered(Rc<RefCell<AnyElement<V>>>),
     Removed(f32),
 }
 
-impl<V: View> Clone for ListItem<V> {
+impl<V> Clone for ListItem<V> {
     fn clone(&self) -> Self {
         match self {
             Self::Unrendered => Self::Unrendered,
@@ -56,7 +56,7 @@ impl<V: View> Clone for ListItem<V> {
     }
 }
 
-impl<V: View> Debug for ListItem<V> {
+impl<V> Debug for ListItem<V> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             Self::Unrendered => write!(f, "Unrendered"),
@@ -86,13 +86,13 @@ struct UnrenderedCount(usize);
 #[derive(Clone, Debug, Default)]
 struct Height(f32);
 
-impl<V: View> List<V> {
+impl<V> List<V> {
     pub fn new(state: ListState<V>) -> Self {
         Self { state }
     }
 }
 
-impl<V: View> Element<V> for List<V> {
+impl<V: 'static> Element<V> for List<V> {
     type LayoutState = ListOffset;
     type PaintState = ();
 
@@ -347,7 +347,7 @@ impl<V: View> Element<V> for List<V> {
     }
 }
 
-impl<V: View> ListState<V> {
+impl<V: 'static> ListState<V> {
     pub fn new<D, F>(
         element_count: usize,
         orientation: Orientation,
@@ -440,13 +440,13 @@ impl<V: View> ListState<V> {
     }
 }
 
-impl<V: View> Clone for ListState<V> {
+impl<V> Clone for ListState<V> {
     fn clone(&self) -> Self {
         Self(self.0.clone())
     }
 }
 
-impl<V: View> StateInner<V> {
+impl<V: 'static> StateInner<V> {
     fn render_item(
         &mut self,
         ix: usize,
@@ -560,7 +560,7 @@ impl<V: View> StateInner<V> {
     }
 }
 
-impl<V: View> ListItem<V> {
+impl<V> ListItem<V> {
     fn remove(&self) -> Self {
         match self {
             ListItem::Unrendered => ListItem::Unrendered,
@@ -570,7 +570,7 @@ impl<V: View> ListItem<V> {
     }
 }
 
-impl<V: View> sum_tree::Item for ListItem<V> {
+impl<V> sum_tree::Item for ListItem<V> {
     type Summary = ListItemSummary;
 
     fn summary(&self) -> Self::Summary {
@@ -944,7 +944,7 @@ mod tests {
         type Event = ();
     }
 
-    impl View for TestView {
+    impl crate::View for TestView {
         fn ui_name() -> &'static str {
             "TestView"
         }
@@ -968,7 +968,7 @@ mod tests {
         }
     }
 
-    impl<V: View> Element<V> for TestElement {
+    impl<V: 'static> Element<V> for TestElement {
         type LayoutState = ();
         type PaintState = ();
 

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

@@ -11,12 +11,12 @@ use crate::{
         MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
     },
     AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext,
-    SceneBuilder, SizeConstraint, TypeTag, View, ViewContext,
+    SceneBuilder, SizeConstraint, TypeTag, ViewContext,
 };
 use serde_json::json;
 use std::ops::Range;
 
-pub struct MouseEventHandler<V: View> {
+pub struct MouseEventHandler<V: 'static> {
     child: AnyElement<V>,
     region_id: usize,
     cursor_style: Option<CursorStyle>,
@@ -31,7 +31,7 @@ pub struct MouseEventHandler<V: View> {
 
 /// Element which provides a render_child callback with a MouseState and paints a mouse
 /// region under (or above) it for easy mouse event handling.
-impl<V: View> MouseEventHandler<V> {
+impl<V: 'static> MouseEventHandler<V> {
     pub fn for_child<Tag: 'static>(child: impl Element<V>, region_id: usize) -> Self {
         Self {
             child: child.into_any(),
@@ -267,7 +267,7 @@ impl<V: View> MouseEventHandler<V> {
     }
 }
 
-impl<V: View> Element<V> for MouseEventHandler<V> {
+impl<V: 'static> Element<V> for MouseEventHandler<V> {
     type LayoutState = ();
     type PaintState = ();
 

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

@@ -4,11 +4,11 @@ use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
     AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
-    SizeConstraint, View, ViewContext,
+    SizeConstraint, ViewContext,
 };
 use serde_json::json;
 
-pub struct Overlay<V: View> {
+pub struct Overlay<V> {
     child: AnyElement<V>,
     anchor_position: Option<Vector2F>,
     anchor_corner: AnchorCorner,
@@ -73,7 +73,7 @@ impl AnchorCorner {
     }
 }
 
-impl<V: View> Overlay<V> {
+impl<V: 'static> Overlay<V> {
     pub fn new(child: impl Element<V>) -> Self {
         Self {
             child: child.into_any(),
@@ -117,7 +117,7 @@ impl<V: View> Overlay<V> {
     }
 }
 
-impl<V: View> Element<V> for Overlay<V> {
+impl<V: 'static> Element<V> for Overlay<V> {
     type LayoutState = Vector2F;
     type PaintState = ();
 

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

@@ -1,14 +1,14 @@
 use std::{cell::RefCell, rc::Rc};
 
+use collections::HashMap;
 use pathfinder_geometry::vector::{vec2f, Vector2F};
 use serde_json::json;
 
 use crate::{
     geometry::rect::RectF,
     platform::{CursorStyle, MouseButton},
-    scene::MouseDrag,
-    AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
-    SizeConstraint, View, ViewContext,
+    AnyElement, AppContext, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
+    SizeConstraint, TypeTag, View, ViewContext,
 };
 
 #[derive(Copy, Clone, Debug)]
@@ -27,15 +27,6 @@ impl HandleSide {
         }
     }
 
-    /// 'before' is in reference to the standard english document ordering of left-to-right
-    /// then top-to-bottom
-    fn before_content(self) -> bool {
-        match self {
-            HandleSide::Left | HandleSide::Top => true,
-            HandleSide::Right | HandleSide::Bottom => false,
-        }
-    }
-
     fn relevant_component(&self, vector: Vector2F) -> f32 {
         match self.axis() {
             Axis::Horizontal => vector.x(),
@@ -43,14 +34,6 @@ impl HandleSide {
         }
     }
 
-    fn compute_delta(&self, e: MouseDrag) -> f32 {
-        if self.before_content() {
-            self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
-        } else {
-            self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
-        }
-    }
-
     fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
         match self {
             HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
@@ -69,21 +52,29 @@ impl HandleSide {
     }
 }
 
-pub struct Resizable<V: View> {
+fn get_bounds(tag: TypeTag, cx: &AppContext) -> Option<&(RectF, RectF)>
+where
+{
+    cx.optional_global::<ProviderMap>()
+        .and_then(|map| map.0.get(&tag))
+}
+
+pub struct Resizable<V: 'static> {
     child: AnyElement<V>,
+    tag: TypeTag,
     handle_side: HandleSide,
     handle_size: f32,
-    on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
+    on_resize: Rc<RefCell<dyn FnMut(&mut V, Option<f32>, &mut ViewContext<V>)>>,
 }
 
 const DEFAULT_HANDLE_SIZE: f32 = 4.0;
 
-impl<V: View> Resizable<V> {
-    pub fn new(
+impl<V: 'static> Resizable<V> {
+    pub fn new<Tag: 'static>(
         child: AnyElement<V>,
         handle_side: HandleSide,
         size: f32,
-        on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
+        on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
     ) -> Self {
         let child = match handle_side.axis() {
             Axis::Horizontal => child.constrained().with_max_width(size),
@@ -94,6 +85,7 @@ impl<V: View> Resizable<V> {
         Self {
             child,
             handle_side,
+            tag: TypeTag::new::<Tag>(),
             handle_size: DEFAULT_HANDLE_SIZE,
             on_resize: Rc::new(RefCell::new(on_resize)),
         }
@@ -105,7 +97,7 @@ impl<V: View> Resizable<V> {
     }
 }
 
-impl<V: View> Element<V> for Resizable<V> {
+impl<V: 'static> Element<V> for Resizable<V> {
     type LayoutState = SizeConstraint;
     type PaintState = ();
 
@@ -139,6 +131,14 @@ impl<V: View> Element<V> for Resizable<V> {
                 handle_region,
             )
             .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
+            .on_click(MouseButton::Left, {
+                let on_resize = self.on_resize.clone();
+                move |click, v, cx| {
+                    if click.click_count == 2 {
+                        on_resize.borrow_mut()(v, None, cx);
+                    }
+                }
+            })
             .on_drag(MouseButton::Left, {
                 let bounds = bounds.clone();
                 let side = self.handle_side;
@@ -146,16 +146,30 @@ impl<V: View> Element<V> for Resizable<V> {
                 let min_size = side.relevant_component(constraint.min);
                 let max_size = side.relevant_component(constraint.max);
                 let on_resize = self.on_resize.clone();
+                let tag = self.tag;
                 move |event, view: &mut V, cx| {
                     if event.end {
                         return;
                     }
-                    let new_size = min_size
-                        .max(prev_size + side.compute_delta(event))
-                        .min(max_size)
-                        .round();
+
+                    let Some((bounds, _)) = get_bounds(tag, cx) else {
+                        return;
+                    };
+
+                    let new_size_raw = match side {
+                        // Handle on top side of element => Element is on bottom
+                        HandleSide::Top => bounds.height() + bounds.origin_y() - event.position.y(),
+                        // Handle on right side of element => Element is on left
+                        HandleSide::Right => event.position.x() - bounds.lower_left().x(),
+                        // Handle on left side of element => Element is on the right
+                        HandleSide::Left => bounds.width() + bounds.origin_x() - event.position.x(),
+                        // Handle on bottom side of element => Element is on the top
+                        HandleSide::Bottom => event.position.y() - bounds.lower_left().y(),
+                    };
+
+                    let new_size = min_size.max(new_size_raw).min(max_size).round();
                     if new_size != prev_size {
-                        on_resize.borrow_mut()(view, new_size, cx);
+                        on_resize.borrow_mut()(view, Some(new_size), cx);
                     }
                 }
             }),
@@ -201,3 +215,80 @@ impl<V: View> Element<V> for Resizable<V> {
         })
     }
 }
+
+#[derive(Debug, Default)]
+struct ProviderMap(HashMap<TypeTag, (RectF, RectF)>);
+
+pub struct BoundsProvider<V: 'static, P> {
+    child: AnyElement<V>,
+    phantom: std::marker::PhantomData<P>,
+}
+
+impl<V: 'static, P: 'static> BoundsProvider<V, P> {
+    pub fn new(child: AnyElement<V>) -> Self {
+        Self {
+            child,
+            phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<V: View, P: 'static> Element<V> for BoundsProvider<V, P> {
+    type LayoutState = ();
+
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: crate::SizeConstraint,
+        view: &mut V,
+        cx: &mut crate::LayoutContext<V>,
+    ) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) {
+        (self.child.layout(constraint, view, cx), ())
+    }
+
+    fn paint(
+        &mut self,
+        scene: &mut crate::SceneBuilder,
+        bounds: pathfinder_geometry::rect::RectF,
+        visible_bounds: pathfinder_geometry::rect::RectF,
+        _: &mut Self::LayoutState,
+        view: &mut V,
+        cx: &mut crate::PaintContext<V>,
+    ) -> Self::PaintState {
+        cx.update_default_global::<ProviderMap, _, _>(|map, _| {
+            map.0.insert(TypeTag::new::<P>(), (bounds, visible_bounds));
+        });
+
+        self.child
+            .paint(scene, bounds.origin(), visible_bounds, view, cx)
+    }
+
+    fn rect_for_text_range(
+        &self,
+        range_utf16: std::ops::Range<usize>,
+        _: pathfinder_geometry::rect::RectF,
+        _: pathfinder_geometry::rect::RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        view: &V,
+        cx: &crate::ViewContext<V>,
+    ) -> Option<pathfinder_geometry::rect::RectF> {
+        self.child.rect_for_text_range(range_utf16, view, cx)
+    }
+
+    fn debug(
+        &self,
+        _: pathfinder_geometry::rect::RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        view: &V,
+        cx: &crate::ViewContext<V>,
+    ) -> serde_json::Value {
+        serde_json::json!({
+            "type": "Provider",
+            "providing": format!("{:?}", TypeTag::new::<P>()),
+            "child": self.child.debug(view, cx),
+        })
+    }
+}

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

@@ -3,17 +3,16 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::{self, json, ToJson},
-    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
 };
 
 /// Element which renders it's children in a stack on top of each other.
 /// The first child determines the size of the others.
-pub struct Stack<V: View> {
+pub struct Stack<V> {
     children: Vec<AnyElement<V>>,
 }
 
-impl<V: View> Default for Stack<V> {
+impl<V> Default for Stack<V> {
     fn default() -> Self {
         Self {
             children: Vec::new(),
@@ -21,13 +20,13 @@ impl<V: View> Default for Stack<V> {
     }
 }
 
-impl<V: View> Stack<V> {
+impl<V> Stack<V> {
     pub fn new() -> Self {
         Self::default()
     }
 }
 
-impl<V: View> Element<V> for Stack<V> {
+impl<V: 'static> Element<V> for Stack<V> {
     type LayoutState = ();
     type PaintState = ();
 
@@ -99,7 +98,7 @@ impl<V: View> Element<V> for Stack<V> {
     }
 }
 
-impl<V: View> Extend<AnyElement<V>> for Stack<V> {
+impl<V> Extend<AnyElement<V>> for Stack<V> {
     fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
         self.children.extend(children)
     }

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

@@ -7,7 +7,7 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    scene, Element, LayoutContext, SceneBuilder, SizeConstraint, ViewContext,
 };
 use schemars::JsonSchema;
 use serde_derive::Deserialize;
@@ -27,7 +27,7 @@ impl Svg {
         }
     }
 
-    pub fn for_style<V: View>(style: SvgStyle) -> impl Element<V> {
+    pub fn for_style<V: 'static>(style: SvgStyle) -> impl Element<V> {
         Self::new(style.asset)
             .with_color(style.color)
             .constrained()
@@ -41,7 +41,7 @@ impl Svg {
     }
 }
 
-impl<V: View> Element<V> for Svg {
+impl<V: 'static> Element<V> for Svg {
     type LayoutState = Option<usvg::Tree>;
     type PaintState = ();
 

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

@@ -8,7 +8,7 @@ use crate::{
     json::{ToJson, Value},
     text_layout::{Line, RunStyle, ShapedBoundary},
     AppContext, Element, FontCache, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    TextLayoutCache, View, ViewContext,
+    TextLayoutCache, ViewContext,
 };
 use log::warn;
 use serde_json::json;
@@ -70,7 +70,7 @@ impl Text {
     }
 }
 
-impl<V: View> Element<V> for Text {
+impl<V: 'static> Element<V> for Text {
     type LayoutState = LayoutState;
     type PaintState = ();
 
@@ -338,7 +338,7 @@ impl<V: View> Element<V> for Text {
 }
 
 /// Perform text layout on a series of highlighted chunks of text.
-fn layout_highlighted_chunks<'a>(
+pub fn layout_highlighted_chunks<'a>(
     chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
     text_style: &TextStyle,
     text_layout_cache: &TextLayoutCache,

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

@@ -7,7 +7,7 @@ use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
     Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
-    Task, View, ViewContext,
+    Task, TypeTag, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -22,7 +22,7 @@ use util::ResultExt;
 
 const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 
-pub struct Tooltip<V: View> {
+pub struct Tooltip<V> {
     child: AnyElement<V>,
     tooltip: Option<AnyElement<V>>,
     _state: ElementStateHandle<Rc<TooltipState>>,
@@ -52,7 +52,7 @@ pub struct KeystrokeStyle {
     text: TextStyle,
 }
 
-impl<V: View> Tooltip<V> {
+impl<V: 'static> Tooltip<V> {
     pub fn new<Tag: 'static>(
         id: usize,
         text: impl Into<Cow<'static, str>>,
@@ -61,11 +61,23 @@ impl<V: View> Tooltip<V> {
         child: AnyElement<V>,
         cx: &mut ViewContext<V>,
     ) -> Self {
-        struct ElementState<Tag>(Tag);
-        struct MouseEventHandlerState<Tag>(Tag);
+        Self::new_dynamic(TypeTag::new::<Tag>(), id, text, action, style, child, cx)
+    }
+
+    pub fn new_dynamic(
+        mut tag: TypeTag,
+        id: usize,
+        text: impl Into<Cow<'static, str>>,
+        action: Option<Box<dyn Action>>,
+        style: TooltipStyle,
+        child: AnyElement<V>,
+        cx: &mut ViewContext<V>,
+    ) -> Self {
+        tag = tag.compose(TypeTag::new::<Self>());
+
         let focused_view_id = cx.focused_view_id();
 
-        let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
+        let state_handle = cx.default_element_state_dynamic::<Rc<TooltipState>>(tag, id);
         let state = state_handle.read(cx).clone();
         let text = text.into();
 
@@ -95,7 +107,7 @@ impl<V: View> Tooltip<V> {
         } else {
             None
         };
-        let child = MouseEventHandler::new::<MouseEventHandlerState<Tag>, _>(id, cx, |_, _| child)
+        let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child)
             .on_hover(move |e, _, cx| {
                 let position = e.position;
                 if e.started {
@@ -169,7 +181,7 @@ impl<V: View> Tooltip<V> {
     }
 }
 
-impl<V: View> Element<V> for Tooltip<V> {
+impl<V: 'static> Element<V> for Tooltip<V> {
     type LayoutState = ();
     type PaintState = ();
 

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

@@ -6,7 +6,7 @@ use crate::{
     },
     json::{self, json},
     platform::ScrollWheelEvent,
-    AnyElement, LayoutContext, MouseRegion, PaintContext, SceneBuilder, View, ViewContext,
+    AnyElement, LayoutContext, MouseRegion, PaintContext, SceneBuilder, ViewContext,
 };
 use json::ToJson;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -36,13 +36,13 @@ struct StateInner {
     scroll_to: Option<ScrollTarget>,
 }
 
-pub struct UniformListLayoutState<V: View> {
+pub struct UniformListLayoutState<V> {
     scroll_max: f32,
     item_height: f32,
     items: Vec<AnyElement<V>>,
 }
 
-pub struct UniformList<V: View> {
+pub struct UniformList<V> {
     state: UniformListState,
     item_count: usize,
     #[allow(clippy::type_complexity)]
@@ -53,7 +53,7 @@ pub struct UniformList<V: View> {
     view_id: usize,
 }
 
-impl<V: View> UniformList<V> {
+impl<V: 'static> UniformList<V> {
     pub fn new<F>(
         state: UniformListState,
         item_count: usize,
@@ -61,7 +61,6 @@ impl<V: View> UniformList<V> {
         append_items: F,
     ) -> Self
     where
-        V: View,
         F: 'static + Fn(&mut V, Range<usize>, &mut Vec<AnyElement<V>>, &mut ViewContext<V>),
     {
         Self {
@@ -151,7 +150,7 @@ impl<V: View> UniformList<V> {
     }
 }
 
-impl<V: View> Element<V> for UniformList<V> {
+impl<V: 'static> Element<V> for UniformList<V> {
     type LayoutState = UniformListLayoutState<V>;
     type PaintState = ();
 

crates/gpui/src/fonts.rs 🔗

@@ -11,6 +11,7 @@ pub use font_kit::{
     properties::{Properties, Stretch, Style, Weight},
 };
 use ordered_float::OrderedFloat;
+use refineable::Refineable;
 use schemars::JsonSchema;
 use serde::{de, Deserialize, Serialize};
 use serde_json::Value;
@@ -59,7 +60,7 @@ pub struct Features {
     pub zero: Option<bool>,
 }
 
-#[derive(Clone, Debug, JsonSchema)]
+#[derive(Clone, Debug, JsonSchema, Refineable)]
 pub struct TextStyle {
     pub color: Color,
     pub font_family_name: Arc<str>,
@@ -69,6 +70,7 @@ pub struct TextStyle {
     #[schemars(with = "PropertiesDef")]
     pub font_properties: Properties,
     pub underline: Underline,
+    pub soft_wrap: bool,
 }
 
 impl TextStyle {
@@ -90,20 +92,11 @@ impl TextStyle {
             font_size: refinement.font_size.unwrap_or(self.font_size),
             font_properties: refinement.font_properties.unwrap_or(self.font_properties),
             underline: refinement.underline.unwrap_or(self.underline),
+            soft_wrap: refinement.soft_wrap.unwrap_or(self.soft_wrap),
         }
     }
 }
 
-pub struct TextStyleRefinement {
-    pub color: Option<Color>,
-    pub font_family_name: Option<Arc<str>>,
-    pub font_family_id: Option<FamilyId>,
-    pub font_id: Option<FontId>,
-    pub font_size: Option<f32>,
-    pub font_properties: Option<Properties>,
-    pub underline: Option<Underline>,
-}
-
 #[derive(JsonSchema)]
 #[serde(remote = "Properties")]
 pub struct PropertiesDef {
@@ -222,9 +215,31 @@ impl TextStyle {
             font_size,
             font_properties,
             underline,
+            soft_wrap: false,
         })
     }
 
+    pub fn default(font_cache: &FontCache) -> Self {
+        let font_family_id = font_cache.known_existing_family();
+        let font_id = font_cache
+            .select_font(font_family_id, &Default::default())
+            .expect("did not have any font in system-provided family");
+        let font_family_name = font_cache
+            .family_name(font_family_id)
+            .expect("we loaded this family from the font cache, so this should work");
+
+        Self {
+            color: Color::default(),
+            font_family_name,
+            font_family_id,
+            font_id,
+            font_size: 14.,
+            font_properties: Default::default(),
+            underline: Default::default(),
+            soft_wrap: true,
+        }
+    }
+
     pub fn with_font_size(mut self, font_size: f32) -> Self {
         self.font_size = font_size;
         self
@@ -352,24 +367,7 @@ impl Default for TextStyle {
             let font_cache = font_cache
                 .as_ref()
                 .expect("TextStyle::default can only be called within a call to with_font_cache");
-
-            let font_family_id = font_cache.known_existing_family();
-            let font_id = font_cache
-                .select_font(font_family_id, &Default::default())
-                .expect("did not have any font in system-provided family");
-            let font_family_name = font_cache
-                .family_name(font_family_id)
-                .expect("we loaded this family from the font cache, so this should work");
-
-            Self {
-                color: Default::default(),
-                font_family_name,
-                font_family_id,
-                font_id,
-                font_size: 14.,
-                font_properties: Default::default(),
-                underline: Default::default(),
-            }
+            Self::default(font_cache)
         })
     }
 }

crates/gpui/src/geometry.rs 🔗

@@ -2,6 +2,7 @@ use super::scene::{Path, PathVertex};
 use crate::{color::Color, json::ToJson};
 pub use pathfinder_geometry::*;
 use rect::RectF;
+use refineable::Refineable;
 use serde::{Deserialize, Deserializer};
 use serde_json::json;
 use vector::{vec2f, Vector2F};
@@ -131,3 +132,258 @@ impl ToJson for RectF {
         json!({"origin": self.origin().to_json(), "size": self.size().to_json()})
     }
 }
+
+#[derive(Refineable)]
+pub struct Point<T: Clone + Default> {
+    pub x: T,
+    pub y: T,
+}
+
+impl<T: Clone + Default> Clone for Point<T> {
+    fn clone(&self) -> Self {
+        Self {
+            x: self.x.clone(),
+            y: self.y.clone(),
+        }
+    }
+}
+
+impl<T: Clone + Default> Into<taffy::geometry::Point<T>> for Point<T> {
+    fn into(self) -> taffy::geometry::Point<T> {
+        taffy::geometry::Point {
+            x: self.x,
+            y: self.y,
+        }
+    }
+}
+
+#[derive(Clone, Refineable)]
+pub struct Size<T: Clone + Default> {
+    pub width: T,
+    pub height: T,
+}
+
+impl<S, T: Clone + Default> From<taffy::geometry::Size<S>> for Size<T>
+where
+    S: Into<T>,
+{
+    fn from(value: taffy::geometry::Size<S>) -> Self {
+        Self {
+            width: value.width.into(),
+            height: value.height.into(),
+        }
+    }
+}
+
+impl<S, T: Clone + Default> Into<taffy::geometry::Size<S>> for Size<T>
+where
+    T: Into<S>,
+{
+    fn into(self) -> taffy::geometry::Size<S> {
+        taffy::geometry::Size {
+            width: self.width.into(),
+            height: self.height.into(),
+        }
+    }
+}
+
+impl Size<DefiniteLength> {
+    pub fn zero() -> Self {
+        Self {
+            width: pixels(0.),
+            height: pixels(0.),
+        }
+    }
+
+    pub fn to_taffy(&self, rem_size: f32) -> taffy::geometry::Size<taffy::style::LengthPercentage> {
+        taffy::geometry::Size {
+            width: self.width.to_taffy(rem_size),
+            height: self.height.to_taffy(rem_size),
+        }
+    }
+}
+
+impl Size<Length> {
+    pub fn auto() -> Self {
+        Self {
+            width: Length::Auto,
+            height: Length::Auto,
+        }
+    }
+
+    pub fn to_taffy<T: From<taffy::prelude::LengthPercentageAuto>>(
+        &self,
+        rem_size: f32,
+    ) -> taffy::geometry::Size<T> {
+        taffy::geometry::Size {
+            width: self.width.to_taffy(rem_size).into(),
+            height: self.height.to_taffy(rem_size).into(),
+        }
+    }
+}
+
+#[derive(Clone, Default, Refineable)]
+pub struct Edges<T: Clone + Default> {
+    pub top: T,
+    pub right: T,
+    pub bottom: T,
+    pub left: T,
+}
+
+impl Edges<DefiniteLength> {
+    pub fn zero() -> Self {
+        Self {
+            top: pixels(0.),
+            right: pixels(0.),
+            bottom: pixels(0.),
+            left: pixels(0.),
+        }
+    }
+
+    pub fn to_taffy(&self, rem_size: f32) -> taffy::geometry::Rect<taffy::style::LengthPercentage> {
+        taffy::geometry::Rect {
+            top: self.top.to_taffy(rem_size),
+            right: self.right.to_taffy(rem_size),
+            bottom: self.bottom.to_taffy(rem_size),
+            left: self.left.to_taffy(rem_size),
+        }
+    }
+}
+
+impl Edges<Length> {
+    pub fn auto() -> Self {
+        Self {
+            top: Length::Auto,
+            right: Length::Auto,
+            bottom: Length::Auto,
+            left: Length::Auto,
+        }
+    }
+
+    pub fn zero() -> Self {
+        Self {
+            top: pixels(0.),
+            right: pixels(0.),
+            bottom: pixels(0.),
+            left: pixels(0.),
+        }
+    }
+
+    pub fn to_taffy(
+        &self,
+        rem_size: f32,
+    ) -> taffy::geometry::Rect<taffy::style::LengthPercentageAuto> {
+        taffy::geometry::Rect {
+            top: self.top.to_taffy(rem_size),
+            right: self.right.to_taffy(rem_size),
+            bottom: self.bottom.to_taffy(rem_size),
+            left: self.left.to_taffy(rem_size),
+        }
+    }
+}
+
+#[derive(Clone, Copy)]
+pub enum AbsoluteLength {
+    Pixels(f32),
+    Rems(f32),
+}
+
+impl AbsoluteLength {
+    pub fn to_pixels(&self, rem_size: f32) -> f32 {
+        match self {
+            AbsoluteLength::Pixels(pixels) => *pixels,
+            AbsoluteLength::Rems(rems) => rems * rem_size,
+        }
+    }
+}
+
+impl Default for AbsoluteLength {
+    fn default() -> Self {
+        Self::Pixels(0.0)
+    }
+}
+
+/// A non-auto length that can be defined in pixels, rems, or percent of parent.
+#[derive(Clone, Copy)]
+pub enum DefiniteLength {
+    Absolute(AbsoluteLength),
+    Relative(f32), // Percent, from 0 to 100.
+}
+
+impl DefiniteLength {
+    fn to_taffy(&self, rem_size: f32) -> taffy::style::LengthPercentage {
+        match self {
+            DefiniteLength::Absolute(length) => match length {
+                AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(*pixels),
+                AbsoluteLength::Rems(rems) => {
+                    taffy::style::LengthPercentage::Length(rems * rem_size)
+                }
+            },
+            DefiniteLength::Relative(fraction) => {
+                taffy::style::LengthPercentage::Percent(*fraction)
+            }
+        }
+    }
+}
+
+impl From<AbsoluteLength> for DefiniteLength {
+    fn from(length: AbsoluteLength) -> Self {
+        Self::Absolute(length)
+    }
+}
+
+impl Default for DefiniteLength {
+    fn default() -> Self {
+        Self::Absolute(AbsoluteLength::default())
+    }
+}
+
+/// A length that can be defined in pixels, rems, percent of parent, or auto.
+#[derive(Clone, Copy)]
+pub enum Length {
+    Definite(DefiniteLength),
+    Auto,
+}
+
+pub fn relative<T: From<DefiniteLength>>(fraction: f32) -> T {
+    DefiniteLength::Relative(fraction).into()
+}
+
+pub fn rems<T: From<AbsoluteLength>>(rems: f32) -> T {
+    AbsoluteLength::Rems(rems).into()
+}
+
+pub fn pixels<T: From<AbsoluteLength>>(pixels: f32) -> T {
+    AbsoluteLength::Pixels(pixels).into()
+}
+
+pub fn auto() -> Length {
+    Length::Auto
+}
+
+impl Length {
+    pub fn to_taffy(&self, rem_size: f32) -> taffy::prelude::LengthPercentageAuto {
+        match self {
+            Length::Definite(length) => length.to_taffy(rem_size).into(),
+            Length::Auto => taffy::prelude::LengthPercentageAuto::Auto,
+        }
+    }
+}
+
+impl From<DefiniteLength> for Length {
+    fn from(length: DefiniteLength) -> Self {
+        Self::Definite(length)
+    }
+}
+
+impl From<AbsoluteLength> for Length {
+    fn from(length: AbsoluteLength) -> Self {
+        Self::Definite(length.into())
+    }
+}
+
+impl Default for Length {
+    fn default() -> Self {
+        Self::Definite(DefiniteLength::default())
+    }
+}

crates/gpui/src/gpui.rs 🔗

@@ -27,7 +27,10 @@ pub mod json;
 pub mod keymap_matcher;
 pub mod platform;
 pub use gpui_macros::{test, Element};
-pub use window::{Axis, RectFExt, SizeConstraint, Vector2FExt, WindowContext};
+pub use window::{
+    Axis, EngineLayout, LayoutEngine, LayoutId, RectFExt, SizeConstraint, Vector2FExt,
+    WindowContext,
+};
 
 pub use anyhow;
 pub use serde_json;

crates/gpui/src/keymap_matcher/keymap_context.rs 🔗

@@ -67,7 +67,9 @@ impl KeymapContextPredicate {
     }
 
     pub fn eval(&self, contexts: &[KeymapContext]) -> bool {
-        let Some(context) = contexts.first() else { return false };
+        let Some(context) = contexts.first() else {
+            return false;
+        };
         match self {
             Self::Identifier(name) => (&context.set).contains(name.as_str()),
             Self::Equal(left, right) => context

crates/gpui/src/platform.rs 🔗

@@ -135,6 +135,7 @@ pub trait InputHandler {
 pub trait Screen: Debug {
     fn as_any(&self) -> &dyn Any;
     fn bounds(&self) -> RectF;
+    fn content_bounds(&self) -> RectF;
     fn display_uuid(&self) -> Option<Uuid>;
 }
 
@@ -191,7 +192,7 @@ impl<'a> WindowOptions<'a> {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Default)]
 pub struct TitlebarOptions<'a> {
     pub title: Option<&'a str>,
     pub appears_transparent: bool,

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

@@ -1,4 +1,4 @@
-use std::ops::Deref;
+use std::{any::Any, ops::Deref};
 
 use pathfinder_geometry::vector::vec2f;
 
@@ -142,6 +142,7 @@ pub struct MouseButtonEvent {
     pub position: Vector2F,
     pub modifiers: Modifiers,
     pub click_count: usize,
+    pub is_down: bool,
 }
 
 impl Deref for MouseButtonEvent {
@@ -174,6 +175,7 @@ impl MouseMovedEvent {
             button: self.pressed_button.unwrap_or(button),
             modifiers: self.modifiers,
             click_count: 0,
+            is_down: self.pressed_button.is_some(),
         }
     }
 }
@@ -211,10 +213,24 @@ impl Event {
             Event::KeyDown { .. } => None,
             Event::KeyUp { .. } => None,
             Event::ModifiersChanged { .. } => None,
-            Event::MouseDown(event) | Event::MouseUp(event) => Some(event.position),
+            Event::MouseDown(event) => Some(event.position),
+            Event::MouseUp(event) => Some(event.position),
             Event::MouseMoved(event) => Some(event.position),
             Event::MouseExited(event) => Some(event.position),
             Event::ScrollWheel(event) => Some(event.position),
         }
     }
+
+    pub fn mouse_event<'a>(&'a self) -> Option<&'a dyn Any> {
+        match self {
+            Event::KeyDown { .. } => None,
+            Event::KeyUp { .. } => None,
+            Event::ModifiersChanged { .. } => None,
+            Event::MouseDown(event) => Some(event),
+            Event::MouseUp(event) => Some(event),
+            Event::MouseMoved(event) => Some(event),
+            Event::MouseExited(event) => Some(event),
+            Event::ScrollWheel(event) => Some(event),
+        }
+    }
 }

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

@@ -132,6 +132,7 @@ impl Event {
                         ),
                         modifiers: read_modifiers(native_event),
                         click_count: native_event.clickCount() as usize,
+                        is_down: true,
                     })
                 })
             }
@@ -158,6 +159,7 @@ impl Event {
                         ),
                         modifiers: read_modifiers(native_event),
                         click_count: native_event.clickCount() as usize,
+                        is_down: false,
                     })
                 })
             }

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

@@ -3,10 +3,7 @@ use cocoa::{
     foundation::{NSPoint, NSRect},
 };
 use objc::{msg_send, sel, sel_impl};
-use pathfinder_geometry::{
-    rect::RectF,
-    vector::{vec2f, Vector2F},
-};
+use pathfinder_geometry::vector::{vec2f, Vector2F};
 
 ///! Macos screen have a y axis that goings up from the bottom of the screen and
 ///! an origin at the bottom left of the main display.
@@ -15,6 +12,7 @@ pub trait Vector2FExt {
     /// Converts self to an NSPoint with y axis pointing up.
     fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint;
 }
+
 impl Vector2FExt for Vector2F {
     fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint {
         unsafe {
@@ -25,16 +23,13 @@ impl Vector2FExt for Vector2F {
 }
 
 pub trait NSRectExt {
-    fn to_rectf(&self) -> RectF;
+    fn size_vec(&self) -> Vector2F;
     fn intersects(&self, other: Self) -> bool;
 }
 
 impl NSRectExt for NSRect {
-    fn to_rectf(&self) -> RectF {
-        RectF::new(
-            vec2f(self.origin.x as f32, self.origin.y as f32),
-            vec2f(self.size.width as f32, self.size.height as f32),
-        )
+    fn size_vec(&self) -> Vector2F {
+        vec2f(self.size.width as f32, self.size.height as f32)
     }
 
     fn intersects(&self, other: Self) -> bool {

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

@@ -1,21 +1,19 @@
-use std::{any::Any, ffi::c_void};
-
+use super::ns_string;
 use crate::platform;
 use cocoa::{
     appkit::NSScreen,
     base::{id, nil},
-    foundation::{NSArray, NSDictionary},
+    foundation::{NSArray, NSDictionary, NSPoint, NSRect, NSSize},
 };
 use core_foundation::{
     number::{kCFNumberIntType, CFNumberGetValue, CFNumberRef},
     uuid::{CFUUIDGetUUIDBytes, CFUUIDRef},
 };
 use core_graphics::display::CGDirectDisplayID;
-use pathfinder_geometry::rect::RectF;
+use pathfinder_geometry::{rect::RectF, vector::vec2f};
+use std::{any::Any, ffi::c_void};
 use uuid::Uuid;
 
-use super::{geometry::NSRectExt, ns_string};
-
 #[link(name = "ApplicationServices", kind = "framework")]
 extern "C" {
     pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
@@ -27,29 +25,58 @@ pub struct Screen {
 }
 
 impl Screen {
+    /// Get the screen with the given UUID.
     pub fn find_by_id(uuid: Uuid) -> Option<Self> {
-        unsafe {
-            let native_screens = NSScreen::screens(nil);
-            (0..NSArray::count(native_screens))
-                .into_iter()
-                .map(|ix| Screen {
-                    native_screen: native_screens.objectAtIndex(ix),
-                })
-                .find(|screen| platform::Screen::display_uuid(screen) == Some(uuid))
-        }
+        Self::all().find(|screen| platform::Screen::display_uuid(screen) == Some(uuid))
+    }
+
+    /// Get the primary screen - the one with the menu bar, and whose bottom left
+    /// corner is at the origin of the AppKit coordinate system.
+    fn primary() -> Self {
+        Self::all().next().unwrap()
     }
 
-    pub fn all() -> Vec<Self> {
-        let mut screens = Vec::new();
+    pub fn all() -> impl Iterator<Item = Self> {
         unsafe {
             let native_screens = NSScreen::screens(nil);
-            for ix in 0..NSArray::count(native_screens) {
-                screens.push(Screen {
-                    native_screen: native_screens.objectAtIndex(ix),
-                });
-            }
+            (0..NSArray::count(native_screens)).map(move |ix| Screen {
+                native_screen: native_screens.objectAtIndex(ix),
+            })
         }
-        screens
+    }
+
+    /// Convert the given rectangle in screen coordinates from GPUI's
+    /// coordinate system to the AppKit coordinate system.
+    ///
+    /// In GPUI's coordinates, the origin is at the top left of the primary screen, with
+    /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the
+    /// bottom left of the primary screen, with the Y axis pointing upward.
+    pub(crate) fn screen_rect_to_native(rect: RectF) -> NSRect {
+        let primary_screen_height = unsafe { Self::primary().native_screen.frame().size.height };
+        NSRect::new(
+            NSPoint::new(
+                rect.origin_x() as f64,
+                primary_screen_height - rect.origin_y() as f64 - rect.height() as f64,
+            ),
+            NSSize::new(rect.width() as f64, rect.height() as f64),
+        )
+    }
+
+    /// Convert the given rectangle in screen coordinates from the AppKit
+    /// coordinate system to GPUI's coordinate system.
+    ///
+    /// In GPUI's coordinates, the origin is at the top left of the primary screen, with
+    /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the
+    /// bottom left of the primary screen, with the Y axis pointing upward.
+    pub(crate) fn screen_rect_from_native(rect: NSRect) -> RectF {
+        let primary_screen_height = unsafe { Self::primary().native_screen.frame().size.height };
+        RectF::new(
+            vec2f(
+                rect.origin.x as f32,
+                (primary_screen_height - rect.origin.y - rect.size.height) as f32,
+            ),
+            vec2f(rect.size.width as f32, rect.size.height as f32),
+        )
     }
 }
 
@@ -108,9 +135,10 @@ impl platform::Screen for Screen {
     }
 
     fn bounds(&self) -> RectF {
-        unsafe {
-            let frame = self.native_screen.frame();
-            frame.to_rectf()
-        }
+        unsafe { Self::screen_rect_from_native(self.native_screen.frame()) }
+    }
+
+    fn content_bounds(&self) -> RectF {
+        unsafe { Self::screen_rect_from_native(self.native_screen.visibleFrame()) }
     }
 }

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

@@ -368,32 +368,20 @@ impl WindowState {
                 return WindowBounds::Fullscreen;
             }
 
-            let window_frame = self.frame();
-            let screen_frame = self.native_window.screen().visibleFrame().to_rectf();
-            if window_frame.size() == screen_frame.size() {
+            let frame = self.frame();
+            let screen_size = self.native_window.screen().visibleFrame().size_vec();
+            if frame.size() == screen_size {
                 WindowBounds::Maximized
             } else {
-                WindowBounds::Fixed(window_frame)
+                WindowBounds::Fixed(frame)
             }
         }
     }
 
-    // Returns the window bounds in window coordinates
     fn frame(&self) -> RectF {
         unsafe {
-            let screen_frame = self.native_window.screen().visibleFrame();
-            let window_frame = NSWindow::frame(self.native_window);
-            RectF::new(
-                vec2f(
-                    window_frame.origin.x as f32,
-                    (screen_frame.size.height - window_frame.origin.y - window_frame.size.height)
-                        as f32,
-                ),
-                vec2f(
-                    window_frame.size.width as f32,
-                    window_frame.size.height as f32,
-                ),
-            )
+            let frame = NSWindow::frame(self.native_window);
+            Screen::screen_rect_from_native(frame)
         }
     }
 
@@ -480,21 +468,12 @@ impl MacWindow {
                     native_window.setFrame_display_(screen.visibleFrame(), YES);
                 }
                 WindowBounds::Fixed(rect) => {
-                    let screen_frame = screen.visibleFrame();
-                    let ns_rect = NSRect::new(
-                        NSPoint::new(
-                            rect.origin_x() as f64,
-                            screen_frame.size.height
-                                - rect.origin_y() as f64
-                                - rect.height() as f64,
-                        ),
-                        NSSize::new(rect.width() as f64, rect.height() as f64),
-                    );
-
-                    if ns_rect.intersects(screen_frame) {
-                        native_window.setFrame_display_(ns_rect, YES);
+                    let bounds = Screen::screen_rect_to_native(rect);
+                    let screen_bounds = screen.visibleFrame();
+                    if bounds.intersects(screen_bounds) {
+                        native_window.setFrame_display_(bounds, YES);
                     } else {
-                        native_window.setFrame_display_(screen_frame, YES);
+                        native_window.setFrame_display_(screen_bounds, YES);
                     }
                 }
             }

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

@@ -250,6 +250,10 @@ impl super::Screen for Screen {
         RectF::new(Vector2F::zero(), Vector2F::new(1920., 1080.))
     }
 
+    fn content_bounds(&self) -> RectF {
+        RectF::new(Vector2F::zero(), Vector2F::new(1920., 1080.))
+    }
+
     fn display_uuid(&self) -> Option<uuid::Uuid> {
         Some(uuid::Uuid::new_v4())
     }

crates/gpui/src/scene.rs 🔗

@@ -1,5 +1,6 @@
 mod mouse_event;
 mod mouse_region;
+mod region;
 
 #[cfg(debug_assertions)]
 use collections::HashSet;
@@ -8,7 +9,12 @@ use schemars::JsonSchema;
 use serde::Deserialize;
 use serde_derive::Serialize;
 use serde_json::json;
-use std::{borrow::Cow, sync::Arc};
+use std::{
+    any::{Any, TypeId},
+    borrow::Cow,
+    rc::Rc,
+    sync::Arc,
+};
 
 use crate::{
     color::Color,
@@ -16,7 +22,7 @@ use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
     platform::{current::Surface, CursorStyle},
-    ImageData,
+    ImageData, WindowContext,
 };
 pub use mouse_event::*;
 pub use mouse_region::*;
@@ -25,6 +31,8 @@ pub struct SceneBuilder {
     scale_factor: f32,
     stacking_contexts: Vec<StackingContext>,
     active_stacking_context_stack: Vec<usize>,
+    /// Used by the playground crate.
+    pub event_handlers: Vec<EventHandler>,
     #[cfg(debug_assertions)]
     mouse_region_ids: HashSet<MouseRegionId>,
 }
@@ -32,6 +40,7 @@ pub struct SceneBuilder {
 pub struct Scene {
     scale_factor: f32,
     stacking_contexts: Vec<StackingContext>,
+    event_handlers: Vec<EventHandler>,
 }
 
 struct StackingContext {
@@ -272,6 +281,12 @@ impl Scene {
             })
             .collect()
     }
+
+    pub fn take_event_handlers(&mut self) -> Vec<EventHandler> {
+        self.event_handlers
+            .sort_by(|a, b| a.order.cmp(&b.order).reverse());
+        std::mem::take(&mut self.event_handlers)
+    }
 }
 
 impl SceneBuilder {
@@ -283,6 +298,7 @@ impl SceneBuilder {
             active_stacking_context_stack: vec![0],
             #[cfg(debug_assertions)]
             mouse_region_ids: Default::default(),
+            event_handlers: Vec::new(),
         }
     }
 
@@ -292,6 +308,7 @@ impl SceneBuilder {
         Scene {
             scale_factor: self.scale_factor,
             stacking_contexts: self.stacking_contexts,
+            event_handlers: self.event_handlers,
         }
     }
 
@@ -688,6 +705,13 @@ impl MouseRegion {
     }
 }
 
+pub struct EventHandler {
+    pub order: u32,
+    // The &dyn Any parameter below expects an event.
+    pub handler: Rc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>,
+    pub event_type: TypeId,
+}
+
 fn can_draw(bounds: RectF) -> bool {
     let size = bounds.size();
     size.x() > 0. && size.y() > 0.

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -1,6 +1,4 @@
-use crate::{
-    platform::MouseButton, window::WindowContext, EventContext, TypeTag, View, ViewContext,
-};
+use crate::{platform::MouseButton, window::WindowContext, EventContext, TypeTag, ViewContext};
 use collections::HashMap;
 use pathfinder_geometry::rect::RectF;
 use smallvec::SmallVec;
@@ -72,7 +70,7 @@ impl MouseRegion {
 
     pub fn on_down<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseDown, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_down(button, handler);
@@ -81,7 +79,7 @@ impl MouseRegion {
 
     pub fn on_up<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseUp, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_up(button, handler);
@@ -90,7 +88,7 @@ impl MouseRegion {
 
     pub fn on_click<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_click(button, handler);
@@ -99,7 +97,7 @@ impl MouseRegion {
 
     pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_click_out(button, handler);
@@ -108,7 +106,7 @@ impl MouseRegion {
 
     pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseDownOut, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_down_out(button, handler);
@@ -117,7 +115,7 @@ impl MouseRegion {
 
     pub fn on_up_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseUpOut, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_up_out(button, handler);
@@ -126,7 +124,7 @@ impl MouseRegion {
 
     pub fn on_drag<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseDrag, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_drag(button, handler);
@@ -135,7 +133,7 @@ impl MouseRegion {
 
     pub fn on_hover<V, F>(mut self, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseHover, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_hover(handler);
@@ -144,7 +142,7 @@ impl MouseRegion {
 
     pub fn on_move<V, F>(mut self, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseMove, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_move(handler);
@@ -153,7 +151,7 @@ impl MouseRegion {
 
     pub fn on_move_out<V, F>(mut self, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseMoveOut, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_move_out(handler);
@@ -162,7 +160,7 @@ impl MouseRegion {
 
     pub fn on_scroll<V, F>(mut self, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseScrollWheel, &mut V, &mut EventContext<V>) + 'static,
     {
         self.handlers = self.handlers.on_scroll(handler);
@@ -314,7 +312,7 @@ impl HandlerSet {
 
     pub fn on_move<V, F>(mut self, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseMove, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::move_disc(), None,
@@ -336,7 +334,7 @@ impl HandlerSet {
 
     pub fn on_move_out<V, F>(mut self, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseMoveOut, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::move_out_disc(), None,
@@ -358,7 +356,7 @@ impl HandlerSet {
 
     pub fn on_down<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseDown, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::down_disc(), Some(button),
@@ -380,7 +378,7 @@ impl HandlerSet {
 
     pub fn on_up<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseUp, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::up_disc(), Some(button),
@@ -402,7 +400,7 @@ impl HandlerSet {
 
     pub fn on_click<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::click_disc(), Some(button),
@@ -424,7 +422,7 @@ impl HandlerSet {
 
     pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::click_out_disc(), Some(button),
@@ -446,7 +444,7 @@ impl HandlerSet {
 
     pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseDownOut, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::down_out_disc(), Some(button),
@@ -468,7 +466,7 @@ impl HandlerSet {
 
     pub fn on_up_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseUpOut, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::up_out_disc(), Some(button),
@@ -490,7 +488,7 @@ impl HandlerSet {
 
     pub fn on_drag<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseDrag, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::drag_disc(), Some(button),
@@ -512,7 +510,7 @@ impl HandlerSet {
 
     pub fn on_hover<V, F>(mut self, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseHover, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::hover_disc(), None,
@@ -534,7 +532,7 @@ impl HandlerSet {
 
     pub fn on_scroll<V, F>(mut self, handler: F) -> Self
     where
-        V: View,
+        V: 'static,
         F: Fn(MouseScrollWheel, &mut V, &mut EventContext<V>) + 'static,
     {
         self.insert(MouseEvent::scroll_wheel_disc(), None,

crates/gpui/src/scene/region.rs 🔗

@@ -0,0 +1,7 @@
+// use crate::geometry::rect::RectF;
+// use crate::WindowContext;
+
+// struct Region {
+//     pub bounds: RectF,
+//     pub click_handler: Option<Rc<dyn Fn(&dyn Any, MouseEvent, &mut WindowContext)>>,
+// }

crates/gpui/src/text_layout.rs 🔗

@@ -212,7 +212,7 @@ pub struct Glyph {
 }
 
 impl Line {
-    fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
+    pub fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
         let mut style_runs = SmallVec::new();
         for (len, style) in runs {
             style_runs.push(StyleRun {

crates/gpui/tests/test.rs 🔗

@@ -0,0 +1,14 @@
+use gpui::{elements::Empty, Element, ViewContext};
+// use gpui_macros::Element;
+
+#[test]
+fn test_derive_render_element() {
+    #[derive(Element)]
+    struct TestElement {}
+
+    impl TestElement {
+        fn render<V: 'static>(&mut self, _: &mut V, _: &mut ViewContext<V>) -> impl Element<V> {
+            Empty::new()
+        }
+    }
+}

crates/gpui_macros/Cargo.toml 🔗

@@ -10,9 +10,7 @@ proc-macro = true
 doctest = false
 
 [dependencies]
+lazy_static.workspace = true
+proc-macro2 = "1.0"
 syn = "1.0"
 quote = "1.0"
-proc-macro2 = "1.0"
-
-[dev-dependencies]
-gpui = { path = "../gpui" }

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -4,7 +4,7 @@ use quote::{format_ident, quote};
 use std::mem;
 use syn::{
     parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, DeriveInput, FnArg,
-    ItemFn, Lit, Meta, NestedMeta, Type,
+    GenericParam, Generics, ItemFn, Lit, Meta, NestedMeta, Type, WhereClause,
 };
 
 #[proc_macro_attribute]
@@ -278,18 +278,44 @@ fn parse_bool(literal: &Lit) -> Result<bool, TokenStream> {
 
 #[proc_macro_derive(Element)]
 pub fn element_derive(input: TokenStream) -> TokenStream {
-    // Parse the input tokens into a syntax tree
-    let input = parse_macro_input!(input as DeriveInput);
+    let ast = parse_macro_input!(input as DeriveInput);
+    let type_name = ast.ident;
 
-    // The name of the struct/enum
-    let name = input.ident;
-    let must_implement = format_ident!("{}MustImplementRenderElement", name);
+    let placeholder_view_generics: Generics = parse_quote! { <V: 'static> };
+    let placeholder_view_type_name: Ident = parse_quote! { V };
+    let view_type_name: Ident;
+    let impl_generics: syn::ImplGenerics<'_>;
+    let type_generics: Option<syn::TypeGenerics<'_>>;
+    let where_clause: Option<&'_ WhereClause>;
 
-    let expanded = quote! {
-        trait #must_implement : gpui::elements::RenderElement {}
-        impl #must_implement for #name {}
+    match ast.generics.params.iter().find_map(|param| {
+        if let GenericParam::Type(type_param) = param {
+            Some(type_param.ident.clone())
+        } else {
+            None
+        }
+    }) {
+        Some(type_name) => {
+            view_type_name = type_name;
+            let generics = ast.generics.split_for_impl();
+            impl_generics = generics.0;
+            type_generics = Some(generics.1);
+            where_clause = generics.2;
+        }
+        _ => {
+            view_type_name = placeholder_view_type_name;
+            let generics = placeholder_view_generics.split_for_impl();
+            impl_generics = generics.0;
+            type_generics = None;
+            where_clause = generics.2;
+        }
+    }
+
+    let gen = quote! {
+        impl #impl_generics Element<#view_type_name> for #type_name #type_generics
+        #where_clause
+        {
 
-        impl<V: gpui::View> gpui::elements::Element<V> for #name {
             type LayoutState = gpui::elements::AnyElement<V>;
             type PaintState = ();
 
@@ -299,7 +325,7 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
                 view: &mut V,
                 cx: &mut gpui::LayoutContext<V>,
             ) -> (gpui::geometry::vector::Vector2F, gpui::elements::AnyElement<V>) {
-                let mut element = self.render(view, cx);
+                let mut element = self.render(view, cx).into_any();
                 let size = element.layout(constraint, view, cx);
                 (size, element)
             }
@@ -336,11 +362,11 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
                 _: &(),
                 view: &V,
                 cx: &gpui::ViewContext<V>,
-            ) -> gpui::serde_json::Value {
+            ) -> gpui::json::Value {
                 element.debug(view, cx)
             }
         }
     };
-    // Return generated code
-    TokenStream::from(expanded)
+
+    gen.into()
 }

crates/gpui_macros/tests/test.rs 🔗

@@ -1,14 +0,0 @@
-use gpui::{elements::RenderElement, View, ViewContext};
-use gpui_macros::Element;
-
-#[test]
-fn test_derive_render_element() {
-    #[derive(Element)]
-    struct TestElement {}
-
-    impl RenderElement for TestElement {
-        fn render<V: View>(&mut self, _: &mut V, _: &mut ViewContext<V>) -> gpui::AnyElement<V> {
-            unimplemented!()
-        }
-    }
-}

crates/install_cli/src/install_cli.rs 🔗

@@ -29,7 +29,7 @@ pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
 
     // The symlink could not be created, so use osascript with admin privileges
     // to create it.
-    let status = smol::process::Command::new("osascript")
+    let status = smol::process::Command::new("/usr/bin/osascript")
         .args([
             "-e",
             &format!(

crates/language/src/buffer.rs 🔗

@@ -14,8 +14,7 @@ use crate::{
     CodeLabel, LanguageScope, Outline,
 };
 use anyhow::{anyhow, Result};
-use clock::ReplicaId;
-use fs::LineEnding;
+pub use clock::ReplicaId;
 use futures::FutureExt as _;
 use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
 use lsp::LanguageServerId;
@@ -348,13 +347,17 @@ impl CharKind {
 }
 
 impl Buffer {
-    pub fn new<T: Into<String>>(
-        replica_id: ReplicaId,
-        base_text: T,
-        cx: &mut ModelContext<Self>,
-    ) -> Self {
+    pub fn new<T: Into<String>>(replica_id: ReplicaId, id: u64, base_text: T) -> Self {
         Self::build(
-            TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
+            TextBuffer::new(replica_id, id, base_text.into()),
+            None,
+            None,
+        )
+    }
+
+    pub fn remote(remote_id: u64, replica_id: ReplicaId, base_text: String) -> Self {
+        Self::build(
+            TextBuffer::new(replica_id, remote_id, base_text),
             None,
             None,
         )
@@ -1295,6 +1298,10 @@ impl Buffer {
         self.text.forget_transaction(transaction_id);
     }
 
+    pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
+        self.text.merge_transactions(transaction, destination);
+    }
+
     pub fn wait_for_edits(
         &mut self,
         edit_ids: impl IntoIterator<Item = clock::Local>,
@@ -1661,6 +1668,22 @@ impl Buffer {
         }
     }
 
+    pub fn undo_transaction(
+        &mut self,
+        transaction_id: TransactionId,
+        cx: &mut ModelContext<Self>,
+    ) -> bool {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+        if let Some(operation) = self.text.undo_transaction(transaction_id) {
+            self.send_operation(Operation::Buffer(operation), cx);
+            self.did_edit(&old_version, was_dirty, cx);
+            true
+        } else {
+            false
+        }
+    }
+
     pub fn undo_to_transaction(
         &mut self,
         transaction_id: TransactionId,
@@ -2146,27 +2169,46 @@ impl BufferSnapshot {
 
     pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
         let offset = position.to_offset(self);
-        let mut range = 0..self.len();
-        let mut scope = self.language.clone().map(|language| LanguageScope {
-            language,
-            override_id: None,
-        });
+        let mut scope = None;
+        let mut smallest_range: Option<Range<usize>> = None;
 
         // Use the layer that has the smallest node intersecting the given point.
         for layer in self.syntax.layers_for_range(offset..offset, &self.text) {
             let mut cursor = layer.node().walk();
-            while cursor.goto_first_child_for_byte(offset).is_some() {}
-            let node_range = cursor.node().byte_range();
-            if node_range.to_inclusive().contains(&offset) && node_range.len() < range.len() {
-                range = node_range;
-                scope = Some(LanguageScope {
-                    language: layer.language.clone(),
-                    override_id: layer.override_id(offset, &self.text),
-                });
+
+            let mut range = None;
+            loop {
+                let child_range = cursor.node().byte_range();
+                if !child_range.to_inclusive().contains(&offset) {
+                    break;
+                }
+
+                range = Some(child_range);
+                if cursor.goto_first_child_for_byte(offset).is_none() {
+                    break;
+                }
+            }
+
+            if let Some(range) = range {
+                if smallest_range
+                    .as_ref()
+                    .map_or(true, |smallest_range| range.len() < smallest_range.len())
+                {
+                    smallest_range = Some(range);
+                    scope = Some(LanguageScope {
+                        language: layer.language.clone(),
+                        override_id: layer.override_id(offset, &self.text),
+                    });
+                }
             }
         }
 
-        scope
+        scope.or_else(|| {
+            self.language.clone().map(|language| LanguageScope {
+                language,
+                override_id: None,
+            })
+        })
     }
 
     pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
@@ -2458,7 +2500,9 @@ impl BufferSnapshot {
 
                 matches.advance();
 
-                let Some((open, close)) = open.zip(close) else { continue };
+                let Some((open, close)) = open.zip(close) else {
+                    continue;
+                };
 
                 let bracket_range = open.start..=close.end;
                 if !bracket_range.overlaps(&range) {

crates/language/src/buffer_tests.rs 🔗

@@ -5,7 +5,6 @@ use crate::language_settings::{
 use super::*;
 use clock::ReplicaId;
 use collections::BTreeMap;
-use fs::LineEnding;
 use gpui::{AppContext, ModelHandle};
 use indoc::indoc;
 use proto::deserialize_operation;
@@ -20,6 +19,7 @@ use std::{
     time::{Duration, Instant},
 };
 use text::network::Network;
+use text::LineEnding;
 use unindent::Unindent as _;
 use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
 
@@ -43,8 +43,8 @@ fn test_line_endings(cx: &mut gpui::AppContext) {
     init_settings(cx, |_| {});
 
     cx.add_model(|cx| {
-        let mut buffer =
-            Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx);
+        let mut buffer = Buffer::new(0, cx.model_id() as u64, "one\r\ntwo\rthree")
+            .with_language(Arc::new(rust_lang()), cx);
         assert_eq!(buffer.text(), "one\ntwo\nthree");
         assert_eq!(buffer.line_ending(), LineEnding::Windows);
 
@@ -138,8 +138,8 @@ fn test_edit_events(cx: &mut gpui::AppContext) {
     let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
     let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
 
-    let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx));
-    let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx));
+    let buffer1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcdef"));
+    let buffer2 = cx.add_model(|cx| Buffer::new(1, cx.model_id() as u64, "abcdef"));
     let buffer1_ops = Rc::new(RefCell::new(Vec::new()));
     buffer1.update(cx, {
         let buffer1_ops = buffer1_ops.clone();
@@ -222,7 +222,7 @@ fn test_edit_events(cx: &mut gpui::AppContext) {
 #[gpui::test]
 async fn test_apply_diff(cx: &mut gpui::TestAppContext) {
     let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n";
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text));
     let anchor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3)));
 
     let text = "a\nccc\ndddd\nffffff\n";
@@ -254,7 +254,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
     ]
     .join("\n");
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+    let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text));
 
     // Spawn a task to format the buffer's whitespace.
     // Pause so that the foratting task starts running.
@@ -318,8 +318,9 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
 #[gpui::test]
 async fn test_reparse(cx: &mut gpui::TestAppContext) {
     let text = "fn a() {}";
-    let buffer =
-        cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
+    let buffer = cx.add_model(|cx| {
+        Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx)
+    });
 
     // Wait for the initial text to parse
     buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await;
@@ -443,7 +444,8 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
 #[gpui::test]
 async fn test_resetting_language(cx: &mut gpui::TestAppContext) {
     let buffer = cx.add_model(|cx| {
-        let mut buffer = Buffer::new(0, "{}", cx).with_language(Arc::new(rust_lang()), cx);
+        let mut buffer =
+            Buffer::new(0, cx.model_id() as u64, "{}").with_language(Arc::new(rust_lang()), cx);
         buffer.set_sync_parse_timeout(Duration::ZERO);
         buffer
     });
@@ -491,8 +493,9 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer =
-        cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
+    let buffer = cx.add_model(|cx| {
+        Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx)
+    });
     let outline = buffer
         .read_with(cx, |buffer, _| buffer.snapshot().outline(None))
         .unwrap();
@@ -576,8 +579,9 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer =
-        cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
+    let buffer = cx.add_model(|cx| {
+        Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx)
+    });
     let outline = buffer
         .read_with(cx, |buffer, _| buffer.snapshot().outline(None))
         .unwrap();
@@ -613,7 +617,9 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx));
+    let buffer = cx.add_model(|cx| {
+        Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx)
+    });
     let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
 
     // extra context nodes are included in the outline.
@@ -655,8 +661,9 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer =
-        cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
+    let buffer = cx.add_model(|cx| {
+        Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx)
+    });
     let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
 
     // point is at the start of an item
@@ -877,7 +884,8 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: &
 fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
     cx.add_model(|cx| {
         let text = "fn a() { b(|c| {}) }";
-        let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        let buffer =
+            Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
         let snapshot = buffer.snapshot();
 
         assert_eq!(
@@ -917,7 +925,8 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
 
     cx.add_model(|cx| {
         let text = "fn a() {}";
-        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        let mut buffer =
+            Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
 
         buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
         assert_eq!(buffer.text(), "fn a() {\n    \n}");
@@ -959,7 +968,8 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
 
     cx.add_model(|cx| {
         let text = "fn a() {}";
-        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        let mut buffer =
+            Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
 
         buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
         assert_eq!(buffer.text(), "fn a() {\n\t\n}");
@@ -1000,6 +1010,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
     cx.add_model(|cx| {
         let mut buffer = Buffer::new(
             0,
+            cx.model_id() as u64,
             "
             fn a() {
             c;
@@ -1007,7 +1018,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
             }
             "
             .unindent(),
-            cx,
         )
         .with_language(Arc::new(rust_lang()), cx);
 
@@ -1073,6 +1083,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
     cx.add_model(|cx| {
         let mut buffer = Buffer::new(
             0,
+            cx.model_id() as u64,
             "
             fn a() {
                 b();
@@ -1080,7 +1091,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
             "
             .replace("|", "") // marker to preserve trailing whitespace
             .unindent(),
-            cx,
         )
         .with_language(Arc::new(rust_lang()), cx);
 
@@ -1136,13 +1146,13 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap
     cx.add_model(|cx| {
         let mut buffer = Buffer::new(
             0,
+            cx.model_id() as u64,
             "
             fn a() {
                 i
             }
             "
             .unindent(),
-            cx,
         )
         .with_language(Arc::new(rust_lang()), cx);
 
@@ -1198,11 +1208,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
     cx.add_model(|cx| {
         let mut buffer = Buffer::new(
             0,
+            cx.model_id() as u64,
             "
             fn a() {}
             "
             .unindent(),
-            cx,
         )
         .with_language(Arc::new(rust_lang()), cx);
 
@@ -1254,7 +1264,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
 
     cx.add_model(|cx| {
         let text = "a\nb";
-        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        let mut buffer =
+            Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
         buffer.edit(
             [(0..1, "\n"), (2..3, "\n")],
             Some(AutoindentMode::EachLine),
@@ -1280,7 +1291,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
         "
         .unindent();
 
-        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        let mut buffer =
+            Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
         buffer.edit(
             [(Point::new(3, 0)..Point::new(3, 0), "e(\n    f()\n);\n")],
             Some(AutoindentMode::EachLine),
@@ -1317,7 +1329,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) {
             }
         "#
         .unindent();
-        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        let mut buffer =
+            Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
 
         // When this text was copied, both of the quotation marks were at the same
         // indent level, but the indentation of the first line was not included in
@@ -1402,7 +1415,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
             }
         "#
         .unindent();
-        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        let mut buffer =
+            Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
 
         // The original indent columns are not known, so this text is
         // auto-indented in a block as if the first line was copied in
@@ -1481,7 +1495,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
         "
         .unindent();
 
-        let mut buffer = Buffer::new(0, text, cx).with_language(
+        let mut buffer = Buffer::new(0, cx.model_id() as u64, text).with_language(
             Arc::new(Language::new(
                 LanguageConfig {
                     name: "Markdown".into(),
@@ -1557,7 +1571,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
             false,
         );
 
-        let mut buffer = Buffer::new(0, text, cx);
+        let mut buffer = Buffer::new(0, cx.model_id() as u64, text);
         buffer.set_language_registry(language_registry);
         buffer.set_language(Some(html_language), cx);
         buffer.edit(
@@ -1593,7 +1607,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
     });
 
     cx.add_model(|cx| {
-        let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx);
+        let mut buffer =
+            Buffer::new(0, cx.model_id() as u64, "").with_language(Arc::new(ruby_lang()), cx);
 
         let text = r#"
             class C
@@ -1631,7 +1646,7 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
 }
 
 #[gpui::test]
-fn test_language_scope_at(cx: &mut AppContext) {
+fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
     cx.add_model(|cx| {
@@ -1683,7 +1698,8 @@ fn test_language_scope_at(cx: &mut AppContext) {
 
         let text = r#"a["b"] = <C d="e"></C>;"#;
 
-        let buffer = Buffer::new(0, text, cx).with_language(Arc::new(language), cx);
+        let buffer =
+            Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx);
         let snapshot = buffer.snapshot();
 
         let config = snapshot.language_scope_at(0).unwrap();
@@ -1718,6 +1734,74 @@ fn test_language_scope_at(cx: &mut AppContext) {
     });
 }
 
+#[gpui::test]
+fn test_language_scope_at_with_rust(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.add_model(|cx| {
+        let language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".into(),
+                            end: "}".into(),
+                            close: true,
+                            newline: false,
+                        },
+                        BracketPair {
+                            start: "'".into(),
+                            end: "'".into(),
+                            close: true,
+                            newline: false,
+                        },
+                    ],
+                    disabled_scopes_by_bracket_ix: vec![
+                        Vec::new(), //
+                        vec!["string".into()],
+                    ],
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_override_query(
+            r#"
+                (string_literal) @string
+            "#,
+        )
+        .unwrap();
+
+        let text = r#"
+            const S: &'static str = "hello";
+        "#
+        .unindent();
+
+        let buffer = Buffer::new(0, cx.model_id() as u64, text.clone())
+            .with_language(Arc::new(language), cx);
+        let snapshot = buffer.snapshot();
+
+        // By default, all brackets are enabled
+        let config = snapshot.language_scope_at(0).unwrap();
+        assert_eq!(
+            config.brackets().map(|e| e.1).collect::<Vec<_>>(),
+            &[true, true]
+        );
+
+        // Within a string, the quotation brackets are disabled.
+        let string_config = snapshot
+            .language_scope_at(text.find("ello").unwrap())
+            .unwrap();
+        assert_eq!(
+            string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
+            &[true, false]
+        );
+
+        buffer
+    });
+}
+
 #[gpui::test]
 fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
     init_settings(cx, |_| {});
@@ -1739,7 +1823,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
         language_registry.add(Arc::new(html_lang()));
         language_registry.add(Arc::new(erb_lang()));
 
-        let mut buffer = Buffer::new(0, text, cx);
+        let mut buffer = Buffer::new(0, cx.model_id() as u64, text);
         buffer.set_language_registry(language_registry.clone());
         buffer.set_language(
             language_registry
@@ -1771,7 +1855,7 @@ fn test_serialization(cx: &mut gpui::AppContext) {
     let mut now = Instant::now();
 
     let buffer1 = cx.add_model(|cx| {
-        let mut buffer = Buffer::new(0, "abc", cx);
+        let mut buffer = Buffer::new(0, cx.model_id() as u64, "abc");
         buffer.edit([(3..3, "D")], None, cx);
 
         now += Duration::from_secs(1);
@@ -1826,7 +1910,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
     let mut replica_ids = Vec::new();
     let mut buffers = Vec::new();
     let network = Rc::new(RefCell::new(Network::new(rng.clone())));
-    let base_buffer = cx.add_model(|cx| Buffer::new(0, base_text.as_str(), cx));
+    let base_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text.as_str()));
 
     for i in 0..rng.gen_range(min_peers..=max_peers) {
         let buffer = cx.add_model(|cx| {
@@ -2327,7 +2411,8 @@ fn assert_bracket_pairs(
 ) {
     let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
     let buffer = cx.add_model(|cx| {
-        Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx)
+        Buffer::new(0, cx.model_id() as u64, expected_text.clone())
+            .with_language(Arc::new(language), cx)
     });
     let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot());
 

crates/language/src/language.rs 🔗

@@ -18,7 +18,7 @@ use futures::{
     FutureExt, TryFutureExt as _,
 };
 use gpui::{executor::Background, AppContext, AsyncAppContext, Task};
-use highlight_map::HighlightMap;
+pub use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
 use lsp::{CodeActionKind, LanguageServerBinary};
 use parking_lot::{Mutex, RwLock};
@@ -57,6 +57,7 @@ pub use diagnostic_set::DiagnosticEntry;
 pub use lsp::LanguageServerId;
 pub use outline::{Outline, OutlineItem};
 pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo};
+pub use text::LineEnding;
 pub use tree_sitter::{Parser, Tree};
 
 pub fn init(cx: &mut AppContext) {

crates/language/src/proto.rs 🔗

@@ -20,17 +20,17 @@ pub fn deserialize_fingerprint(fingerprint: &str) -> Result<RopeFingerprint> {
         .map_err(|error| anyhow!("invalid fingerprint: {}", error))
 }
 
-pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding {
+pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
     match message {
-        proto::LineEnding::Unix => fs::LineEnding::Unix,
-        proto::LineEnding::Windows => fs::LineEnding::Windows,
+        proto::LineEnding::Unix => text::LineEnding::Unix,
+        proto::LineEnding::Windows => text::LineEnding::Windows,
     }
 }
 
-pub fn serialize_line_ending(message: fs::LineEnding) -> proto::LineEnding {
+pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding {
     match message {
-        fs::LineEnding::Unix => proto::LineEnding::Unix,
-        fs::LineEnding::Windows => proto::LineEnding::Windows,
+        text::LineEnding::Unix => proto::LineEnding::Unix,
+        text::LineEnding::Windows => proto::LineEnding::Windows,
     }
 }
 
@@ -207,6 +207,7 @@ pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
     }
 }
 
+// This behavior is currently copied in the collab database, for snapshotting channel notes
 pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operation> {
     Ok(
         match message

crates/language/src/syntax_map.rs 🔗

@@ -72,7 +72,7 @@ pub struct SyntaxMapMatch<'a> {
 
 struct SyntaxMapCapturesLayer<'a> {
     depth: usize,
-    captures: QueryCaptures<'a, 'a, TextProvider<'a>>,
+    captures: QueryCaptures<'a, 'a, TextProvider<'a>, &'a [u8]>,
     next_capture: Option<QueryCapture<'a>>,
     grammar_index: usize,
     _query_cursor: QueryCursorHandle,
@@ -83,7 +83,7 @@ struct SyntaxMapMatchesLayer<'a> {
     next_pattern_index: usize,
     next_captures: Vec<QueryCapture<'a>>,
     has_next: bool,
-    matches: QueryMatches<'a, 'a, TextProvider<'a>>,
+    matches: QueryMatches<'a, 'a, TextProvider<'a>, &'a [u8]>,
     grammar_index: usize,
     _query_cursor: QueryCursorHandle,
 }
@@ -310,7 +310,9 @@ impl SyntaxSnapshot {
             // Ignore edits that end before the start of this layer, and don't consider them
             // for any subsequent layers at this same depth.
             loop {
-                let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { continue 'outer };
+                let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else {
+                    continue 'outer;
+                };
                 if edit_range.end.cmp(&layer.range.start, text).is_le() {
                     first_edit_ix_for_depth += 1;
                 } else {
@@ -391,7 +393,9 @@ impl SyntaxSnapshot {
                     .filter::<_, ()>(|summary| summary.contains_unknown_injections);
                 cursor.next(text);
                 while let Some(layer) = cursor.item() {
-                    let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() };
+                    let SyntaxLayerContent::Pending { language_name } = &layer.content else {
+                        unreachable!()
+                    };
                     if registry
                         .language_for_name_or_extension(language_name)
                         .now_or_never()
@@ -533,7 +537,9 @@ impl SyntaxSnapshot {
 
             let content = match step.language {
                 ParseStepLanguage::Loaded { language } => {
-                    let Some(grammar) = language.grammar() else { continue };
+                    let Some(grammar) = language.grammar() else {
+                        continue;
+                    };
                     let tree;
                     let changed_ranges;
 
@@ -1279,7 +1285,9 @@ fn get_injections(
     }
 
     for (language, mut included_ranges) in combined_injection_ranges.drain() {
-        included_ranges.sort_unstable();
+        included_ranges.sort_unstable_by(|a, b| {
+            Ord::cmp(&a.start_byte, &b.start_byte).then_with(|| Ord::cmp(&a.end_byte, &b.end_byte))
+        });
         queue.push(ParseStep {
             depth,
             language: ParseStepLanguage::Loaded { language },
@@ -1697,7 +1705,7 @@ impl std::fmt::Debug for SyntaxLayer {
     }
 }
 
-impl<'a> tree_sitter::TextProvider<'a> for TextProvider<'a> {
+impl<'a> tree_sitter::TextProvider<&'a [u8]> for TextProvider<'a> {
     type I = ByteChunks<'a>;
 
     fn text(&mut self, node: tree_sitter::Node) -> Self::I {

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

@@ -932,8 +932,12 @@ fn check_interpolation(
         .zip(new_syntax_map.layers.iter())
     {
         assert_eq!(old_layer.range, new_layer.range);
-        let Some(old_tree) = old_layer.content.tree() else { continue };
-        let Some(new_tree) = new_layer.content.tree() else { continue };
+        let Some(old_tree) = old_layer.content.tree() else {
+            continue;
+        };
+        let Some(new_tree) = new_layer.content.tree() else {
+            continue;
+        };
         let old_start_byte = old_layer.range.start.to_offset(old_buffer);
         let new_start_byte = new_layer.range.start.to_offset(new_buffer);
         let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point();

crates/language_tools/src/lsp_log.rs 🔗

@@ -176,7 +176,9 @@ impl LogStore {
                     cx.notify();
                     LanguageServerState {
                         rpc_state: None,
-                        log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(),
+                        log_buffer: cx
+                            .add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""))
+                            .clone(),
                     }
                 })
                 .log_buffer
@@ -241,7 +243,7 @@ impl LogStore {
         let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
             let io_tx = self.io_tx.clone();
             let language = project.read(cx).languages().language_for_name("JSON");
-            let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
+            let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
             cx.spawn_weak({
                 let buffer = buffer.clone();
                 |_, mut cx| async move {
@@ -327,7 +329,7 @@ impl LspLogView {
             .projects
             .get(&project.downgrade())
             .and_then(|project| project.servers.keys().copied().next());
-        let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
         let mut this = Self {
             editor: Self::editor_for_buffer(project.clone(), buffer, cx),
             project,
@@ -450,7 +452,7 @@ impl View for LspLogView {
 }
 
 impl Item for LspLogView {
-    fn tab_content<V: View>(
+    fn tab_content<V: 'static>(
         &self,
         _: Option<usize>,
         style: &theme::Tab,
@@ -549,7 +551,9 @@ impl View for LspLogToolbarItemView {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         let theme = theme::current(cx).clone();
-        let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
+        let Some(log_view) = self.log_view.as_ref() else {
+            return Empty::new().into_any();
+        };
         let log_view = log_view.read(cx);
         let menu_rows = log_view.menu_items(cx).unwrap_or_default();
 

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -373,6 +373,7 @@ impl View for SyntaxTreeView {
             font_size,
             font_properties: Default::default(),
             underline: Default::default(),
+            soft_wrap: false,
         };
 
         let line_height = cx.font_cache().line_height(font_size);
@@ -451,7 +452,7 @@ impl View for SyntaxTreeView {
 }
 
 impl Item for SyntaxTreeView {
-    fn tab_content<V: View>(
+    fn tab_content<V: 'static>(
         &self,
         _: Option<usize>,
         style: &theme::Tab,

crates/lsp/src/lsp.rs 🔗

@@ -79,7 +79,7 @@ pub enum Subscription {
 }
 
 #[derive(Serialize, Deserialize)]
-struct Request<'a, T> {
+pub struct Request<'a, T> {
     jsonrpc: &'static str,
     id: usize,
     method: &'a str,
@@ -445,7 +445,13 @@ impl LanguageServer {
                     }),
                     inlay_hint: Some(InlayHintClientCapabilities {
                         resolve_support: Some(InlayHintResolveClientCapabilities {
-                            properties: vec!["textEdits".to_string(), "tooltip".to_string()],
+                            properties: vec![
+                                "textEdits".to_string(),
+                                "tooltip".to_string(),
+                                "label.tooltip".to_string(),
+                                "label.location".to_string(),
+                                "label.command".to_string(),
+                            ],
                         }),
                         dynamic_registration: Some(false),
                     }),

crates/menu/src/menu.rs 🔗

@@ -7,6 +7,7 @@ gpui::actions!(
         SelectPrev,
         SelectNext,
         SelectFirst,
-        SelectLast
+        SelectLast,
+        ShowContextMenu
     ]
 );

crates/picker/src/picker.rs 🔗

@@ -13,6 +13,7 @@ use std::{cmp, sync::Arc};
 use util::ResultExt;
 use workspace::Modal;
 
+#[derive(Clone, Copy)]
 pub enum PickerEvent {
     Dismiss,
 }

crates/project/src/lsp_command.rs 🔗

@@ -1,25 +1,27 @@
 use crate::{
     DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
     InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
-    MarkupContent, Project, ProjectTransaction,
+    MarkupContent, Project, ProjectTransaction, ResolveState,
 };
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use client::proto::{self, PeerId};
-use fs::LineEnding;
+use futures::future;
 use gpui::{AppContext, AsyncAppContext, ModelHandle};
 use language::{
     language_settings::{language_settings, InlayHintKind},
     point_from_lsp, point_to_lsp,
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
-    range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
-    Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
+    range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
+    CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
+    Unclipped,
 };
 use lsp::{
     CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
-    ServerCapabilities,
+    OneOf, ServerCapabilities,
 };
 use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
+use text::LineEnding;
 
 pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
     lsp::FormattingOptions {
@@ -1467,7 +1469,7 @@ impl LspCommand for GetCompletions {
                 })
         });
 
-        Ok(futures::future::join_all(completions).await)
+        Ok(future::join_all(completions).await)
     }
 
     fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions {
@@ -1535,7 +1537,7 @@ impl LspCommand for GetCompletions {
         let completions = message.completions.into_iter().map(|completion| {
             language::proto::deserialize_completion(completion, language.clone())
         });
-        futures::future::try_join_all(completions).await
+        future::try_join_all(completions).await
     }
 
     fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 {
@@ -1689,7 +1691,11 @@ impl LspCommand for OnTypeFormatting {
     type ProtoRequest = proto::OnTypeFormatting;
 
     fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
-        let Some(on_type_formatting_options) = &server_capabilities.document_on_type_formatting_provider else { return false };
+        let Some(on_type_formatting_options) =
+            &server_capabilities.document_on_type_formatting_provider
+        else {
+            return false;
+        };
         on_type_formatting_options
             .first_trigger_character
             .contains(&self.trigger)
@@ -1803,7 +1809,9 @@ impl LspCommand for OnTypeFormatting {
         _: ModelHandle<Buffer>,
         _: AsyncAppContext,
     ) -> Result<Option<Transaction>> {
-        let Some(transaction) = message.transaction else { return Ok(None) };
+        let Some(transaction) = message.transaction else {
+            return Ok(None);
+        };
         Ok(Some(language::proto::deserialize_transaction(transaction)?))
     }
 
@@ -1812,6 +1820,377 @@ impl LspCommand for OnTypeFormatting {
     }
 }
 
+impl InlayHints {
+    pub async fn lsp_to_project_hint(
+        lsp_hint: lsp::InlayHint,
+        buffer_handle: &ModelHandle<Buffer>,
+        server_id: LanguageServerId,
+        resolve_state: ResolveState,
+        force_no_type_left_padding: bool,
+        cx: &mut AsyncAppContext,
+    ) -> anyhow::Result<InlayHint> {
+        let kind = lsp_hint.kind.and_then(|kind| match kind {
+            lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type),
+            lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
+            _ => None,
+        });
+
+        let position = cx.update(|cx| {
+            let buffer = buffer_handle.read(cx);
+            let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
+            if kind == Some(InlayHintKind::Parameter) {
+                buffer.anchor_before(position)
+            } else {
+                buffer.anchor_after(position)
+            }
+        });
+        let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id)
+            .await
+            .context("lsp to project inlay hint conversion")?;
+        let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
+            false
+        } else {
+            lsp_hint.padding_left.unwrap_or(false)
+        };
+
+        Ok(InlayHint {
+            position,
+            padding_left,
+            padding_right: lsp_hint.padding_right.unwrap_or(false),
+            label,
+            kind,
+            tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip {
+                lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s),
+                lsp::InlayHintTooltip::MarkupContent(markup_content) => {
+                    InlayHintTooltip::MarkupContent(MarkupContent {
+                        kind: match markup_content.kind {
+                            lsp::MarkupKind::PlainText => HoverBlockKind::PlainText,
+                            lsp::MarkupKind::Markdown => HoverBlockKind::Markdown,
+                        },
+                        value: markup_content.value,
+                    })
+                }
+            }),
+            resolve_state,
+        })
+    }
+
+    async fn lsp_inlay_label_to_project(
+        lsp_label: lsp::InlayHintLabel,
+        server_id: LanguageServerId,
+    ) -> anyhow::Result<InlayHintLabel> {
+        let label = match lsp_label {
+            lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
+            lsp::InlayHintLabel::LabelParts(lsp_parts) => {
+                let mut parts = Vec::with_capacity(lsp_parts.len());
+                for lsp_part in lsp_parts {
+                    parts.push(InlayHintLabelPart {
+                        value: lsp_part.value,
+                        tooltip: lsp_part.tooltip.map(|tooltip| match tooltip {
+                            lsp::InlayHintLabelPartTooltip::String(s) => {
+                                InlayHintLabelPartTooltip::String(s)
+                            }
+                            lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => {
+                                InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
+                                    kind: match markup_content.kind {
+                                        lsp::MarkupKind::PlainText => HoverBlockKind::PlainText,
+                                        lsp::MarkupKind::Markdown => HoverBlockKind::Markdown,
+                                    },
+                                    value: markup_content.value,
+                                })
+                            }
+                        }),
+                        location: Some(server_id).zip(lsp_part.location),
+                    });
+                }
+                InlayHintLabel::LabelParts(parts)
+            }
+        };
+
+        Ok(label)
+    }
+
+    pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint {
+        let (state, lsp_resolve_state) = match response_hint.resolve_state {
+            ResolveState::Resolved => (0, None),
+            ResolveState::CanResolve(server_id, resolve_data) => (
+                1,
+                resolve_data
+                    .map(|json_data| {
+                        serde_json::to_string(&json_data)
+                            .expect("failed to serialize resolve json data")
+                    })
+                    .map(|value| proto::resolve_state::LspResolveState {
+                        server_id: server_id.0 as u64,
+                        value,
+                    }),
+            ),
+            ResolveState::Resolving => (2, None),
+        };
+        let resolve_state = Some(proto::ResolveState {
+            state,
+            lsp_resolve_state,
+        });
+        proto::InlayHint {
+            position: Some(language::proto::serialize_anchor(&response_hint.position)),
+            padding_left: response_hint.padding_left,
+            padding_right: response_hint.padding_right,
+            label: Some(proto::InlayHintLabel {
+                label: Some(match response_hint.label {
+                    InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
+                    InlayHintLabel::LabelParts(label_parts) => {
+                        proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
+                            parts: label_parts.into_iter().map(|label_part| {
+                                let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string());
+                                let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column });
+                                let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column });
+                                proto::InlayHintLabelPart {
+                                value: label_part.value,
+                                tooltip: label_part.tooltip.map(|tooltip| {
+                                    let proto_tooltip = match tooltip {
+                                        InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
+                                        InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
+                                            is_markdown: markup_content.kind == HoverBlockKind::Markdown,
+                                            value: markup_content.value,
+                                        }),
+                                    };
+                                    proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
+                                }),
+                                location_url,
+                                location_range_start,
+                                location_range_end,
+                                language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64),
+                            }}).collect()
+                        })
+                    }
+                }),
+            }),
+            kind: response_hint.kind.map(|kind| kind.name().to_string()),
+            tooltip: response_hint.tooltip.map(|response_tooltip| {
+                let proto_tooltip = match response_tooltip {
+                    InlayHintTooltip::String(s) => proto::inlay_hint_tooltip::Content::Value(s),
+                    InlayHintTooltip::MarkupContent(markup_content) => {
+                        proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent {
+                            is_markdown: markup_content.kind == HoverBlockKind::Markdown,
+                            value: markup_content.value,
+                        })
+                    }
+                };
+                proto::InlayHintTooltip {
+                    content: Some(proto_tooltip),
+                }
+            }),
+            resolve_state,
+        }
+    }
+
+    pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result<InlayHint> {
+        let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| {
+            panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",)
+        });
+        let resolve_state_data = resolve_state
+            .lsp_resolve_state.as_ref()
+            .map(|lsp_resolve_state| {
+                serde_json::from_str::<Option<lsp::LSPAny>>(&lsp_resolve_state.value)
+                    .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}"))
+                    .map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state))
+            })
+            .transpose()?;
+        let resolve_state = match resolve_state.state {
+            0 => ResolveState::Resolved,
+            1 => {
+                let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| {
+                    format!(
+                        "No lsp resolve data for the hint that can be resolved: {message_hint:?}"
+                    )
+                })?;
+                ResolveState::CanResolve(server_id, lsp_resolve_state)
+            }
+            2 => ResolveState::Resolving,
+            invalid => {
+                anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}")
+            }
+        };
+        Ok(InlayHint {
+            position: message_hint
+                .position
+                .and_then(language::proto::deserialize_anchor)
+                .context("invalid position")?,
+            label: match message_hint
+                .label
+                .and_then(|label| label.label)
+                .context("missing label")?
+            {
+                proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s),
+                proto::inlay_hint_label::Label::LabelParts(parts) => {
+                    let mut label_parts = Vec::new();
+                    for part in parts.parts {
+                        label_parts.push(InlayHintLabelPart {
+                            value: part.value,
+                            tooltip: part.tooltip.map(|tooltip| match tooltip.content {
+                                Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => {
+                                    InlayHintLabelPartTooltip::String(s)
+                                }
+                                Some(
+                                    proto::inlay_hint_label_part_tooltip::Content::MarkupContent(
+                                        markup_content,
+                                    ),
+                                ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
+                                    kind: if markup_content.is_markdown {
+                                        HoverBlockKind::Markdown
+                                    } else {
+                                        HoverBlockKind::PlainText
+                                    },
+                                    value: markup_content.value,
+                                }),
+                                None => InlayHintLabelPartTooltip::String(String::new()),
+                            }),
+                            location: {
+                                match part
+                                    .location_url
+                                    .zip(
+                                        part.location_range_start.and_then(|start| {
+                                            Some(start..part.location_range_end?)
+                                        }),
+                                    )
+                                    .zip(part.language_server_id)
+                                {
+                                    Some(((uri, range), server_id)) => Some((
+                                        LanguageServerId(server_id as usize),
+                                        lsp::Location {
+                                            uri: lsp::Url::parse(&uri)
+                                                .context("invalid uri in hint part {part:?}")?,
+                                            range: lsp::Range::new(
+                                                point_to_lsp(PointUtf16::new(
+                                                    range.start.row,
+                                                    range.start.column,
+                                                )),
+                                                point_to_lsp(PointUtf16::new(
+                                                    range.end.row,
+                                                    range.end.column,
+                                                )),
+                                            ),
+                                        },
+                                    )),
+                                    None => None,
+                                }
+                            },
+                        });
+                    }
+
+                    InlayHintLabel::LabelParts(label_parts)
+                }
+            },
+            padding_left: message_hint.padding_left,
+            padding_right: message_hint.padding_right,
+            kind: message_hint
+                .kind
+                .as_deref()
+                .and_then(InlayHintKind::from_name),
+            tooltip: message_hint.tooltip.and_then(|tooltip| {
+                Some(match tooltip.content? {
+                    proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s),
+                    proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => {
+                        InlayHintTooltip::MarkupContent(MarkupContent {
+                            kind: if markup_content.is_markdown {
+                                HoverBlockKind::Markdown
+                            } else {
+                                HoverBlockKind::PlainText
+                            },
+                            value: markup_content.value,
+                        })
+                    }
+                })
+            }),
+            resolve_state,
+        })
+    }
+
+    pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp::InlayHint {
+        lsp::InlayHint {
+            position: point_to_lsp(hint.position.to_point_utf16(snapshot)),
+            kind: hint.kind.map(|kind| match kind {
+                InlayHintKind::Type => lsp::InlayHintKind::TYPE,
+                InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER,
+            }),
+            text_edits: None,
+            tooltip: hint.tooltip.and_then(|tooltip| {
+                Some(match tooltip {
+                    InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s),
+                    InlayHintTooltip::MarkupContent(markup_content) => {
+                        lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent {
+                            kind: match markup_content.kind {
+                                HoverBlockKind::PlainText => lsp::MarkupKind::PlainText,
+                                HoverBlockKind::Markdown => lsp::MarkupKind::Markdown,
+                                HoverBlockKind::Code { .. } => return None,
+                            },
+                            value: markup_content.value,
+                        })
+                    }
+                })
+            }),
+            label: match hint.label {
+                InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s),
+                InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts(
+                    label_parts
+                        .into_iter()
+                        .map(|part| lsp::InlayHintLabelPart {
+                            value: part.value,
+                            tooltip: part.tooltip.and_then(|tooltip| {
+                                Some(match tooltip {
+                                    InlayHintLabelPartTooltip::String(s) => {
+                                        lsp::InlayHintLabelPartTooltip::String(s)
+                                    }
+                                    InlayHintLabelPartTooltip::MarkupContent(markup_content) => {
+                                        lsp::InlayHintLabelPartTooltip::MarkupContent(
+                                            lsp::MarkupContent {
+                                                kind: match markup_content.kind {
+                                                    HoverBlockKind::PlainText => {
+                                                        lsp::MarkupKind::PlainText
+                                                    }
+                                                    HoverBlockKind::Markdown => {
+                                                        lsp::MarkupKind::Markdown
+                                                    }
+                                                    HoverBlockKind::Code { .. } => return None,
+                                                },
+                                                value: markup_content.value,
+                                            },
+                                        )
+                                    }
+                                })
+                            }),
+                            location: part.location.map(|(_, location)| location),
+                            command: None,
+                        })
+                        .collect(),
+                ),
+            },
+            padding_left: Some(hint.padding_left),
+            padding_right: Some(hint.padding_right),
+            data: match hint.resolve_state {
+                ResolveState::CanResolve(_, data) => data,
+                ResolveState::Resolving | ResolveState::Resolved => None,
+            },
+        }
+    }
+
+    pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool {
+        capabilities
+            .inlay_hint_provider
+            .as_ref()
+            .and_then(|options| match options {
+                OneOf::Left(_is_supported) => None,
+                OneOf::Right(capabilities) => match capabilities {
+                    lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider,
+                    lsp::InlayHintServerCapabilities::RegistrationOptions(o) => {
+                        o.inlay_hint_options.resolve_provider
+                    }
+                },
+            })
+            .unwrap_or(false)
+    }
+}
+
 #[async_trait(?Send)]
 impl LspCommand for InlayHints {
     type Response = Vec<InlayHint>;
@@ -1819,7 +2198,9 @@ impl LspCommand for InlayHints {
     type ProtoRequest = proto::InlayHints;
 
     fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
-        let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false };
+        let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else {
+            return false;
+        };
         match inlay_hint_provider {
             lsp::OneOf::Left(enabled) => *enabled,
             lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities {
@@ -1852,8 +2233,9 @@ impl LspCommand for InlayHints {
         buffer: ModelHandle<Buffer>,
         server_id: LanguageServerId,
         mut cx: AsyncAppContext,
-    ) -> Result<Vec<InlayHint>> {
-        let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+    ) -> anyhow::Result<Vec<InlayHint>> {
+        let (lsp_adapter, lsp_server) =
+            language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
         // `typescript-language-server` adds padding to the left for type hints, turning
         // `const foo: boolean` into `const foo : boolean` which looks odd.
         // `rust-analyzer` does not have the padding for this case, and we have to accomodate both.
@@ -1863,93 +2245,32 @@ impl LspCommand for InlayHints {
         // Hence let's use a heuristic first to handle the most awkward case and look for more.
         let force_no_type_left_padding =
             lsp_adapter.name.0.as_ref() == "typescript-language-server";
-        cx.read(|cx| {
-            let origin_buffer = buffer.read(cx);
-            Ok(message
-                .unwrap_or_default()
-                .into_iter()
-                .map(|lsp_hint| {
-                    let kind = lsp_hint.kind.and_then(|kind| match kind {
-                        lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type),
-                        lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
-                        _ => None,
-                    });
-                    let position = origin_buffer
-                        .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
-                    let padding_left =
-                        if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
-                            false
-                        } else {
-                            lsp_hint.padding_left.unwrap_or(false)
-                        };
-                    InlayHint {
-                        buffer_id: origin_buffer.remote_id(),
-                        position: if kind == Some(InlayHintKind::Parameter) {
-                            origin_buffer.anchor_before(position)
-                        } else {
-                            origin_buffer.anchor_after(position)
-                        },
-                        padding_left,
-                        padding_right: lsp_hint.padding_right.unwrap_or(false),
-                        label: match lsp_hint.label {
-                            lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
-                            lsp::InlayHintLabel::LabelParts(lsp_parts) => {
-                                InlayHintLabel::LabelParts(
-                                    lsp_parts
-                                        .into_iter()
-                                        .map(|label_part| InlayHintLabelPart {
-                                            value: label_part.value,
-                                            tooltip: label_part.tooltip.map(
-                                                |tooltip| {
-                                                    match tooltip {
-                                        lsp::InlayHintLabelPartTooltip::String(s) => {
-                                            InlayHintLabelPartTooltip::String(s)
-                                        }
-                                        lsp::InlayHintLabelPartTooltip::MarkupContent(
-                                            markup_content,
-                                        ) => InlayHintLabelPartTooltip::MarkupContent(
-                                            MarkupContent {
-                                                kind: format!("{:?}", markup_content.kind),
-                                                value: markup_content.value,
-                                            },
-                                        ),
-                                    }
-                                                },
-                                            ),
-                                            location: label_part.location.map(|lsp_location| {
-                                                let target_start = origin_buffer.clip_point_utf16(
-                                                    point_from_lsp(lsp_location.range.start),
-                                                    Bias::Left,
-                                                );
-                                                let target_end = origin_buffer.clip_point_utf16(
-                                                    point_from_lsp(lsp_location.range.end),
-                                                    Bias::Left,
-                                                );
-                                                Location {
-                                                    buffer: buffer.clone(),
-                                                    range: origin_buffer.anchor_after(target_start)
-                                                        ..origin_buffer.anchor_before(target_end),
-                                                }
-                                            }),
-                                        })
-                                        .collect(),
-                                )
-                            }
-                        },
-                        kind,
-                        tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip {
-                            lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s),
-                            lsp::InlayHintTooltip::MarkupContent(markup_content) => {
-                                InlayHintTooltip::MarkupContent(MarkupContent {
-                                    kind: format!("{:?}", markup_content.kind),
-                                    value: markup_content.value,
-                                })
-                            }
-                        }),
-                    }
-                })
-                .collect())
-        })
+
+        let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| {
+            let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) {
+                ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone())
+            } else {
+                ResolveState::Resolved
+            };
+
+            let buffer = buffer.clone();
+            cx.spawn(|mut cx| async move {
+                InlayHints::lsp_to_project_hint(
+                    lsp_hint,
+                    &buffer,
+                    server_id,
+                    resolve_state,
+                    force_no_type_left_padding,
+                    &mut cx,
+                )
+                .await
+            })
+        });
+        future::join_all(hints)
+            .await
+            .into_iter()
+            .collect::<anyhow::Result<_>>()
+            .context("lsp to project inlay hints conversion")
     }
 
     fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
@@ -1995,23 +2316,7 @@ impl LspCommand for InlayHints {
         proto::InlayHintsResponse {
             hints: response
                 .into_iter()
-                .map(|response_hint| proto::InlayHint {
-                    position: Some(language::proto::serialize_anchor(&response_hint.position)),
-                    padding_left: response_hint.padding_left,
-                    padding_right: response_hint.padding_right,
-                    kind: response_hint.kind.map(|kind| kind.name().to_string()),
-                    // Do not pass extra data such as tooltips to clients: host can put tooltip data from the cache during resolution.
-                    tooltip: None,
-                    // Similarly, do not pass label parts to clients: host can return a detailed list during resolution.
-                    label: Some(proto::InlayHintLabel {
-                        label: Some(proto::inlay_hint_label::Label::Value(
-                            match response_hint.label {
-                                InlayHintLabel::String(s) => s,
-                                InlayHintLabel::LabelParts(_) => response_hint.text(),
-                            },
-                        )),
-                    }),
-                })
+                .map(|response_hint| InlayHints::project_to_proto_hint(response_hint))
                 .collect(),
             version: serialize_version(buffer_version),
         }
@@ -2020,10 +2325,10 @@ impl LspCommand for InlayHints {
     async fn response_from_proto(
         self,
         message: proto::InlayHintsResponse,
-        project: ModelHandle<Project>,
+        _: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
         mut cx: AsyncAppContext,
-    ) -> Result<Vec<InlayHint>> {
+    ) -> anyhow::Result<Vec<InlayHint>> {
         buffer
             .update(&mut cx, |buffer, _| {
                 buffer.wait_for_version(deserialize_version(&message.version))
@@ -2032,82 +2337,7 @@ impl LspCommand for InlayHints {
 
         let mut hints = Vec::new();
         for message_hint in message.hints {
-            let buffer_id = message_hint
-                .position
-                .as_ref()
-                .and_then(|location| location.buffer_id)
-                .context("missing buffer id")?;
-            let hint = InlayHint {
-                buffer_id,
-                position: message_hint
-                    .position
-                    .and_then(language::proto::deserialize_anchor)
-                    .context("invalid position")?,
-                label: match message_hint
-                    .label
-                    .and_then(|label| label.label)
-                    .context("missing label")?
-                {
-                    proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s),
-                    proto::inlay_hint_label::Label::LabelParts(parts) => {
-                        let mut label_parts = Vec::new();
-                        for part in parts.parts {
-                            label_parts.push(InlayHintLabelPart {
-                                value: part.value,
-                                tooltip: part.tooltip.map(|tooltip| match tooltip.content {
-                                    Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s),
-                                    Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
-                                        kind: markup_content.kind,
-                                        value: markup_content.value,
-                                    }),
-                                    None => InlayHintLabelPartTooltip::String(String::new()),
-                                }),
-                                location: match part.location {
-                                    Some(location) => {
-                                        let target_buffer = project
-                                            .update(&mut cx, |this, cx| {
-                                                this.wait_for_remote_buffer(location.buffer_id, cx)
-                                            })
-                                            .await?;
-                                        Some(Location {
-                                        range: location
-                                            .start
-                                            .and_then(language::proto::deserialize_anchor)
-                                            .context("invalid start")?
-                                            ..location
-                                                .end
-                                                .and_then(language::proto::deserialize_anchor)
-                                                .context("invalid end")?,
-                                        buffer: target_buffer,
-                                    })},
-                                    None => None,
-                                },
-                            });
-                        }
-
-                        InlayHintLabel::LabelParts(label_parts)
-                    }
-                },
-                padding_left: message_hint.padding_left,
-                padding_right: message_hint.padding_right,
-                kind: message_hint
-                    .kind
-                    .as_deref()
-                    .and_then(InlayHintKind::from_name),
-                tooltip: message_hint.tooltip.and_then(|tooltip| {
-                    Some(match tooltip.content? {
-                        proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s),
-                        proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => {
-                            InlayHintTooltip::MarkupContent(MarkupContent {
-                                kind: markup_content.kind,
-                                value: markup_content.value,
-                            })
-                        }
-                    })
-                }),
-            };
-
-            hints.push(hint);
+            hints.push(InlayHints::proto_to_project_hint(message_hint)?);
         }
 
         Ok(hints)

crates/project/src/project.rs 🔗

@@ -11,7 +11,7 @@ mod project_tests;
 mod worktree_tests;
 
 use anyhow::{anyhow, Context, Result};
-use client::{proto, Client, TypedEnvelope, UserStore};
+use client::{proto, Client, TypedEnvelope, UserId, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use copilot::Copilot;
@@ -26,8 +26,8 @@ use futures::{
 };
 use globset::{Glob, GlobSet, GlobSetBuilder};
 use gpui::{
-    AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
-    ModelHandle, Task, WeakModelHandle,
+    executor::Background, AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity,
+    ModelContext, ModelHandle, Task, WeakModelHandle,
 };
 use itertools::Itertools;
 use language::{
@@ -37,11 +37,11 @@ use language::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
         serialize_anchor, serialize_version,
     },
-    range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
-    Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
-    Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt,
-    Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
-    ToPointUtf16, Transaction, Unclipped,
+    range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
+    CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
+    File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
+    OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
+    ToOffset, ToPointUtf16, Transaction, Unclipped,
 };
 use log::error;
 use lsp::{
@@ -57,8 +57,8 @@ use serde::Serialize;
 use settings::SettingsStore;
 use sha2::{Digest, Sha256};
 use similar::{ChangeTag, TextDiff};
+use smol::channel::{Receiver, Sender};
 use std::{
-    cell::RefCell,
     cmp::{self, Ordering},
     convert::TryInto,
     hash::Hash,
@@ -67,7 +67,6 @@ use std::{
     ops::Range,
     path::{self, Component, Path, PathBuf},
     process::Stdio,
-    rc::Rc,
     str,
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
@@ -250,6 +249,7 @@ enum ProjectClientState {
 pub struct Collaborator {
     pub peer_id: proto::PeerId,
     pub replica_id: ReplicaId,
+    pub user_id: UserId,
 }
 
 #[derive(Clone, Debug, PartialEq)]
@@ -281,8 +281,9 @@ pub enum Event {
         old_peer_id: proto::PeerId,
         new_peer_id: proto::PeerId,
     },
+    CollaboratorJoined(proto::PeerId),
     CollaboratorLeft(proto::PeerId),
-    RefreshInlays,
+    RefreshInlayHints,
 }
 
 pub enum LanguageServerState {
@@ -331,15 +332,22 @@ pub struct Location {
     pub range: Range<language::Anchor>,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct InlayHint {
-    pub buffer_id: u64,
     pub position: language::Anchor,
     pub label: InlayHintLabel,
     pub kind: Option<InlayHintKind>,
     pub padding_left: bool,
     pub padding_right: bool,
     pub tooltip: Option<InlayHintTooltip>,
+    pub resolve_state: ResolveState,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ResolveState {
+    Resolved,
+    CanResolve(LanguageServerId, Option<lsp::LSPAny>),
+    Resolving,
 }
 
 impl InlayHint {
@@ -351,34 +359,34 @@ impl InlayHint {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum InlayHintLabel {
     String(String),
     LabelParts(Vec<InlayHintLabelPart>),
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct InlayHintLabelPart {
     pub value: String,
     pub tooltip: Option<InlayHintLabelPartTooltip>,
-    pub location: Option<Location>,
+    pub location: Option<(LanguageServerId, lsp::Location)>,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum InlayHintTooltip {
     String(String),
     MarkupContent(MarkupContent),
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum InlayHintLabelPartTooltip {
     String(String),
     MarkupContent(MarkupContent),
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct MarkupContent {
-    pub kind: String,
+    pub kind: HoverBlockKind,
     pub value: String,
 }
 
@@ -412,7 +420,7 @@ pub struct HoverBlock {
     pub kind: HoverBlockKind,
 }
 
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum HoverBlockKind {
     PlainText,
     Markdown,
@@ -516,6 +524,28 @@ impl FormatTrigger {
         }
     }
 }
+#[derive(Clone, Debug, PartialEq)]
+enum SearchMatchCandidate {
+    OpenBuffer {
+        buffer: ModelHandle<Buffer>,
+        // This might be an unnamed file without representation on filesystem
+        path: Option<Arc<Path>>,
+    },
+    Path {
+        worktree_id: WorktreeId,
+        path: Arc<Path>,
+    },
+}
+
+type SearchMatchCandidateIndex = usize;
+impl SearchMatchCandidate {
+    fn path(&self) -> Option<Arc<Path>> {
+        match self {
+            SearchMatchCandidate::OpenBuffer { path, .. } => path.clone(),
+            SearchMatchCandidate::Path { path, .. } => Some(path.clone()),
+        }
+    }
+}
 
 impl Project {
     pub fn init_settings(cx: &mut AppContext) {
@@ -549,6 +579,7 @@ impl Project {
         client.add_model_request_handler(Self::handle_apply_code_action);
         client.add_model_request_handler(Self::handle_on_type_formatting);
         client.add_model_request_handler(Self::handle_inlay_hints);
+        client.add_model_request_handler(Self::handle_resolve_inlay_hint);
         client.add_model_request_handler(Self::handle_refresh_inlay_hints);
         client.add_model_request_handler(Self::handle_reload_buffers);
         client.add_model_request_handler(Self::handle_synchronize_buffers);
@@ -1537,9 +1568,9 @@ impl Project {
         if self.is_remote() {
             return Err(anyhow!("creating buffers as a guest is not supported yet"));
         }
-
+        let id = post_inc(&mut self.next_buffer_id);
         let buffer = cx.add_model(|cx| {
-            Buffer::new(self.replica_id(), text, cx)
+            Buffer::new(self.replica_id(), id, text)
                 .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx)
         });
         self.register_buffer(&buffer, cx)?;
@@ -1677,7 +1708,7 @@ impl Project {
     }
 
     /// LanguageServerName is owned, because it is inserted into a map
-    fn open_local_buffer_via_lsp(
+    pub fn open_local_buffer_via_lsp(
         &mut self,
         abs_path: lsp::Url,
         language_server_id: LanguageServerId,
@@ -2872,7 +2903,7 @@ impl Project {
                         .upgrade(&cx)
                         .ok_or_else(|| anyhow!("project dropped"))?;
                     this.update(&mut cx, |project, cx| {
-                        cx.emit(Event::RefreshInlays);
+                        cx.emit(Event::RefreshInlayHints);
                         project.remote_id().map(|project_id| {
                             project.client.send(proto::RefreshInlayHints { project_id })
                         })
@@ -3438,7 +3469,7 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) {
         if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
-            cx.emit(Event::RefreshInlays);
+            cx.emit(Event::RefreshInlayHints);
             status.pending_work.remove(&token);
             cx.notify();
         }
@@ -4496,10 +4527,20 @@ impl Project {
             };
 
             cx.spawn(|this, mut cx| async move {
-                let additional_text_edits = lang_server
-                    .request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
-                    .await?
-                    .additional_text_edits;
+                let can_resolve = lang_server
+                    .capabilities()
+                    .completion_provider
+                    .as_ref()
+                    .and_then(|options| options.resolve_provider)
+                    .unwrap_or(false);
+                let additional_text_edits = if can_resolve {
+                    lang_server
+                        .request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
+                        .await?
+                        .additional_text_edits
+                } else {
+                    completion.lsp_completion.additional_text_edits
+                };
                 if let Some(edits) = additional_text_edits {
                     let edits = this
                         .update(&mut cx, |this, cx| {
@@ -4999,7 +5040,7 @@ impl Project {
         buffer_handle: ModelHandle<Buffer>,
         range: Range<T>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Vec<InlayHint>>> {
+    ) -> Task<anyhow::Result<Vec<InlayHint>>> {
         let buffer = buffer_handle.read(cx);
         let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
         let range_start = range.start;
@@ -5049,192 +5090,79 @@ impl Project {
         }
     }
 
-    #[allow(clippy::type_complexity)]
-    pub fn search(
+    pub fn resolve_inlay_hint(
         &self,
-        query: SearchQuery,
+        hint: InlayHint,
+        buffer_handle: ModelHandle<Buffer>,
+        server_id: LanguageServerId,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<HashMap<ModelHandle<Buffer>, Vec<Range<Anchor>>>>> {
+    ) -> Task<anyhow::Result<InlayHint>> {
         if self.is_local() {
-            let snapshots = self
-                .visible_worktrees(cx)
-                .filter_map(|tree| {
-                    let tree = tree.read(cx).as_local()?;
-                    Some(tree.snapshot())
-                })
-                .collect::<Vec<_>>();
-
-            let background = cx.background().clone();
-            let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum();
-            if path_count == 0 {
-                return Task::ready(Ok(Default::default()));
+            let buffer = buffer_handle.read(cx);
+            let (_, lang_server) = if let Some((adapter, server)) =
+                self.language_server_for_buffer(buffer, server_id, cx)
+            {
+                (adapter.clone(), server.clone())
+            } else {
+                return Task::ready(Ok(hint));
+            };
+            if !InlayHints::can_resolve_inlays(lang_server.capabilities()) {
+                return Task::ready(Ok(hint));
             }
-            let workers = background.num_cpus().min(path_count);
-            let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024);
-            cx.background()
-                .spawn({
-                    let fs = self.fs.clone();
-                    let background = cx.background().clone();
-                    let query = query.clone();
-                    async move {
-                        let fs = &fs;
-                        let query = &query;
-                        let matching_paths_tx = &matching_paths_tx;
-                        let paths_per_worker = (path_count + workers - 1) / workers;
-                        let snapshots = &snapshots;
-                        background
-                            .scoped(|scope| {
-                                for worker_ix in 0..workers {
-                                    let worker_start_ix = worker_ix * paths_per_worker;
-                                    let worker_end_ix = worker_start_ix + paths_per_worker;
-                                    scope.spawn(async move {
-                                        let mut snapshot_start_ix = 0;
-                                        let mut abs_path = PathBuf::new();
-                                        for snapshot in snapshots {
-                                            let snapshot_end_ix =
-                                                snapshot_start_ix + snapshot.visible_file_count();
-                                            if worker_end_ix <= snapshot_start_ix {
-                                                break;
-                                            } else if worker_start_ix > snapshot_end_ix {
-                                                snapshot_start_ix = snapshot_end_ix;
-                                                continue;
-                                            } else {
-                                                let start_in_snapshot = worker_start_ix
-                                                    .saturating_sub(snapshot_start_ix);
-                                                let end_in_snapshot =
-                                                    cmp::min(worker_end_ix, snapshot_end_ix)
-                                                        - snapshot_start_ix;
-
-                                                for entry in snapshot
-                                                    .files(false, start_in_snapshot)
-                                                    .take(end_in_snapshot - start_in_snapshot)
-                                                {
-                                                    if matching_paths_tx.is_closed() {
-                                                        break;
-                                                    }
-                                                    let matches = if query
-                                                        .file_matches(Some(&entry.path))
-                                                    {
-                                                        abs_path.clear();
-                                                        abs_path.push(&snapshot.abs_path());
-                                                        abs_path.push(&entry.path);
-                                                        if let Some(file) =
-                                                            fs.open_sync(&abs_path).await.log_err()
-                                                        {
-                                                            query.detect(file).unwrap_or(false)
-                                                        } else {
-                                                            false
-                                                        }
-                                                    } else {
-                                                        false
-                                                    };
-
-                                                    if matches {
-                                                        let project_path =
-                                                            (snapshot.id(), entry.path.clone());
-                                                        if matching_paths_tx
-                                                            .send(project_path)
-                                                            .await
-                                                            .is_err()
-                                                        {
-                                                            break;
-                                                        }
-                                                    }
-                                                }
-
-                                                snapshot_start_ix = snapshot_end_ix;
-                                            }
-                                        }
-                                    });
-                                }
-                            })
-                            .await;
-                    }
-                })
-                .detach();
-
-            let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
-            let open_buffers = self
-                .opened_buffers
-                .values()
-                .filter_map(|b| b.upgrade(cx))
-                .collect::<HashSet<_>>();
-            cx.spawn(|this, cx| async move {
-                for buffer in &open_buffers {
-                    let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
-                    buffers_tx.send((buffer.clone(), snapshot)).await?;
-                }
-
-                let open_buffers = Rc::new(RefCell::new(open_buffers));
-                while let Some(project_path) = matching_paths_rx.next().await {
-                    if buffers_tx.is_closed() {
-                        break;
-                    }
-
-                    let this = this.clone();
-                    let open_buffers = open_buffers.clone();
-                    let buffers_tx = buffers_tx.clone();
-                    cx.spawn(|mut cx| async move {
-                        if let Some(buffer) = this
-                            .update(&mut cx, |this, cx| this.open_buffer(project_path, cx))
-                            .await
-                            .log_err()
-                        {
-                            if open_buffers.borrow_mut().insert(buffer.clone()) {
-                                let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
-                                buffers_tx.send((buffer, snapshot)).await?;
-                            }
-                        }
-
-                        Ok::<_, anyhow::Error>(())
-                    })
-                    .detach();
-                }
 
-                Ok::<_, anyhow::Error>(())
+            let buffer_snapshot = buffer.snapshot();
+            cx.spawn(|_, mut cx| async move {
+                let resolve_task = lang_server.request::<lsp::request::InlayHintResolveRequest>(
+                    InlayHints::project_to_lsp_hint(hint, &buffer_snapshot),
+                );
+                let resolved_hint = resolve_task
+                    .await
+                    .context("inlay hint resolve LSP request")?;
+                let resolved_hint = InlayHints::lsp_to_project_hint(
+                    resolved_hint,
+                    &buffer_handle,
+                    server_id,
+                    ResolveState::Resolved,
+                    false,
+                    &mut cx,
+                )
+                .await?;
+                Ok(resolved_hint)
             })
-            .detach_and_log_err(cx);
-
-            let background = cx.background().clone();
-            cx.background().spawn(async move {
-                let query = &query;
-                let mut matched_buffers = Vec::new();
-                for _ in 0..workers {
-                    matched_buffers.push(HashMap::default());
+        } else if let Some(project_id) = self.remote_id() {
+            let client = self.client.clone();
+            let request = proto::ResolveInlayHint {
+                project_id,
+                buffer_id: buffer_handle.read(cx).remote_id(),
+                language_server_id: server_id.0 as u64,
+                hint: Some(InlayHints::project_to_proto_hint(hint.clone())),
+            };
+            cx.spawn(|_, _| async move {
+                let response = client
+                    .request(request)
+                    .await
+                    .context("inlay hints proto request")?;
+                match response.hint {
+                    Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint)
+                        .context("inlay hints proto resolve response conversion"),
+                    None => Ok(hint),
                 }
-                background
-                    .scoped(|scope| {
-                        for worker_matched_buffers in matched_buffers.iter_mut() {
-                            let mut buffers_rx = buffers_rx.clone();
-                            scope.spawn(async move {
-                                while let Some((buffer, snapshot)) = buffers_rx.next().await {
-                                    let buffer_matches = if query.file_matches(
-                                        snapshot.file().map(|file| file.path().as_ref()),
-                                    ) {
-                                        query
-                                            .search(&snapshot, None)
-                                            .await
-                                            .iter()
-                                            .map(|range| {
-                                                snapshot.anchor_before(range.start)
-                                                    ..snapshot.anchor_after(range.end)
-                                            })
-                                            .collect()
-                                    } else {
-                                        Vec::new()
-                                    };
-                                    if !buffer_matches.is_empty() {
-                                        worker_matched_buffers
-                                            .insert(buffer.clone(), buffer_matches);
-                                    }
-                                }
-                            });
-                        }
-                    })
-                    .await;
-                Ok(matched_buffers.into_iter().flatten().collect())
             })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
+    #[allow(clippy::type_complexity)]
+    pub fn search(
+        &self,
+        query: SearchQuery,
+        cx: &mut ModelContext<Self>,
+    ) -> Receiver<(ModelHandle<Buffer>, Vec<Range<Anchor>>)> {
+        if self.is_local() {
+            self.search_local(query, cx)
         } else if let Some(project_id) = self.remote_id() {
+            let (tx, rx) = smol::channel::unbounded();
             let request = self.client.request(query.to_proto(project_id));
             cx.spawn(|this, mut cx| async move {
                 let response = request.await?;
@@ -5258,13 +5186,303 @@ impl Project {
                         .or_insert(Vec::new())
                         .push(start..end)
                 }
-                Ok(result)
+                for (buffer, ranges) in result {
+                    let _ = tx.send((buffer, ranges)).await;
+                }
+                Result::<(), anyhow::Error>::Ok(())
             })
+            .detach_and_log_err(cx);
+            rx
         } else {
-            Task::ready(Ok(Default::default()))
+            unimplemented!();
         }
     }
 
+    pub fn search_local(
+        &self,
+        query: SearchQuery,
+        cx: &mut ModelContext<Self>,
+    ) -> Receiver<(ModelHandle<Buffer>, Vec<Range<Anchor>>)> {
+        // Local search is split into several phases.
+        // TL;DR is that we do 2 passes; initial pass to pick files which contain at least one match
+        // and the second phase that finds positions of all the matches found in the candidate files.
+        // The Receiver obtained from this function returns matches sorted by buffer path. Files without a buffer path are reported first.
+        //
+        // It gets a bit hairy though, because we must account for files that do not have a persistent representation
+        // on FS. Namely, if you have an untitled buffer or unsaved changes in a buffer, we want to scan that too.
+        //
+        // 1. We initialize a queue of match candidates and feed all opened buffers into it (== unsaved files / untitled buffers).
+        //    Then, we go through a worktree and check for files that do match a predicate. If the file had an opened version, we skip the scan
+        //    of FS version for that file altogether - after all, what we have in memory is more up-to-date than what's in FS.
+        // 2. At this point, we have a list of all potentially matching buffers/files.
+        //    We sort that list by buffer path - this list is retained for later use.
+        //    We ensure that all buffers are now opened and available in project.
+        // 3. We run a scan over all the candidate buffers on multiple background threads.
+        //    We cannot assume that there will even be a match - while at least one match
+        //    is guaranteed for files obtained from FS, the buffers we got from memory (unsaved files/unnamed buffers) might not have a match at all.
+        //    There is also an auxilliary background thread responsible for result gathering.
+        //    This is where the sorted list of buffers comes into play to maintain sorted order; Whenever this background thread receives a notification (buffer has/doesn't have matches),
+        //    it keeps it around. It reports matches in sorted order, though it accepts them in unsorted order as well.
+        //    As soon as the match info on next position in sorted order becomes available, it reports it (if it's a match) or skips to the next
+        //    entry - which might already be available thanks to out-of-order processing.
+        //
+        // We could also report matches fully out-of-order, without maintaining a sorted list of matching paths.
+        // This however would mean that project search (that is the main user of this function) would have to do the sorting itself, on the go.
+        // This isn't as straightforward as running an insertion sort sadly, and would also mean that it would have to care about maintaining match index
+        // in face of constantly updating list of sorted matches.
+        // Meanwhile, this implementation offers index stability, since the matches are already reported in a sorted order.
+        let snapshots = self
+            .visible_worktrees(cx)
+            .filter_map(|tree| {
+                let tree = tree.read(cx).as_local()?;
+                Some(tree.snapshot())
+            })
+            .collect::<Vec<_>>();
+
+        let background = cx.background().clone();
+        let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum();
+        if path_count == 0 {
+            let (_, rx) = smol::channel::bounded(1024);
+            return rx;
+        }
+        let workers = background.num_cpus().min(path_count);
+        let (matching_paths_tx, matching_paths_rx) = smol::channel::bounded(1024);
+        let mut unnamed_files = vec![];
+        let opened_buffers = self
+            .opened_buffers
+            .iter()
+            .filter_map(|(_, b)| {
+                let buffer = b.upgrade(cx)?;
+                let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+                if let Some(path) = snapshot.file().map(|file| file.path()) {
+                    Some((path.clone(), (buffer, snapshot)))
+                } else {
+                    unnamed_files.push(buffer);
+                    None
+                }
+            })
+            .collect();
+        cx.background()
+            .spawn(Self::background_search(
+                unnamed_files,
+                opened_buffers,
+                cx.background().clone(),
+                self.fs.clone(),
+                workers,
+                query.clone(),
+                path_count,
+                snapshots,
+                matching_paths_tx,
+            ))
+            .detach();
+
+        let (buffers, buffers_rx) = Self::sort_candidates_and_open_buffers(matching_paths_rx, cx);
+        let background = cx.background().clone();
+        let (result_tx, result_rx) = smol::channel::bounded(1024);
+        cx.background()
+            .spawn(async move {
+                let Ok(buffers) = buffers.await else {
+                    return;
+                };
+
+                let buffers_len = buffers.len();
+                if buffers_len == 0 {
+                    return;
+                }
+                let query = &query;
+                let (finished_tx, mut finished_rx) = smol::channel::unbounded();
+                background
+                    .scoped(|scope| {
+                        #[derive(Clone)]
+                        struct FinishedStatus {
+                            entry: Option<(ModelHandle<Buffer>, Vec<Range<Anchor>>)>,
+                            buffer_index: SearchMatchCandidateIndex,
+                        }
+
+                        for _ in 0..workers {
+                            let finished_tx = finished_tx.clone();
+                            let mut buffers_rx = buffers_rx.clone();
+                            scope.spawn(async move {
+                                while let Some((entry, buffer_index)) = buffers_rx.next().await {
+                                    let buffer_matches = if let Some((_, snapshot)) = entry.as_ref()
+                                    {
+                                        if query.file_matches(
+                                            snapshot.file().map(|file| file.path().as_ref()),
+                                        ) {
+                                            query
+                                                .search(&snapshot, None)
+                                                .await
+                                                .iter()
+                                                .map(|range| {
+                                                    snapshot.anchor_before(range.start)
+                                                        ..snapshot.anchor_after(range.end)
+                                                })
+                                                .collect()
+                                        } else {
+                                            Vec::new()
+                                        }
+                                    } else {
+                                        Vec::new()
+                                    };
+
+                                    let status = if !buffer_matches.is_empty() {
+                                        let entry = if let Some((buffer, _)) = entry.as_ref() {
+                                            Some((buffer.clone(), buffer_matches))
+                                        } else {
+                                            None
+                                        };
+                                        FinishedStatus {
+                                            entry,
+                                            buffer_index,
+                                        }
+                                    } else {
+                                        FinishedStatus {
+                                            entry: None,
+                                            buffer_index,
+                                        }
+                                    };
+                                    if finished_tx.send(status).await.is_err() {
+                                        break;
+                                    }
+                                }
+                            });
+                        }
+                        // Report sorted matches
+                        scope.spawn(async move {
+                            let mut current_index = 0;
+                            let mut scratch = vec![None; buffers_len];
+                            while let Some(status) = finished_rx.next().await {
+                                debug_assert!(
+                                    scratch[status.buffer_index].is_none(),
+                                    "Got match status of position {} twice",
+                                    status.buffer_index
+                                );
+                                let index = status.buffer_index;
+                                scratch[index] = Some(status);
+                                while current_index < buffers_len {
+                                    let Some(current_entry) = scratch[current_index].take() else {
+                                        // We intentionally **do not** increment `current_index` here. When next element arrives
+                                        // from `finished_rx`, we will inspect the same position again, hoping for it to be Some(_)
+                                        // this time.
+                                        break;
+                                    };
+                                    if let Some(entry) = current_entry.entry {
+                                        result_tx.send(entry).await.log_err();
+                                    }
+                                    current_index += 1;
+                                }
+                                if current_index == buffers_len {
+                                    break;
+                                }
+                            }
+                        });
+                    })
+                    .await;
+            })
+            .detach();
+        result_rx
+    }
+    /// Pick paths that might potentially contain a match of a given search query.
+    async fn background_search(
+        unnamed_buffers: Vec<ModelHandle<Buffer>>,
+        opened_buffers: HashMap<Arc<Path>, (ModelHandle<Buffer>, BufferSnapshot)>,
+        background: Arc<Background>,
+        fs: Arc<dyn Fs>,
+        workers: usize,
+        query: SearchQuery,
+        path_count: usize,
+        snapshots: Vec<LocalSnapshot>,
+        matching_paths_tx: Sender<SearchMatchCandidate>,
+    ) {
+        let fs = &fs;
+        let query = &query;
+        let matching_paths_tx = &matching_paths_tx;
+        let snapshots = &snapshots;
+        let paths_per_worker = (path_count + workers - 1) / workers;
+        for buffer in unnamed_buffers {
+            matching_paths_tx
+                .send(SearchMatchCandidate::OpenBuffer {
+                    buffer: buffer.clone(),
+                    path: None,
+                })
+                .await
+                .log_err();
+        }
+        for (path, (buffer, _)) in opened_buffers.iter() {
+            matching_paths_tx
+                .send(SearchMatchCandidate::OpenBuffer {
+                    buffer: buffer.clone(),
+                    path: Some(path.clone()),
+                })
+                .await
+                .log_err();
+        }
+        background
+            .scoped(|scope| {
+                for worker_ix in 0..workers {
+                    let worker_start_ix = worker_ix * paths_per_worker;
+                    let worker_end_ix = worker_start_ix + paths_per_worker;
+                    let unnamed_buffers = opened_buffers.clone();
+                    scope.spawn(async move {
+                        let mut snapshot_start_ix = 0;
+                        let mut abs_path = PathBuf::new();
+                        for snapshot in snapshots {
+                            let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count();
+                            if worker_end_ix <= snapshot_start_ix {
+                                break;
+                            } else if worker_start_ix > snapshot_end_ix {
+                                snapshot_start_ix = snapshot_end_ix;
+                                continue;
+                            } else {
+                                let start_in_snapshot =
+                                    worker_start_ix.saturating_sub(snapshot_start_ix);
+                                let end_in_snapshot =
+                                    cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
+
+                                for entry in snapshot
+                                    .files(false, start_in_snapshot)
+                                    .take(end_in_snapshot - start_in_snapshot)
+                                {
+                                    if matching_paths_tx.is_closed() {
+                                        break;
+                                    }
+                                    if unnamed_buffers.contains_key(&entry.path) {
+                                        continue;
+                                    }
+                                    let matches = if query.file_matches(Some(&entry.path)) {
+                                        abs_path.clear();
+                                        abs_path.push(&snapshot.abs_path());
+                                        abs_path.push(&entry.path);
+                                        if let Some(file) = fs.open_sync(&abs_path).await.log_err()
+                                        {
+                                            query.detect(file).unwrap_or(false)
+                                        } else {
+                                            false
+                                        }
+                                    } else {
+                                        false
+                                    };
+
+                                    if matches {
+                                        let project_path = SearchMatchCandidate::Path {
+                                            worktree_id: snapshot.id(),
+                                            path: entry.path.clone(),
+                                        };
+                                        if matching_paths_tx.send(project_path).await.is_err() {
+                                            break;
+                                        }
+                                    }
+                                }
+
+                                snapshot_start_ix = snapshot_end_ix;
+                            }
+                        }
+                    });
+                }
+            })
+            .await;
+    }
+
     fn request_primary_lsp<R: LspCommand>(
         &self,
         buffer_handle: ModelHandle<Buffer>,
@@ -5359,6 +5577,61 @@ impl Project {
         Task::ready(Ok(Default::default()))
     }
 
+    fn sort_candidates_and_open_buffers(
+        mut matching_paths_rx: Receiver<SearchMatchCandidate>,
+        cx: &mut ModelContext<Self>,
+    ) -> (
+        futures::channel::oneshot::Receiver<Vec<SearchMatchCandidate>>,
+        Receiver<(
+            Option<(ModelHandle<Buffer>, BufferSnapshot)>,
+            SearchMatchCandidateIndex,
+        )>,
+    ) {
+        let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
+        let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
+        cx.spawn(|this, cx| async move {
+            let mut buffers = vec![];
+            while let Some(entry) = matching_paths_rx.next().await {
+                buffers.push(entry);
+            }
+            buffers.sort_by_key(|candidate| candidate.path());
+            let matching_paths = buffers.clone();
+            let _ = sorted_buffers_tx.send(buffers);
+            for (index, candidate) in matching_paths.into_iter().enumerate() {
+                if buffers_tx.is_closed() {
+                    break;
+                }
+                let this = this.clone();
+                let buffers_tx = buffers_tx.clone();
+                cx.spawn(|mut cx| async move {
+                    let buffer = match candidate {
+                        SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
+                        SearchMatchCandidate::Path { worktree_id, path } => this
+                            .update(&mut cx, |this, cx| {
+                                this.open_buffer((worktree_id, path), cx)
+                            })
+                            .await
+                            .log_err(),
+                    };
+                    if let Some(buffer) = buffer {
+                        let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
+                        buffers_tx
+                            .send((Some((buffer, snapshot)), index))
+                            .await
+                            .log_err();
+                    } else {
+                        buffers_tx.send((None, index)).await.log_err();
+                    }
+
+                    Ok::<_, anyhow::Error>(())
+                })
+                .detach();
+            }
+        })
+        .detach();
+        (sorted_buffers_rx, buffers_rx)
+    }
+
     pub fn find_or_create_local_worktree(
         &mut self,
         abs_path: impl AsRef<Path>,
@@ -5982,6 +6255,7 @@ impl Project {
         let collaborator = Collaborator::from_proto(collaborator)?;
         this.update(&mut cx, |this, cx| {
             this.shared_buffers.remove(&collaborator.peer_id);
+            cx.emit(Event::CollaboratorJoined(collaborator.peer_id));
             this.collaborators
                 .insert(collaborator.peer_id, collaborator);
             cx.notify();
@@ -6865,6 +7139,40 @@ impl Project {
         }))
     }
 
+    async fn handle_resolve_inlay_hint(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::ResolveInlayHint>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ResolveInlayHintResponse> {
+        let proto_hint = envelope
+            .payload
+            .hint
+            .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint");
+        let hint = InlayHints::proto_to_project_hint(proto_hint)
+            .context("resolved proto inlay hint conversion")?;
+        let buffer = this.update(&mut cx, |this, cx| {
+            this.opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade(cx))
+                .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
+        })?;
+        let response_hint = this
+            .update(&mut cx, |project, cx| {
+                project.resolve_inlay_hint(
+                    hint,
+                    buffer,
+                    LanguageServerId(envelope.payload.language_server_id as usize),
+                    cx,
+                )
+            })
+            .await
+            .context("inlay hints fetch")?;
+        Ok(proto::ResolveInlayHintResponse {
+            hint: Some(InlayHints::project_to_proto_hint(response_hint)),
+        })
+    }
+
     async fn handle_refresh_inlay_hints(
         this: ModelHandle<Self>,
         _: TypedEnvelope<proto::RefreshInlayHints>,
@@ -6872,7 +7180,7 @@ impl Project {
         mut cx: AsyncAppContext,
     ) -> Result<proto::Ack> {
         this.update(&mut cx, |_, cx| {
-            cx.emit(Event::RefreshInlays);
+            cx.emit(Event::RefreshInlayHints);
         });
         Ok(proto::Ack {})
     }
@@ -6943,17 +7251,17 @@ impl Project {
     ) -> Result<proto::SearchProjectResponse> {
         let peer_id = envelope.original_sender_id()?;
         let query = SearchQuery::from_proto(envelope.payload)?;
-        let result = this
-            .update(&mut cx, |this, cx| this.search(query, cx))
-            .await?;
+        let mut result = this.update(&mut cx, |this, cx| this.search(query, cx));
 
-        this.update(&mut cx, |this, cx| {
+        cx.spawn(|mut cx| async move {
             let mut locations = Vec::new();
-            for (buffer, ranges) in result {
+            while let Some((buffer, ranges)) = result.next().await {
                 for range in ranges {
                     let start = serialize_anchor(&range.start);
                     let end = serialize_anchor(&range.end);
-                    let buffer_id = this.create_buffer_for_peer(&buffer, peer_id, cx);
+                    let buffer_id = this.update(&mut cx, |this, cx| {
+                        this.create_buffer_for_peer(&buffer, peer_id, cx)
+                    });
                     locations.push(proto::Location {
                         buffer_id,
                         start: Some(start),
@@ -6963,6 +7271,7 @@ impl Project {
             }
             Ok(proto::SearchProjectResponse { locations })
         })
+        .await
     }
 
     async fn handle_open_buffer_for_symbol(
@@ -7628,7 +7937,7 @@ impl Project {
         self.language_servers_for_buffer(buffer, cx).next()
     }
 
-    fn language_server_for_buffer(
+    pub fn language_server_for_buffer(
         &self,
         buffer: &Buffer,
         server_id: LanguageServerId,
@@ -7808,6 +8117,7 @@ impl Collaborator {
         Ok(Self {
             peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
             replica_id: message.replica_id as ReplicaId,
+            user_id: message.user_id as UserId,
         })
     }
 }

crates/project/src/project_tests.rs 🔗

@@ -1,11 +1,11 @@
-use crate::{search::PathMatcher, worktree::WorktreeHandle, Event, *};
-use fs::{FakeFs, LineEnding, RealFs};
+use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *};
+use fs::{FakeFs, RealFs};
 use futures::{future, StreamExt};
 use gpui::{executor::Deterministic, test::subscribe, AppContext};
 use language::{
     language_settings::{AllLanguageSettings, LanguageSettingsContent},
     tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
-    OffsetRangeExt, Point, ToPoint,
+    LineEnding, OffsetRangeExt, Point, ToPoint,
 };
 use lsp::Url;
 use parking_lot::Mutex;
@@ -3953,11 +3953,12 @@ async fn search(
     query: SearchQuery,
     cx: &mut gpui::TestAppContext,
 ) -> Result<HashMap<String, Vec<Range<usize>>>> {
-    let results = project
-        .update(cx, |project, cx| project.search(query, cx))
-        .await?;
-
-    Ok(results
+    let mut search_rx = project.update(cx, |project, cx| project.search(query, cx));
+    let mut result = HashMap::default();
+    while let Some((buffer, range)) = search_rx.next().await {
+        result.entry(buffer).or_insert(range);
+    }
+    Ok(result
         .into_iter()
         .map(|(buffer, ranges)| {
             buffer.read_with(cx, |buffer, _| {

crates/project/src/search.rs 🔗

@@ -13,25 +13,40 @@ use std::{
     sync::Arc,
 };
 
+#[derive(Clone, Debug)]
+pub struct SearchInputs {
+    query: Arc<str>,
+    files_to_include: Vec<PathMatcher>,
+    files_to_exclude: Vec<PathMatcher>,
+}
+
+impl SearchInputs {
+    pub fn as_str(&self) -> &str {
+        self.query.as_ref()
+    }
+    pub fn files_to_include(&self) -> &[PathMatcher] {
+        &self.files_to_include
+    }
+    pub fn files_to_exclude(&self) -> &[PathMatcher] {
+        &self.files_to_exclude
+    }
+}
 #[derive(Clone, Debug)]
 pub enum SearchQuery {
     Text {
         search: Arc<AhoCorasick<usize>>,
-        query: Arc<str>,
         whole_word: bool,
         case_sensitive: bool,
-        files_to_include: Vec<PathMatcher>,
-        files_to_exclude: Vec<PathMatcher>,
+        inner: SearchInputs,
     },
 
     Regex {
         regex: Regex,
-        query: Arc<str>,
+
         multiline: bool,
         whole_word: bool,
         case_sensitive: bool,
-        files_to_include: Vec<PathMatcher>,
-        files_to_exclude: Vec<PathMatcher>,
+        inner: SearchInputs,
     },
 }
 
@@ -73,13 +88,16 @@ impl SearchQuery {
             .auto_configure(&[&query])
             .ascii_case_insensitive(!case_sensitive)
             .build(&[&query]);
+        let inner = SearchInputs {
+            query: query.into(),
+            files_to_exclude,
+            files_to_include,
+        };
         Self::Text {
             search: Arc::new(search),
-            query: Arc::from(query),
             whole_word,
             case_sensitive,
-            files_to_include,
-            files_to_exclude,
+            inner,
         }
     }
 
@@ -105,14 +123,17 @@ impl SearchQuery {
             .case_insensitive(!case_sensitive)
             .multi_line(multiline)
             .build()?;
+        let inner = SearchInputs {
+            query: initial_query,
+            files_to_exclude,
+            files_to_include,
+        };
         Ok(Self::Regex {
             regex,
-            query: initial_query,
             multiline,
             whole_word,
             case_sensitive,
-            files_to_include,
-            files_to_exclude,
+            inner,
         })
     }
 
@@ -284,10 +305,7 @@ impl SearchQuery {
     }
 
     pub fn as_str(&self) -> &str {
-        match self {
-            Self::Text { query, .. } => query.as_ref(),
-            Self::Regex { query, .. } => query.as_ref(),
-        }
+        self.as_inner().as_str()
     }
 
     pub fn whole_word(&self) -> bool {
@@ -309,25 +327,11 @@ impl SearchQuery {
     }
 
     pub fn files_to_include(&self) -> &[PathMatcher] {
-        match self {
-            Self::Text {
-                files_to_include, ..
-            } => files_to_include,
-            Self::Regex {
-                files_to_include, ..
-            } => files_to_include,
-        }
+        self.as_inner().files_to_include()
     }
 
     pub fn files_to_exclude(&self) -> &[PathMatcher] {
-        match self {
-            Self::Text {
-                files_to_exclude, ..
-            } => files_to_exclude,
-            Self::Regex {
-                files_to_exclude, ..
-            } => files_to_exclude,
-        }
+        self.as_inner().files_to_exclude()
     }
 
     pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
@@ -346,6 +350,11 @@ impl SearchQuery {
             None => self.files_to_include().is_empty(),
         }
     }
+    pub fn as_inner(&self) -> &SearchInputs {
+        match self {
+            Self::Regex { inner, .. } | Self::Text { inner, .. } => inner,
+        }
+    }
 }
 
 fn deserialize_path_matches(glob_set: &str) -> anyhow::Result<Vec<PathMatcher>> {

crates/project/src/terminals.rs 🔗

@@ -1,7 +1,13 @@
 use crate::Project;
 use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle};
-use std::path::PathBuf;
-use terminal::{Terminal, TerminalBuilder, TerminalSettings};
+use std::path::{Path, PathBuf};
+use terminal::{
+    terminal_settings::{self, TerminalSettings, VenvSettingsContent},
+    Terminal, TerminalBuilder,
+};
+
+#[cfg(target_os = "macos")]
+use std::os::unix::ffi::OsStrExt;
 
 pub struct Terminals {
     pub(crate) local_handles: Vec<WeakModelHandle<terminal::Terminal>>,
@@ -20,10 +26,12 @@ impl Project {
             ));
         } else {
             let settings = settings::get::<TerminalSettings>(cx);
+            let python_settings = settings.detect_venv.clone();
+            let shell = settings.shell.clone();
 
             let terminal = TerminalBuilder::new(
                 working_directory.clone(),
-                settings.shell.clone(),
+                shell.clone(),
                 settings.env.clone(),
                 Some(settings.blinking.clone()),
                 settings.alternate_scroll,
@@ -47,6 +55,15 @@ impl Project {
                 })
                 .detach();
 
+                if let Some(python_settings) = &python_settings.as_option() {
+                    let activate_script_path =
+                        self.find_activate_script_path(&python_settings, working_directory);
+                    self.activate_python_virtual_environment(
+                        activate_script_path,
+                        &terminal_handle,
+                        cx,
+                    );
+                }
                 terminal_handle
             });
 
@@ -54,6 +71,50 @@ impl Project {
         }
     }
 
+    pub fn find_activate_script_path(
+        &mut self,
+        settings: &VenvSettingsContent,
+        working_directory: Option<PathBuf>,
+    ) -> Option<PathBuf> {
+        // When we are unable to resolve the working directory, the terminal builder
+        // defaults to '/'. We should probably encode this directly somewhere, but for
+        // now, let's just hard code it here.
+        let working_directory = working_directory.unwrap_or_else(|| Path::new("/").to_path_buf());
+        let activate_script_name = match settings.activate_script {
+            terminal_settings::ActivateScript::Default => "activate",
+            terminal_settings::ActivateScript::Csh => "activate.csh",
+            terminal_settings::ActivateScript::Fish => "activate.fish",
+        };
+
+        for virtual_environment_name in settings.directories {
+            let mut path = working_directory.join(virtual_environment_name);
+            path.push("bin/");
+            path.push(activate_script_name);
+
+            if path.exists() {
+                return Some(path);
+            }
+        }
+
+        None
+    }
+
+    fn activate_python_virtual_environment(
+        &mut self,
+        activate_script: Option<PathBuf>,
+        terminal_handle: &ModelHandle<Terminal>,
+        cx: &mut ModelContext<Project>,
+    ) {
+        if let Some(activate_script) = activate_script {
+            // Paths are not strings so we need to jump through some hoops to format the command without `format!`
+            let mut command = Vec::from("source ".as_bytes());
+            command.extend_from_slice(activate_script.as_os_str().as_bytes());
+            command.push(b'\n');
+
+            terminal_handle.update(cx, |this, _| this.input_bytes(command));
+        }
+    }
+
     pub fn local_terminal_handles(&self) -> &Vec<WeakModelHandle<terminal::Terminal>> {
         &self.terminals.local_handles
     }

crates/project/src/worktree.rs 🔗

@@ -8,7 +8,7 @@ use clock::ReplicaId;
 use collections::{HashMap, HashSet, VecDeque};
 use fs::{
     repository::{GitFileStatus, GitRepository, RepoPath},
-    Fs, LineEnding,
+    Fs,
 };
 use futures::{
     channel::{
@@ -27,7 +27,7 @@ use language::{
         deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
         serialize_version,
     },
-    Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped,
+    Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped,
 };
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
@@ -2317,9 +2317,10 @@ impl BackgroundScannerState {
         for changed_path in changed_paths {
             let Some(dot_git_dir) = changed_path
                 .ancestors()
-                .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) else {
-                    continue;
-                };
+                .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
+            else {
+                continue;
+            };
 
             // Avoid processing the same repository multiple times, if multiple paths
             // within it have changed.
@@ -2348,7 +2349,10 @@ impl BackgroundScannerState {
                     let Some(work_dir) = self
                         .snapshot
                         .entry_for_id(entry_id)
-                        .map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue };
+                        .map(|entry| RepositoryWorkDirectory(entry.path.clone()))
+                    else {
+                        continue;
+                    };
 
                     log::info!("reload git repository {:?}", dot_git_dir);
                     let repository = repository.repo_ptr.lock();
@@ -4026,7 +4030,7 @@ struct UpdateIgnoreStatusJob {
     scan_queue: Sender<ScanJob>,
 }
 
-pub trait WorktreeHandle {
+pub trait WorktreeModelHandle {
     #[cfg(any(test, feature = "test-support"))]
     fn flush_fs_events<'a>(
         &self,
@@ -4034,7 +4038,7 @@ pub trait WorktreeHandle {
     ) -> futures::future::LocalBoxFuture<'a, ()>;
 }
 
-impl WorktreeHandle for ModelHandle<Worktree> {
+impl WorktreeModelHandle for ModelHandle<Worktree> {
     // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
     // occurred before the worktree was constructed. These events can cause the worktree to perform
     // extra directory scans, and emit extra scan-state notifications.

crates/project/src/worktree_tests.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    worktree::{Event, Snapshot, WorktreeHandle},
+    worktree::{Event, Snapshot, WorktreeModelHandle},
     Entry, EntryKind, PathChange, Worktree,
 };
 use anyhow::Result;

crates/project_panel/src/project_panel.rs 🔗

@@ -1320,7 +1320,7 @@ impl ProjectPanel {
         }
     }
 
-    fn render_entry_visual_element<V: View>(
+    fn render_entry_visual_element<V: 'static>(
         details: &EntryDetails,
         editor: Option<&ViewHandle<Editor>>,
         padding: f32,
@@ -1651,30 +1651,14 @@ impl workspace::dock::Panel for ProjectPanel {
             .unwrap_or_else(|| settings::get::<ProjectPanelSettings>(cx).default_width)
     }
 
-    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
-        self.width = Some(size);
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
         self.serialize(cx);
         cx.notify();
     }
 
-    fn should_zoom_in_on_event(_: &Self::Event) -> bool {
-        false
-    }
-
-    fn should_zoom_out_on_event(_: &Self::Event) -> bool {
-        false
-    }
-
-    fn is_zoomed(&self, _: &WindowContext) -> bool {
-        false
-    }
-
-    fn set_zoomed(&mut self, _: bool, _: &mut ViewContext<Self>) {}
-
-    fn set_active(&mut self, _: bool, _: &mut ViewContext<Self>) {}
-
-    fn icon_path(&self) -> &'static str {
-        "icons/folder_tree_16.svg"
+    fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
+        Some("icons/project.svg")
     }
 
     fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
@@ -1685,14 +1669,6 @@ impl workspace::dock::Panel for ProjectPanel {
         matches!(event, Event::DockPositionChanged)
     }
 
-    fn should_activate_on_event(_: &Self::Event) -> bool {
-        false
-    }
-
-    fn should_close_on_event(_: &Self::Event) -> bool {
-        false
-    }
-
     fn has_focus(&self, _: &WindowContext) -> bool {
         self.has_focus
     }

crates/quick_action_bar/Cargo.toml 🔗

@@ -0,0 +1,23 @@
+[package]
+name = "quick_action_bar"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/quick_action_bar.rs"
+doctest = false
+
+[dependencies]
+ai = { path = "../ai" }
+editor = { path = "../editor" }
+gpui = { path = "../gpui" }
+search = { path = "../search" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }

crates/quick_action_bar/src/quick_action_bar.rs 🔗

@@ -0,0 +1,185 @@
+use ai::{assistant::InlineAssist, AssistantPanel};
+use editor::Editor;
+use gpui::{
+    elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
+    platform::{CursorStyle, MouseButton},
+    Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
+    WeakViewHandle,
+};
+
+use search::{buffer_search, BufferSearchBar};
+use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace};
+
+pub struct QuickActionBar {
+    buffer_search_bar: ViewHandle<BufferSearchBar>,
+    active_item: Option<Box<dyn ItemHandle>>,
+    _inlay_hints_enabled_subscription: Option<Subscription>,
+    workspace: WeakViewHandle<Workspace>,
+}
+
+impl QuickActionBar {
+    pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>, workspace: &Workspace) -> Self {
+        Self {
+            buffer_search_bar,
+            active_item: None,
+            _inlay_hints_enabled_subscription: None,
+            workspace: workspace.weak_handle(),
+        }
+    }
+
+    fn active_editor(&self) -> Option<ViewHandle<Editor>> {
+        self.active_item
+            .as_ref()
+            .and_then(|item| item.downcast::<Editor>())
+    }
+}
+
+impl Entity for QuickActionBar {
+    type Event = ();
+}
+
+impl View for QuickActionBar {
+    fn ui_name() -> &'static str {
+        "QuickActionsBar"
+    }
+
+    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+        let Some(editor) = self.active_editor() else {
+            return Empty::new().into_any();
+        };
+
+        let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+        let mut bar = Flex::row().with_child(render_quick_action_bar_button(
+            0,
+            "icons/inlay_hint.svg",
+            inlay_hints_enabled,
+            (
+                "Toggle Inlay Hints".to_string(),
+                Some(Box::new(editor::ToggleInlayHints)),
+            ),
+            cx,
+            |this, cx| {
+                if let Some(editor) = this.active_editor() {
+                    editor.update(cx, |editor, cx| {
+                        editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
+                    });
+                }
+            },
+        ));
+
+        if editor.read(cx).buffer().read(cx).is_singleton() {
+            let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
+            let search_action = buffer_search::Deploy { focus: true };
+
+            bar = bar.with_child(render_quick_action_bar_button(
+                1,
+                "icons/magnifying_glass.svg",
+                search_bar_shown,
+                (
+                    "Buffer Search".to_string(),
+                    Some(Box::new(search_action.clone())),
+                ),
+                cx,
+                move |this, cx| {
+                    this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
+                        if search_bar_shown {
+                            buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
+                        } else {
+                            buffer_search_bar.deploy(&search_action, cx);
+                        }
+                    });
+                },
+            ));
+        }
+
+        bar.add_child(render_quick_action_bar_button(
+            2,
+            "icons/radix/magic-wand.svg",
+            false,
+            ("Inline Assist".into(), Some(Box::new(InlineAssist))),
+            cx,
+            move |this, cx| {
+                if let Some(workspace) = this.workspace.upgrade(cx) {
+                    workspace.update(cx, |workspace, cx| {
+                        AssistantPanel::inline_assist(workspace, &Default::default(), cx);
+                    });
+                }
+            },
+        ));
+
+        bar.into_any()
+    }
+}
+
+fn render_quick_action_bar_button<
+    F: 'static + Fn(&mut QuickActionBar, &mut EventContext<QuickActionBar>),
+>(
+    index: usize,
+    icon: &'static str,
+    toggled: bool,
+    tooltip: (String, Option<Box<dyn Action>>),
+    cx: &mut ViewContext<QuickActionBar>,
+    on_click: F,
+) -> AnyElement<QuickActionBar> {
+    enum QuickActionBarButton {}
+
+    let theme = theme::current(cx);
+    let (tooltip_text, action) = tooltip;
+
+    MouseEventHandler::new::<QuickActionBarButton, _>(index, cx, |mouse_state, _| {
+        let style = theme
+            .workspace
+            .toolbar
+            .toggleable_tool
+            .in_state(toggled)
+            .style_for(mouse_state);
+        Svg::new(icon)
+            .with_color(style.color)
+            .constrained()
+            .with_width(style.icon_width)
+            .aligned()
+            .constrained()
+            .with_width(style.button_width)
+            .with_height(style.button_width)
+            .contained()
+            .with_style(style.container)
+    })
+    .with_cursor_style(CursorStyle::PointingHand)
+    .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
+    .with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
+    .into_any_named("quick action bar button")
+}
+
+impl ToolbarItemView for QuickActionBar {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
+        match active_pane_item {
+            Some(active_item) => {
+                self.active_item = Some(active_item.boxed_clone());
+                self._inlay_hints_enabled_subscription.take();
+
+                if let Some(editor) = active_item.downcast::<Editor>() {
+                    let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+                    self._inlay_hints_enabled_subscription =
+                        Some(cx.observe(&editor, move |_, editor, cx| {
+                            let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+                            if inlay_hints_enabled != new_inlay_hints_enabled {
+                                inlay_hints_enabled = new_inlay_hints_enabled;
+                                cx.notify();
+                            }
+                        }));
+                    ToolbarItemLocation::PrimaryRight { flex: None }
+                } else {
+                    ToolbarItemLocation::Hidden
+                }
+            }
+            None => {
+                self.active_item = None;
+                ToolbarItemLocation::Hidden
+            }
+        }
+    }
+}

crates/recent_projects/src/highlighted_workspace_location.rs 🔗

@@ -3,7 +3,7 @@ use std::path::Path;
 use fuzzy::StringMatch;
 use gpui::{
     elements::{Label, LabelStyle},
-    AnyElement, Element, View,
+    AnyElement, Element,
 };
 use util::paths::PathExt;
 use workspace::WorkspaceLocation;
@@ -43,7 +43,7 @@ impl HighlightedText {
         }
     }
 
-    pub fn render<V: View>(self, style: impl Into<LabelStyle>) -> AnyElement<V> {
+    pub fn render<V: 'static>(self, style: impl Into<LabelStyle>) -> AnyElement<V> {
         Label::new(self.text, style)
             .with_highlights(self.highlight_positions)
             .into_any()

crates/refineable/Cargo.toml 🔗

@@ -0,0 +1,15 @@
+[package]
+name = "refineable"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/refineable.rs"
+doctest = false
+
+[dependencies]
+syn = "1.0.72"
+quote = "1.0.9"
+proc-macro2 = "1.0.66"
+derive_refineable = { path = "./derive_refineable" }

crates/refineable/derive_refineable/Cargo.toml 🔗

@@ -0,0 +1,15 @@
+[package]
+name = "derive_refineable"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/derive_refineable.rs"
+proc-macro = true
+doctest = false
+
+[dependencies]
+syn = "1.0.72"
+quote = "1.0.9"
+proc-macro2 = "1.0.66"

crates/refineable/derive_refineable/src/derive_refineable.rs 🔗

@@ -0,0 +1,188 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{
+    parse_macro_input, parse_quote, DeriveInput, Field, FieldsNamed, PredicateType, TraitBound,
+    Type, TypeParamBound, WhereClause, WherePredicate,
+};
+
+#[proc_macro_derive(Refineable, attributes(refineable))]
+pub fn derive_refineable(input: TokenStream) -> TokenStream {
+    let DeriveInput {
+        ident,
+        data,
+        generics,
+        ..
+    } = parse_macro_input!(input);
+
+    let refinement_ident = format_ident!("{}Refinement", ident);
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+
+    let fields = match data {
+        syn::Data::Struct(syn::DataStruct {
+            fields: syn::Fields::Named(FieldsNamed { named, .. }),
+            ..
+        }) => named.into_iter().collect::<Vec<Field>>(),
+        _ => panic!("This derive macro only supports structs with named fields"),
+    };
+
+    let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect();
+    let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect();
+    let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect();
+
+    // Create trait bound that each wrapped type must implement Clone & Default
+    let type_param_bounds: Vec<_> = wrapped_types
+        .iter()
+        .map(|ty| {
+            WherePredicate::Type(PredicateType {
+                lifetimes: None,
+                bounded_ty: ty.clone(),
+                colon_token: Default::default(),
+                bounds: {
+                    let mut punctuated = syn::punctuated::Punctuated::new();
+                    punctuated.push_value(TypeParamBound::Trait(TraitBound {
+                        paren_token: None,
+                        modifier: syn::TraitBoundModifier::None,
+                        lifetimes: None,
+                        path: parse_quote!(Clone),
+                    }));
+                    punctuated.push_punct(syn::token::Add::default());
+                    punctuated.push_value(TypeParamBound::Trait(TraitBound {
+                        paren_token: None,
+                        modifier: syn::TraitBoundModifier::None,
+                        lifetimes: None,
+                        path: parse_quote!(Default),
+                    }));
+                    punctuated
+                },
+            })
+        })
+        .collect();
+
+    // Append to where_clause or create a new one if it doesn't exist
+    let where_clause = match where_clause.cloned() {
+        Some(mut where_clause) => {
+            where_clause
+                .predicates
+                .extend(type_param_bounds.into_iter());
+            where_clause.clone()
+        }
+        None => WhereClause {
+            where_token: Default::default(),
+            predicates: type_param_bounds.into_iter().collect(),
+        },
+    };
+
+    let field_assignments: Vec<TokenStream2> = fields
+        .iter()
+        .map(|field| {
+            let name = &field.ident;
+            let is_refineable = is_refineable_field(field);
+            let is_optional = is_optional_field(field);
+
+            if is_refineable {
+                quote! {
+                    self.#name.refine(&refinement.#name);
+                }
+            } else if is_optional {
+                quote! {
+                    if let Some(ref value) = &refinement.#name {
+                        self.#name = Some(value.clone());
+                    }
+                }
+            } else {
+                quote! {
+                    if let Some(ref value) = &refinement.#name {
+                        self.#name = value.clone();
+                    }
+                }
+            }
+        })
+        .collect();
+
+    let refinement_field_assignments: Vec<TokenStream2> = fields
+        .iter()
+        .map(|field| {
+            let name = &field.ident;
+            let is_refineable = is_refineable_field(field);
+
+            if is_refineable {
+                quote! {
+                    self.#name.refine(&refinement.#name);
+                }
+            } else {
+                quote! {
+                    if let Some(ref value) = &refinement.#name {
+                        self.#name = Some(value.clone());
+                    }
+                }
+            }
+        })
+        .collect();
+
+    let gen = quote! {
+        #[derive(Default, Clone)]
+        pub struct #refinement_ident #impl_generics {
+            #( #field_visibilities #field_names: #wrapped_types ),*
+        }
+
+        impl #impl_generics Refineable for #ident #ty_generics
+            #where_clause
+        {
+            type Refinement = #refinement_ident #ty_generics;
+
+            fn refine(&mut self, refinement: &Self::Refinement) {
+                #( #field_assignments )*
+            }
+        }
+
+        impl #impl_generics Refineable for #refinement_ident #ty_generics
+            #where_clause
+        {
+            type Refinement = #refinement_ident #ty_generics;
+
+            fn refine(&mut self, refinement: &Self::Refinement) {
+                #( #refinement_field_assignments )*
+            }
+        }
+    };
+
+    gen.into()
+}
+
+fn is_refineable_field(f: &Field) -> bool {
+    f.attrs.iter().any(|attr| attr.path.is_ident("refineable"))
+}
+
+fn is_optional_field(f: &Field) -> bool {
+    if let Type::Path(typepath) = &f.ty {
+        if typepath.qself.is_none() {
+            let segments = &typepath.path.segments;
+            if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") {
+                return true;
+            }
+        }
+    }
+    false
+}
+
+fn get_wrapper_type(field: &Field, ty: &Type) -> syn::Type {
+    if is_refineable_field(field) {
+        let struct_name = if let Type::Path(tp) = ty {
+            tp.path.segments.last().unwrap().ident.clone()
+        } else {
+            panic!("Expected struct type for a refineable field");
+        };
+        let refinement_struct_name = format_ident!("{}Refinement", struct_name);
+        let generics = if let Type::Path(tp) = ty {
+            &tp.path.segments.last().unwrap().arguments
+        } else {
+            &syn::PathArguments::None
+        };
+        parse_quote!(#refinement_struct_name #generics)
+    } else if is_optional_field(field) {
+        ty.clone()
+    } else {
+        parse_quote!(Option<#ty>)
+    }
+}

crates/refineable/src/refineable.rs 🔗

@@ -0,0 +1,14 @@
+pub use derive_refineable::Refineable;
+
+pub trait Refineable {
+    type Refinement: Default;
+
+    fn refine(&mut self, refinement: &Self::Refinement);
+    fn refined(mut self, refinement: &Self::Refinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.refine(refinement);
+        self
+    }
+}

crates/rope/src/rope.rs 🔗

@@ -384,6 +384,16 @@ impl<'a> From<&'a str> for Rope {
     }
 }
 
+impl<'a> FromIterator<&'a str> for Rope {
+    fn from_iter<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
+        let mut rope = Rope::new();
+        for chunk in iter {
+            rope.push(chunk);
+        }
+        rope
+    }
+}
+
 impl From<String> for Rope {
     fn from(text: String) -> Self {
         Rope::from(text.as_str())

crates/rpc/Cargo.toml 🔗

@@ -23,7 +23,7 @@ async-tungstenite = "0.16"
 base64 = "0.13"
 futures.workspace = true
 parking_lot.workspace = true
-prost = "0.8"
+prost.workspace = true
 rand.workspace = true
 rsa = "0.4"
 serde.workspace = true

crates/rpc/proto/zed.proto 🔗

@@ -102,17 +102,6 @@ message Envelope {
         SearchProject search_project = 80;
         SearchProjectResponse search_project_response = 81;
 
-        GetChannels get_channels = 82;
-        GetChannelsResponse get_channels_response = 83;
-        JoinChannel join_channel = 84;
-        JoinChannelResponse join_channel_response = 85;
-        LeaveChannel leave_channel = 86;
-        SendChannelMessage send_channel_message = 87;
-        SendChannelMessageResponse send_channel_message_response = 88;
-        ChannelMessageSent channel_message_sent = 89;
-        GetChannelMessages get_channel_messages = 90;
-        GetChannelMessagesResponse get_channel_messages_response = 91;
-
         UpdateContacts update_contacts = 92;
         UpdateInviteInfo update_invite_info = 93;
         ShowContacts show_contacts = 94;
@@ -139,7 +128,29 @@ message Envelope {
 
         InlayHints inlay_hints = 116;
         InlayHintsResponse inlay_hints_response = 117;
+        ResolveInlayHint resolve_inlay_hint = 137;
+        ResolveInlayHintResponse resolve_inlay_hint_response = 138;
         RefreshInlayHints refresh_inlay_hints = 118;
+
+        CreateChannel create_channel = 119;
+        ChannelResponse channel_response = 120;
+        InviteChannelMember invite_channel_member = 121;
+        RemoveChannelMember remove_channel_member = 122;
+        RespondToChannelInvite respond_to_channel_invite = 123;
+        UpdateChannels update_channels = 124;
+        JoinChannel join_channel = 125;
+        RemoveChannel remove_channel = 126;
+        GetChannelMembers get_channel_members = 127;
+        GetChannelMembersResponse get_channel_members_response = 128;
+        SetChannelMemberAdmin set_channel_member_admin = 129;
+        RenameChannel rename_channel = 130;
+
+        JoinChannelBuffer join_channel_buffer = 131;
+        JoinChannelBufferResponse join_channel_buffer_response = 132;
+        UpdateChannelBuffer update_channel_buffer = 133;
+        LeaveChannelBuffer leave_channel_buffer = 134;
+        AddChannelBufferCollaborator add_channel_buffer_collaborator = 135;
+        RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
     }
 }
 
@@ -174,7 +185,8 @@ message JoinRoom {
 
 message JoinRoomResponse {
     Room room = 1;
-    optional LiveKitConnectionInfo live_kit_connection_info = 2;
+    optional uint64 channel_id = 2;
+    optional LiveKitConnectionInfo live_kit_connection_info = 3;
 }
 
 message RejoinRoom {
@@ -408,6 +420,16 @@ message RemoveProjectCollaborator {
     PeerId peer_id = 2;
 }
 
+message AddChannelBufferCollaborator {
+    uint64 channel_id = 1;
+    Collaborator collaborator = 2;
+}
+
+message RemoveChannelBufferCollaborator {
+    uint64 channel_id = 1;
+    PeerId peer_id = 2;
+}
+
 message GetDefinition {
      uint64 project_id = 1;
      uint64 buffer_id = 2;
@@ -537,6 +559,11 @@ message UpdateBuffer {
     repeated Operation operations = 3;
 }
 
+message UpdateChannelBuffer {
+    uint64 channel_id = 1;
+    repeated Operation operations = 2;
+}
+
 message UpdateBufferFile {
     uint64 project_id = 1;
     uint64 buffer_id = 2;
@@ -730,6 +757,7 @@ message InlayHint {
     bool padding_left = 4;
     bool padding_right = 5;
     InlayHintTooltip tooltip = 6;
+    ResolveState resolve_state = 7;
 }
 
 message InlayHintLabel {
@@ -746,7 +774,10 @@ message InlayHintLabelParts {
 message InlayHintLabelPart {
     string value = 1;
     InlayHintLabelPartTooltip tooltip = 2;
-    Location location = 3;
+    optional string location_url = 3;
+    PointUtf16 location_range_start = 4;
+    PointUtf16 location_range_end = 5;
+    optional uint64 language_server_id = 6;
 }
 
 message InlayHintTooltip {
@@ -763,12 +794,39 @@ message InlayHintLabelPartTooltip {
     }
 }
 
+message ResolveState {
+    State state = 1;
+    LspResolveState lsp_resolve_state = 2;
+
+    enum State {
+        Resolved = 0;
+        CanResolve = 1;
+        Resolving = 2;
+    }
+
+    message LspResolveState {
+        string value = 1;
+        uint64 server_id = 2;
+    }
+}
+
+message ResolveInlayHint {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    uint64 language_server_id = 3;
+    InlayHint hint = 4;
+}
+
+message ResolveInlayHintResponse {
+    InlayHint hint = 1;
+}
+
 message RefreshInlayHints {
     uint64 project_id = 1;
 }
 
 message MarkupContent {
-    string kind = 1;
+    bool is_markdown = 1;
     string value = 2;
 }
 
@@ -868,25 +926,105 @@ message LspDiskBasedDiagnosticsUpdating {}
 
 message LspDiskBasedDiagnosticsUpdated {}
 
-message GetChannels {}
-
-message GetChannelsResponse {
+message UpdateChannels {
     repeated Channel channels = 1;
+    repeated uint64 remove_channels = 2;
+    repeated Channel channel_invitations = 3;
+    repeated uint64 remove_channel_invitations = 4;
+    repeated ChannelParticipants channel_participants = 5;
+    repeated ChannelPermission channel_permissions = 6;
+}
+
+message ChannelPermission {
+    uint64 channel_id = 1;
+    bool is_admin = 2;
+}
+
+message ChannelParticipants {
+    uint64 channel_id = 1;
+    repeated uint64 participant_user_ids = 2;
 }
 
 message JoinChannel {
     uint64 channel_id = 1;
 }
 
-message JoinChannelResponse {
-    repeated ChannelMessage messages = 1;
-    bool done = 2;
+message RemoveChannel {
+    uint64 channel_id = 1;
+}
+
+message GetChannelMembers {
+    uint64 channel_id = 1;
+}
+
+message GetChannelMembersResponse {
+    repeated ChannelMember members = 1;
+}
+
+message ChannelMember {
+    uint64 user_id = 1;
+    bool admin = 2;
+    Kind kind = 3;
+
+    enum Kind {
+        Member = 0;
+        Invitee = 1;
+        AncestorMember = 2;
+    }
+}
+
+message CreateChannel {
+    string name = 1;
+    optional uint64 parent_id = 2;
+}
+
+message ChannelResponse {
+    Channel channel = 1;
+}
+
+message InviteChannelMember {
+    uint64 channel_id = 1;
+    uint64 user_id = 2;
+    bool admin = 3;
+}
+
+message RemoveChannelMember {
+    uint64 channel_id = 1;
+    uint64 user_id = 2;
+}
+
+message SetChannelMemberAdmin {
+    uint64 channel_id = 1;
+    uint64 user_id = 2;
+    bool admin = 3;
+}
+
+message RenameChannel {
+    uint64 channel_id = 1;
+    string name = 2;
 }
 
-message LeaveChannel {
+message JoinChannelBuffer {
     uint64 channel_id = 1;
 }
 
+message JoinChannelBufferResponse {
+    uint64 buffer_id = 1;
+    uint32 replica_id = 2;
+    string base_text = 3;
+    repeated Operation operations = 4;
+    repeated Collaborator collaborators = 5;
+}
+
+message LeaveChannelBuffer {
+    uint64 channel_id = 1;
+}
+
+message RespondToChannelInvite {
+    uint64 channel_id = 1;
+    bool accept = 2;
+}
+
 message GetUsers {
     repeated uint64 user_ids = 1;
 }
@@ -919,31 +1057,6 @@ enum ContactRequestResponse {
     Dismiss = 3;
 }
 
-message SendChannelMessage {
-    uint64 channel_id = 1;
-    string body = 2;
-    Nonce nonce = 3;
-}
-
-message SendChannelMessageResponse {
-    ChannelMessage message = 1;
-}
-
-message ChannelMessageSent {
-    uint64 channel_id = 1;
-    ChannelMessage message = 2;
-}
-
-message GetChannelMessages {
-    uint64 channel_id = 1;
-    uint64 before_message_id = 2;
-}
-
-message GetChannelMessagesResponse {
-    repeated ChannelMessage messages = 1;
-    bool done = 2;
-}
-
 message UpdateContacts {
     repeated Contact contacts = 1;
     repeated uint64 remove_contacts = 2;
@@ -1002,6 +1115,7 @@ message GetPrivateUserInfo {}
 message GetPrivateUserInfoResponse {
     string metrics_id = 1;
     bool staff = 2;
+    repeated string flags = 3;
 }
 
 // Entities
@@ -1041,6 +1155,7 @@ message View {
 
     oneof variant {
         Editor editor = 3;
+        ChannelView channel_view = 4;
     }
 
     message Editor {
@@ -1053,6 +1168,11 @@ message View {
         float scroll_x = 7;
         float scroll_y = 8;
     }
+
+    message ChannelView {
+        uint64 channel_id = 1;
+        Editor editor = 2;
+    }
 }
 
 message Collaborator {
@@ -1103,7 +1223,6 @@ enum GitStatus {
     Conflict = 2;
 }
 
-
 message BufferState {
     uint64 id = 1;
     optional File file = 2;
@@ -1275,14 +1394,7 @@ message Nonce {
 message Channel {
     uint64 id = 1;
     string name = 2;
-}
-
-message ChannelMessage {
-    uint64 id = 1;
-    string body = 2;
-    uint64 timestamp = 3;
-    uint64 sender_id = 4;
-    Nonce nonce = 5;
+    optional uint64 parent_id = 3;
 }
 
 message Contact {

crates/rpc/src/peer.rs 🔗

@@ -171,12 +171,12 @@ impl Peer {
         let this = self.clone();
         let response_channels = connection_state.response_channels.clone();
         let handle_io = async move {
-            tracing::debug!(%connection_id, "handle io future: start");
+            tracing::trace!(%connection_id, "handle io future: start");
 
             let _end_connection = util::defer(|| {
                 response_channels.lock().take();
                 this.connections.write().remove(&connection_id);
-                tracing::debug!(%connection_id, "handle io future: end");
+                tracing::trace!(%connection_id, "handle io future: end");
             });
 
             // Send messages on this frequency so the connection isn't closed.
@@ -188,68 +188,68 @@ impl Peer {
             futures::pin_mut!(receive_timeout);
 
             loop {
-                tracing::debug!(%connection_id, "outer loop iteration start");
+                tracing::trace!(%connection_id, "outer loop iteration start");
                 let read_message = reader.read().fuse();
                 futures::pin_mut!(read_message);
 
                 loop {
-                    tracing::debug!(%connection_id, "inner loop iteration start");
+                    tracing::trace!(%connection_id, "inner loop iteration start");
                     futures::select_biased! {
                         outgoing = outgoing_rx.next().fuse() => match outgoing {
                             Some(outgoing) => {
-                                tracing::debug!(%connection_id, "outgoing rpc message: writing");
+                                tracing::trace!(%connection_id, "outgoing rpc message: writing");
                                 futures::select_biased! {
                                     result = writer.write(outgoing).fuse() => {
-                                        tracing::debug!(%connection_id, "outgoing rpc message: done writing");
+                                        tracing::trace!(%connection_id, "outgoing rpc message: done writing");
                                         result.context("failed to write RPC message")?;
-                                        tracing::debug!(%connection_id, "keepalive interval: resetting after sending message");
+                                        tracing::trace!(%connection_id, "keepalive interval: resetting after sending message");
                                         keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse());
                                     }
                                     _ = create_timer(WRITE_TIMEOUT).fuse() => {
-                                        tracing::debug!(%connection_id, "outgoing rpc message: writing timed out");
+                                        tracing::trace!(%connection_id, "outgoing rpc message: writing timed out");
                                         Err(anyhow!("timed out writing message"))?;
                                     }
                                 }
                             }
                             None => {
-                                tracing::debug!(%connection_id, "outgoing rpc message: channel closed");
+                                tracing::trace!(%connection_id, "outgoing rpc message: channel closed");
                                 return Ok(())
                             },
                         },
                         _ = keepalive_timer => {
-                            tracing::debug!(%connection_id, "keepalive interval: pinging");
+                            tracing::trace!(%connection_id, "keepalive interval: pinging");
                             futures::select_biased! {
                                 result = writer.write(proto::Message::Ping).fuse() => {
-                                    tracing::debug!(%connection_id, "keepalive interval: done pinging");
+                                    tracing::trace!(%connection_id, "keepalive interval: done pinging");
                                     result.context("failed to send keepalive")?;
-                                    tracing::debug!(%connection_id, "keepalive interval: resetting after pinging");
+                                    tracing::trace!(%connection_id, "keepalive interval: resetting after pinging");
                                     keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse());
                                 }
                                 _ = create_timer(WRITE_TIMEOUT).fuse() => {
-                                    tracing::debug!(%connection_id, "keepalive interval: pinging timed out");
+                                    tracing::trace!(%connection_id, "keepalive interval: pinging timed out");
                                     Err(anyhow!("timed out sending keepalive"))?;
                                 }
                             }
                         }
                         incoming = read_message => {
                             let incoming = incoming.context("error reading rpc message from socket")?;
-                            tracing::debug!(%connection_id, "incoming rpc message: received");
-                            tracing::debug!(%connection_id, "receive timeout: resetting");
+                            tracing::trace!(%connection_id, "incoming rpc message: received");
+                            tracing::trace!(%connection_id, "receive timeout: resetting");
                             receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse());
                             if let proto::Message::Envelope(incoming) = incoming {
-                                tracing::debug!(%connection_id, "incoming rpc message: processing");
+                                tracing::trace!(%connection_id, "incoming rpc message: processing");
                                 futures::select_biased! {
                                     result = incoming_tx.send(incoming).fuse() => match result {
                                         Ok(_) => {
-                                            tracing::debug!(%connection_id, "incoming rpc message: processed");
+                                            tracing::trace!(%connection_id, "incoming rpc message: processed");
                                         }
                                         Err(_) => {
-                                            tracing::debug!(%connection_id, "incoming rpc message: channel closed");
+                                            tracing::trace!(%connection_id, "incoming rpc message: channel closed");
                                             return Ok(())
                                         }
                                     },
                                     _ = create_timer(WRITE_TIMEOUT).fuse() => {
-                                        tracing::debug!(%connection_id, "incoming rpc message: processing timed out");
+                                        tracing::trace!(%connection_id, "incoming rpc message: processing timed out");
                                         Err(anyhow!("timed out processing incoming message"))?
                                     }
                                 }
@@ -257,7 +257,7 @@ impl Peer {
                             break;
                         },
                         _ = receive_timeout => {
-                            tracing::debug!(%connection_id, "receive timeout: delay between messages too long");
+                            tracing::trace!(%connection_id, "receive timeout: delay between messages too long");
                             Err(anyhow!("delay between messages too long"))?
                         }
                     }
@@ -274,13 +274,13 @@ impl Peer {
             let response_channels = response_channels.clone();
             async move {
                 let message_id = incoming.id;
-                tracing::debug!(?incoming, "incoming message future: start");
+                tracing::trace!(?incoming, "incoming message future: start");
                 let _end = util::defer(move || {
-                    tracing::debug!(%connection_id, message_id, "incoming message future: end");
+                    tracing::trace!(%connection_id, message_id, "incoming message future: end");
                 });
 
                 if let Some(responding_to) = incoming.responding_to {
-                    tracing::debug!(
+                    tracing::trace!(
                         %connection_id,
                         message_id,
                         responding_to,
@@ -290,7 +290,7 @@ impl Peer {
                     if let Some(tx) = channel {
                         let requester_resumed = oneshot::channel();
                         if let Err(error) = tx.send((incoming, requester_resumed.0)) {
-                            tracing::debug!(
+                            tracing::trace!(
                                 %connection_id,
                                 message_id,
                                 responding_to = responding_to,
@@ -299,14 +299,14 @@ impl Peer {
                             );
                         }
 
-                        tracing::debug!(
+                        tracing::trace!(
                             %connection_id,
                             message_id,
                             responding_to,
                             "incoming response: waiting to resume requester"
                         );
                         let _ = requester_resumed.1.await;
-                        tracing::debug!(
+                        tracing::trace!(
                             %connection_id,
                             message_id,
                             responding_to,
@@ -323,7 +323,7 @@ impl Peer {
 
                     None
                 } else {
-                    tracing::debug!(%connection_id, message_id, "incoming message: received");
+                    tracing::trace!(%connection_id, message_id, "incoming message: received");
                     proto::build_typed_envelope(connection_id, incoming).or_else(|| {
                         tracing::error!(
                             %connection_id,

crates/rpc/src/proto.rs 🔗

@@ -1,3 +1,5 @@
+#![allow(non_snake_case)]
+
 use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope};
 use anyhow::{anyhow, Result};
 use async_tungstenite::tungstenite::Message as WebSocketMessage;
@@ -141,9 +143,10 @@ messages!(
     (Call, Foreground),
     (CallCanceled, Foreground),
     (CancelCall, Foreground),
-    (ChannelMessageSent, Foreground),
     (CopyProjectEntry, Foreground),
     (CreateBufferForPeer, Foreground),
+    (CreateChannel, Foreground),
+    (ChannelResponse, Foreground),
     (CreateProjectEntry, Foreground),
     (CreateRoom, Foreground),
     (CreateRoomResponse, Foreground),
@@ -156,10 +159,6 @@ messages!(
     (FormatBuffers, Foreground),
     (FormatBuffersResponse, Foreground),
     (FuzzySearchUsers, Foreground),
-    (GetChannelMessages, Foreground),
-    (GetChannelMessagesResponse, Foreground),
-    (GetChannels, Foreground),
-    (GetChannelsResponse, Foreground),
     (GetCodeActions, Background),
     (GetCodeActionsResponse, Background),
     (GetHover, Background),
@@ -179,14 +178,12 @@ messages!(
     (GetUsers, Foreground),
     (Hello, Foreground),
     (IncomingCall, Foreground),
+    (InviteChannelMember, Foreground),
     (UsersResponse, Foreground),
-    (JoinChannel, Foreground),
-    (JoinChannelResponse, Foreground),
     (JoinProject, Foreground),
     (JoinProjectResponse, Foreground),
     (JoinRoom, Foreground),
     (JoinRoomResponse, Foreground),
-    (LeaveChannel, Foreground),
     (LeaveProject, Foreground),
     (LeaveRoom, Foreground),
     (OpenBufferById, Background),
@@ -200,6 +197,8 @@ messages!(
     (OnTypeFormattingResponse, Background),
     (InlayHints, Background),
     (InlayHintsResponse, Background),
+    (ResolveInlayHint, Background),
+    (ResolveInlayHintResponse, Background),
     (RefreshInlayHints, Foreground),
     (Ping, Foreground),
     (PrepareRename, Background),
@@ -209,18 +208,21 @@ messages!(
     (RejoinRoom, Foreground),
     (RejoinRoomResponse, Foreground),
     (RemoveContact, Foreground),
+    (RemoveChannelMember, Foreground),
     (ReloadBuffers, Foreground),
     (ReloadBuffersResponse, Foreground),
     (RemoveProjectCollaborator, Foreground),
     (RenameProjectEntry, Foreground),
     (RequestContact, Foreground),
     (RespondToContactRequest, Foreground),
+    (RespondToChannelInvite, Foreground),
+    (JoinChannel, Foreground),
     (RoomUpdated, Foreground),
     (SaveBuffer, Foreground),
+    (RenameChannel, Foreground),
+    (SetChannelMemberAdmin, Foreground),
     (SearchProject, Background),
     (SearchProjectResponse, Background),
-    (SendChannelMessage, Foreground),
-    (SendChannelMessageResponse, Foreground),
     (ShareProject, Foreground),
     (ShareProjectResponse, Foreground),
     (ShowContacts, Foreground),
@@ -233,6 +235,8 @@ messages!(
     (UpdateBuffer, Foreground),
     (UpdateBufferFile, Foreground),
     (UpdateContacts, Foreground),
+    (RemoveChannel, Foreground),
+    (UpdateChannels, Foreground),
     (UpdateDiagnosticSummary, Foreground),
     (UpdateFollowers, Foreground),
     (UpdateInviteInfo, Foreground),
@@ -245,6 +249,14 @@ messages!(
     (UpdateDiffBase, Foreground),
     (GetPrivateUserInfo, Foreground),
     (GetPrivateUserInfoResponse, Foreground),
+    (GetChannelMembers, Foreground),
+    (GetChannelMembersResponse, Foreground),
+    (JoinChannelBuffer, Foreground),
+    (JoinChannelBufferResponse, Foreground),
+    (LeaveChannelBuffer, Background),
+    (UpdateChannelBuffer, Foreground),
+    (RemoveChannelBufferCollaborator, Foreground),
+    (AddChannelBufferCollaborator, Foreground),
 );
 
 request_messages!(
@@ -258,13 +270,12 @@ request_messages!(
     (CopyProjectEntry, ProjectEntryResponse),
     (CreateProjectEntry, ProjectEntryResponse),
     (CreateRoom, CreateRoomResponse),
+    (CreateChannel, ChannelResponse),
     (DeclineCall, Ack),
     (DeleteProjectEntry, ProjectEntryResponse),
     (ExpandProjectEntry, ExpandProjectEntryResponse),
     (Follow, FollowResponse),
     (FormatBuffers, FormatBuffersResponse),
-    (GetChannelMessages, GetChannelMessagesResponse),
-    (GetChannels, GetChannelsResponse),
     (GetCodeActions, GetCodeActionsResponse),
     (GetHover, GetHoverResponse),
     (GetCompletions, GetCompletionsResponse),
@@ -276,7 +287,7 @@ request_messages!(
     (GetProjectSymbols, GetProjectSymbolsResponse),
     (FuzzySearchUsers, UsersResponse),
     (GetUsers, UsersResponse),
-    (JoinChannel, JoinChannelResponse),
+    (InviteChannelMember, Ack),
     (JoinProject, JoinProjectResponse),
     (JoinRoom, JoinRoomResponse),
     (LeaveRoom, Ack),
@@ -290,15 +301,22 @@ request_messages!(
     (PrepareRename, PrepareRenameResponse),
     (OnTypeFormatting, OnTypeFormattingResponse),
     (InlayHints, InlayHintsResponse),
+    (ResolveInlayHint, ResolveInlayHintResponse),
     (RefreshInlayHints, Ack),
     (ReloadBuffers, ReloadBuffersResponse),
     (RequestContact, Ack),
+    (RemoveChannelMember, Ack),
     (RemoveContact, Ack),
     (RespondToContactRequest, Ack),
+    (RespondToChannelInvite, Ack),
+    (SetChannelMemberAdmin, Ack),
+    (GetChannelMembers, GetChannelMembersResponse),
+    (JoinChannel, JoinRoomResponse),
+    (RemoveChannel, Ack),
     (RenameProjectEntry, ProjectEntryResponse),
+    (RenameChannel, ChannelResponse),
     (SaveBuffer, BufferSaved),
     (SearchProject, SearchProjectResponse),
-    (SendChannelMessage, SendChannelMessageResponse),
     (ShareProject, ShareProjectResponse),
     (SynchronizeBuffers, SynchronizeBuffersResponse),
     (Test, Test),
@@ -306,6 +324,8 @@ request_messages!(
     (UpdateParticipantLocation, Ack),
     (UpdateProject, Ack),
     (UpdateWorktree, Ack),
+    (JoinChannelBuffer, JoinChannelBufferResponse),
+    (LeaveChannelBuffer, Ack)
 );
 
 entity_messages!(
@@ -338,6 +358,7 @@ entity_messages!(
     PerformRename,
     OnTypeFormatting,
     InlayHints,
+    ResolveInlayHint,
     RefreshInlayHints,
     PrepareRename,
     ReloadBuffers,
@@ -361,7 +382,12 @@ entity_messages!(
     UpdateDiffBase
 );
 
-entity_messages!(channel_id, ChannelMessageSent);
+entity_messages!(
+    channel_id,
+    UpdateChannelBuffer,
+    RemoveChannelBufferCollaborator,
+    AddChannelBufferCollaborator
+);
 
 const KIB: usize = 1024;
 const MIB: usize = KIB * 1024;

crates/rpc/src/rpc.rs 🔗

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 60;
+pub const PROTOCOL_VERSION: u32 = 61;

crates/search/Cargo.toml 🔗

@@ -30,11 +30,11 @@ serde_derive.workspace = true
 smallvec.workspace = true
 smol.workspace = true
 globset.workspace = true
-
+serde_json.workspace = true
 [dev-dependencies]
 client = { path = "../client", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
-serde_json.workspace = true
+
 workspace = { path = "../workspace", features = ["test-support"] }
 unindent.workspace = true

crates/search/src/buffer_search.rs 🔗

@@ -1,6 +1,9 @@
 use crate::{
-    NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectAllMatches,
-    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord,
+    history::SearchHistory,
+    mode::{next_mode, SearchMode, Side},
+    search_bar::{render_nav_button, render_search_mode_button},
+    CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches,
+    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
 };
 use collections::HashMap;
 use editor::Editor;
@@ -16,6 +19,7 @@ use gpui::{
 use project::search::SearchQuery;
 use serde::Deserialize;
 use std::{any::Any, sync::Arc};
+
 use util::ResultExt;
 use workspace::{
     item::ItemHandle,
@@ -36,7 +40,7 @@ pub enum Event {
 }
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(BufferSearchBar::deploy);
+    cx.add_action(BufferSearchBar::deploy_bar);
     cx.add_action(BufferSearchBar::dismiss);
     cx.add_action(BufferSearchBar::focus_editor);
     cx.add_action(BufferSearchBar::select_next_match);
@@ -48,9 +52,10 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(BufferSearchBar::handle_editor_cancel);
     cx.add_action(BufferSearchBar::next_history_query);
     cx.add_action(BufferSearchBar::previous_history_query);
+    cx.add_action(BufferSearchBar::cycle_mode);
+    cx.add_action(BufferSearchBar::cycle_mode_on_pane);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
     add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
-    add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
 }
 
 fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
@@ -79,6 +84,7 @@ pub struct BufferSearchBar {
     query_contains_error: bool,
     dismissed: bool,
     search_history: SearchHistory,
+    current_mode: SearchMode,
 }
 
 impl Entity for BufferSearchBar {
@@ -98,7 +104,7 @@ impl View for BufferSearchBar {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         let theme = theme::current(cx).clone();
-        let editor_container = if self.query_contains_error {
+        let query_container_style = if self.query_contains_error {
             theme.search.invalid_editor
         } else {
             theme.search.editor.input.container
@@ -150,79 +156,123 @@ impl View for BufferSearchBar {
         self.query_editor.update(cx, |editor, cx| {
             editor.set_placeholder_text(new_placeholder_text, cx);
         });
+        let search_button_for_mode = |mode, side, cx: &mut ViewContext<BufferSearchBar>| {
+            let is_active = self.current_mode == mode;
 
-        Flex::row()
+            render_search_mode_button(
+                mode,
+                side,
+                is_active,
+                move |_, this, cx| {
+                    this.activate_search_mode(mode, cx);
+                },
+                cx,
+            )
+        };
+        let search_option_button = |option| {
+            let is_active = self.search_options.contains(option);
+            option.as_button(
+                is_active,
+                theme.tooltip.clone(),
+                theme.search.option_button_component.clone(),
+            )
+        };
+        let match_count = self
+            .active_searchable_item
+            .as_ref()
+            .and_then(|searchable_item| {
+                if self.query(cx).is_empty() {
+                    return None;
+                }
+                let matches = self
+                    .searchable_items_with_matches
+                    .get(&searchable_item.downgrade())?;
+                let message = if let Some(match_ix) = self.active_match_index {
+                    format!("{}/{}", match_ix + 1, matches.len())
+                } else {
+                    "No matches".to_string()
+                };
+
+                Some(
+                    Label::new(message, theme.search.match_index.text.clone())
+                        .contained()
+                        .with_style(theme.search.match_index.container)
+                        .aligned(),
+                )
+            });
+        let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
+            render_nav_button(
+                label,
+                direction,
+                self.active_match_index.is_some(),
+                move |_, this, cx| match direction {
+                    Direction::Prev => this.select_prev_match(&Default::default(), cx),
+                    Direction::Next => this.select_next_match(&Default::default(), cx),
+                },
+                cx,
+            )
+        };
+
+        let query_column = Flex::row()
+            .with_child(
+                Svg::for_style(theme.search.editor_icon.clone().icon)
+                    .contained()
+                    .with_style(theme.search.editor_icon.clone().container),
+            )
+            .with_child(ChildView::new(&self.query_editor, cx).flex(1., true))
             .with_child(
                 Flex::row()
-                    .with_child(
-                        Flex::row()
-                            .with_child(
-                                ChildView::new(&self.query_editor, cx)
-                                    .aligned()
-                                    .left()
-                                    .flex(1., true),
-                            )
-                            .with_children(self.active_searchable_item.as_ref().and_then(
-                                |searchable_item| {
-                                    let matches = self
-                                        .searchable_items_with_matches
-                                        .get(&searchable_item.downgrade())?;
-                                    let message = if let Some(match_ix) = self.active_match_index {
-                                        format!("{}/{}", match_ix + 1, matches.len())
-                                    } else {
-                                        "No matches".to_string()
-                                    };
-
-                                    Some(
-                                        Label::new(message, theme.search.match_index.text.clone())
-                                            .contained()
-                                            .with_style(theme.search.match_index.container)
-                                            .aligned(),
-                                    )
-                                },
-                            ))
-                            .contained()
-                            .with_style(editor_container)
-                            .aligned()
-                            .constrained()
-                            .with_min_width(theme.search.editor.min_width)
-                            .with_max_width(theme.search.editor.max_width)
-                            .flex(1., false),
+                    .with_children(
+                        supported_options
+                            .case
+                            .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)),
                     )
-                    .with_child(
-                        Flex::row()
-                            .with_child(self.render_nav_button("<", Direction::Prev, cx))
-                            .with_child(self.render_nav_button(">", Direction::Next, cx))
-                            .with_child(self.render_action_button("Select All", cx))
-                            .aligned(),
+                    .with_children(
+                        supported_options
+                            .word
+                            .then(|| search_option_button(SearchOptions::WHOLE_WORD)),
                     )
-                    .with_child(
-                        Flex::row()
-                            .with_children(self.render_search_option(
-                                supported_options.case,
-                                "Case",
-                                SearchOptions::CASE_SENSITIVE,
-                                cx,
-                            ))
-                            .with_children(self.render_search_option(
-                                supported_options.word,
-                                "Word",
-                                SearchOptions::WHOLE_WORD,
-                                cx,
-                            ))
-                            .with_children(self.render_search_option(
-                                supported_options.regex,
-                                "Regex",
-                                SearchOptions::REGEX,
-                                cx,
-                            ))
-                            .contained()
-                            .with_style(theme.search.option_button_group)
-                            .aligned(),
-                    )
-                    .flex(1., true),
+                    .flex_float()
+                    .contained(),
             )
-            .with_child(self.render_close_button(&theme.search, cx))
+            .align_children_center()
+            .contained()
+            .with_style(query_container_style)
+            .constrained()
+            .with_min_width(theme.search.editor.min_width)
+            .with_max_width(theme.search.editor.max_width)
+            .with_height(theme.search.search_bar_row_height)
+            .flex(1., false);
+
+        let mode_column = Flex::row()
+            .with_child(search_button_for_mode(
+                SearchMode::Text,
+                Some(Side::Left),
+                cx,
+            ))
+            .with_child(search_button_for_mode(
+                SearchMode::Regex,
+                Some(Side::Right),
+                cx,
+            ))
+            .contained()
+            .with_style(theme.search.modes_container)
+            .constrained()
+            .with_height(theme.search.search_bar_row_height);
+
+        let nav_column = Flex::row()
+            .with_child(self.render_action_button("all", cx))
+            .with_child(Flex::row().with_children(match_count))
+            .with_child(nav_button_for_direction("<", Direction::Prev, cx))
+            .with_child(nav_button_for_direction(">", Direction::Next, cx))
+            .constrained()
+            .with_height(theme.search.search_bar_row_height)
+            .flex_float();
+
+        Flex::row()
+            .with_child(query_column)
+            .with_child(mode_column)
+            .with_child(nav_column)
             .contained()
             .with_style(theme.search.container)
             .into_any_named("search bar")
@@ -278,6 +328,10 @@ impl ToolbarItemView for BufferSearchBar {
             ToolbarItemLocation::Hidden
         }
     }
+
+    fn row_count(&self, _: &ViewContext<Self>) -> usize {
+        1
+    }
 }
 
 impl BufferSearchBar {
@@ -304,6 +358,7 @@ impl BufferSearchBar {
             query_contains_error: false,
             dismissed: true,
             search_history: SearchHistory::default(),
+            current_mode: SearchMode::default(),
         }
     }
 
@@ -327,6 +382,19 @@ impl BufferSearchBar {
         cx.notify();
     }
 
+    pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
+        if self.show(cx) {
+            self.search_suggested(cx);
+            if deploy.focus {
+                self.select_query(cx);
+                cx.focus_self();
+            }
+            return true;
+        }
+
+        false
+    }
+
     pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if self.active_searchable_item.is_none() {
             return false;
@@ -402,91 +470,6 @@ impl BufferSearchBar {
         self.update_matches(cx)
     }
 
-    fn render_search_option(
-        &self,
-        option_supported: bool,
-        icon: &'static str,
-        option: SearchOptions,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        if !option_supported {
-            return None;
-        }
-
-        let tooltip_style = theme::current(cx).tooltip.clone();
-        let is_active = self.search_options.contains(option);
-        Some(
-            MouseEventHandler::new::<Self, _>(option.bits as usize, cx, |state, cx| {
-                let theme = theme::current(cx);
-                let style = theme
-                    .search
-                    .option_button
-                    .in_state(is_active)
-                    .style_for(state);
-                Label::new(icon, style.text.clone())
-                    .contained()
-                    .with_style(style.container)
-            })
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                this.toggle_search_option(option, cx);
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .with_tooltip::<Self>(
-                option.bits as usize,
-                format!("Toggle {}", option.label()),
-                Some(option.to_toggle_action()),
-                tooltip_style,
-                cx,
-            )
-            .into_any(),
-        )
-    }
-
-    fn render_nav_button(
-        &self,
-        icon: &'static str,
-        direction: Direction,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let action: Box<dyn Action>;
-        let tooltip;
-        match direction {
-            Direction::Prev => {
-                action = Box::new(SelectPrevMatch);
-                tooltip = "Select Previous Match";
-            }
-            Direction::Next => {
-                action = Box::new(SelectNextMatch);
-                tooltip = "Select Next Match";
-            }
-        };
-        let tooltip_style = theme::current(cx).tooltip.clone();
-
-        enum NavButton {}
-        MouseEventHandler::new::<NavButton, _>(direction as usize, cx, |state, cx| {
-            let theme = theme::current(cx);
-            let style = theme.search.option_button.inactive_state().style_for(state);
-            Label::new(icon, style.text.clone())
-                .contained()
-                .with_style(style.container)
-        })
-        .on_click(MouseButton::Left, {
-            move |_, this, cx| match direction {
-                Direction::Prev => this.select_prev_match(&Default::default(), cx),
-                Direction::Next => this.select_next_match(&Default::default(), cx),
-            }
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .with_tooltip::<NavButton>(
-            direction as usize,
-            tooltip.to_string(),
-            Some(action),
-            tooltip_style,
-            cx,
-        )
-        .into_any()
-    }
-
     fn render_action_button(
         &self,
         icon: &'static str,
@@ -495,19 +478,29 @@ impl BufferSearchBar {
         let tooltip = "Select All Matches";
         let tooltip_style = theme::current(cx).tooltip.clone();
         let action_type_id = 0_usize;
-
+        let has_matches = self.active_match_index.is_some();
+        let cursor_style = if has_matches {
+            CursorStyle::PointingHand
+        } else {
+            CursorStyle::default()
+        };
         enum ActionButton {}
         MouseEventHandler::new::<ActionButton, _>(action_type_id, cx, |state, cx| {
             let theme = theme::current(cx);
-            let style = theme.search.action_button.style_for(state);
+            let style = theme
+                .search
+                .action_button
+                .in_state(has_matches)
+                .style_for(state);
             Label::new(icon, style.text.clone())
+                .aligned()
                 .contained()
                 .with_style(style.container)
         })
         .on_click(MouseButton::Left, move |_, this, cx| {
             this.select_all_matches(&SelectAllMatches, cx)
         })
-        .with_cursor_style(CursorStyle::PointingHand)
+        .with_cursor_style(cursor_style)
         .with_tooltip::<ActionButton>(
             action_type_id,
             tooltip.to_string(),
@@ -518,56 +511,29 @@ impl BufferSearchBar {
         .into_any()
     }
 
-    fn render_close_button(
-        &self,
-        theme: &theme::Search,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let tooltip = "Dismiss Buffer Search";
-        let tooltip_style = theme::current(cx).tooltip.clone();
-
-        enum CloseButton {}
-        MouseEventHandler::new::<CloseButton, _>(0, cx, |state, _| {
-            let style = theme.dismiss_button.style_for(state);
-            Svg::new("icons/x_mark_8.svg")
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .contained()
-                .with_style(style.container)
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.dismiss(&Default::default(), cx)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .with_tooltip::<CloseButton>(
-            0,
-            tooltip.to_string(),
-            Some(Box::new(Dismiss)),
-            tooltip_style,
-            cx,
-        )
-        .into_any()
+    pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+        assert_ne!(
+            mode,
+            SearchMode::Semantic,
+            "Semantic search is not supported in buffer search"
+        );
+        if mode == self.current_mode {
+            return;
+        }
+        self.current_mode = mode;
+        let _ = self.update_matches(cx);
+        cx.notify();
     }
 
-    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
+    fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
         let mut propagate_action = true;
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
             search_bar.update(cx, |search_bar, cx| {
-                if search_bar.show(cx) {
-                    search_bar.search_suggested(cx);
-                    if action.focus {
-                        search_bar.select_query(cx);
-                        cx.focus_self();
-                    }
+                if search_bar.deploy(action, cx) {
                     propagate_action = false;
                 }
             });
         }
-
         if propagate_action {
             cx.propagate_action();
         }
@@ -727,8 +693,9 @@ impl BufferSearchBar {
                 self.active_match_index.take();
                 active_searchable_item.clear_matches(cx);
                 let _ = done_tx.send(());
+                cx.notify();
             } else {
-                let query = if self.search_options.contains(SearchOptions::REGEX) {
+                let query = if self.current_mode == SearchMode::Regex {
                     match SearchQuery::regex(
                         query,
                         self.search_options.contains(SearchOptions::WHOLE_WORD),
@@ -823,6 +790,26 @@ impl BufferSearchBar {
             let _ = self.search(&new_query, Some(self.search_options), cx);
         }
     }
+    fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
+        self.activate_search_mode(next_mode(&self.current_mode, false), cx);
+    }
+    fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext<Pane>) {
+        let mut should_propagate = true;
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| {
+                if bar.show(cx) {
+                    should_propagate = false;
+                    bar.cycle_mode(action, cx);
+                    false
+                } else {
+                    true
+                }
+            });
+        }
+        if should_propagate {
+            cx.propagate_action();
+        }
+    }
 }
 
 #[cfg(test)]
@@ -839,6 +826,7 @@ mod tests {
         let buffer = cx.add_model(|cx| {
             Buffer::new(
                 0,
+                cx.model_id() as u64,
                 r#"
                 A regular expression (shortened as regex or regexp;[1] also referred to as
                 rational expression[2][3]) is a sequence of characters that specifies a search
@@ -846,7 +834,6 @@ mod tests {
                 for "find" or "find and replace" operations on strings, or for input validation.
                 "#
                 .unindent(),
-                cx,
             )
         });
         let window = cx.add_window(|_| EmptyView);
@@ -1227,7 +1214,7 @@ mod tests {
             expected_query_matches_count > 1,
             "Should pick a query with multiple results"
         );
-        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text));
         let window = cx.add_window(|_| EmptyView);
         let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 
@@ -1414,7 +1401,7 @@ mod tests {
         for "find" or "find and replace" operations on strings, or for input validation.
         "#
         .unindent();
-        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text));
         let window = cx.add_window(|_| EmptyView);
 
         let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));

crates/search/src/history.rs 🔗

@@ -0,0 +1,184 @@
+use smallvec::SmallVec;
+const SEARCH_HISTORY_LIMIT: usize = 20;
+
+#[derive(Default, Debug, Clone)]
+pub struct SearchHistory {
+    history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
+    selected: Option<usize>,
+}
+
+impl SearchHistory {
+    pub fn add(&mut self, search_string: String) {
+        if let Some(i) = self.selected {
+            if search_string == self.history[i] {
+                return;
+            }
+        }
+
+        if let Some(previously_searched) = self.history.last_mut() {
+            if search_string.find(previously_searched.as_str()).is_some() {
+                *previously_searched = search_string;
+                self.selected = Some(self.history.len() - 1);
+                return;
+            }
+        }
+
+        self.history.push(search_string);
+        if self.history.len() > SEARCH_HISTORY_LIMIT {
+            self.history.remove(0);
+        }
+        self.selected = Some(self.history.len() - 1);
+    }
+
+    pub fn next(&mut self) -> Option<&str> {
+        let history_size = self.history.len();
+        if history_size == 0 {
+            return None;
+        }
+
+        let selected = self.selected?;
+        if selected == history_size - 1 {
+            return None;
+        }
+        let next_index = selected + 1;
+        self.selected = Some(next_index);
+        Some(&self.history[next_index])
+    }
+
+    pub fn current(&self) -> Option<&str> {
+        Some(&self.history[self.selected?])
+    }
+
+    pub fn previous(&mut self) -> Option<&str> {
+        let history_size = self.history.len();
+        if history_size == 0 {
+            return None;
+        }
+
+        let prev_index = match self.selected {
+            Some(selected_index) => {
+                if selected_index == 0 {
+                    return None;
+                } else {
+                    selected_index - 1
+                }
+            }
+            None => history_size - 1,
+        };
+
+        self.selected = Some(prev_index);
+        Some(&self.history[prev_index])
+    }
+
+    pub fn reset_selection(&mut self) {
+        self.selected = None;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_add() {
+        let mut search_history = SearchHistory::default();
+        assert_eq!(
+            search_history.current(),
+            None,
+            "No current selection should be set fo the default search history"
+        );
+
+        search_history.add("rust".to_string());
+        assert_eq!(
+            search_history.current(),
+            Some("rust"),
+            "Newly added item should be selected"
+        );
+
+        // check if duplicates are not added
+        search_history.add("rust".to_string());
+        assert_eq!(
+            search_history.history.len(),
+            1,
+            "Should not add a duplicate"
+        );
+        assert_eq!(search_history.current(), Some("rust"));
+
+        // check if new string containing the previous string replaces it
+        search_history.add("rustlang".to_string());
+        assert_eq!(
+            search_history.history.len(),
+            1,
+            "Should replace previous item if it's a substring"
+        );
+        assert_eq!(search_history.current(), Some("rustlang"));
+
+        // push enough items to test SEARCH_HISTORY_LIMIT
+        for i in 0..SEARCH_HISTORY_LIMIT * 2 {
+            search_history.add(format!("item{i}"));
+        }
+        assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
+    }
+
+    #[test]
+    fn test_next_and_previous() {
+        let mut search_history = SearchHistory::default();
+        assert_eq!(
+            search_history.next(),
+            None,
+            "Default search history should not have a next item"
+        );
+
+        search_history.add("Rust".to_string());
+        assert_eq!(search_history.next(), None);
+        search_history.add("JavaScript".to_string());
+        assert_eq!(search_history.next(), None);
+        search_history.add("TypeScript".to_string());
+        assert_eq!(search_history.next(), None);
+
+        assert_eq!(search_history.current(), Some("TypeScript"));
+
+        assert_eq!(search_history.previous(), Some("JavaScript"));
+        assert_eq!(search_history.current(), Some("JavaScript"));
+
+        assert_eq!(search_history.previous(), Some("Rust"));
+        assert_eq!(search_history.current(), Some("Rust"));
+
+        assert_eq!(search_history.previous(), None);
+        assert_eq!(search_history.current(), Some("Rust"));
+
+        assert_eq!(search_history.next(), Some("JavaScript"));
+        assert_eq!(search_history.current(), Some("JavaScript"));
+
+        assert_eq!(search_history.next(), Some("TypeScript"));
+        assert_eq!(search_history.current(), Some("TypeScript"));
+
+        assert_eq!(search_history.next(), None);
+        assert_eq!(search_history.current(), Some("TypeScript"));
+    }
+
+    #[test]
+    fn test_reset_selection() {
+        let mut search_history = SearchHistory::default();
+        search_history.add("Rust".to_string());
+        search_history.add("JavaScript".to_string());
+        search_history.add("TypeScript".to_string());
+
+        assert_eq!(search_history.current(), Some("TypeScript"));
+        search_history.reset_selection();
+        assert_eq!(search_history.current(), None);
+        assert_eq!(
+            search_history.previous(),
+            Some("TypeScript"),
+            "Should start from the end after reset on previous item query"
+        );
+
+        search_history.previous();
+        assert_eq!(search_history.current(), Some("JavaScript"));
+        search_history.previous();
+        assert_eq!(search_history.current(), Some("Rust"));
+
+        search_history.reset_selection();
+        assert_eq!(search_history.current(), None);
+    }
+}

crates/search/src/mode.rs 🔗

@@ -0,0 +1,65 @@
+use gpui::Action;
+
+use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
+// TODO: Update the default search mode to get from config
+#[derive(Copy, Clone, Debug, Default, PartialEq)]
+pub enum SearchMode {
+    #[default]
+    Text,
+    Semantic,
+    Regex,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub(crate) enum Side {
+    Left,
+    Right,
+}
+
+impl SearchMode {
+    pub(crate) fn label(&self) -> &'static str {
+        match self {
+            SearchMode::Text => "Text",
+            SearchMode::Semantic => "Semantic",
+            SearchMode::Regex => "Regex",
+        }
+    }
+
+    pub(crate) fn region_id(&self) -> usize {
+        match self {
+            SearchMode::Text => 3,
+            SearchMode::Semantic => 4,
+            SearchMode::Regex => 5,
+        }
+    }
+
+    pub(crate) fn tooltip_text(&self) -> &'static str {
+        match self {
+            SearchMode::Text => "Activate Text Search",
+            SearchMode::Semantic => "Activate Semantic Search",
+            SearchMode::Regex => "Activate Regex Search",
+        }
+    }
+
+    pub(crate) fn activate_action(&self) -> Box<dyn Action> {
+        match self {
+            SearchMode::Text => Box::new(ActivateTextMode),
+            SearchMode::Semantic => Box::new(ActivateSemanticMode),
+            SearchMode::Regex => Box::new(ActivateRegexMode),
+        }
+    }
+}
+
+pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {
+    match mode {
+        SearchMode::Text => SearchMode::Regex,
+        SearchMode::Regex => {
+            if semantic_enabled {
+                SearchMode::Semantic
+            } else {
+                SearchMode::Text
+            }
+        }
+        SearchMode::Semantic => SearchMode::Text,
+    }
+}

crates/search/src/project_search.rs 🔗

@@ -1,25 +1,30 @@
 use crate::{
-    NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch,
-    SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord,
+    history::SearchHistory,
+    mode::{SearchMode, Side},
+    search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
+    ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions,
+    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
 };
-use anyhow::Context;
+use anyhow::{Context, Result};
 use collections::HashMap;
 use editor::{
     items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
     SelectAll, MAX_TAB_TITLE_LEN,
 };
 use futures::StreamExt;
+
+use gpui::platform::PromptLevel;
+
 use gpui::{
-    actions,
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription,
-    Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
+    actions, elements::*, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext,
+    Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
+    WeakModelHandle, WeakViewHandle,
 };
+
 use menu::Confirm;
 use postage::stream::Stream;
 use project::{
-    search::{PathMatcher, SearchQuery},
+    search::{PathMatcher, SearchInputs, SearchQuery},
     Entry, Project,
 };
 use semantic_index::SemanticIndex;
@@ -42,7 +47,7 @@ use workspace::{
 
 actions!(
     project_search,
-    [SearchInNew, ToggleFocus, NextField, ToggleSemanticSearch]
+    [SearchInNew, ToggleFocus, NextField, ToggleFilters,]
 );
 
 #[derive(Default)]
@@ -56,13 +61,26 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(ProjectSearchBar::search_in_new);
     cx.add_action(ProjectSearchBar::select_next_match);
     cx.add_action(ProjectSearchBar::select_prev_match);
+    cx.add_action(ProjectSearchBar::cycle_mode);
     cx.add_action(ProjectSearchBar::next_history_query);
     cx.add_action(ProjectSearchBar::previous_history_query);
+    cx.add_action(ProjectSearchBar::activate_regex_mode);
     cx.capture_action(ProjectSearchBar::tab);
     cx.capture_action(ProjectSearchBar::tab_previous);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
     add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
-    add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
+    add_toggle_filters_action::<ToggleFilters>(cx);
+}
+
+fn add_toggle_filters_action<A: Action>(cx: &mut AppContext) {
+    cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
+            if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) {
+                return;
+            }
+        }
+        cx.propagate_action();
+    });
 }
 
 fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
@@ -86,6 +104,7 @@ struct ProjectSearch {
     active_query: Option<SearchQuery>,
     search_id: usize,
     search_history: SearchHistory,
+    no_results: Option<bool>,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -99,7 +118,8 @@ pub struct ProjectSearchView {
     model: ModelHandle<ProjectSearch>,
     query_editor: ViewHandle<Editor>,
     results_editor: ViewHandle<Editor>,
-    semantic: Option<SemanticSearchState>,
+    semantic_state: Option<SemanticSearchState>,
+    semantic_permissioned: Option<bool>,
     search_options: SearchOptions,
     panels_with_errors: HashSet<InputPanel>,
     active_match_index: Option<usize>,
@@ -107,6 +127,8 @@ pub struct ProjectSearchView {
     query_editor_was_focused: bool,
     included_files_editor: ViewHandle<Editor>,
     excluded_files_editor: ViewHandle<Editor>,
+    filters_enabled: bool,
+    current_mode: SearchMode,
 }
 
 struct SemanticSearchState {
@@ -135,6 +157,7 @@ impl ProjectSearch {
             active_query: None,
             search_id: 0,
             search_history: SearchHistory::default(),
+            no_results: None,
         }
     }
 
@@ -149,6 +172,7 @@ impl ProjectSearch {
             active_query: self.active_query.clone(),
             search_id: self.search_id,
             search_history: self.search_history.clone(),
+            no_results: self.no_results.clone(),
         })
     }
 
@@ -161,26 +185,26 @@ impl ProjectSearch {
         self.active_query = Some(query);
         self.match_ranges.clear();
         self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
-            let matches = search.await.log_err()?;
+            let mut matches = search;
             let this = this.upgrade(&cx)?;
-            let mut matches = matches.into_iter().collect::<Vec<_>>();
-            let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| {
+            this.update(&mut cx, |this, cx| {
                 this.match_ranges.clear();
-                matches.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path()));
-                this.excerpts.update(cx, |excerpts, cx| {
-                    excerpts.clear(cx);
-                    excerpts.stream_excerpts_with_context_lines(matches, 1, cx)
-                })
+                this.excerpts.update(cx, |this, cx| this.clear(cx));
+                this.no_results = Some(true);
             });
 
-            while let Some(match_range) = match_ranges.next().await {
-                this.update(&mut cx, |this, cx| {
-                    this.match_ranges.push(match_range);
-                    while let Ok(Some(match_range)) = match_ranges.try_next() {
-                        this.match_ranges.push(match_range);
-                    }
-                    cx.notify();
+            while let Some((buffer, anchors)) = matches.next().await {
+                let mut ranges = this.update(&mut cx, |this, cx| {
+                    this.no_results = Some(false);
+                    this.excerpts.update(cx, |excerpts, cx| {
+                        excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx)
+                    })
                 });
+
+                while let Some(range) = ranges.next().await {
+                    this.update(&mut cx, |this, _| this.match_ranges.push(range));
+                }
+                this.update(&mut cx, |_, cx| cx.notify());
             }
 
             this.update(&mut cx, |this, cx| {
@@ -193,46 +217,50 @@ impl ProjectSearch {
         cx.notify();
     }
 
-    fn semantic_search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
+    fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext<Self>) {
         let search = SemanticIndex::global(cx).map(|index| {
             index.update(cx, |semantic_index, cx| {
                 semantic_index.search_project(
                     self.project.clone(),
-                    query.as_str().to_owned(),
+                    inputs.as_str().to_owned(),
                     10,
-                    query.files_to_include().to_vec(),
-                    query.files_to_exclude().to_vec(),
+                    inputs.files_to_include().to_vec(),
+                    inputs.files_to_exclude().to_vec(),
                     cx,
                 )
             })
         });
         self.search_id += 1;
         self.match_ranges.clear();
-        self.search_history.add(query.as_str().to_string());
+        self.search_history.add(inputs.as_str().to_string());
+        self.no_results = Some(true);
         self.pending_search = Some(cx.spawn(|this, mut cx| async move {
             let results = search?.await.log_err()?;
+            let matches = results
+                .into_iter()
+                .map(|result| (result.buffer, vec![result.range.start..result.range.start]));
 
-            let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| {
+            this.update(&mut cx, |this, cx| {
                 this.excerpts.update(cx, |excerpts, cx| {
                     excerpts.clear(cx);
-
-                    let matches = results
-                        .into_iter()
-                        .map(|result| (result.buffer, vec![result.range.start..result.range.start]))
-                        .collect();
-
-                    excerpts.stream_excerpts_with_context_lines(matches, 3, cx)
                 })
             });
-
-            while let Some(match_range) = match_ranges.next().await {
-                this.update(&mut cx, |this, cx| {
-                    this.match_ranges.push(match_range);
-                    while let Ok(Some(match_range)) = match_ranges.try_next() {
-                        this.match_ranges.push(match_range);
-                    }
-                    cx.notify();
+            for (buffer, ranges) in matches {
+                let mut match_ranges = this.update(&mut cx, |this, cx| {
+                    this.no_results = Some(false);
+                    this.excerpts.update(cx, |excerpts, cx| {
+                        excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx)
+                    })
                 });
+                while let Some(match_range) = match_ranges.next().await {
+                    this.update(&mut cx, |this, cx| {
+                        this.match_ranges.push(match_range);
+                        while let Ok(Some(match_range)) = match_ranges.try_next() {
+                            this.match_ranges.push(match_range);
+                        }
+                        cx.notify();
+                    });
+                }
             }
 
             this.update(&mut cx, |this, cx| {
@@ -246,10 +274,12 @@ impl ProjectSearch {
     }
 }
 
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum ViewEvent {
     UpdateTab,
     Activate,
     EditorEvent(editor::Event),
+    Dismiss,
 }
 
 impl Entity for ProjectSearchView {
@@ -267,22 +297,61 @@ impl View for ProjectSearchView {
             enum Status {}
 
             let theme = theme::current(cx).clone();
-            let text = if model.pending_search.is_some() {
+
+            // If Search is Active -> Major: Searching..., Minor: None
+            // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...}
+            // If Regex -> Major: "Search using Regex", Minor: {ex...}
+            // If Text -> Major: "Text search all files and folders", Minor: {...}
+
+            let current_mode = self.current_mode;
+            let major_text = if model.pending_search.is_some() {
                 Cow::Borrowed("Searching...")
-            } else if let Some(semantic) = &self.semantic {
+            } else if model.no_results.is_some_and(|v| v) {
+                Cow::Borrowed("No Results")
+            } else {
+                match current_mode {
+                    SearchMode::Text => Cow::Borrowed("Text search all files and folders"),
+                    SearchMode::Semantic => {
+                        Cow::Borrowed("Search all code objects using Natural Language")
+                    }
+                    SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"),
+                }
+            };
+
+            let semantic_status = if let Some(semantic) = &self.semantic_state {
                 if semantic.outstanding_file_count > 0 {
-                    Cow::Owned(format!(
-                        "Indexing. {} of {}...",
+                    format!(
+                        "Indexing: {} of {}...",
                         semantic.file_count - semantic.outstanding_file_count,
                         semantic.file_count
-                    ))
+                    )
                 } else {
-                    Cow::Borrowed("Indexing complete")
+                    "Indexing complete".to_string()
                 }
-            } else if self.query_editor.read(cx).text(cx).is_empty() {
-                Cow::Borrowed("")
             } else {
-                Cow::Borrowed("No results")
+                "Indexing: ...".to_string()
+            };
+
+            let minor_text = if let Some(no_results) = model.no_results {
+                if model.pending_search.is_none() && no_results {
+                    vec!["No results found in this project for the provided query".to_owned()]
+                } else {
+                    vec![]
+                }
+            } else {
+                match current_mode {
+                    SearchMode::Semantic => vec![
+                        "".to_owned(),
+                        semantic_status,
+                        "Simply explain the code you are looking to find.".to_owned(),
+                        "ex. 'prompt user for permissions to index their project'".to_owned(),
+                    ],
+                    _ => vec![
+                        "".to_owned(),
+                        "Include/exclude specific paths with the filter option.".to_owned(),
+                        "Matching exact word and/or casing is available too.".to_owned(),
+                    ],
+                }
             };
 
             let previous_query_keystrokes =
@@ -329,11 +398,27 @@ impl View for ProjectSearchView {
             });
 
             MouseEventHandler::new::<Status, _>(0, cx, |_, _| {
-                Label::new(text, theme.search.results_status.clone())
-                    .aligned()
+                Flex::column()
+                    .with_child(Flex::column().contained().flex(1., true))
+                    .with_child(
+                        Flex::column()
+                            .align_children_center()
+                            .with_child(Label::new(
+                                major_text,
+                                theme.search.major_results_status.clone(),
+                            ))
+                            .with_children(
+                                minor_text.into_iter().map(|x| {
+                                    Label::new(x, theme.search.minor_results_status.clone())
+                                }),
+                            )
+                            .aligned()
+                            .top()
+                            .contained()
+                            .flex(7., true),
+                    )
                     .contained()
                     .with_background_color(theme.editor.background)
-                    .flex(1., true)
             })
             .on_down(MouseButton::Left, |_, _, cx| {
                 cx.focus_parent();
@@ -374,7 +459,9 @@ impl Item for ProjectSearchView {
             .then(|| query_text.into())
             .or_else(|| Some("Project Search".into()))
     }
-
+    fn should_close_item_on_event(event: &Self::Event) -> bool {
+        event == &Self::Event::Dismiss
+    }
     fn act_as_type<'a>(
         &'a self,
         type_id: TypeId,
@@ -395,7 +482,7 @@ impl Item for ProjectSearchView {
             .update(cx, |editor, cx| editor.deactivated(cx));
     }
 
-    fn tab_content<T: View>(
+    fn tab_content<T: 'static>(
         &self,
         _detail: Option<usize>,
         tab_theme: &theme::Tab,
@@ -411,11 +498,25 @@ impl Item for ProjectSearchView {
                     .contained()
                     .with_margin_right(tab_theme.spacing),
             )
-            .with_children(self.model.read(cx).active_query.as_ref().map(|query| {
-                let query_text = util::truncate_and_trailoff(query.as_str(), MAX_TAB_TITLE_LEN);
-
-                Label::new(query_text, tab_theme.label.clone()).aligned()
-            }))
+            .with_child({
+                let tab_name: Option<Cow<_>> = self
+                    .model
+                    .read(cx)
+                    .search_history
+                    .current()
+                    .as_ref()
+                    .map(|query| {
+                        let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN);
+                        query_text.into()
+                    });
+                Label::new(
+                    tab_name
+                        .filter(|name| !name.is_empty())
+                        .unwrap_or("Project search".into()),
+                    tab_theme.label.clone(),
+                )
+                .aligned()
+            })
             .into_any()
     }
 
@@ -496,6 +597,7 @@ impl Item for ProjectSearchView {
                 smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab]
             }
             ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event),
+            ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem],
             _ => SmallVec::new(),
         }
     }
@@ -528,6 +630,135 @@ impl Item for ProjectSearchView {
 }
 
 impl ProjectSearchView {
+    fn toggle_search_option(&mut self, option: SearchOptions) {
+        self.search_options.toggle(option);
+    }
+
+    fn index_project(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(semantic_index) = SemanticIndex::global(cx) {
+            // Semantic search uses no options
+            self.search_options = SearchOptions::none();
+
+            let project = self.model.read(cx).project.clone();
+
+            let index_task = semantic_index.update(cx, |semantic_index, cx| {
+                semantic_index.index_project(project, cx)
+            });
+
+            cx.spawn(|search_view, mut cx| async move {
+                let (files_to_index, mut files_remaining_rx) = index_task.await?;
+
+                search_view.update(&mut cx, |search_view, cx| {
+                    cx.notify();
+                    search_view.semantic_state = Some(SemanticSearchState {
+                        file_count: files_to_index,
+                        outstanding_file_count: files_to_index,
+                        _progress_task: cx.spawn(|search_view, mut cx| async move {
+                            while let Some(count) = files_remaining_rx.recv().await {
+                                search_view
+                                    .update(&mut cx, |search_view, cx| {
+                                        if let Some(semantic_search_state) =
+                                            &mut search_view.semantic_state
+                                        {
+                                            semantic_search_state.outstanding_file_count = count;
+                                            cx.notify();
+                                            if count == 0 {
+                                                return;
+                                            }
+                                        }
+                                    })
+                                    .ok();
+                            }
+                        }),
+                    });
+                })?;
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    }
+
+    fn clear_search(&mut self, cx: &mut ViewContext<Self>) {
+        self.model.update(cx, |model, cx| {
+            model.pending_search = None;
+            model.no_results = None;
+            model.match_ranges.clear();
+
+            model.excerpts.update(cx, |excerpts, cx| {
+                excerpts.clear(cx);
+            });
+        });
+    }
+
+    fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+        let previous_mode = self.current_mode;
+        if previous_mode == mode {
+            return;
+        }
+
+        self.clear_search(cx);
+        self.current_mode = mode;
+        self.active_match_index = None;
+
+        match mode {
+            SearchMode::Semantic => {
+                let has_permission = self.semantic_permissioned(cx);
+                self.active_match_index = None;
+                cx.spawn(|this, mut cx| async move {
+                    let has_permission = has_permission.await?;
+
+                    if !has_permission {
+                        let mut answer = this.update(&mut cx, |this, cx| {
+                            let project = this.model.read(cx).project.clone();
+                            let project_name = project
+                                .read(cx)
+                                .worktree_root_names(cx)
+                                .collect::<Vec<&str>>()
+                                .join("/");
+                            let is_plural =
+                                project_name.chars().filter(|letter| *letter == '/').count() > 0;
+                            let prompt_text = format!("Would you like to index the '{}' project{} for semantic search? This requires sending code to the OpenAI API", project_name,
+                                if is_plural {
+                                    "s"
+                                } else {""});
+                            cx.prompt(
+                                PromptLevel::Info,
+                                prompt_text.as_str(),
+                                &["Continue", "Cancel"],
+                            )
+                        })?;
+
+                        if answer.next().await == Some(0) {
+                            this.update(&mut cx, |this, _| {
+                                this.semantic_permissioned = Some(true);
+                            })?;
+                        } else {
+                            this.update(&mut cx, |this, cx| {
+                                this.semantic_permissioned = Some(false);
+                                debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected");
+                                this.activate_search_mode(previous_mode, cx);
+                            })?;
+                            return anyhow::Ok(());
+                        }
+                    }
+
+                    this.update(&mut cx, |this, cx| {
+                        this.index_project(cx);
+                    })?;
+
+                    anyhow::Ok(())
+                }).detach_and_log_err(cx);
+            }
+            SearchMode::Regex | SearchMode::Text => {
+                self.semantic_state = None;
+                self.active_match_index = None;
+                self.search(cx);
+            }
+        }
+
+        cx.notify();
+    }
+
     fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> Self {
         let project;
         let excerpts;
@@ -551,6 +782,7 @@ impl ProjectSearchView {
                 Some(Arc::new(|theme| theme.search.editor.input.clone())),
                 cx,
             );
+            editor.set_placeholder_text("Text search all files", cx);
             editor.set_text(query_text, cx);
             editor
         });
@@ -561,7 +793,7 @@ impl ProjectSearchView {
         .detach();
 
         let results_editor = cx.add_view(|cx| {
-            let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx);
+            let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx);
             editor.set_searchable(false);
             editor
         });
@@ -610,24 +842,41 @@ impl ProjectSearchView {
             cx.emit(ViewEvent::EditorEvent(event.clone()))
         })
         .detach();
+        let filters_enabled = false;
 
+        // Check if Worktrees have all been previously indexed
         let mut this = ProjectSearchView {
             search_id: model.read(cx).search_id,
             model,
             query_editor,
             results_editor,
-            semantic: None,
+            semantic_state: None,
+            semantic_permissioned: None,
             search_options: options,
             panels_with_errors: HashSet::new(),
             active_match_index: None,
             query_editor_was_focused: false,
             included_files_editor,
             excluded_files_editor,
+            filters_enabled,
+            current_mode: Default::default(),
         };
         this.model_changed(cx);
         this
     }
 
+    fn semantic_permissioned(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
+        if let Some(value) = self.semantic_permissioned {
+            return Task::ready(Ok(value));
+        }
+
+        SemanticIndex::global(cx)
+            .map(|semantic| {
+                let project = self.model.read(cx).project.clone();
+                semantic.update(cx, |this, cx| this.project_previously_indexed(project, cx))
+            })
+            .unwrap_or(Task::ready(Ok(false)))
+    }
     pub fn new_search_in_directory(
         workspace: &mut Workspace,
         dir_entry: &Entry,
@@ -636,7 +885,9 @@ impl ProjectSearchView {
         if !dir_entry.is_dir() {
             return;
         }
-        let Some(filter_str) = dir_entry.path.to_str() else { return; };
+        let Some(filter_str) = dir_entry.path.to_str() else {
+            return;
+        };
 
         let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
         let search = cx.add_view(|cx| ProjectSearchView::new(model, cx));
@@ -645,6 +896,7 @@ impl ProjectSearchView {
             search
                 .included_files_editor
                 .update(cx, |editor, cx| editor.set_text(filter_str, cx));
+            search.filters_enabled = true;
             search.focus_query_editor(cx)
         });
     }
@@ -703,18 +955,26 @@ impl ProjectSearchView {
     }
 
     fn search(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(semantic) = &mut self.semantic {
-            if semantic.outstanding_file_count > 0 {
-                return;
-            }
-            if let Some(query) = self.build_search_query(cx) {
-                self.model
-                    .update(cx, |model, cx| model.semantic_search(query, cx));
+        let mode = self.current_mode;
+        match mode {
+            SearchMode::Semantic => {
+                if let Some(semantic) = &mut self.semantic_state {
+                    if semantic.outstanding_file_count > 0 {
+                        return;
+                    }
+
+                    if let Some(query) = self.build_search_query(cx) {
+                        self.model
+                            .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx));
+                    }
+                }
             }
-        }
 
-        if let Some(query) = self.build_search_query(cx) {
-            self.model.update(cx, |model, cx| model.search(query, cx));
+            _ => {
+                if let Some(query) = self.build_search_query(cx) {
+                    self.model.update(cx, |model, cx| model.search(query, cx));
+                }
+            }
         }
     }
 
@@ -744,32 +1004,34 @@ impl ProjectSearchView {
                     return None;
                 }
             };
-        if self.search_options.contains(SearchOptions::REGEX) {
-            match SearchQuery::regex(
-                text,
-                self.search_options.contains(SearchOptions::WHOLE_WORD),
-                self.search_options.contains(SearchOptions::CASE_SENSITIVE),
-                included_files,
-                excluded_files,
-            ) {
-                Ok(query) => {
-                    self.panels_with_errors.remove(&InputPanel::Query);
-                    Some(query)
-                }
-                Err(_e) => {
-                    self.panels_with_errors.insert(InputPanel::Query);
-                    cx.notify();
-                    None
+        let current_mode = self.current_mode;
+        match current_mode {
+            SearchMode::Regex => {
+                match SearchQuery::regex(
+                    text,
+                    self.search_options.contains(SearchOptions::WHOLE_WORD),
+                    self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+                    included_files,
+                    excluded_files,
+                ) {
+                    Ok(query) => {
+                        self.panels_with_errors.remove(&InputPanel::Query);
+                        Some(query)
+                    }
+                    Err(_e) => {
+                        self.panels_with_errors.insert(InputPanel::Query);
+                        cx.notify();
+                        None
+                    }
                 }
             }
-        } else {
-            Some(SearchQuery::text(
+            _ => Some(SearchQuery::text(
                 text,
                 self.search_options.contains(SearchOptions::WHOLE_WORD),
                 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                 included_files,
                 excluded_files,
-            ))
+            )),
         }
     }
 
@@ -906,7 +1168,19 @@ impl ProjectSearchBar {
             subscription: Default::default(),
         }
     }
-
+    fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext<Workspace>) {
+        if let Some(search_view) = workspace
+            .active_item(cx)
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |this, cx| {
+                let new_mode =
+                    crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx));
+                this.activate_search_mode(new_mode, cx);
+                cx.focus(&this.query_editor);
+            })
+        }
+    }
     fn search(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| search_view.search(cx));
@@ -1016,8 +1290,7 @@ impl ProjectSearchBar {
     fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
-                search_view.search_options.toggle(option);
-                search_view.semantic = None;
+                search_view.toggle_search_option(option);
                 search_view.search(cx);
             });
             cx.notify();
@@ -1027,52 +1300,30 @@ impl ProjectSearchBar {
         }
     }
 
-    fn toggle_semantic_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
+    fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext<Pane>) {
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |view, cx| {
+                view.activate_search_mode(SearchMode::Regex, cx)
+            });
+        } else {
+            cx.propagate_action();
+        }
+    }
+
+    fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
-                if search_view.semantic.is_some() {
-                    search_view.semantic = None;
-                } else if let Some(semantic_index) = SemanticIndex::global(cx) {
-                    // TODO: confirm that it's ok to send this project
-                    search_view.search_options = SearchOptions::none();
-
-                    let project = search_view.model.read(cx).project.clone();
-                    let index_task = semantic_index.update(cx, |semantic_index, cx| {
-                        semantic_index.index_project(project, cx)
-                    });
-
-                    cx.spawn(|search_view, mut cx| async move {
-                        let (files_to_index, mut files_remaining_rx) = index_task.await?;
-
-                        search_view.update(&mut cx, |search_view, cx| {
-                            cx.notify();
-                            search_view.semantic = Some(SemanticSearchState {
-                                file_count: files_to_index,
-                                outstanding_file_count: files_to_index,
-                                _progress_task: cx.spawn(|search_view, mut cx| async move {
-                                    while let Some(count) = files_remaining_rx.recv().await {
-                                        search_view
-                                            .update(&mut cx, |search_view, cx| {
-                                                if let Some(semantic_search_state) =
-                                                    &mut search_view.semantic
-                                                {
-                                                    semantic_search_state.outstanding_file_count =
-                                                        count;
-                                                    cx.notify();
-                                                    if count == 0 {
-                                                        return;
-                                                    }
-                                                }
-                                            })
-                                            .ok();
-                                    }
-                                }),
-                            });
-                        })?;
-                        anyhow::Ok(())
-                    })
-                    .detach_and_log_err(cx);
-                }
+                search_view.filters_enabled = !search_view.filters_enabled;
+                search_view
+                    .included_files_editor
+                    .update(cx, |_, cx| cx.notify());
+                search_view
+                    .excluded_files_editor
+                    .update(cx, |_, cx| cx.notify());
+                cx.refresh_windows();
                 cx.notify();
             });
             cx.notify();
@@ -1082,117 +1333,14 @@ impl ProjectSearchBar {
         }
     }
 
-    fn render_nav_button(
-        &self,
-        icon: &'static str,
-        direction: Direction,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let action: Box<dyn Action>;
-        let tooltip;
-        match direction {
-            Direction::Prev => {
-                action = Box::new(SelectPrevMatch);
-                tooltip = "Select Previous Match";
-            }
-            Direction::Next => {
-                action = Box::new(SelectNextMatch);
-                tooltip = "Select Next Match";
-            }
-        };
-        let tooltip_style = theme::current(cx).tooltip.clone();
-
-        enum NavButton {}
-        MouseEventHandler::new::<NavButton, _>(direction as usize, cx, |state, cx| {
-            let theme = theme::current(cx);
-            let style = theme.search.option_button.inactive_state().style_for(state);
-            Label::new(icon, style.text.clone())
-                .contained()
-                .with_style(style.container)
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            if let Some(search) = this.active_project_search.as_ref() {
-                search.update(cx, |search, cx| search.select_match(direction, cx));
-            }
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .with_tooltip::<NavButton>(
-            direction as usize,
-            tooltip.to_string(),
-            Some(action),
-            tooltip_style,
-            cx,
-        )
-        .into_any()
-    }
-
-    fn render_option_button(
-        &self,
-        icon: &'static str,
-        option: SearchOptions,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let tooltip_style = theme::current(cx).tooltip.clone();
-        let is_active = self.is_option_enabled(option, cx);
-        MouseEventHandler::new::<Self, _>(option.bits as usize, cx, |state, cx| {
-            let theme = theme::current(cx);
-            let style = theme
-                .search
-                .option_button
-                .in_state(is_active)
-                .style_for(state);
-            Label::new(icon, style.text.clone())
-                .contained()
-                .with_style(style.container)
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.toggle_search_option(option, cx);
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .with_tooltip::<Self>(
-            option.bits as usize,
-            format!("Toggle {}", option.label()),
-            Some(option.to_toggle_action()),
-            tooltip_style,
-            cx,
-        )
-        .into_any()
-    }
-
-    fn render_semantic_search_button(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let tooltip_style = theme::current(cx).tooltip.clone();
-        let is_active = if let Some(search) = self.active_project_search.as_ref() {
-            let search = search.read(cx);
-            search.semantic.is_some()
-        } else {
-            false
-        };
-
-        let region_id = 3;
-
-        MouseEventHandler::new::<Self, _>(region_id, cx, |state, cx| {
-            let theme = theme::current(cx);
-            let style = theme
-                .search
-                .option_button
-                .in_state(is_active)
-                .style_for(state);
-            Label::new("Semantic", style.text.clone())
-                .contained()
-                .with_style(style.container)
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.toggle_semantic_search(cx);
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .with_tooltip::<Self>(
-            region_id,
-            format!("Toggle Semantic Search"),
-            Some(Box::new(ToggleSemanticSearch)),
-            tooltip_style,
-            cx,
-        )
-        .into_any()
+    fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+        // Update Current Mode
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| {
+                search_view.activate_search_mode(mode, cx);
+            });
+            cx.notify();
+        }
     }
 
     fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
@@ -1255,20 +1403,86 @@ impl View for ProjectSearchBar {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        if let Some(search) = self.active_project_search.as_ref() {
-            let search = search.read(cx);
+        if let Some(_search) = self.active_project_search.as_ref() {
+            let search = _search.read(cx);
             let theme = theme::current(cx).clone();
             let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
                 theme.search.invalid_editor
             } else {
                 theme.search.editor.input.container
             };
+
+            let search = _search.read(cx);
+            let filter_button = render_option_button_icon(
+                search.filters_enabled,
+                "icons/filter_12.svg",
+                0,
+                "Toggle filters",
+                Box::new(ToggleFilters),
+                move |_, this, cx| {
+                    this.toggle_filters(cx);
+                },
+                cx,
+            );
+
+            let search = _search.read(cx);
+            let is_semantic_available = SemanticIndex::enabled(cx);
+            let is_semantic_disabled = search.semantic_state.is_none();
+            let icon_style = theme.search.editor_icon.clone();
+            let is_active = search.active_match_index.is_some();
+
+            let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| {
+                crate::search_bar::render_option_button_icon(
+                    self.is_option_enabled(option, cx),
+                    path,
+                    option.bits as usize,
+                    format!("Toggle {}", option.label()),
+                    option.to_toggle_action(),
+                    move |_, this, cx| {
+                        this.toggle_search_option(option, cx);
+                    },
+                    cx,
+                )
+            };
+            let case_sensitive = is_semantic_disabled.then(|| {
+                render_option_button_icon(
+                    "icons/case_insensitive_12.svg",
+                    SearchOptions::CASE_SENSITIVE,
+                    cx,
+                )
+            });
+
+            let whole_word = is_semantic_disabled.then(|| {
+                render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx)
+            });
+
+            let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
+                let is_active = if let Some(search) = self.active_project_search.as_ref() {
+                    let search = search.read(cx);
+                    search.current_mode == mode
+                } else {
+                    false
+                };
+                render_search_mode_button(
+                    mode,
+                    side,
+                    is_active,
+                    move |_, this, cx| {
+                        this.activate_search_mode(mode, cx);
+                    },
+                    cx,
+                )
+            };
+
+            let search = _search.read(cx);
+
             let include_container_style =
                 if search.panels_with_errors.contains(&InputPanel::Include) {
                     theme.search.invalid_include_exclude_editor
                 } else {
                     theme.search.include_exclude_editor.input.container
                 };
+
             let exclude_container_style =
                 if search.panels_with_errors.contains(&InputPanel::Exclude) {
                     theme.search.invalid_include_exclude_editor

crates/search/src/search.rs 🔗

@@ -1,12 +1,20 @@
 use bitflags::bitflags;
 pub use buffer_search::BufferSearchBar;
-use gpui::{actions, Action, AppContext};
+use gpui::{
+    actions,
+    elements::{Component, SafeStylable, TooltipStyle},
+    Action, AnyElement, AppContext, Element, View,
+};
+pub use mode::SearchMode;
 use project::search::SearchQuery;
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
-use smallvec::SmallVec;
+use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle};
 
 pub mod buffer_search;
+mod history;
+mod mode;
 pub mod project_search;
+pub(crate) mod search_bar;
 
 pub fn init(cx: &mut AppContext) {
     buffer_search::init(cx);
@@ -16,14 +24,17 @@ pub fn init(cx: &mut AppContext) {
 actions!(
     search,
     [
+        CycleMode,
         ToggleWholeWord,
         ToggleCaseSensitive,
-        ToggleRegex,
         SelectNextMatch,
         SelectPrevMatch,
         SelectAllMatches,
         NextHistoryQuery,
         PreviousHistoryQuery,
+        ActivateTextMode,
+        ActivateSemanticMode,
+        ActivateRegexMode
     ]
 );
 
@@ -33,7 +44,6 @@ bitflags! {
         const NONE = 0b000;
         const WHOLE_WORD = 0b001;
         const CASE_SENSITIVE = 0b010;
-        const REGEX = 0b100;
     }
 }
 
@@ -42,7 +52,14 @@ impl SearchOptions {
         match *self {
             SearchOptions::WHOLE_WORD => "Match Whole Word",
             SearchOptions::CASE_SENSITIVE => "Match Case",
-            SearchOptions::REGEX => "Use Regular Expression",
+            _ => panic!("{:?} is not a named SearchOption", self),
+        }
+    }
+
+    pub fn icon(&self) -> &'static str {
+        match *self {
+            SearchOptions::WHOLE_WORD => "icons/word_search_12.svg",
+            SearchOptions::CASE_SENSITIVE => "icons/case_insensitive_12.svg",
             _ => panic!("{:?} is not a named SearchOption", self),
         }
     }
@@ -51,7 +68,6 @@ impl SearchOptions {
         match *self {
             SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
             SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
-            SearchOptions::REGEX => Box::new(ToggleRegex),
             _ => panic!("{:?} is not a named SearchOption", self),
         }
     }
@@ -64,191 +80,21 @@ impl SearchOptions {
         let mut options = SearchOptions::NONE;
         options.set(SearchOptions::WHOLE_WORD, query.whole_word());
         options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
-        options.set(SearchOptions::REGEX, query.is_regex());
         options
     }
-}
-
-const SEARCH_HISTORY_LIMIT: usize = 20;
-
-#[derive(Default, Debug, Clone)]
-pub struct SearchHistory {
-    history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
-    selected: Option<usize>,
-}
-
-impl SearchHistory {
-    pub fn add(&mut self, search_string: String) {
-        if let Some(i) = self.selected {
-            if search_string == self.history[i] {
-                return;
-            }
-        }
-
-        if let Some(previously_searched) = self.history.last_mut() {
-            if search_string.find(previously_searched.as_str()).is_some() {
-                *previously_searched = search_string;
-                self.selected = Some(self.history.len() - 1);
-                return;
-            }
-        }
-
-        self.history.push(search_string);
-        if self.history.len() > SEARCH_HISTORY_LIMIT {
-            self.history.remove(0);
-        }
-        self.selected = Some(self.history.len() - 1);
-    }
-
-    pub fn next(&mut self) -> Option<&str> {
-        let history_size = self.history.len();
-        if history_size == 0 {
-            return None;
-        }
-
-        let selected = self.selected?;
-        if selected == history_size - 1 {
-            return None;
-        }
-        let next_index = selected + 1;
-        self.selected = Some(next_index);
-        Some(&self.history[next_index])
-    }
-
-    pub fn current(&self) -> Option<&str> {
-        Some(&self.history[self.selected?])
-    }
-
-    pub fn previous(&mut self) -> Option<&str> {
-        let history_size = self.history.len();
-        if history_size == 0 {
-            return None;
-        }
-
-        let prev_index = match self.selected {
-            Some(selected_index) => {
-                if selected_index == 0 {
-                    return None;
-                } else {
-                    selected_index - 1
-                }
-            }
-            None => history_size - 1,
-        };
-
-        self.selected = Some(prev_index);
-        Some(&self.history[prev_index])
-    }
-
-    pub fn reset_selection(&mut self) {
-        self.selected = None;
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_add() {
-        let mut search_history = SearchHistory::default();
-        assert_eq!(
-            search_history.current(),
-            None,
-            "No current selection should be set fo the default search history"
-        );
-
-        search_history.add("rust".to_string());
-        assert_eq!(
-            search_history.current(),
-            Some("rust"),
-            "Newly added item should be selected"
-        );
-
-        // check if duplicates are not added
-        search_history.add("rust".to_string());
-        assert_eq!(
-            search_history.history.len(),
-            1,
-            "Should not add a duplicate"
-        );
-        assert_eq!(search_history.current(), Some("rust"));
-
-        // check if new string containing the previous string replaces it
-        search_history.add("rustlang".to_string());
-        assert_eq!(
-            search_history.history.len(),
-            1,
-            "Should replace previous item if it's a substring"
-        );
-        assert_eq!(search_history.current(), Some("rustlang"));
-
-        // push enough items to test SEARCH_HISTORY_LIMIT
-        for i in 0..SEARCH_HISTORY_LIMIT * 2 {
-            search_history.add(format!("item{i}"));
-        }
-        assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
-    }
-
-    #[test]
-    fn test_next_and_previous() {
-        let mut search_history = SearchHistory::default();
-        assert_eq!(
-            search_history.next(),
-            None,
-            "Default search history should not have a next item"
-        );
-
-        search_history.add("Rust".to_string());
-        assert_eq!(search_history.next(), None);
-        search_history.add("JavaScript".to_string());
-        assert_eq!(search_history.next(), None);
-        search_history.add("TypeScript".to_string());
-        assert_eq!(search_history.next(), None);
-
-        assert_eq!(search_history.current(), Some("TypeScript"));
-
-        assert_eq!(search_history.previous(), Some("JavaScript"));
-        assert_eq!(search_history.current(), Some("JavaScript"));
-
-        assert_eq!(search_history.previous(), Some("Rust"));
-        assert_eq!(search_history.current(), Some("Rust"));
-
-        assert_eq!(search_history.previous(), None);
-        assert_eq!(search_history.current(), Some("Rust"));
-
-        assert_eq!(search_history.next(), Some("JavaScript"));
-        assert_eq!(search_history.current(), Some("JavaScript"));
-
-        assert_eq!(search_history.next(), Some("TypeScript"));
-        assert_eq!(search_history.current(), Some("TypeScript"));
-
-        assert_eq!(search_history.next(), None);
-        assert_eq!(search_history.current(), Some("TypeScript"));
-    }
-
-    #[test]
-    fn test_reset_selection() {
-        let mut search_history = SearchHistory::default();
-        search_history.add("Rust".to_string());
-        search_history.add("JavaScript".to_string());
-        search_history.add("TypeScript".to_string());
-
-        assert_eq!(search_history.current(), Some("TypeScript"));
-        search_history.reset_selection();
-        assert_eq!(search_history.current(), None);
-        assert_eq!(
-            search_history.previous(),
-            Some("TypeScript"),
-            "Should start from the end after reset on previous item query"
-        );
-
-        search_history.previous();
-        assert_eq!(search_history.current(), Some("JavaScript"));
-        search_history.previous();
-        assert_eq!(search_history.current(), Some("Rust"));
 
-        search_history.reset_selection();
-        assert_eq!(search_history.current(), None);
+    pub fn as_button<V: View>(
+        &self,
+        active: bool,
+        tooltip_style: TooltipStyle,
+        button_style: ToggleIconButtonStyle,
+    ) -> AnyElement<V> {
+        Button::dynamic_action(self.to_toggle_action())
+            .with_tooltip(format!("Toggle {}", self.label()), tooltip_style)
+            .with_contents(Svg::new(self.icon()))
+            .toggleable(active)
+            .with_style(button_style)
+            .element()
+            .into_any()
     }
 }

crates/search/src/search_bar.rs 🔗

@@ -0,0 +1,174 @@
+use std::borrow::Cow;
+
+use gpui::{
+    elements::{Label, MouseEventHandler, Svg},
+    platform::{CursorStyle, MouseButton},
+    scene::{CornerRadii, MouseClick},
+    Action, AnyElement, Element, EventContext, View, ViewContext,
+};
+use workspace::searchable::Direction;
+
+use crate::{
+    mode::{SearchMode, Side},
+    SelectNextMatch, SelectPrevMatch,
+};
+
+pub(super) fn render_nav_button<V: View>(
+    icon: &'static str,
+    direction: Direction,
+    active: bool,
+    on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
+    cx: &mut ViewContext<V>,
+) -> AnyElement<V> {
+    let action: Box<dyn Action>;
+    let tooltip;
+
+    match direction {
+        Direction::Prev => {
+            action = Box::new(SelectPrevMatch);
+            tooltip = "Select Previous Match";
+        }
+        Direction::Next => {
+            action = Box::new(SelectNextMatch);
+            tooltip = "Select Next Match";
+        }
+    };
+    let tooltip_style = theme::current(cx).tooltip.clone();
+    let cursor_style = if active {
+        CursorStyle::PointingHand
+    } else {
+        CursorStyle::default()
+    };
+    enum NavButton {}
+    MouseEventHandler::new::<NavButton, _>(direction as usize, cx, |state, cx| {
+        let theme = theme::current(cx);
+        let style = theme
+            .search
+            .nav_button
+            .in_state(active)
+            .style_for(state)
+            .clone();
+        let mut container_style = style.container.clone();
+        let label = Label::new(icon, style.label.clone()).aligned().contained();
+        container_style.corner_radii = match direction {
+            Direction::Prev => CornerRadii {
+                bottom_right: 0.,
+                top_right: 0.,
+                ..container_style.corner_radii
+            },
+            Direction::Next => CornerRadii {
+                bottom_left: 0.,
+                top_left: 0.,
+                ..container_style.corner_radii
+            },
+        };
+        if direction == Direction::Prev {
+            // Remove right border so that when both Next and Prev buttons are
+            // next to one another, there's no double border between them.
+            container_style.border.right = false;
+        }
+        label.with_style(container_style)
+    })
+    .on_click(MouseButton::Left, on_click)
+    .with_cursor_style(cursor_style)
+    .with_tooltip::<NavButton>(
+        direction as usize,
+        tooltip.to_string(),
+        Some(action),
+        tooltip_style,
+        cx,
+    )
+    .into_any()
+}
+
+pub(crate) fn render_search_mode_button<V: View>(
+    mode: SearchMode,
+    side: Option<Side>,
+    is_active: bool,
+    on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
+    cx: &mut ViewContext<V>,
+) -> AnyElement<V> {
+    let tooltip_style = theme::current(cx).tooltip.clone();
+    enum SearchModeButton {}
+    MouseEventHandler::new::<SearchModeButton, _>(mode.region_id(), cx, |state, cx| {
+        let theme = theme::current(cx);
+        let style = theme
+            .search
+            .mode_button
+            .in_state(is_active)
+            .style_for(state)
+            .clone();
+
+        let mut container_style = style.container;
+        if let Some(button_side) = side {
+            if button_side == Side::Left {
+                container_style.border.left = true;
+                container_style.corner_radii = CornerRadii {
+                    bottom_right: 0.,
+                    top_right: 0.,
+                    ..container_style.corner_radii
+                };
+            } else {
+                container_style.border.left = false;
+                container_style.corner_radii = CornerRadii {
+                    bottom_left: 0.,
+                    top_left: 0.,
+                    ..container_style.corner_radii
+                };
+            }
+        } else {
+            container_style.border.left = false;
+            container_style.corner_radii = CornerRadii::default();
+        }
+
+        Label::new(mode.label(), style.text)
+            .aligned()
+            .contained()
+            .with_style(container_style)
+            .constrained()
+            .with_height(theme.search.search_bar_row_height)
+    })
+    .on_click(MouseButton::Left, on_click)
+    .with_cursor_style(CursorStyle::PointingHand)
+    .with_tooltip::<SearchModeButton>(
+        mode.region_id(),
+        mode.tooltip_text().to_owned(),
+        Some(mode.activate_action()),
+        tooltip_style,
+        cx,
+    )
+    .into_any()
+}
+
+pub(crate) fn render_option_button_icon<V: View>(
+    is_active: bool,
+    icon: &'static str,
+    id: usize,
+    label: impl Into<Cow<'static, str>>,
+    action: Box<dyn Action>,
+    on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
+    cx: &mut ViewContext<V>,
+) -> AnyElement<V> {
+    let tooltip_style = theme::current(cx).tooltip.clone();
+    MouseEventHandler::new::<V, _>(id, cx, |state, cx| {
+        let theme = theme::current(cx);
+        let style = theme
+            .search
+            .option_button
+            .in_state(is_active)
+            .style_for(state);
+        Svg::new(icon)
+            .with_color(style.color.clone())
+            .constrained()
+            .with_width(style.icon_width)
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_height(theme.search.option_button_height)
+            .with_width(style.button_width)
+    })
+    .on_click(MouseButton::Left, on_click)
+    .with_cursor_style(CursorStyle::PointingHand)
+    .with_tooltip::<V>(id, label, Some(action), tooltip_style, cx)
+    .into_any()
+}

crates/semantic_index/Cargo.toml 🔗

@@ -38,6 +38,7 @@ parking_lot.workspace = true
 rand.workspace = true
 schemars.workspace = true
 globset.workspace = true
+sha1 = "0.10.5"
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }

crates/semantic_index/src/db.rs 🔗

@@ -26,6 +26,9 @@ pub struct FileRecord {
 #[derive(Debug)]
 struct Embedding(pub Vec<f32>);
 
+#[derive(Debug)]
+struct Sha1(pub Vec<u8>);
+
 impl FromSql for Embedding {
     fn column_result(value: ValueRef) -> FromSqlResult<Self> {
         let bytes = value.as_blob()?;
@@ -37,6 +40,17 @@ impl FromSql for Embedding {
     }
 }
 
+impl FromSql for Sha1 {
+    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
+        let bytes = value.as_blob()?;
+        let sha1: Result<Vec<u8>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
+        if sha1.is_err() {
+            return Err(rusqlite::types::FromSqlError::Other(sha1.unwrap_err()));
+        }
+        return Ok(Sha1(sha1.unwrap()));
+    }
+}
+
 pub struct VectorDatabase {
     db: rusqlite::Connection,
 }
@@ -132,6 +146,7 @@ impl VectorDatabase {
                 end_byte INTEGER NOT NULL,
                 name VARCHAR NOT NULL,
                 embedding BLOB NOT NULL,
+                sha1 BLOB NOT NULL,
                 FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
             )",
             [],
@@ -156,39 +171,43 @@ impl VectorDatabase {
         mtime: SystemTime,
         documents: Vec<Document>,
     ) -> Result<()> {
-        // Write to files table, and return generated id.
-        self.db.execute(
-            "
-            DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2;
-            ",
-            params![worktree_id, path.to_str()],
-        )?;
+        // Return the existing ID, if both the file and mtime match
         let mtime = Timestamp::from(mtime);
-        self.db.execute(
-            "
-            INSERT INTO files
-            (worktree_id, relative_path, mtime_seconds, mtime_nanos)
-            VALUES
-            (?1, ?2, $3, $4);
-            ",
-            params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos],
-        )?;
-
-        let file_id = self.db.last_insert_rowid();
+        let mut existing_id_query = self.db.prepare("SELECT id FROM files WHERE worktree_id = ?1 AND relative_path = ?2 AND mtime_seconds = ?3 AND mtime_nanos = ?4")?;
+        let existing_id = existing_id_query
+            .query_row(
+                params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos],
+                |row| Ok(row.get::<_, i64>(0)?),
+            )
+            .map_err(|err| anyhow!(err));
+        let file_id = if existing_id.is_ok() {
+            // If already exists, just return the existing id
+            existing_id.unwrap()
+        } else {
+            // Delete Existing Row
+            self.db.execute(
+                "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2;",
+                params![worktree_id, path.to_str()],
+            )?;
+            self.db.execute("INSERT INTO files (worktree_id, relative_path, mtime_seconds, mtime_nanos) VALUES (?1, ?2, ?3, ?4);", params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos])?;
+            self.db.last_insert_rowid()
+        };
 
         // Currently inserting at approximately 3400 documents a second
         // I imagine we can speed this up with a bulk insert of some kind.
         for document in documents {
             let embedding_blob = bincode::serialize(&document.embedding)?;
+            let sha_blob = bincode::serialize(&document.sha1)?;
 
             self.db.execute(
-                "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding) VALUES (?1, ?2, ?3, ?4, $5)",
+                "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding, sha1) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
                 params![
                     file_id,
                     document.range.start.to_string(),
                     document.range.end.to_string(),
                     document.name,
-                    embedding_blob
+                    embedding_blob,
+                    sha_blob
                 ],
             )?;
         }

crates/semantic_index/src/embedding.rs 🔗

@@ -106,8 +106,8 @@ impl OpenAIEmbeddings {
 #[async_trait]
 impl EmbeddingProvider for OpenAIEmbeddings {
     async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
-        const BACKOFF_SECONDS: [usize; 3] = [45, 75, 125];
-        const MAX_RETRIES: usize = 3;
+        const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
+        const MAX_RETRIES: usize = 4;
 
         let api_key = OPENAI_API_KEY
             .as_ref()

crates/semantic_index/src/parsing.rs 🔗

@@ -1,5 +1,6 @@
 use anyhow::{anyhow, Ok, Result};
 use language::{Grammar, Language};
+use sha1::{Digest, Sha1};
 use std::{
     cmp::{self, Reverse},
     collections::HashSet,
@@ -15,6 +16,7 @@ pub struct Document {
     pub range: Range<usize>,
     pub content: String,
     pub embedding: Vec<f32>,
+    pub sha1: [u8; 20],
 }
 
 const CODE_CONTEXT_TEMPLATE: &str =
@@ -63,11 +65,15 @@ impl CodeContextRetriever {
             .replace("<language>", language_name.as_ref())
             .replace("<item>", &content);
 
+        let mut sha1 = Sha1::new();
+        sha1.update(&document_span);
+
         Ok(vec![Document {
             range: 0..content.len(),
             content: document_span,
             embedding: Vec::new(),
             name: language_name.to_string(),
+            sha1: sha1.finalize().into(),
         }])
     }
 
@@ -76,11 +82,15 @@ impl CodeContextRetriever {
             .replace("<path>", relative_path.to_string_lossy().as_ref())
             .replace("<item>", &content);
 
+        let mut sha1 = Sha1::new();
+        sha1.update(&document_span);
+
         Ok(vec![Document {
             range: 0..content.len(),
             content: document_span,
             embedding: Vec::new(),
             name: "Markdown".to_string(),
+            sha1: sha1.finalize().into(),
         }])
     }
 
@@ -253,11 +263,15 @@ impl CodeContextRetriever {
                 );
             }
 
+            let mut sha1 = Sha1::new();
+            sha1.update(&document_content);
+
             documents.push(Document {
                 name,
                 content: document_content,
                 range: item_range.clone(),
                 embedding: vec![],
+                sha1: sha1.finalize().into(),
             })
         }
 

crates/semantic_index/src/semantic_index.rs 🔗

@@ -16,7 +16,7 @@ use language::{Anchor, Buffer, Language, LanguageRegistry};
 use parking_lot::Mutex;
 use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES};
 use postage::watch;
-use project::{search::PathMatcher, Fs, Project, WorktreeId};
+use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, WorktreeId};
 use smol::channel;
 use std::{
     cmp::Ordering,
@@ -33,8 +33,9 @@ use util::{
     paths::EMBEDDINGS_DIR,
     ResultExt,
 };
+use workspace::WorkspaceCreated;
 
-const SEMANTIC_INDEX_VERSION: usize = 6;
+const SEMANTIC_INDEX_VERSION: usize = 7;
 const EMBEDDINGS_BATCH_SIZE: usize = 80;
 
 pub fn init(
@@ -49,12 +50,29 @@ pub fn init(
         .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
         .join("embeddings_db");
 
-    if *RELEASE_CHANNEL == ReleaseChannel::Stable
-        || !settings::get::<SemanticIndexSettings>(cx).enabled
-    {
+    // This needs to be removed at some point before stable.
+    if *RELEASE_CHANNEL == ReleaseChannel::Stable {
         return;
     }
 
+    cx.subscribe_global::<WorkspaceCreated, _>({
+        move |event, cx| {
+            let Some(semantic_index) = SemanticIndex::global(cx) else {
+                return;
+            };
+            let workspace = &event.0;
+            if let Some(workspace) = workspace.upgrade(cx) {
+                let project = workspace.read(cx).project().clone();
+                if project.read(cx).is_local() {
+                    semantic_index.update(cx, |index, cx| {
+                        index.initialize_project(project, cx).detach_and_log_err(cx)
+                    });
+                }
+            }
+        }
+    })
+    .detach();
+
     cx.spawn(move |mut cx| async move {
         let semantic_index = SemanticIndex::new(
             fs,
@@ -93,15 +111,95 @@ pub struct SemanticIndex {
 
 struct ProjectState {
     worktree_db_ids: Vec<(WorktreeId, i64)>,
+    _subscription: gpui::Subscription,
     outstanding_job_count_rx: watch::Receiver<usize>,
     _outstanding_job_count_tx: Arc<Mutex<watch::Sender<usize>>>,
+    job_queue_tx: channel::Sender<IndexOperation>,
+    _queue_update_task: Task<()>,
 }
 
+#[derive(Clone)]
 struct JobHandle {
-    tx: Weak<Mutex<watch::Sender<usize>>>,
+    /// The outer Arc is here to count the clones of a JobHandle instance;
+    /// when the last handle to a given job is dropped, we decrement a counter (just once).
+    tx: Arc<Weak<Mutex<watch::Sender<usize>>>>,
 }
 
+impl JobHandle {
+    fn new(tx: &Arc<Mutex<watch::Sender<usize>>>) -> Self {
+        *tx.lock().borrow_mut() += 1;
+        Self {
+            tx: Arc::new(Arc::downgrade(&tx)),
+        }
+    }
+}
 impl ProjectState {
+    fn new(
+        cx: &mut AppContext,
+        subscription: gpui::Subscription,
+        worktree_db_ids: Vec<(WorktreeId, i64)>,
+        outstanding_job_count_rx: watch::Receiver<usize>,
+        _outstanding_job_count_tx: Arc<Mutex<watch::Sender<usize>>>,
+    ) -> Self {
+        let (job_queue_tx, job_queue_rx) = channel::unbounded();
+        let _queue_update_task = cx.background().spawn({
+            let mut worktree_queue = HashMap::new();
+            async move {
+                while let Ok(operation) = job_queue_rx.recv().await {
+                    Self::update_queue(&mut worktree_queue, operation);
+                }
+            }
+        });
+
+        Self {
+            worktree_db_ids,
+            outstanding_job_count_rx,
+            _outstanding_job_count_tx,
+            _subscription: subscription,
+            _queue_update_task,
+            job_queue_tx,
+        }
+    }
+
+    pub fn get_outstanding_count(&self) -> usize {
+        self.outstanding_job_count_rx.borrow().clone()
+    }
+
+    fn update_queue(queue: &mut HashMap<PathBuf, IndexOperation>, operation: IndexOperation) {
+        match operation {
+            IndexOperation::FlushQueue => {
+                let queue = std::mem::take(queue);
+                for (_, op) in queue {
+                    match op {
+                        IndexOperation::IndexFile {
+                            absolute_path: _,
+                            payload,
+                            tx,
+                        } => {
+                            let _ = tx.try_send(payload);
+                        }
+                        IndexOperation::DeleteFile {
+                            absolute_path: _,
+                            payload,
+                            tx,
+                        } => {
+                            let _ = tx.try_send(payload);
+                        }
+                        _ => {}
+                    }
+                }
+            }
+            IndexOperation::IndexFile {
+                ref absolute_path, ..
+            }
+            | IndexOperation::DeleteFile {
+                ref absolute_path, ..
+            } => {
+                queue.insert(absolute_path.clone(), operation);
+            }
+        }
+    }
+
     fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option<i64> {
         self.worktree_db_ids
             .iter()
@@ -127,6 +225,7 @@ impl ProjectState {
     }
 }
 
+#[derive(Clone)]
 pub struct PendingFile {
     worktree_db_id: i64,
     relative_path: PathBuf,
@@ -135,6 +234,19 @@ pub struct PendingFile {
     modified_time: SystemTime,
     job_handle: JobHandle,
 }
+enum IndexOperation {
+    IndexFile {
+        absolute_path: PathBuf,
+        payload: PendingFile,
+        tx: channel::Sender<PendingFile>,
+    },
+    DeleteFile {
+        absolute_path: PathBuf,
+        payload: DbOperation,
+        tx: channel::Sender<DbOperation>,
+    },
+    FlushQueue,
+}
 
 pub struct SearchResult {
     pub buffer: ModelHandle<Buffer>,
@@ -381,6 +493,20 @@ impl SemanticIndex {
                     .await
                     .unwrap();
             }
+        } else {
+            // Insert the file in spite of failure so that future attempts to index it do not take place (unless the file is changed).
+            for (worktree_id, _, path, mtime, job_handle) in embeddings_queue.into_iter() {
+                db_update_tx
+                    .send(DbOperation::InsertFile {
+                        worktree_id,
+                        documents: vec![],
+                        path,
+                        mtime,
+                        job_handle,
+                    })
+                    .await
+                    .unwrap();
+            }
         }
     }
 
@@ -390,6 +516,7 @@ impl SemanticIndex {
         embeddings_queue: &mut Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>,
         embed_batch_tx: &channel::Sender<Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>>,
     ) {
+        // Handle edge case where individual file has more documents than max batch size
         let should_flush = match job {
             EmbeddingJob::Enqueue {
                 documents,
@@ -398,9 +525,43 @@ impl SemanticIndex {
                 mtime,
                 job_handle,
             } => {
-                *queue_len += &documents.len();
-                embeddings_queue.push((worktree_id, documents, path, mtime, job_handle));
-                *queue_len >= EMBEDDINGS_BATCH_SIZE
+                // If documents is greater than embeddings batch size, recursively batch existing rows.
+                if &documents.len() > &EMBEDDINGS_BATCH_SIZE {
+                    let first_job = EmbeddingJob::Enqueue {
+                        documents: documents[..EMBEDDINGS_BATCH_SIZE].to_vec(),
+                        worktree_id,
+                        path: path.clone(),
+                        mtime,
+                        job_handle: job_handle.clone(),
+                    };
+
+                    Self::enqueue_documents_to_embed(
+                        first_job,
+                        queue_len,
+                        embeddings_queue,
+                        embed_batch_tx,
+                    );
+
+                    let second_job = EmbeddingJob::Enqueue {
+                        documents: documents[EMBEDDINGS_BATCH_SIZE..].to_vec(),
+                        worktree_id,
+                        path: path.clone(),
+                        mtime,
+                        job_handle: job_handle.clone(),
+                    };
+
+                    Self::enqueue_documents_to_embed(
+                        second_job,
+                        queue_len,
+                        embeddings_queue,
+                        embed_batch_tx,
+                    );
+                    return;
+                } else {
+                    *queue_len += &documents.len();
+                    embeddings_queue.push((worktree_id, documents, path, mtime, job_handle));
+                    *queue_len >= EMBEDDINGS_BATCH_SIZE
+                }
             }
             EmbeddingJob::Flush => true,
         };
@@ -501,26 +662,12 @@ impl SemanticIndex {
         project: ModelHandle<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<bool>> {
-        let worktree_scans_complete = project
-            .read(cx)
-            .worktrees(cx)
-            .map(|worktree| {
-                let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete();
-                async move {
-                    scan_complete.await;
-                }
-            })
-            .collect::<Vec<_>>();
-
         let worktrees_indexed_previously = project
             .read(cx)
             .worktrees(cx)
             .map(|worktree| self.worktree_previously_indexed(worktree.read(cx).abs_path()))
             .collect::<Vec<_>>();
-
         cx.spawn(|_, _cx| async move {
-            futures::future::join_all(worktree_scans_complete).await;
-
             let worktree_indexed_previously =
                 futures::future::join_all(worktrees_indexed_previously).await;
 
@@ -531,12 +678,112 @@ impl SemanticIndex {
         })
     }
 
-    pub fn index_project(
+    fn project_entries_changed(
+        &self,
+        project: ModelHandle<Project>,
+        changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
+        cx: &mut ModelContext<'_, SemanticIndex>,
+        worktree_id: &WorktreeId,
+    ) -> Result<()> {
+        let parsing_files_tx = self.parsing_files_tx.clone();
+        let db_update_tx = self.db_update_tx.clone();
+        let (job_queue_tx, outstanding_job_tx, worktree_db_id) = {
+            let state = self
+                .projects
+                .get(&project.downgrade())
+                .ok_or(anyhow!("Project not yet initialized"))?;
+            let worktree_db_id = state
+                .db_id_for_worktree_id(*worktree_id)
+                .ok_or(anyhow!("Worktree ID in Database Not Available"))?;
+            (
+                state.job_queue_tx.clone(),
+                state._outstanding_job_count_tx.clone(),
+                worktree_db_id,
+            )
+        };
+
+        let language_registry = self.language_registry.clone();
+        let parsing_files_tx = parsing_files_tx.clone();
+        let db_update_tx = db_update_tx.clone();
+
+        let worktree = project
+            .read(cx)
+            .worktree_for_id(worktree_id.clone(), cx)
+            .ok_or(anyhow!("Worktree not available"))?
+            .read(cx)
+            .snapshot();
+        cx.spawn(|_, _| async move {
+            let worktree = worktree.clone();
+            for (path, entry_id, path_change) in changes.iter() {
+                let relative_path = path.to_path_buf();
+                let absolute_path = worktree.absolutize(path);
+
+                let Some(entry) = worktree.entry_for_id(*entry_id) else {
+                    continue;
+                };
+                if entry.is_ignored || entry.is_symlink || entry.is_external {
+                    continue;
+                }
+
+                log::trace!("File Event: {:?}, Path: {:?}", &path_change, &path);
+                match path_change {
+                    PathChange::AddedOrUpdated | PathChange::Updated | PathChange::Added => {
+                        if let Ok(language) = language_registry
+                            .language_for_file(&relative_path, None)
+                            .await
+                        {
+                            if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
+                                && &language.name().as_ref() != &"Markdown"
+                                && language
+                                    .grammar()
+                                    .and_then(|grammar| grammar.embedding_config.as_ref())
+                                    .is_none()
+                            {
+                                continue;
+                            }
+
+                            let job_handle = JobHandle::new(&outstanding_job_tx);
+                            let new_operation = IndexOperation::IndexFile {
+                                absolute_path: absolute_path.clone(),
+                                payload: PendingFile {
+                                    worktree_db_id,
+                                    relative_path,
+                                    absolute_path,
+                                    language,
+                                    modified_time: entry.mtime,
+                                    job_handle,
+                                },
+                                tx: parsing_files_tx.clone(),
+                            };
+                            let _ = job_queue_tx.try_send(new_operation);
+                        }
+                    }
+                    PathChange::Removed => {
+                        let new_operation = IndexOperation::DeleteFile {
+                            absolute_path,
+                            payload: DbOperation::Delete {
+                                worktree_id: worktree_db_id,
+                                path: relative_path,
+                            },
+                            tx: db_update_tx.clone(),
+                        };
+                        let _ = job_queue_tx.try_send(new_operation);
+                    }
+                    _ => {}
+                }
+            }
+        })
+        .detach();
+
+        Ok(())
+    }
+
+    pub fn initialize_project(
         &mut self,
         project: ModelHandle<Project>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<(usize, watch::Receiver<usize>)>> {
-        let t0 = Instant::now();
+    ) -> Task<Result<()>> {
+        log::trace!("Initializing Project for Semantic Index");
         let worktree_scans_complete = project
             .read(cx)
             .worktrees(cx)
@@ -547,6 +794,7 @@ impl SemanticIndex {
                 }
             })
             .collect::<Vec<_>>();
+
         let worktree_db_ids = project
             .read(cx)
             .worktrees(cx)
@@ -555,15 +803,21 @@ impl SemanticIndex {
             })
             .collect::<Vec<_>>();
 
+        let _subscription = cx.subscribe(&project, |this, project, event, cx| {
+            if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event {
+                let _ =
+                    this.project_entries_changed(project.clone(), changes.clone(), cx, worktree_id);
+            };
+        });
+
         let language_registry = self.language_registry.clone();
-        let db_update_tx = self.db_update_tx.clone();
         let parsing_files_tx = self.parsing_files_tx.clone();
+        let db_update_tx = self.db_update_tx.clone();
 
         cx.spawn(|this, mut cx| async move {
             futures::future::join_all(worktree_scans_complete).await;
 
             let worktree_db_ids = futures::future::join_all(worktree_db_ids).await;
-
             let worktrees = project.read_with(&cx, |project, cx| {
                 project
                     .worktrees(cx)
@@ -573,6 +827,7 @@ impl SemanticIndex {
 
             let mut worktree_file_mtimes = HashMap::new();
             let mut db_ids_by_worktree_id = HashMap::new();
+
             for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) {
                 let db_id = db_id?;
                 db_ids_by_worktree_id.insert(worktree.id(), db_id);
@@ -583,34 +838,34 @@ impl SemanticIndex {
                 );
             }
 
+            let worktree_db_ids = db_ids_by_worktree_id
+                .iter()
+                .map(|(a, b)| (*a, *b))
+                .collect();
+
             let (job_count_tx, job_count_rx) = watch::channel_with(0);
             let job_count_tx = Arc::new(Mutex::new(job_count_tx));
-            this.update(&mut cx, |this, _| {
-                this.projects.insert(
-                    project.downgrade(),
-                    ProjectState {
-                        worktree_db_ids: db_ids_by_worktree_id
-                            .iter()
-                            .map(|(a, b)| (*a, *b))
-                            .collect(),
-                        outstanding_job_count_rx: job_count_rx.clone(),
-                        _outstanding_job_count_tx: job_count_tx.clone(),
-                    },
-                );
-            });
+            let job_count_tx_longlived = job_count_tx.clone();
 
-            cx.background()
+            let worktree_files = cx
+                .background()
                 .spawn(async move {
-                    let mut count = 0;
+                    let mut worktree_files = Vec::new();
                     for worktree in worktrees.into_iter() {
                         let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap();
+                        let worktree_db_id = db_ids_by_worktree_id[&worktree.id()];
                         for file in worktree.files(false, 0) {
                             let absolute_path = worktree.absolutize(&file.path);
 
+                            if file.is_external || file.is_ignored || file.is_symlink {
+                                continue;
+                            }
+
                             if let Ok(language) = language_registry
                                 .language_for_file(&absolute_path, None)
                                 .await
                             {
+                                // Test if file is valid parseable file
                                 if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
                                     && &language.name().as_ref() != &"Markdown"
                                     && language
@@ -627,41 +882,84 @@ impl SemanticIndex {
                                     .map_or(false, |existing_mtime| existing_mtime == file.mtime);
 
                                 if !already_stored {
-                                    count += 1;
-                                    *job_count_tx.lock().borrow_mut() += 1;
-                                    let job_handle = JobHandle {
-                                        tx: Arc::downgrade(&job_count_tx),
-                                    };
-                                    parsing_files_tx
-                                        .try_send(PendingFile {
-                                            worktree_db_id: db_ids_by_worktree_id[&worktree.id()],
+                                    let job_handle = JobHandle::new(&job_count_tx);
+                                    worktree_files.push(IndexOperation::IndexFile {
+                                        absolute_path: absolute_path.clone(),
+                                        payload: PendingFile {
+                                            worktree_db_id,
                                             relative_path: path_buf,
                                             absolute_path,
                                             language,
                                             job_handle,
                                             modified_time: file.mtime,
-                                        })
-                                        .unwrap();
+                                        },
+                                        tx: parsing_files_tx.clone(),
+                                    });
                                 }
                             }
                         }
-                        for file in file_mtimes.keys() {
-                            db_update_tx
-                                .try_send(DbOperation::Delete {
-                                    worktree_id: db_ids_by_worktree_id[&worktree.id()],
-                                    path: file.to_owned(),
-                                })
-                                .unwrap();
+                        // Clean up entries from database that are no longer in the worktree.
+                        for (path, _) in file_mtimes {
+                            worktree_files.push(IndexOperation::DeleteFile {
+                                absolute_path: worktree.absolutize(path.as_path()),
+                                payload: DbOperation::Delete {
+                                    worktree_id: worktree_db_id,
+                                    path,
+                                },
+                                tx: db_update_tx.clone(),
+                            });
                         }
                     }
 
-                    log::trace!(
-                        "walking worktree took {:?} milliseconds",
-                        t0.elapsed().as_millis()
-                    );
-                    anyhow::Ok((count, job_count_rx))
+                    anyhow::Ok(worktree_files)
                 })
-                .await
+                .await?;
+
+            this.update(&mut cx, |this, cx| {
+                let project_state = ProjectState::new(
+                    cx,
+                    _subscription,
+                    worktree_db_ids,
+                    job_count_rx,
+                    job_count_tx_longlived,
+                );
+
+                for op in worktree_files {
+                    let _ = project_state.job_queue_tx.try_send(op);
+                }
+
+                this.projects.insert(project.downgrade(), project_state);
+            });
+            Result::<(), _>::Ok(())
+        })
+    }
+
+    pub fn index_project(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<(usize, watch::Receiver<usize>)>> {
+        let state = self.projects.get_mut(&project.downgrade());
+        let state = if state.is_none() {
+            return Task::Ready(Some(Err(anyhow!("Project not yet initialized"))));
+        } else {
+            state.unwrap()
+        };
+
+        // let parsing_files_tx = self.parsing_files_tx.clone();
+        // let db_update_tx = self.db_update_tx.clone();
+        let job_count_rx = state.outstanding_job_count_rx.clone();
+        let count = state.get_outstanding_count();
+
+        cx.spawn(|this, mut cx| async move {
+            this.update(&mut cx, |this, _| {
+                let Some(state) = this.projects.get_mut(&project.downgrade()) else {
+                    return;
+                };
+                let _ = state.job_queue_tx.try_send(IndexOperation::FlushQueue);
+            });
+
+            Ok((count, job_count_rx))
         })
     }
 
@@ -705,6 +1003,7 @@ impl SemanticIndex {
         let database_url = self.database_url.clone();
         let fs = self.fs.clone();
         cx.spawn(|this, mut cx| async move {
+            let t0 = Instant::now();
             let database = VectorDatabase::new(fs.clone(), database_url.clone()).await?;
 
             let phrase_embedding = embedding_provider
@@ -714,6 +1013,11 @@ impl SemanticIndex {
                 .next()
                 .unwrap();
 
+            log::trace!(
+                "Embedding search phrase took: {:?} milliseconds",
+                t0.elapsed().as_millis()
+            );
+
             let file_ids =
                 database.retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes)?;
 
@@ -788,6 +1092,11 @@ impl SemanticIndex {
 
             let buffers = futures::future::join_all(tasks).await;
 
+            log::trace!(
+                "Semantic Searching took: {:?} milliseconds in total",
+                t0.elapsed().as_millis()
+            );
+
             Ok(buffers
                 .into_iter()
                 .zip(ranges)
@@ -809,9 +1118,32 @@ impl Entity for SemanticIndex {
 
 impl Drop for JobHandle {
     fn drop(&mut self) {
-        if let Some(tx) = self.tx.upgrade() {
-            let mut tx = tx.lock();
-            *tx.borrow_mut() -= 1;
+        if let Some(inner) = Arc::get_mut(&mut self.tx) {
+            // This is the last instance of the JobHandle (regardless of it's origin - whether it was cloned or not)
+            if let Some(tx) = inner.upgrade() {
+                let mut tx = tx.lock();
+                *tx.borrow_mut() -= 1;
+            }
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+    #[test]
+    fn test_job_handle() {
+        let (job_count_tx, job_count_rx) = watch::channel_with(0);
+        let tx = Arc::new(Mutex::new(job_count_tx));
+        let job_handle = JobHandle::new(&tx);
+
+        assert_eq!(1, *job_count_rx.borrow());
+        let new_job_handle = job_handle.clone();
+        assert_eq!(1, *job_count_rx.borrow());
+        drop(job_handle);
+        assert_eq!(1, *job_count_rx.borrow());
+        drop(new_job_handle);
+        assert_eq!(0, *job_count_rx.borrow());
+    }
+}

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -86,6 +86,13 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
     .unwrap();
 
     let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+
+    let _ = store
+        .update(cx, |store, cx| {
+            store.initialize_project(project.clone(), cx)
+        })
+        .await;
+
     let (file_count, outstanding_file_count) = store
         .update(cx, |store, cx| store.index_project(project.clone(), cx))
         .await

crates/settings/Cargo.toml 🔗

@@ -16,7 +16,7 @@ collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 sqlez = { path = "../sqlez" }
 fs = { path = "../fs" }
-staff_mode = { path = "../staff_mode" }
+feature_flags = { path = "../feature_flags" }
 util = { path = "../util" }
 
 anyhow.workspace = true

crates/settings/src/keymap_file.rs 🔗

@@ -63,20 +63,23 @@ impl KeymapFile {
                     // string. But `RawValue` currently does not work inside of an untagged enum.
                     match action {
                         Value::Array(items) => {
-                            let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
+                            let Ok([name, data]): Result<[serde_json::Value; 2], _> =
+                                items.try_into()
+                            else {
                                 return Some(Err(anyhow!("Expected array of length 2")));
                             };
                             let serde_json::Value::String(name) = name else {
-                                return Some(Err(anyhow!("Expected first item in array to be a string.")))
+                                return Some(Err(anyhow!(
+                                    "Expected first item in array to be a string."
+                                )));
                             };
-                            cx.deserialize_action(
-                                &name,
-                                Some(data),
-                            )
-                        },
+                            cx.deserialize_action(&name, Some(data))
+                        }
                         Value::String(name) => cx.deserialize_action(&name, None),
                         Value::Null => Ok(no_action()),
-                        _ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))),
+                        _ => {
+                            return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
+                        }
                     }
                     .with_context(|| {
                         format!(

crates/settings/src/settings_store.rs 🔗

@@ -1,4 +1,4 @@
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
 use collections::{btree_map, hash_map, BTreeMap, HashMap};
 use gpui::AppContext;
 use lazy_static::lazy_static;
@@ -162,6 +162,7 @@ impl SettingsStore {
 
             if let Some(setting) = setting_value
                 .load_setting(&default_settings, &user_values_stack, cx)
+                .context("A default setting must be added to the `default.json` file")
                 .log_err()
             {
                 setting_value.set_global_value(setting);

crates/staff_mode/src/staff_mode.rs 🔗

@@ -1,36 +0,0 @@
-use gpui::AppContext;
-
-#[derive(Debug, Default)]
-pub struct StaffMode(pub bool);
-
-impl std::ops::Deref for StaffMode {
-    type Target = bool;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-/// Despite what the type system requires me to tell you, the init function will only be called a once
-/// as soon as we know that the staff mode is enabled.
-pub fn staff_mode<F: FnMut(&mut AppContext) + 'static>(cx: &mut AppContext, mut init: F) {
-    if **cx.default_global::<StaffMode>() {
-        init(cx)
-    } else {
-        let mut once = Some(());
-        cx.observe_global::<StaffMode, _>(move |cx| {
-            if **cx.global::<StaffMode>() && once.take().is_some() {
-                init(cx);
-            }
-        })
-        .detach();
-    }
-}
-
-/// Immediately checks and runs the init function if the staff mode is not enabled.
-/// This is only included for symettry with staff_mode() above
-pub fn not_staff_mode<F: FnOnce(&mut AppContext) + 'static>(cx: &mut AppContext, init: F) {
-    if !**cx.default_global::<StaffMode>() {
-        init(cx)
-    }
-}

crates/sum_tree/src/tree_map.rs 🔗

@@ -2,7 +2,7 @@ use std::{cmp::Ordering, fmt::Debug};
 
 use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary};
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, PartialEq, Eq)]
 pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
 where
     K: Clone + Debug + Default + Ord,
@@ -162,6 +162,16 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
     }
 }
 
+impl<K: Debug, V: Debug> Debug for TreeMap<K, V>
+where
+    K: Clone + Debug + Default + Ord,
+    V: Clone + Debug,
+{
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_map().entries(self.iter()).finish()
+    }
+}
+
 #[derive(Debug)]
 struct MapSeekTargetAdaptor<'a, T>(&'a T);
 

crates/terminal/Cargo.toml 🔗

@@ -16,7 +16,7 @@ db = { path = "../db" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 
-alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "7b9f32300ee0a249c0872302c97635b460e45ba5" }
+alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "33306142195b354ef3485ca2b1d8a85dfc6605ca" }
 procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
 smallvec.workspace = true
 smol.workspace = true

crates/terminal/src/terminal.rs 🔗

@@ -1,5 +1,6 @@
 pub mod mappings;
 pub use alacritty_terminal;
+pub mod terminal_settings;
 
 use alacritty_terminal::{
     ansi::{ClearMode, Handler},
@@ -7,7 +8,7 @@ use alacritty_terminal::{
     event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
     event_loop::{EventLoop, Msg, Notifier},
     grid::{Dimensions, Scroll as AlacScroll},
-    index::{Column, Direction as AlacDirection, Line, Point},
+    index::{Boundary, Column, Direction as AlacDirection, Line, Point},
     selection::{Selection, SelectionRange, SelectionType},
     sync::FairMutex,
     term::{
@@ -31,8 +32,8 @@ use mappings::mouse::{
 };
 
 use procinfo::LocalProcessInfo;
-use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings};
 use util::truncate_and_trailoff;
 
 use std::{
@@ -48,7 +49,6 @@ use std::{
 use thiserror::Error;
 
 use gpui::{
-    fonts,
     geometry::vector::{vec2f, Vector2F},
     keymap_matcher::Keystroke,
     platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase},
@@ -78,7 +78,7 @@ lazy_static! {
     // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings.
     static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
 
-    static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.:/@\-~]+"#).unwrap();
+    static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap();
 }
 
 ///Upward flowing events, for changing the title and such
@@ -134,122 +134,6 @@ pub fn init(cx: &mut AppContext) {
     settings::register::<TerminalSettings>(cx);
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalDockPosition {
-    Left,
-    Bottom,
-    Right,
-}
-
-#[derive(Deserialize)]
-pub struct TerminalSettings {
-    pub shell: Shell,
-    pub working_directory: WorkingDirectory,
-    font_size: Option<f32>,
-    pub font_family: Option<String>,
-    pub line_height: TerminalLineHeight,
-    pub font_features: Option<fonts::Features>,
-    pub env: HashMap<String, String>,
-    pub blinking: TerminalBlink,
-    pub alternate_scroll: AlternateScroll,
-    pub option_as_meta: bool,
-    pub copy_on_select: bool,
-    pub dock: TerminalDockPosition,
-    pub default_width: f32,
-    pub default_height: f32,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct TerminalSettingsContent {
-    pub shell: Option<Shell>,
-    pub working_directory: Option<WorkingDirectory>,
-    pub font_size: Option<f32>,
-    pub font_family: Option<String>,
-    pub line_height: Option<TerminalLineHeight>,
-    pub font_features: Option<fonts::Features>,
-    pub env: Option<HashMap<String, String>>,
-    pub blinking: Option<TerminalBlink>,
-    pub alternate_scroll: Option<AlternateScroll>,
-    pub option_as_meta: Option<bool>,
-    pub copy_on_select: Option<bool>,
-    pub dock: Option<TerminalDockPosition>,
-    pub default_width: Option<f32>,
-    pub default_height: Option<f32>,
-}
-
-impl TerminalSettings {
-    pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
-        self.font_size
-            .map(|size| theme::adjusted_font_size(size, cx))
-    }
-}
-
-impl settings::Setting for TerminalSettings {
-    const KEY: Option<&'static str> = Some("terminal");
-
-    type FileContent = TerminalSettingsContent;
-
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &AppContext,
-    ) -> Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
-    }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalLineHeight {
-    #[default]
-    Comfortable,
-    Standard,
-    Custom(f32),
-}
-
-impl TerminalLineHeight {
-    pub fn value(&self) -> f32 {
-        match self {
-            TerminalLineHeight::Comfortable => 1.618,
-            TerminalLineHeight::Standard => 1.3,
-            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
-        }
-    }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalBlink {
-    Off,
-    TerminalControlled,
-    On,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Shell {
-    System,
-    Program(String),
-    WithArguments { program: String, args: Vec<String> },
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum AlternateScroll {
-    On,
-    Off,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum WorkingDirectory {
-    CurrentProjectDirectory,
-    FirstProjectDirectory,
-    AlwaysHome,
-    Always { directory: String },
-}
-
 #[derive(Clone, Copy, Debug, Serialize, Deserialize)]
 pub struct TerminalSize {
     pub cell_width: f32,
@@ -840,14 +724,13 @@ impl Terminal {
                     self.last_content.size,
                     term.grid().display_offset(),
                 )
-                .grid_clamp(term, alacritty_terminal::index::Boundary::Grid);
+                .grid_clamp(term, Boundary::Grid);
 
                 let link = term.grid().index(point).hyperlink();
                 let found_word = if link.is_some() {
                     let mut min_index = point;
                     loop {
-                        let new_min_index =
-                            min_index.sub(term, alacritty_terminal::index::Boundary::Cursor, 1);
+                        let new_min_index = min_index.sub(term, Boundary::Cursor, 1);
                         if new_min_index == min_index {
                             break;
                         } else if term.grid().index(new_min_index).hyperlink() != link {
@@ -859,8 +742,7 @@ impl Terminal {
 
                     let mut max_index = point;
                     loop {
-                        let new_max_index =
-                            max_index.add(term, alacritty_terminal::index::Boundary::Cursor, 1);
+                        let new_max_index = max_index.add(term, Boundary::Cursor, 1);
                         if new_max_index == max_index {
                             break;
                         } else if term.grid().index(new_max_index).hyperlink() != link {
@@ -877,11 +759,34 @@ impl Terminal {
                 } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) {
                     let maybe_url_or_path =
                         term.bounds_to_string(*word_match.start(), *word_match.end());
+                    let original_match = word_match.clone();
+                    let (sanitized_match, sanitized_word) =
+                        if maybe_url_or_path.starts_with('[') && maybe_url_or_path.ends_with(']') {
+                            (
+                                Match::new(
+                                    word_match.start().add(term, Boundary::Cursor, 1),
+                                    word_match.end().sub(term, Boundary::Cursor, 1),
+                                ),
+                                maybe_url_or_path[1..maybe_url_or_path.len() - 1].to_owned(),
+                            )
+                        } else {
+                            (word_match, maybe_url_or_path)
+                        };
+
                     let is_url = match regex_match_at(term, point, &URL_REGEX) {
-                        Some(url_match) => url_match == word_match,
+                        Some(url_match) => {
+                            // `]` is a valid symbol in the `file://` URL, so the regex match will include it
+                            // consider that when ensuring that the URL match is the same as the original word
+                            if sanitized_match != original_match {
+                                url_match.start() == sanitized_match.start()
+                                    && url_match.end() == original_match.end()
+                            } else {
+                                url_match == sanitized_match
+                            }
+                        }
                         None => false,
                     };
-                    Some((maybe_url_or_path, is_url, word_match))
+                    Some((sanitized_word, is_url, sanitized_match))
                 } else {
                     None
                 };
@@ -987,6 +892,14 @@ impl Terminal {
         }
     }
 
+    pub fn select_all(&mut self) {
+        let term = self.term.lock();
+        let start = Point::new(term.topmost_line(), Column(0));
+        let end = Point::new(term.bottommost_line(), term.last_column());
+        drop(term);
+        self.set_selection(Some((make_selection(&(start..=end)), end)));
+    }
+
     fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
         self.events
             .push_back(InternalEvent::SetSelection(selection));
@@ -1010,6 +923,10 @@ impl Terminal {
         self.pty_tx.notify(input.into_bytes());
     }
 
+    fn write_bytes_to_pty(&self, input: Vec<u8>) {
+        self.pty_tx.notify(input);
+    }
+
     pub fn input(&mut self, input: String) {
         self.events
             .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
@@ -1018,6 +935,14 @@ impl Terminal {
         self.write_to_pty(input);
     }
 
+    pub fn input_bytes(&mut self, input: Vec<u8>) {
+        self.events
+            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
+        self.events.push_back(InternalEvent::SetSelection(None));
+
+        self.write_bytes_to_pty(input);
+    }
+
     pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
         let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
         if let Some(esc) = esc {

crates/terminal/src/terminal_settings.rs 🔗

@@ -0,0 +1,163 @@
+use std::{collections::HashMap, path::PathBuf};
+
+use gpui::{fonts, AppContext};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalDockPosition {
+    Left,
+    Bottom,
+    Right,
+}
+
+#[derive(Deserialize)]
+pub struct TerminalSettings {
+    pub shell: Shell,
+    pub working_directory: WorkingDirectory,
+    font_size: Option<f32>,
+    pub font_family: Option<String>,
+    pub line_height: TerminalLineHeight,
+    pub font_features: Option<fonts::Features>,
+    pub env: HashMap<String, String>,
+    pub blinking: TerminalBlink,
+    pub alternate_scroll: AlternateScroll,
+    pub option_as_meta: bool,
+    pub copy_on_select: bool,
+    pub dock: TerminalDockPosition,
+    pub default_width: f32,
+    pub default_height: f32,
+    pub detect_venv: VenvSettings,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum VenvSettings {
+    #[default]
+    Off,
+    On {
+        activate_script: Option<ActivateScript>,
+        directories: Option<Vec<PathBuf>>,
+    },
+}
+
+pub struct VenvSettingsContent<'a> {
+    pub activate_script: ActivateScript,
+    pub directories: &'a [PathBuf],
+}
+
+impl VenvSettings {
+    pub fn as_option(&self) -> Option<VenvSettingsContent> {
+        match self {
+            VenvSettings::Off => None,
+            VenvSettings::On {
+                activate_script,
+                directories,
+            } => Some(VenvSettingsContent {
+                activate_script: activate_script.unwrap_or(ActivateScript::Default),
+                directories: directories.as_deref().unwrap_or(&[]),
+            }),
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ActivateScript {
+    #[default]
+    Default,
+    Csh,
+    Fish,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct TerminalSettingsContent {
+    pub shell: Option<Shell>,
+    pub working_directory: Option<WorkingDirectory>,
+    pub font_size: Option<f32>,
+    pub font_family: Option<String>,
+    pub line_height: Option<TerminalLineHeight>,
+    pub font_features: Option<fonts::Features>,
+    pub env: Option<HashMap<String, String>>,
+    pub blinking: Option<TerminalBlink>,
+    pub alternate_scroll: Option<AlternateScroll>,
+    pub option_as_meta: Option<bool>,
+    pub copy_on_select: Option<bool>,
+    pub dock: Option<TerminalDockPosition>,
+    pub default_width: Option<f32>,
+    pub default_height: Option<f32>,
+    pub detect_venv: Option<VenvSettings>,
+}
+
+impl TerminalSettings {
+    pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
+        self.font_size
+            .map(|size| theme::adjusted_font_size(size, cx))
+    }
+}
+
+impl settings::Setting for TerminalSettings {
+    const KEY: Option<&'static str> = Some("terminal");
+
+    type FileContent = TerminalSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalLineHeight {
+    #[default]
+    Comfortable,
+    Standard,
+    Custom(f32),
+}
+
+impl TerminalLineHeight {
+    pub fn value(&self) -> f32 {
+        match self {
+            TerminalLineHeight::Comfortable => 1.618,
+            TerminalLineHeight::Standard => 1.3,
+            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalBlink {
+    Off,
+    TerminalControlled,
+    On,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Shell {
+    System,
+    Program(String),
+    WithArguments { program: String, args: Vec<String> },
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum AlternateScroll {
+    On,
+    Off,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum WorkingDirectory {
+    CurrentProjectDirectory,
+    FirstProjectDirectory,
+    AlwaysHome,
+    Always { directory: String },
+}

crates/terminal_view/src/terminal_element.rs 🔗

@@ -25,7 +25,8 @@ use terminal::{
         term::{cell::Flags, TermMode},
     },
     mappings::colors::convert_color,
-    IndexedCell, Terminal, TerminalContent, TerminalSettings, TerminalSize,
+    terminal_settings::TerminalSettings,
+    IndexedCell, Terminal, TerminalContent, TerminalSize,
 };
 use theme::{TerminalStyle, ThemeSettings};
 use util::ResultExt;
@@ -400,7 +401,8 @@ impl TerminalElement {
         region = region
             // Start selections
             .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
-                cx.focus_parent();
+                let terminal_view = cx.handle();
+                cx.focus(&terminal_view);
                 v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
                 if let Some(conn_handle) = connection.upgrade(cx) {
                     conn_handle.update(cx, |terminal, cx| {
@@ -566,6 +568,7 @@ impl Element<TerminalView> for TerminalElement {
             font_size,
             font_properties: Default::default(),
             underline: Default::default(),
+            soft_wrap: false,
         };
         let selection_color = settings.theme.editor.selection.selection;
         let match_color = settings.theme.search.match_background;

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
 use project::Fs;
 use serde::{Deserialize, Serialize};
 use settings::SettingsStore;
-use terminal::{TerminalDockPosition, TerminalSettings};
+use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
@@ -362,10 +362,10 @@ impl Panel for TerminalPanel {
         }
     }
 
-    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
         match self.position(cx) {
-            DockPosition::Left | DockPosition::Right => self.width = Some(size),
-            DockPosition::Bottom => self.height = Some(size),
+            DockPosition::Left | DockPosition::Right => self.width = size,
+            DockPosition::Bottom => self.height = size,
         }
         self.serialize(cx);
         cx.notify();
@@ -393,8 +393,8 @@ impl Panel for TerminalPanel {
         }
     }
 
-    fn icon_path(&self) -> &'static str {
-        "icons/terminal_12.svg"
+    fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
+        Some("icons/terminal.svg")
     }
 
     fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -33,7 +33,8 @@ use terminal::{
         index::Point,
         term::{search::RegexSearch, TermMode},
     },
-    Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory,
+    terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
+    Event, MaybeNavigationTarget, Terminal,
 };
 use util::{paths::PathLikeWithPosition, ResultExt};
 use workspace::{
@@ -44,8 +45,6 @@ use workspace::{
     NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
 };
 
-pub use terminal::TerminalSettings;
-
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 
 ///Event to transmit the scroll from the element to the view
@@ -80,6 +79,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(TerminalView::paste);
     cx.add_action(TerminalView::clear);
     cx.add_action(TerminalView::show_character_palette);
+    cx.add_action(TerminalView::select_all)
 }
 
 ///A terminal view, maintains the PTY's file handles and communicates with the terminal
@@ -312,6 +312,11 @@ impl TerminalView {
         }
     }
 
+    fn select_all(&mut self, _: &editor::SelectAll, cx: &mut ViewContext<Self>) {
+        self.terminal.update(cx, |term, _| term.select_all());
+        cx.notify();
+    }
+
     fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
         self.terminal.update(cx, |term, _| term.clear());
         cx.notify();
@@ -477,10 +482,8 @@ fn possible_open_targets(
 }
 
 pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
-    let searcher = match query {
-        project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query),
-        project::search::SearchQuery::Regex { query, .. } => RegexSearch::new(&query),
-    };
+    let query = query.as_str();
+    let searcher = RegexSearch::new(&query);
     searcher.ok()
 }
 
@@ -657,7 +660,7 @@ impl Item for TerminalView {
         Some(self.terminal().read(cx).title().into())
     }
 
-    fn tab_content<T: View>(
+    fn tab_content<T: 'static>(
         &self,
         _detail: Option<usize>,
         tab_theme: &theme::Tab,
@@ -667,7 +670,7 @@ impl Item for TerminalView {
 
         Flex::row()
             .with_child(
-                gpui::elements::Svg::new("icons/terminal_12.svg")
+                gpui::elements::Svg::new("icons/terminal.svg")
                     .with_color(tab_theme.label.text.color)
                     .constrained()
                     .with_width(tab_theme.type_icon_width)

crates/text/Cargo.toml 🔗

@@ -14,7 +14,6 @@ test-support = ["rand"]
 [dependencies]
 clock = { path = "../clock" }
 collections = { path = "../collections" }
-fs = { path = "../fs" }
 rope = { path = "../rope" }
 sum_tree = { path = "../sum_tree" }
 util = { path = "../util" }

crates/text/src/text.rs 🔗

@@ -12,18 +12,19 @@ mod undo_map;
 
 pub use anchor::*;
 use anyhow::{anyhow, Result};
-use clock::ReplicaId;
+pub use clock::ReplicaId;
 use collections::{HashMap, HashSet};
-use fs::LineEnding;
 use locator::Locator;
 use operation_queue::OperationQueue;
 pub use patch::Patch;
 use postage::{oneshot, prelude::*};
 
+use lazy_static::lazy_static;
+use regex::Regex;
 pub use rope::*;
 pub use selection::*;
-
 use std::{
+    borrow::Cow,
     cmp::{self, Ordering, Reverse},
     future::Future,
     iter::Iterator,
@@ -36,10 +37,15 @@ pub use subscription::*;
 pub use sum_tree::Bias;
 use sum_tree::{FilterCursor, SumTree, TreeMap};
 use undo_map::UndoMap;
+use util::ResultExt;
 
 #[cfg(any(test, feature = "test-support"))]
 use util::RandomCharIter;
 
+lazy_static! {
+    static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
+}
+
 pub type TransactionId = clock::Local;
 
 pub struct Buffer {
@@ -263,7 +269,19 @@ impl History {
         }
     }
 
-    fn remove_from_undo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
+    fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&HistoryEntry> {
+        assert_eq!(self.transaction_depth, 0);
+
+        let entry_ix = self
+            .undo_stack
+            .iter()
+            .rposition(|entry| entry.transaction.id == transaction_id)?;
+        let entry = self.undo_stack.remove(entry_ix);
+        self.redo_stack.push(entry);
+        self.redo_stack.last()
+    }
+
+    fn remove_from_undo_until(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] {
         assert_eq!(self.transaction_depth, 0);
 
         let redo_stack_start_len = self.redo_stack.len();
@@ -278,20 +296,43 @@ impl History {
         &self.redo_stack[redo_stack_start_len..]
     }
 
-    fn forget(&mut self, transaction_id: TransactionId) {
+    fn forget(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
         assert_eq!(self.transaction_depth, 0);
         if let Some(entry_ix) = self
             .undo_stack
             .iter()
             .rposition(|entry| entry.transaction.id == transaction_id)
         {
-            self.undo_stack.remove(entry_ix);
+            Some(self.undo_stack.remove(entry_ix).transaction)
         } else if let Some(entry_ix) = self
             .redo_stack
             .iter()
             .rposition(|entry| entry.transaction.id == transaction_id)
         {
-            self.undo_stack.remove(entry_ix);
+            Some(self.redo_stack.remove(entry_ix).transaction)
+        } else {
+            None
+        }
+    }
+
+    fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
+        let entry = self
+            .undo_stack
+            .iter_mut()
+            .rfind(|entry| entry.transaction.id == transaction_id)
+            .or_else(|| {
+                self.redo_stack
+                    .iter_mut()
+                    .rfind(|entry| entry.transaction.id == transaction_id)
+            })?;
+        Some(&mut entry.transaction)
+    }
+
+    fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
+        if let Some(transaction) = self.forget(transaction) {
+            if let Some(destination) = self.transaction_mut(destination) {
+                destination.edit_ids.extend(transaction.edit_ids);
+            }
         }
     }
 
@@ -1183,11 +1224,20 @@ impl Buffer {
         }
     }
 
+    pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
+        let transaction = self
+            .history
+            .remove_from_undo(transaction_id)?
+            .transaction
+            .clone();
+        self.undo_or_redo(transaction).log_err()
+    }
+
     #[allow(clippy::needless_collect)]
     pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec<Operation> {
         let transactions = self
             .history
-            .remove_from_undo(transaction_id)
+            .remove_from_undo_until(transaction_id)
             .iter()
             .map(|entry| entry.transaction.clone())
             .collect::<Vec<_>>();
@@ -1202,6 +1252,10 @@ impl Buffer {
         self.history.forget(transaction_id);
     }
 
+    pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
+        self.history.merge_transactions(transaction, destination);
+    }
+
     pub fn redo(&mut self) -> Option<(TransactionId, Operation)> {
         if let Some(entry) = self.history.pop_redo() {
             let transaction = entry.transaction.clone();
@@ -2622,3 +2676,59 @@ impl FromAnchor for usize {
         snapshot.summary_for_anchor(anchor)
     }
 }
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum LineEnding {
+    Unix,
+    Windows,
+}
+
+impl Default for LineEnding {
+    fn default() -> Self {
+        #[cfg(unix)]
+        return Self::Unix;
+
+        #[cfg(not(unix))]
+        return Self::CRLF;
+    }
+}
+
+impl LineEnding {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            LineEnding::Unix => "\n",
+            LineEnding::Windows => "\r\n",
+        }
+    }
+
+    pub fn detect(text: &str) -> Self {
+        let mut max_ix = cmp::min(text.len(), 1000);
+        while !text.is_char_boundary(max_ix) {
+            max_ix -= 1;
+        }
+
+        if let Some(ix) = text[..max_ix].find(&['\n']) {
+            if ix > 0 && text.as_bytes()[ix - 1] == b'\r' {
+                Self::Windows
+            } else {
+                Self::Unix
+            }
+        } else {
+            Self::default()
+        }
+    }
+
+    pub fn normalize(text: &mut String) {
+        if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") {
+            *text = replaced;
+        }
+    }
+
+    pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
+        if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
+            replaced.into()
+        } else {
+            text
+        }
+    }
+}

crates/theme/src/components.rs 🔗

@@ -0,0 +1,481 @@
+use gpui::{elements::SafeStylable, Action};
+
+use crate::{Interactive, Toggleable};
+
+use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle};
+
+pub type IconButtonStyle = Interactive<ButtonStyle<SvgStyle>>;
+pub type ToggleIconButtonStyle = Toggleable<IconButtonStyle>;
+
+pub trait ComponentExt<C: SafeStylable> {
+    fn toggleable(self, active: bool) -> Toggle<C, ()>;
+    fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()>;
+}
+
+impl<C: SafeStylable> ComponentExt<C> for C {
+    fn toggleable(self, active: bool) -> Toggle<C, ()> {
+        Toggle::new(self, active)
+    }
+
+    /// Some(True) => disclosed => content is visible
+    /// Some(false) => closed => content is hidden
+    /// None => No disclosure button, but reserve disclosure spacing
+    fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()> {
+        Disclosable::new(disclosed, self, action)
+    }
+}
+
+pub mod disclosure {
+
+    use gpui::{
+        elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable},
+        Action, Element,
+    };
+    use schemars::JsonSchema;
+    use serde_derive::Deserialize;
+
+    use super::{action_button::Button, svg::Svg, IconButtonStyle};
+
+    #[derive(Clone, Default, Deserialize, JsonSchema)]
+    pub struct DisclosureStyle<S> {
+        pub button: IconButtonStyle,
+        #[serde(flatten)]
+        pub container: ContainerStyle,
+        pub spacing: f32,
+        #[serde(flatten)]
+        content: S,
+    }
+
+    impl<S> DisclosureStyle<S> {
+        pub fn button_space(&self) -> f32 {
+            self.spacing + self.button.button_width.unwrap()
+        }
+    }
+
+    pub struct Disclosable<C, S> {
+        disclosed: Option<bool>,
+        action: Box<dyn Action>,
+        id: usize,
+        content: C,
+        style: S,
+    }
+
+    impl Disclosable<(), ()> {
+        pub fn new<C>(
+            disclosed: Option<bool>,
+            content: C,
+            action: Box<dyn Action>,
+        ) -> Disclosable<C, ()> {
+            Disclosable {
+                disclosed,
+                content,
+                action,
+                id: 0,
+                style: (),
+            }
+        }
+    }
+
+    impl<C> Disclosable<C, ()> {
+        pub fn with_id(mut self, id: usize) -> Disclosable<C, ()> {
+            self.id = id;
+            self
+        }
+    }
+
+    impl<C: SafeStylable> SafeStylable for Disclosable<C, ()> {
+        type Style = DisclosureStyle<C::Style>;
+
+        type Output = Disclosable<C, Self::Style>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            Disclosable {
+                disclosed: self.disclosed,
+                action: self.action,
+                content: self.content,
+                id: self.id,
+                style,
+            }
+        }
+    }
+
+    impl<C: SafeStylable> Component for Disclosable<C, DisclosureStyle<C::Style>> {
+        fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+            Flex::row()
+                .with_spacing(self.style.spacing)
+                .with_child(if let Some(disclosed) = self.disclosed {
+                    Button::dynamic_action(self.action)
+                        .with_id(self.id)
+                        .with_contents(Svg::new(if disclosed {
+                            "icons/file_icons/chevron_down.svg"
+                        } else {
+                            "icons/file_icons/chevron_right.svg"
+                        }))
+                        .with_style(self.style.button)
+                        .element()
+                        .into_any()
+                } else {
+                    Empty::new()
+                        .into_any()
+                        .constrained()
+                        // TODO: Why is this optional at all?
+                        .with_width(self.style.button.button_width.unwrap())
+                        .into_any()
+                })
+                .with_child(
+                    self.content
+                        .with_style(self.style.content)
+                        .render(cx)
+                        .flex(1., true),
+                )
+                .align_children_center()
+                .contained()
+                .with_style(self.style.container)
+                .into_any()
+        }
+    }
+}
+
+pub mod toggle {
+    use gpui::elements::{Component, SafeStylable};
+
+    use crate::Toggleable;
+
+    pub struct Toggle<C, S> {
+        style: S,
+        active: bool,
+        component: C,
+    }
+
+    impl<C: SafeStylable> Toggle<C, ()> {
+        pub fn new(component: C, active: bool) -> Self {
+            Toggle {
+                active,
+                component,
+                style: (),
+            }
+        }
+    }
+
+    impl<C: SafeStylable> SafeStylable for Toggle<C, ()> {
+        type Style = Toggleable<C::Style>;
+
+        type Output = Toggle<C, Self::Style>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            Toggle {
+                active: self.active,
+                component: self.component,
+                style,
+            }
+        }
+    }
+
+    impl<C: SafeStylable> Component for Toggle<C, Toggleable<C::Style>> {
+        fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+            self.component
+                .with_style(self.style.in_state(self.active).clone())
+                .render(cx)
+        }
+    }
+}
+
+pub mod action_button {
+    use std::borrow::Cow;
+
+    use gpui::{
+        elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle},
+        platform::{CursorStyle, MouseButton},
+        Action, Element, TypeTag,
+    };
+    use schemars::JsonSchema;
+    use serde_derive::Deserialize;
+
+    use crate::Interactive;
+
+    #[derive(Clone, Deserialize, Default, JsonSchema)]
+    pub struct ButtonStyle<C> {
+        #[serde(flatten)]
+        pub container: ContainerStyle,
+        // TODO: These are incorrect for the intended usage of the buttons.
+        // The size should be constant, but putting them here duplicates them
+        // across the states the buttons can be in
+        pub button_width: Option<f32>,
+        pub button_height: Option<f32>,
+        #[serde(flatten)]
+        contents: C,
+    }
+
+    pub struct Button<C, S> {
+        action: Box<dyn Action>,
+        tooltip: Option<(Cow<'static, str>, TooltipStyle)>,
+        tag: TypeTag,
+        id: usize,
+        contents: C,
+        style: Interactive<S>,
+    }
+
+    impl Button<(), ()> {
+        pub fn dynamic_action(action: Box<dyn Action>) -> Button<(), ()> {
+            Self {
+                contents: (),
+                tag: action.type_tag(),
+                action,
+                style: Interactive::new_blank(),
+                tooltip: None,
+                id: 0,
+            }
+        }
+
+        pub fn action<A: Action + Clone>(action: A) -> Self {
+            Self::dynamic_action(Box::new(action))
+        }
+
+        pub fn with_tooltip(
+            mut self,
+            tooltip: impl Into<Cow<'static, str>>,
+            tooltip_style: TooltipStyle,
+        ) -> Self {
+            self.tooltip = Some((tooltip.into(), tooltip_style));
+            self
+        }
+
+        pub fn with_id(mut self, id: usize) -> Self {
+            self.id = id;
+            self
+        }
+
+        pub fn with_contents<C: SafeStylable>(self, contents: C) -> Button<C, ()> {
+            Button {
+                action: self.action,
+                tag: self.tag,
+                style: self.style,
+                tooltip: self.tooltip,
+                id: self.id,
+                contents,
+            }
+        }
+    }
+
+    impl<C: SafeStylable> SafeStylable for Button<C, ()> {
+        type Style = Interactive<ButtonStyle<C::Style>>;
+        type Output = Button<C, ButtonStyle<C::Style>>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            Button {
+                action: self.action,
+                tag: self.tag,
+                contents: self.contents,
+                tooltip: self.tooltip,
+                id: self.id,
+                style,
+            }
+        }
+    }
+
+    impl<C: SafeStylable> Component for Button<C, ButtonStyle<C::Style>> {
+        fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+            let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| {
+                let style = self.style.style_for(state);
+                let mut contents = self
+                    .contents
+                    .with_style(style.contents.to_owned())
+                    .render(cx)
+                    .contained()
+                    .with_style(style.container)
+                    .constrained();
+
+                if let Some(height) = style.button_height {
+                    contents = contents.with_height(height);
+                }
+
+                if let Some(width) = style.button_width {
+                    contents = contents.with_width(width);
+                }
+
+                contents.into_any()
+            })
+            .on_click(MouseButton::Left, {
+                let action = self.action.boxed_clone();
+                move |_, _, cx| {
+                    let window = cx.window();
+                    let view = cx.view_id();
+                    let action = action.boxed_clone();
+                    cx.spawn(|_, mut cx| async move {
+                        window.dispatch_action(view, action.as_ref(), &mut cx)
+                    })
+                    .detach();
+                }
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .into_any();
+
+            if let Some((tooltip, style)) = self.tooltip {
+                button = button
+                    .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx)
+                    .into_any()
+            }
+
+            button
+        }
+    }
+}
+
+pub mod svg {
+    use std::borrow::Cow;
+
+    use gpui::{
+        elements::{Component, Empty, SafeStylable},
+        Element,
+    };
+    use schemars::JsonSchema;
+    use serde::Deserialize;
+
+    #[derive(Clone, Default, JsonSchema)]
+    pub struct SvgStyle {
+        icon_width: f32,
+        icon_height: f32,
+        color: gpui::color::Color,
+    }
+
+    impl<'de> Deserialize<'de> for SvgStyle {
+        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: serde::Deserializer<'de>,
+        {
+            #[derive(Deserialize)]
+            #[serde(untagged)]
+            pub enum IconSize {
+                IconSize { icon_size: f32 },
+                Dimensions { width: f32, height: f32 },
+                IconDimensions { icon_width: f32, icon_height: f32 },
+            }
+
+            #[derive(Deserialize)]
+            struct SvgStyleHelper {
+                #[serde(flatten)]
+                size: IconSize,
+                color: gpui::color::Color,
+            }
+
+            let json = SvgStyleHelper::deserialize(deserializer)?;
+            let color = json.color;
+
+            let result = match json.size {
+                IconSize::IconSize { icon_size } => SvgStyle {
+                    icon_width: icon_size,
+                    icon_height: icon_size,
+                    color,
+                },
+                IconSize::Dimensions { width, height } => SvgStyle {
+                    icon_width: width,
+                    icon_height: height,
+                    color,
+                },
+                IconSize::IconDimensions {
+                    icon_width,
+                    icon_height,
+                } => SvgStyle {
+                    icon_width,
+                    icon_height,
+                    color,
+                },
+            };
+
+            Ok(result)
+        }
+    }
+
+    pub struct Svg<S> {
+        path: Option<Cow<'static, str>>,
+        style: S,
+    }
+
+    impl Svg<()> {
+        pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
+            Self {
+                path: Some(path.into()),
+                style: (),
+            }
+        }
+
+        pub fn optional(path: Option<impl Into<Cow<'static, str>>>) -> Self {
+            Self {
+                path: path.map(Into::into),
+                style: (),
+            }
+        }
+    }
+
+    impl SafeStylable for Svg<()> {
+        type Style = SvgStyle;
+
+        type Output = Svg<SvgStyle>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            Svg {
+                path: self.path,
+                style,
+            }
+        }
+    }
+
+    impl Component for Svg<SvgStyle> {
+        fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+            if let Some(path) = self.path {
+                gpui::elements::Svg::new(path)
+                    .with_color(self.style.color)
+                    .constrained()
+            } else {
+                Empty::new().constrained()
+            }
+            .constrained()
+            .with_width(self.style.icon_width)
+            .with_height(self.style.icon_height)
+            .into_any()
+        }
+    }
+}
+
+pub mod label {
+    use std::borrow::Cow;
+
+    use gpui::{
+        elements::{Component, LabelStyle, SafeStylable},
+        fonts::TextStyle,
+        Element,
+    };
+
+    pub struct Label<S> {
+        text: Cow<'static, str>,
+        style: S,
+    }
+
+    impl Label<()> {
+        pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
+            Self {
+                text: text.into(),
+                style: (),
+            }
+        }
+    }
+
+    impl SafeStylable for Label<()> {
+        type Style = TextStyle;
+
+        type Output = Label<LabelStyle>;
+
+        fn with_style(self, style: Self::Style) -> Self::Output {
+            Label {
+                text: self.text,
+                style: style.into(),
+            }
+        }
+    }
+
+    impl Component for Label<LabelStyle> {
+        fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
+            gpui::elements::Label::new(self.text, self.style).into_any()
+        }
+    }
+}

crates/theme/src/theme.rs 🔗

@@ -1,7 +1,9 @@
+pub mod components;
 mod theme_registry;
 mod theme_settings;
 pub mod ui;
 
+use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle};
 use gpui::{
     color::Color,
     elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@@ -12,8 +14,8 @@ use schemars::JsonSchema;
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use settings::SettingsStore;
-use std::{collections::HashMap, sync::Arc};
-use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle};
+use std::{collections::HashMap, ops::Deref, sync::Arc};
+use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
 
 pub use theme_registry::*;
 pub use theme_settings::*;
@@ -43,11 +45,9 @@ pub struct Theme {
     pub meta: ThemeMeta,
     pub workspace: Workspace,
     pub context_menu: ContextMenu,
-    pub contacts_popover: ContactsPopover,
-    pub contact_list: ContactList,
     pub toolbar_dropdown_menu: DropdownMenu,
     pub copilot: Copilot,
-    pub contact_finder: ContactFinder,
+    pub collab_panel: CollabPanel,
     pub project_panel: ProjectPanel,
     pub command_palette: CommandPalette,
     pub picker: Picker,
@@ -66,6 +66,7 @@ pub struct Theme {
     pub feedback: FeedbackStyle,
     pub welcome: WelcomeStyle,
     pub titlebar: Titlebar,
+    pub component_test: ComponentTest,
 }
 
 #[derive(Deserialize, Default, Clone, JsonSchema)]
@@ -87,8 +88,6 @@ pub struct Workspace {
     pub dock: Dock,
     pub status_bar: StatusBar,
     pub toolbar: Toolbar,
-    pub breadcrumb_height: f32,
-    pub breadcrumbs: Interactive<ContainedText>,
     pub disconnected_overlay: ContainedText,
     pub modal: ContainerStyle,
     pub zoomed_panel_foreground: ContainerStyle,
@@ -117,8 +116,8 @@ pub struct Titlebar {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub height: f32,
+    pub menu: TitlebarMenu,
     pub project_menu_button: Toggleable<Interactive<ContainedText>>,
-    pub project_name_divider: ContainedText,
     pub git_menu_button: Toggleable<Interactive<ContainedText>>,
     pub item_spacing: f32,
     pub face_pile_spacing: f32,
@@ -143,6 +142,12 @@ pub struct Titlebar {
     pub user_menu: UserMenu,
 }
 
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct TitlebarMenu {
+    pub width: f32,
+    pub height: f32,
+}
+
 #[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct UserMenu {
     pub user_menu_button_online: UserMenuButton,
@@ -177,7 +182,7 @@ pub struct CopilotAuth {
     pub prompting: CopilotAuthPrompting,
     pub not_authorized: CopilotAuthNotAuthorized,
     pub authorized: CopilotAuthAuthorized,
-    pub cta_button: ButtonStyle,
+    pub cta_button: CopilotCTAButton,
     pub header: IconStyle,
 }
 
@@ -191,7 +196,7 @@ pub struct CopilotAuthPrompting {
 #[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct DeviceCode {
     pub text: TextStyle,
-    pub cta: ButtonStyle,
+    pub cta: CopilotCTAButton,
     pub left: f32,
     pub left_container: ContainerStyle,
     pub right: f32,
@@ -211,33 +216,77 @@ pub struct CopilotAuthAuthorized {
 }
 
 #[derive(Deserialize, Default, JsonSchema)]
-pub struct ContactsPopover {
+pub struct CollabPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub height: f32,
-    pub width: f32,
-}
-
-#[derive(Deserialize, Default, JsonSchema)]
-pub struct ContactList {
+    pub disclosure: DisclosureStyle<()>,
+    pub list_empty_state: Toggleable<Interactive<ContainedText>>,
+    pub list_empty_icon: Icon,
+    pub list_empty_label_container: ContainerStyle,
+    pub log_in_button: Interactive<ContainedText>,
+    pub channel_editor: ContainerStyle,
+    pub channel_hash: Icon,
+    pub tabbed_modal: TabbedModal,
+    pub contact_finder: ContactFinder,
+    pub channel_modal: ChannelModal,
     pub user_query_editor: FieldEditor,
     pub user_query_editor_height: f32,
-    pub add_contact_button: IconButton,
-    pub header_row: Toggleable<Interactive<ContainedText>>,
+    pub leave_call_button: Toggleable<Interactive<IconButton>>,
+    pub add_contact_button: Toggleable<Interactive<IconButton>>,
+    pub add_channel_button: Toggleable<Interactive<IconButton>>,
+    pub header_row: ContainedText,
+    pub subheader_row: Toggleable<Interactive<ContainedText>>,
     pub leave_call: Interactive<ContainedText>,
     pub contact_row: Toggleable<Interactive<ContainerStyle>>,
+    pub channel_row: Toggleable<Interactive<ContainerStyle>>,
+    pub channel_name: ContainedText,
     pub row_height: f32,
     pub project_row: Toggleable<Interactive<ProjectRow>>,
     pub tree_branch: Toggleable<Interactive<TreeBranch>>,
     pub contact_avatar: ImageStyle,
+    pub channel_avatar: ImageStyle,
+    pub extra_participant_label: ContainedText,
     pub contact_status_free: ContainerStyle,
     pub contact_status_busy: ContainerStyle,
     pub contact_username: ContainedText,
     pub contact_button: Interactive<IconButton>,
     pub contact_button_spacing: f32,
+    pub channel_indent: f32,
     pub disabled_button: IconButton,
     pub section_icon_size: f32,
     pub calling_indicator: ContainedText,
+    pub face_overlap: f32,
+}
+
+#[derive(Deserialize, Default, JsonSchema)]
+pub struct ComponentTest {
+    pub button: Interactive<ButtonStyle<TextStyle>>,
+    pub toggle: Toggleable<Interactive<ButtonStyle<TextStyle>>>,
+    pub disclosure: DisclosureStyle<TextStyle>,
+}
+
+#[derive(Deserialize, Default, JsonSchema)]
+pub struct TabbedModal {
+    pub tab_button: Toggleable<Interactive<ContainedText>>,
+    pub modal: ContainerStyle,
+    pub header: ContainerStyle,
+    pub body: ContainerStyle,
+    pub title: ContainedText,
+    pub picker: Picker,
+    pub max_height: f32,
+    pub max_width: f32,
+    pub row_height: f32,
+}
+
+#[derive(Deserialize, Default, JsonSchema)]
+pub struct ChannelModal {
+    pub contact_avatar: ImageStyle,
+    pub contact_username: ContainerStyle,
+    pub remove_member_button: ContainedText,
+    pub cancel_invite_button: ContainedText,
+    pub member_icon: IconButton,
+    pub invitee_icon: IconButton,
+    pub member_tag: ContainedText,
 }
 
 #[derive(Deserialize, Default, JsonSchema)]
@@ -256,8 +305,6 @@ pub struct TreeBranch {
 
 #[derive(Deserialize, Default, JsonSchema)]
 pub struct ContactFinder {
-    pub picker: Picker,
-    pub row_height: f32,
     pub contact_avatar: ImageStyle,
     pub contact_username: ContainerStyle,
     pub contact_button: IconButton,
@@ -295,6 +342,7 @@ pub struct TabBar {
     pub inactive_pane: TabStyles,
     pub dragged_tab: Tab,
     pub height: f32,
+    pub nav_button: Interactive<IconButton>,
 }
 
 impl TabBar {
@@ -359,7 +407,9 @@ pub struct Toolbar {
     pub container: ContainerStyle,
     pub height: f32,
     pub item_spacing: f32,
-    pub nav_button: Interactive<IconButton>,
+    pub toggleable_tool: Toggleable<Interactive<IconButton>>,
+    pub breadcrumb_height: f32,
+    pub breadcrumbs: Interactive<ContainedText>,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]
@@ -379,12 +429,20 @@ pub struct Search {
     pub include_exclude_editor: FindEditor,
     pub invalid_include_exclude_editor: ContainerStyle,
     pub include_exclude_inputs: ContainedText,
-    pub option_button: Toggleable<Interactive<ContainedText>>,
-    pub action_button: Interactive<ContainedText>,
+    pub option_button: Toggleable<Interactive<IconButton>>,
+    pub option_button_component: ToggleIconButtonStyle,
+    pub action_button: Toggleable<Interactive<ContainedText>>,
     pub match_background: Color,
     pub match_index: ContainedText,
-    pub results_status: TextStyle,
-    pub dismiss_button: Interactive<IconButton>,
+    pub major_results_status: TextStyle,
+    pub minor_results_status: TextStyle,
+    pub editor_icon: IconStyle,
+    pub mode_button: Toggleable<Interactive<ContainedText>>,
+    pub nav_button: Toggleable<Interactive<ContainedLabel>>,
+    pub search_bar_row_height: f32,
+    pub search_row_spacing: f32,
+    pub option_button_height: f32,
+    pub modes_container: ContainerStyle,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]
@@ -697,6 +755,7 @@ pub struct Editor {
     pub line_number: Color,
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
+    pub absent_selection: SelectionStyle,
     pub syntax: Arc<SyntaxTheme>,
     pub hint: HighlightStyle,
     pub suggestion: HighlightStyle,
@@ -843,12 +902,48 @@ pub struct Interactive<T> {
     pub disabled: Option<T>,
 }
 
+impl<T> Deref for Interactive<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &self.default
+    }
+}
+
+impl Interactive<()> {
+    pub fn new_blank() -> Self {
+        Self {
+            default: (),
+            hovered: None,
+            clicked: None,
+            disabled: None,
+        }
+    }
+}
+
 #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
 pub struct Toggleable<T> {
     active: T,
     inactive: T,
 }
 
+impl<T> Deref for Toggleable<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inactive
+    }
+}
+
+impl Toggleable<()> {
+    pub fn new_blank() -> Self {
+        Self {
+            active: (),
+            inactive: (),
+        }
+    }
+}
+
 impl<T> Toggleable<T> {
     pub fn new(active: T, inactive: T) -> Self {
         Self { active, inactive }
@@ -863,6 +958,7 @@ impl<T> Toggleable<T> {
     pub fn active_state(&self) -> &T {
         self.in_state(true)
     }
+
     pub fn inactive_state(&self) -> &T {
         self.in_state(false)
     }
@@ -883,6 +979,16 @@ impl<T> Interactive<T> {
     }
 }
 
+impl<T> Toggleable<Interactive<T>> {
+    pub fn style_for(&self, active: bool, state: &mut MouseState) -> &T {
+        self.in_state(active).style_for(state)
+    }
+
+    pub fn default_style(&self) -> &T {
+        &self.inactive.default
+    }
+}
+
 impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
@@ -1046,6 +1152,23 @@ pub struct AssistantStyle {
     pub api_key_editor: FieldEditor,
     pub api_key_prompt: ContainedText,
     pub saved_conversation: SavedConversation,
+    pub inline: InlineAssistantStyle,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct InlineAssistantStyle {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
+    pub editor: FieldEditor,
+    pub disabled_editor: FieldEditor,
+    pub pending_edit_background: Color,
+    pub include_conversation: ToggleIconButtonStyle,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct Contained<T> {
+    container: ContainerStyle,
+    contained: T,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]

crates/theme/src/ui.rs 🔗

@@ -10,7 +10,7 @@ use gpui::{
     platform,
     platform::MouseButton,
     scene::MouseClick,
-    Action, Element, EventContext, MouseState, View, ViewContext,
+    Action, Element, EventContext, MouseState, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -37,7 +37,7 @@ pub fn checkbox<Tag, V, F>(
 ) -> MouseEventHandler<V>
 where
     Tag: 'static,
-    V: View,
+    V: 'static,
     F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
 {
     let label = Label::new(label, style.label.text.clone())
@@ -57,7 +57,7 @@ pub fn checkbox_with_label<Tag, D, V, F>(
 where
     Tag: 'static,
     D: Element<V>,
-    V: View,
+    V: 'static,
     F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
 {
     MouseEventHandler::new::<Tag, _>(id, cx, |state, _| {
@@ -93,7 +93,7 @@ where
     .with_cursor_style(platform::CursorStyle::PointingHand)
 }
 
-pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
+pub fn svg<V: 'static>(style: &SvgStyle) -> ConstrainedBox<V> {
     Svg::new(style.asset.clone())
         .with_color(style.color)
         .constrained()
@@ -107,11 +107,21 @@ pub struct IconStyle {
     pub container: ContainerStyle,
 }
 
-pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
+impl IconStyle {
+    pub fn width(&self) -> f32 {
+        self.icon.dimensions.width
+            + self.container.padding.left
+            + self.container.padding.right
+            + self.container.margin.left
+            + self.container.margin.right
+    }
+}
+
+pub fn icon<V: 'static>(style: &IconStyle) -> Container<V> {
     svg(&style.icon).contained().with_style(style.container)
 }
 
-pub fn keystroke_label<V: View>(
+pub fn keystroke_label<V: 'static>(
     label_text: &'static str,
     label_style: &ContainedText,
     keystroke_style: &ContainedText,
@@ -135,19 +145,19 @@ pub fn keystroke_label<V: View>(
         .with_style(label_style.container)
 }
 
-pub type ButtonStyle = Interactive<ContainedText>;
+pub type CopilotCTAButton = Interactive<ContainedText>;
 
 pub fn cta_button<Tag, L, V, F>(
     label: L,
     max_width: f32,
-    style: &ButtonStyle,
+    style: &CopilotCTAButton,
     cx: &mut ViewContext<V>,
     f: F,
 ) -> MouseEventHandler<V>
 where
     Tag: 'static,
     L: Into<Cow<'static, str>>,
-    V: View,
+    V: 'static,
     F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
 {
     MouseEventHandler::new::<Tag, _>(0, cx, |state, _| {
@@ -186,9 +196,9 @@ pub fn modal<Tag, V, I, D, F>(
 ) -> impl Element<V>
 where
     Tag: 'static,
-    V: View,
     I: Into<Cow<'static, str>>,
     D: Element<V>,
+    V: 'static,
     F: FnOnce(&mut gpui::ViewContext<V>) -> D,
 {
     const TITLEBAR_HEIGHT: f32 = 28.;

crates/theme_selector/Cargo.toml 🔗

@@ -16,7 +16,7 @@ gpui = { path = "../gpui" }
 picker = { path = "../picker" }
 theme = { path = "../theme" }
 settings = { path = "../settings" }
-staff_mode = { path = "../staff_mode" }
+feature_flags = { path = "../feature_flags" }
 workspace = { path = "../workspace" }
 util = { path = "../util" }
 log.workspace = true

crates/theme_selector/src/theme_selector.rs 🔗

@@ -1,9 +1,9 @@
+use feature_flags::FeatureFlagAppExt;
 use fs::Fs;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext};
 use picker::{Picker, PickerDelegate, PickerEvent};
 use settings::{update_settings_file, SettingsStore};
-use staff_mode::StaffMode;
 use std::sync::Arc;
 use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
 use util::ResultExt;
@@ -54,7 +54,7 @@ impl ThemeSelectorDelegate {
     fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<ThemeSelector>) -> Self {
         let original_theme = theme::current(cx).clone();
 
-        let staff_mode = **cx.default_global::<StaffMode>();
+        let staff_mode = cx.is_staff();
         let registry = cx.global::<Arc<ThemeRegistry>>();
         let mut theme_names = registry.list(staff_mode).collect::<Vec<_>>();
         theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));

crates/vcs_menu/src/lib.rs 🔗

@@ -107,20 +107,15 @@ impl PickerDelegate for BranchListDelegate {
                     let delegate = view.delegate();
                     let project = delegate.workspace.read(cx).project().read(&cx);
 
-                    let Some(worktree) = project
-                        .visible_worktrees(cx)
-                        .next()
-                    else {
+                    let Some(worktree) = project.visible_worktrees(cx).next() else {
                         bail!("Cannot update branch list as there are no visible worktrees")
                     };
-                    let mut cwd = worktree .read(cx)
-                        .abs_path()
-                        .to_path_buf();
+                    let mut cwd = worktree.read(cx).abs_path().to_path_buf();
                     cwd.push(".git");
-                    let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")};
-                    let mut branches = repo
-                        .lock()
-                        .branches()?;
+                    let Some(repo) = project.fs().open_repo(&cwd) else {
+                        bail!("Project does not have associated git repository.")
+                    };
+                    let mut branches = repo.lock().branches()?;
                     const RECENT_BRANCHES_COUNT: usize = 10;
                     if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
                         // Truncate list of recent branches
@@ -142,8 +137,13 @@ impl PickerDelegate for BranchListDelegate {
                         })
                         .collect::<Vec<_>>())
                 })
-                .log_err() else { return; };
-            let Some(candidates) = candidates.log_err() else {return;};
+                .log_err()
+            else {
+                return;
+            };
+            let Some(candidates) = candidates.log_err() else {
+                return;
+            };
             let matches = if query.is_empty() {
                 candidates
                     .into_iter()
@@ -184,7 +184,11 @@ impl PickerDelegate for BranchListDelegate {
 
     fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         let current_pick = self.selected_index();
-        let Some(current_pick) = self.matches.get(current_pick).map(|pick| pick.string.clone()) else {
+        let Some(current_pick) = self
+            .matches
+            .get(current_pick)
+            .map(|pick| pick.string.clone())
+        else {
             return;
         };
         cx.spawn(|picker, mut cx| async move {
@@ -256,7 +260,7 @@ impl PickerDelegate for BranchListDelegate {
             .contained()
             .with_style(style.container)
             .constrained()
-            .with_height(theme.contact_finder.row_height)
+            .with_height(theme.collab_panel.tabbed_modal.row_height)
             .into_any()
     }
     fn render_header(

crates/vim/src/editor_events.rs 🔗

@@ -1,4 +1,4 @@
-use crate::Vim;
+use crate::{Vim, VimEvent};
 use editor::{EditorBlurred, EditorFocused, EditorReleased};
 use gpui::AppContext;
 
@@ -22,6 +22,11 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
     editor.window().update(cx, |cx| {
         Vim::update(cx, |vim, cx| {
             vim.set_active_editor(editor.clone(), cx);
+            if vim.enabled {
+                cx.emit_global(VimEvent::ModeChanged {
+                    mode: vim.state().mode,
+                });
+            }
         });
     });
 }
@@ -48,6 +53,7 @@ fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) {
                     vim.active_editor = None;
                 }
             }
+            vim.editor_states.remove(&editor.id())
         });
     });
 }

crates/vim/src/mode_indicator.rs 🔗

@@ -34,7 +34,7 @@ impl ModeIndicator {
             if settings::get::<VimModeSetting>(cx).0 {
                 mode_indicator.mode = cx
                     .has_global::<Vim>()
-                    .then(|| cx.global::<Vim>().state.mode);
+                    .then(|| cx.global::<Vim>().state().mode);
             } else {
                 mode_indicator.mode.take();
             }
@@ -46,7 +46,7 @@ impl ModeIndicator {
             .has_global::<Vim>()
             .then(|| {
                 let vim = cx.global::<Vim>();
-                vim.enabled.then(|| vim.state.mode)
+                vim.enabled.then(|| vim.state().mode)
             })
             .flatten();
 
@@ -80,14 +80,12 @@ impl View for ModeIndicator {
 
         let theme = &theme::current(cx).workspace.status_bar;
 
-        // we always choose text to be 12 monospace characters
-        // so that as the mode indicator changes, the rest of the
-        // UI stays still.
         let text = match mode {
             Mode::Normal => "-- NORMAL --",
             Mode::Insert => "-- INSERT --",
-            Mode::Visual { line: false } => "-- VISUAL --",
-            Mode::Visual { line: true } => "VISUAL  LINE",
+            Mode::Visual => "-- VISUAL --",
+            Mode::VisualLine => "-- VISUAL LINE --",
+            Mode::VisualBlock => "-- VISUAL BLOCK --",
         };
         Label::new(text, theme.vim_mode_indicator.text.clone())
             .contained()

crates/vim/src/motion.rs 🔗

@@ -1,8 +1,8 @@
-use std::sync::Arc;
+use std::{cmp, sync::Arc};
 
 use editor::{
     char_kind,
-    display_map::{DisplaySnapshot, ToDisplayPoint},
+    display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
     movement, Bias, CharKind, DisplayPoint, ToOffset,
 };
 use gpui::{actions, impl_actions, AppContext, WindowContext};
@@ -21,16 +21,16 @@ use crate::{
 pub enum Motion {
     Left,
     Backspace,
-    Down,
-    Up,
+    Down { display_lines: bool },
+    Up { display_lines: bool },
     Right,
     NextWordStart { ignore_punctuation: bool },
     NextWordEnd { ignore_punctuation: bool },
     PreviousWordStart { ignore_punctuation: bool },
-    FirstNonWhitespace,
+    FirstNonWhitespace { display_lines: bool },
     CurrentLine,
-    StartOfLine,
-    EndOfLine,
+    StartOfLine { display_lines: bool },
+    EndOfLine { display_lines: bool },
     StartOfParagraph,
     EndOfParagraph,
     StartOfDocument,
@@ -62,6 +62,41 @@ struct PreviousWordStart {
     ignore_punctuation: bool,
 }
 
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct Up {
+    #[serde(default)]
+    display_lines: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct Down {
+    #[serde(default)]
+    display_lines: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct FirstNonWhitespace {
+    #[serde(default)]
+    display_lines: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct EndOfLine {
+    #[serde(default)]
+    display_lines: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct StartOfLine {
+    #[serde(default)]
+    display_lines: bool,
+}
+
 #[derive(Clone, Deserialize, PartialEq)]
 struct RepeatFind {
     #[serde(default)]
@@ -73,12 +108,7 @@ actions!(
     [
         Left,
         Backspace,
-        Down,
-        Up,
         Right,
-        FirstNonWhitespace,
-        StartOfLine,
-        EndOfLine,
         CurrentLine,
         StartOfParagraph,
         EndOfParagraph,
@@ -90,20 +120,63 @@ actions!(
 );
 impl_actions!(
     vim,
-    [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind]
+    [
+        NextWordStart,
+        NextWordEnd,
+        PreviousWordStart,
+        RepeatFind,
+        Up,
+        Down,
+        FirstNonWhitespace,
+        EndOfLine,
+        StartOfLine,
+    ]
 );
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
     cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
-    cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
-    cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
+    cx.add_action(|_: &mut Workspace, action: &Down, cx: _| {
+        motion(
+            Motion::Down {
+                display_lines: action.display_lines,
+            },
+            cx,
+        )
+    });
+    cx.add_action(|_: &mut Workspace, action: &Up, cx: _| {
+        motion(
+            Motion::Up {
+                display_lines: action.display_lines,
+            },
+            cx,
+        )
+    });
     cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
-    cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
-        motion(Motion::FirstNonWhitespace, cx)
+    cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
+        motion(
+            Motion::FirstNonWhitespace {
+                display_lines: action.display_lines,
+            },
+            cx,
+        )
+    });
+    cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
+        motion(
+            Motion::StartOfLine {
+                display_lines: action.display_lines,
+            },
+            cx,
+        )
+    });
+    cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
+        motion(
+            Motion::EndOfLine {
+                display_lines: action.display_lines,
+            },
+            cx,
+        )
     });
-    cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
-    cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
     cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
     cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
         motion(Motion::StartOfParagraph, cx)
@@ -147,9 +220,9 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
 
     let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
     let operator = Vim::read(cx).active_operator();
-    match Vim::read(cx).state.mode {
+    match Vim::read(cx).state().mode {
         Mode::Normal => normal_motion(motion, operator, times, cx),
-        Mode::Visual { .. } => visual_motion(motion, times, cx),
+        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx),
         Mode::Insert => {
             // Shouldn't execute a motion in insert mode. Ignoring
         }
@@ -158,7 +231,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
 }
 
 fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
-    let find = match Vim::read(cx).state.last_find.clone() {
+    let find = match Vim::read(cx).workspace_state.last_find.clone() {
         Some(Motion::FindForward { before, text }) => {
             if backwards {
                 Motion::FindBackward {
@@ -192,19 +265,25 @@ impl Motion {
     pub fn linewise(&self) -> bool {
         use Motion::*;
         match self {
-            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
-            | StartOfParagraph | EndOfParagraph => true,
-            EndOfLine
+            Down { .. }
+            | Up { .. }
+            | StartOfDocument
+            | EndOfDocument
+            | CurrentLine
+            | NextLineStart
+            | StartOfParagraph
+            | EndOfParagraph => true,
+            EndOfLine { .. }
             | NextWordEnd { .. }
             | Matching
             | FindForward { .. }
             | Left
             | Backspace
             | Right
-            | StartOfLine
+            | StartOfLine { .. }
             | NextWordStart { .. }
             | PreviousWordStart { .. }
-            | FirstNonWhitespace
+            | FirstNonWhitespace { .. }
             | FindBackward { .. } => false,
         }
     }
@@ -213,21 +292,21 @@ impl Motion {
         use Motion::*;
         match self {
             StartOfDocument | EndOfDocument | CurrentLine => true,
-            Down
-            | Up
-            | EndOfLine
+            Down { .. }
+            | Up { .. }
+            | EndOfLine { .. }
             | NextWordEnd { .. }
             | Matching
             | FindForward { .. }
             | Left
             | Backspace
             | Right
-            | StartOfLine
+            | StartOfLine { .. }
             | StartOfParagraph
             | EndOfParagraph
             | NextWordStart { .. }
             | PreviousWordStart { .. }
-            | FirstNonWhitespace
+            | FirstNonWhitespace { .. }
             | FindBackward { .. }
             | NextLineStart => false,
         }
@@ -236,12 +315,12 @@ impl Motion {
     pub fn inclusive(&self) -> bool {
         use Motion::*;
         match self {
-            Down
-            | Up
+            Down { .. }
+            | Up { .. }
             | StartOfDocument
             | EndOfDocument
             | CurrentLine
-            | EndOfLine
+            | EndOfLine { .. }
             | NextWordEnd { .. }
             | Matching
             | FindForward { .. }
@@ -249,12 +328,12 @@ impl Motion {
             Left
             | Backspace
             | Right
-            | StartOfLine
+            | StartOfLine { .. }
             | StartOfParagraph
             | EndOfParagraph
             | NextWordStart { .. }
             | PreviousWordStart { .. }
-            | FirstNonWhitespace
+            | FirstNonWhitespace { .. }
             | FindBackward { .. } => false,
         }
     }
@@ -272,8 +351,18 @@ impl Motion {
         let (new_point, goal) = match self {
             Left => (left(map, point, times), SelectionGoal::None),
             Backspace => (backspace(map, point, times), SelectionGoal::None),
-            Down => down(map, point, goal, times),
-            Up => up(map, point, goal, times),
+            Down {
+                display_lines: false,
+            } => down(map, point, goal, times),
+            Down {
+                display_lines: true,
+            } => down_display(map, point, goal, times),
+            Up {
+                display_lines: false,
+            } => up(map, point, goal, times),
+            Up {
+                display_lines: true,
+            } => up_display(map, point, goal, times),
             Right => (right(map, point, times), SelectionGoal::None),
             NextWordStart { ignore_punctuation } => (
                 next_word_start(map, point, *ignore_punctuation, times),
@@ -287,9 +376,17 @@ impl Motion {
                 previous_word_start(map, point, *ignore_punctuation, times),
                 SelectionGoal::None,
             ),
-            FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
-            StartOfLine => (start_of_line(map, point), SelectionGoal::None),
-            EndOfLine => (end_of_line(map, point), SelectionGoal::None),
+            FirstNonWhitespace { display_lines } => (
+                first_non_whitespace(map, *display_lines, point),
+                SelectionGoal::None,
+            ),
+            StartOfLine { display_lines } => (
+                start_of_line(map, *display_lines, point),
+                SelectionGoal::None,
+            ),
+            EndOfLine { display_lines } => {
+                (end_of_line(map, *display_lines, point), SelectionGoal::None)
+            }
             StartOfParagraph => (
                 movement::start_of_paragraph(map, point, times),
                 SelectionGoal::None,
@@ -298,7 +395,7 @@ impl Motion {
                 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
                 SelectionGoal::None,
             ),
-            CurrentLine => (end_of_line(map, point), SelectionGoal::None),
+            CurrentLine => (end_of_line(map, false, point), SelectionGoal::None),
             StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
             EndOfDocument => (
                 end_of_document(map, point, maybe_times),
@@ -399,6 +496,33 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
 }
 
 fn down(
+    map: &DisplaySnapshot,
+    point: DisplayPoint,
+    mut goal: SelectionGoal,
+    times: usize,
+) -> (DisplayPoint, SelectionGoal) {
+    let start = map.display_point_to_fold_point(point, Bias::Left);
+
+    let goal_column = match goal {
+        SelectionGoal::Column(column) => column,
+        SelectionGoal::ColumnRange { end, .. } => end,
+        _ => {
+            goal = SelectionGoal::Column(start.column());
+            start.column()
+        }
+    };
+
+    let new_row = cmp::min(
+        start.row() + times as u32,
+        map.buffer_snapshot.max_point().row,
+    );
+    let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
+    let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
+
+    (map.clip_point(point, Bias::Left), goal)
+}
+
+fn down_display(
     map: &DisplaySnapshot,
     mut point: DisplayPoint,
     mut goal: SelectionGoal,
@@ -407,10 +531,35 @@ fn down(
     for _ in 0..times {
         (point, goal) = movement::down(map, point, goal, true);
     }
+
     (point, goal)
 }
 
-fn up(
+pub(crate) fn up(
+    map: &DisplaySnapshot,
+    point: DisplayPoint,
+    mut goal: SelectionGoal,
+    times: usize,
+) -> (DisplayPoint, SelectionGoal) {
+    let start = map.display_point_to_fold_point(point, Bias::Left);
+
+    let goal_column = match goal {
+        SelectionGoal::Column(column) => column,
+        SelectionGoal::ColumnRange { end, .. } => end,
+        _ => {
+            goal = SelectionGoal::Column(start.column());
+            start.column()
+        }
+    };
+
+    let new_row = start.row().saturating_sub(times as u32);
+    let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
+    let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
+
+    (map.clip_point(point, Bias::Left), goal)
+}
+
+fn up_display(
     map: &DisplaySnapshot,
     mut point: DisplayPoint,
     mut goal: SelectionGoal,
@@ -419,6 +568,7 @@ fn up(
     for _ in 0..times {
         (point, goal) = movement::up(map, point, goal, true);
     }
+
     (point, goal)
 }
 
@@ -509,8 +659,12 @@ fn previous_word_start(
     point
 }
 
-fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
-    let mut last_point = DisplayPoint::new(from.row(), 0);
+fn first_non_whitespace(
+    map: &DisplaySnapshot,
+    display_lines: bool,
+    from: DisplayPoint,
+) -> DisplayPoint {
+    let mut last_point = start_of_line(map, display_lines, from);
     let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
     for (ch, point) in map.chars_at(last_point) {
         if ch == '\n' {
@@ -527,12 +681,31 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi
     map.clip_point(last_point, Bias::Left)
 }
 
-fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
-    map.prev_line_boundary(point.to_point(map)).1
+pub(crate) fn start_of_line(
+    map: &DisplaySnapshot,
+    display_lines: bool,
+    point: DisplayPoint,
+) -> DisplayPoint {
+    if display_lines {
+        map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
+    } else {
+        map.prev_line_boundary(point.to_point(map)).1
+    }
 }
 
-fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
-    map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
+pub(crate) fn end_of_line(
+    map: &DisplaySnapshot,
+    display_lines: bool,
+    point: DisplayPoint,
+) -> DisplayPoint {
+    if display_lines {
+        map.clip_point(
+            DisplayPoint::new(point.row(), map.line_len(point.row())),
+            Bias::Left,
+        )
+    } else {
+        map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
+    }
 }
 
 fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
@@ -654,8 +827,8 @@ fn find_backward(
 }
 
 fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
-    let new_row = (point.row() + times as u32).min(map.max_buffer_row());
-    map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
+    let correct_line = down(map, point, SelectionGoal::None, times).0;
+    first_non_whitespace(map, false, correct_line)
 }
 
 #[cfg(test)]
@@ -803,4 +976,12 @@ mod test {
         cx.simulate_shared_keystrokes([","]).await;
         cx.assert_shared_state("one two thˇree four").await;
     }
+
+    #[gpui::test]
+    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state("ˇone\n  two\nthree").await;
+        cx.simulate_shared_keystrokes(["enter"]).await;
+        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
+    }
 }

crates/vim/src/normal.rs 🔗

@@ -1,26 +1,25 @@
 mod case;
 mod change;
 mod delete;
+mod paste;
 mod scroll;
 mod search;
 pub mod substitute;
 mod yank;
 
-use std::{borrow::Cow, sync::Arc};
+use std::sync::Arc;
 
 use crate::{
-    motion::Motion,
+    motion::{self, Motion},
     object::Object,
     state::{Mode, Operator},
     Vim,
 };
-use collections::{HashMap, HashSet};
-use editor::{
-    display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection,
-    DisplayPoint,
-};
+use collections::HashSet;
+use editor::scroll::autoscroll::Autoscroll;
+use editor::{Bias, DisplayPoint};
 use gpui::{actions, AppContext, ViewContext, WindowContext};
-use language::{AutoindentMode, Point, SelectionGoal};
+use language::SelectionGoal;
 use log::error;
 use workspace::Workspace;
 
@@ -44,7 +43,6 @@ actions!(
         DeleteRight,
         ChangeToEndOfLine,
         DeleteToEndOfLine,
-        Paste,
         Yank,
         Substitute,
         ChangeCase,
@@ -80,18 +78,31 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
         Vim::update(cx, |vim, cx| {
             let times = vim.pop_number_operator(cx);
-            change_motion(vim, Motion::EndOfLine, times, cx);
+            change_motion(
+                vim,
+                Motion::EndOfLine {
+                    display_lines: false,
+                },
+                times,
+                cx,
+            );
         })
     });
     cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
         Vim::update(cx, |vim, cx| {
             let times = vim.pop_number_operator(cx);
-            delete_motion(vim, Motion::EndOfLine, times, cx);
+            delete_motion(
+                vim,
+                Motion::EndOfLine {
+                    display_lines: false,
+                },
+                times,
+                cx,
+            );
         })
     });
-    cx.add_action(paste);
-
     scroll::init(cx);
+    paste::init(cx);
 }
 
 pub fn normal_motion(
@@ -116,8 +127,8 @@ pub fn normal_motion(
 
 pub fn normal_object(object: Object, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
-        match vim.state.operator_stack.pop() {
-            Some(Operator::Object { around }) => match vim.state.operator_stack.pop() {
+        match vim.maybe_pop_operator() {
+            Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
                 Some(Operator::Change) => change_object(vim, object, around, cx),
                 Some(Operator::Delete) => delete_object(vim, object, around, cx),
                 Some(Operator::Yank) => yank_object(vim, object, around, cx),
@@ -168,7 +179,10 @@ fn insert_first_non_whitespace(
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::FirstNonWhitespace.move_point(map, cursor, goal, None)
+                    Motion::FirstNonWhitespace {
+                        display_lines: false,
+                    }
+                    .move_point(map, cursor, goal, None)
                 });
             });
         });
@@ -181,7 +195,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::EndOfLine.move_point(map, cursor, goal, None)
+                    Motion::CurrentLine.move_point(map, cursor, goal, None)
                 });
             });
         });
@@ -200,19 +214,19 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
                     .collect();
                 let edits = selection_start_rows.into_iter().map(|row| {
                     let (indent, _) = map.line_indent(row);
-                    let start_of_line = map
-                        .clip_point(DisplayPoint::new(row, 0), Bias::Left)
-                        .to_point(&map);
+                    let start_of_line =
+                        motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
+                            .to_point(&map);
                     let mut new_text = " ".repeat(indent as usize);
                     new_text.push('\n');
                     (start_of_line..start_of_line, new_text)
                 });
                 editor.edit_with_autoindent(edits, cx);
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                    s.move_cursors_with(|map, mut cursor, _| {
-                        *cursor.row_mut() -= 1;
-                        *cursor.column_mut() = map.line_len(cursor.row());
-                        (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+                    s.move_cursors_with(|map, cursor, _| {
+                        let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
+                        let insert_point = motion::end_of_line(map, false, previous_line);
+                        (insert_point, SelectionGoal::None)
                     });
                 });
             });
@@ -226,22 +240,23 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
         vim.update_active_editor(cx, |editor, cx| {
             editor.transact(cx, |editor, cx| {
                 let (map, old_selections) = editor.selections.all_display(cx);
+
                 let selection_end_rows: HashSet<u32> = old_selections
                     .into_iter()
                     .map(|selection| selection.end.row())
                     .collect();
                 let edits = selection_end_rows.into_iter().map(|row| {
                     let (indent, _) = map.line_indent(row);
-                    let end_of_line = map
-                        .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
-                        .to_point(&map);
+                    let end_of_line =
+                        motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map);
+
                     let mut new_text = "\n".to_string();
                     new_text.push_str(&" ".repeat(indent as usize));
                     (end_of_line..end_of_line, new_text)
                 });
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.maybe_move_cursors_with(|map, cursor, goal| {
-                        Motion::EndOfLine.move_point(map, cursor, goal, None)
+                        Motion::CurrentLine.move_point(map, cursor, goal, None)
                     });
                 });
                 editor.edit_with_autoindent(edits, cx);
@@ -250,144 +265,6 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
     });
 }
 
-fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
-    Vim::update(cx, |vim, cx| {
-        vim.update_active_editor(cx, |editor, cx| {
-            editor.transact(cx, |editor, cx| {
-                editor.set_clip_at_line_ends(false, cx);
-                if let Some(item) = cx.read_from_clipboard() {
-                    let mut clipboard_text = Cow::Borrowed(item.text());
-                    if let Some(mut clipboard_selections) =
-                        item.metadata::<Vec<ClipboardSelection>>()
-                    {
-                        let (display_map, selections) = editor.selections.all_display(cx);
-                        let all_selections_were_entire_line =
-                            clipboard_selections.iter().all(|s| s.is_entire_line);
-                        if clipboard_selections.len() != selections.len() {
-                            let mut newline_separated_text = String::new();
-                            let mut clipboard_selections =
-                                clipboard_selections.drain(..).peekable();
-                            let mut ix = 0;
-                            while let Some(clipboard_selection) = clipboard_selections.next() {
-                                newline_separated_text
-                                    .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
-                                ix += clipboard_selection.len;
-                                if clipboard_selections.peek().is_some() {
-                                    newline_separated_text.push('\n');
-                                }
-                            }
-                            clipboard_text = Cow::Owned(newline_separated_text);
-                        }
-
-                        // If the pasted text is a single line, the cursor should be placed after
-                        // the newly pasted text. This is easiest done with an anchor after the
-                        // insertion, and then with a fixup to move the selection back one position.
-                        // However if the pasted text is linewise, the cursor should be placed at the start
-                        // of the new text on the following line. This is easiest done with a manually adjusted
-                        // point.
-                        // This enum lets us represent both cases
-                        enum NewPosition {
-                            Inside(Point),
-                            After(Anchor),
-                        }
-                        let mut new_selections: HashMap<usize, NewPosition> = Default::default();
-                        editor.buffer().update(cx, |buffer, cx| {
-                            let snapshot = buffer.snapshot(cx);
-                            let mut start_offset = 0;
-                            let mut edits = Vec::new();
-                            for (ix, selection) in selections.iter().enumerate() {
-                                let to_insert;
-                                let linewise;
-                                if let Some(clipboard_selection) = clipboard_selections.get(ix) {
-                                    let end_offset = start_offset + clipboard_selection.len;
-                                    to_insert = &clipboard_text[start_offset..end_offset];
-                                    linewise = clipboard_selection.is_entire_line;
-                                    start_offset = end_offset;
-                                } else {
-                                    to_insert = clipboard_text.as_str();
-                                    linewise = all_selections_were_entire_line;
-                                }
-
-                                // If the clipboard text was copied linewise, and the current selection
-                                // is empty, then paste the text after this line and move the selection
-                                // to the start of the pasted text
-                                let insert_at = if linewise {
-                                    let (point, _) = display_map
-                                        .next_line_boundary(selection.start.to_point(&display_map));
-
-                                    if !to_insert.starts_with('\n') {
-                                        // Add newline before pasted text so that it shows up
-                                        edits.push((point..point, "\n"));
-                                    }
-                                    // Drop selection at the start of the next line
-                                    new_selections.insert(
-                                        selection.id,
-                                        NewPosition::Inside(Point::new(point.row + 1, 0)),
-                                    );
-                                    point
-                                } else {
-                                    let mut point = selection.end;
-                                    // Paste the text after the current selection
-                                    *point.column_mut() = point.column() + 1;
-                                    let point = display_map
-                                        .clip_point(point, Bias::Right)
-                                        .to_point(&display_map);
-
-                                    new_selections.insert(
-                                        selection.id,
-                                        if to_insert.contains('\n') {
-                                            NewPosition::Inside(point)
-                                        } else {
-                                            NewPosition::After(snapshot.anchor_after(point))
-                                        },
-                                    );
-                                    point
-                                };
-
-                                if linewise && to_insert.ends_with('\n') {
-                                    edits.push((
-                                        insert_at..insert_at,
-                                        &to_insert[0..to_insert.len().saturating_sub(1)],
-                                    ))
-                                } else {
-                                    edits.push((insert_at..insert_at, to_insert));
-                                }
-                            }
-                            drop(snapshot);
-                            buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
-                        });
-
-                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                            s.move_with(|map, selection| {
-                                if let Some(new_position) = new_selections.get(&selection.id) {
-                                    match new_position {
-                                        NewPosition::Inside(new_point) => {
-                                            selection.collapse_to(
-                                                new_point.to_display_point(map),
-                                                SelectionGoal::None,
-                                            );
-                                        }
-                                        NewPosition::After(after_point) => {
-                                            let mut new_point = after_point.to_display_point(map);
-                                            *new_point.column_mut() =
-                                                new_point.column().saturating_sub(1);
-                                            new_point = map.clip_point(new_point, Bias::Left);
-                                            selection.collapse_to(new_point, SelectionGoal::None);
-                                        }
-                                    }
-                                }
-                            });
-                        });
-                    } else {
-                        editor.insert(&clipboard_text, cx);
-                    }
-                }
-                editor.set_clip_at_line_ends(true, cx);
-            });
-        });
-    });
-}
-
 pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
@@ -883,36 +760,6 @@ mod test {
             .await;
     }
 
-    #[gpui::test]
-    async fn test_p(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await;
-        cx.set_shared_state(indoc! {"
-                The quick brown
-                fox juˇmps over
-                the lazy dog"})
-            .await;
-
-        cx.simulate_shared_keystrokes(["d", "d"]).await;
-        cx.assert_state_matches().await;
-
-        cx.simulate_shared_keystroke("p").await;
-        cx.assert_state_matches().await;
-
-        cx.set_shared_state(indoc! {"
-                The quick brown
-                fox ˇjumps over
-                the lazy dog"})
-            .await;
-        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
-        cx.set_shared_state(indoc! {"
-                The quick brown
-                fox jumps oveˇr
-                the lazy dog"})
-            .await;
-        cx.simulate_shared_keystroke("p").await;
-        cx.assert_state_matches().await;
-    }
-
     #[gpui::test]
     async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/normal/case.rs 🔗

@@ -13,15 +13,15 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
             let mut cursor_positions = Vec::new();
             let snapshot = editor.buffer().read(cx).snapshot(cx);
             for selection in editor.selections.all::<Point>(cx) {
-                match vim.state.mode {
-                    Mode::Visual { line: true } => {
+                match vim.state().mode {
+                    Mode::VisualLine => {
                         let start = Point::new(selection.start.row, 0);
                         let end =
                             Point::new(selection.end.row, snapshot.line_len(selection.end.row));
                         ranges.push(start..end);
                         cursor_positions.push(start..start);
                     }
-                    Mode::Visual { line: false } => {
+                    Mode::Visual | Mode::VisualBlock => {
                         ranges.push(selection.start..selection.end);
                         cursor_positions.push(selection.start..selection.start);
                     }

crates/vim/src/normal/change.rs 🔗

@@ -10,7 +10,11 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
     // Some motions ignore failure when switching to normal mode
     let mut motion_succeeded = matches!(
         motion,
-        Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine
+        Motion::Left
+            | Motion::Right
+            | Motion::EndOfLine { .. }
+            | Motion::Backspace
+            | Motion::StartOfLine { .. }
     );
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {

crates/vim/src/normal/paste.rs 🔗

@@ -0,0 +1,468 @@
+use std::{borrow::Cow, cmp};
+
+use editor::{
+    display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection,
+    DisplayPoint,
+};
+use gpui::{impl_actions, AppContext, ViewContext};
+use language::{Bias, SelectionGoal};
+use serde::Deserialize;
+use workspace::Workspace;
+
+use crate::{state::Mode, utils::copy_selections_content, Vim};
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct Paste {
+    #[serde(default)]
+    before: bool,
+    #[serde(default)]
+    preserve_clipboard: bool,
+}
+
+impl_actions!(vim, [Paste]);
+
+pub(crate) fn init(cx: &mut AppContext) {
+    cx.add_action(paste);
+}
+
+fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.transact(cx, |editor, cx| {
+                editor.set_clip_at_line_ends(false, cx);
+
+                let Some(item) = cx.read_from_clipboard() else {
+                    return;
+                };
+                let clipboard_text = Cow::Borrowed(item.text());
+                if clipboard_text.is_empty() {
+                    return;
+                }
+
+                if !action.preserve_clipboard && vim.state().mode.is_visual() {
+                    copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
+                }
+
+                // if we are copying from multi-cursor (of visual block mode), we want
+                // to
+                let clipboard_selections =
+                    item.metadata::<Vec<ClipboardSelection>>()
+                        .filter(|clipboard_selections| {
+                            clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
+                        });
+
+                let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
+
+                // unlike zed, if you have a multi-cursor selection from vim block mode,
+                // pasting it will paste it on subsequent lines, even if you don't yet
+                // have a cursor there.
+                let mut selections_to_process = Vec::new();
+                let mut i = 0;
+                while i < current_selections.len() {
+                    selections_to_process
+                        .push((current_selections[i].start..current_selections[i].end, true));
+                    i += 1;
+                }
+                if let Some(clipboard_selections) = clipboard_selections.as_ref() {
+                    let left = current_selections
+                        .iter()
+                        .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
+                        .min()
+                        .unwrap();
+                    let mut row = current_selections.last().unwrap().end.row() + 1;
+                    while i < clipboard_selections.len() {
+                        let cursor =
+                            display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
+                        selections_to_process.push((cursor..cursor, false));
+                        i += 1;
+                        row += 1;
+                    }
+                }
+
+                let first_selection_indent_column =
+                    clipboard_selections.as_ref().and_then(|zed_selections| {
+                        zed_selections
+                            .first()
+                            .map(|selection| selection.first_line_indent)
+                    });
+                let before = action.before || vim.state().mode == Mode::VisualLine;
+
+                let mut edits = Vec::new();
+                let mut new_selections = Vec::new();
+                let mut original_indent_columns = Vec::new();
+                let mut start_offset = 0;
+
+                for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
+                    let (mut to_insert, original_indent_column) =
+                        if let Some(clipboard_selections) = &clipboard_selections {
+                            if let Some(clipboard_selection) = clipboard_selections.get(ix) {
+                                let end_offset = start_offset + clipboard_selection.len;
+                                let text = clipboard_text[start_offset..end_offset].to_string();
+                                start_offset = end_offset + 1;
+                                (text, Some(clipboard_selection.first_line_indent))
+                            } else {
+                                ("".to_string(), first_selection_indent_column)
+                            }
+                        } else {
+                            (clipboard_text.to_string(), first_selection_indent_column)
+                        };
+                    let line_mode = to_insert.ends_with("\n");
+                    let is_multiline = to_insert.contains("\n");
+
+                    if line_mode && !before {
+                        if selection.is_empty() {
+                            to_insert =
+                                "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
+                        } else {
+                            to_insert = "\n".to_owned() + &to_insert;
+                        }
+                    } else if !line_mode && vim.state().mode == Mode::VisualLine {
+                        to_insert = to_insert + "\n";
+                    }
+
+                    let display_range = if !selection.is_empty() {
+                        selection.start..selection.end
+                    } else if line_mode {
+                        let point = if before {
+                            movement::line_beginning(&display_map, selection.start, false)
+                        } else {
+                            movement::line_end(&display_map, selection.start, false)
+                        };
+                        point..point
+                    } else {
+                        let point = if before {
+                            selection.start
+                        } else {
+                            movement::saturating_right(&display_map, selection.start)
+                        };
+                        point..point
+                    };
+
+                    let point_range = display_range.start.to_point(&display_map)
+                        ..display_range.end.to_point(&display_map);
+                    let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
+                        display_map.buffer_snapshot.anchor_before(point_range.start)
+                    } else {
+                        display_map.buffer_snapshot.anchor_after(point_range.end)
+                    };
+
+                    if *preserve {
+                        new_selections.push((anchor, line_mode, is_multiline));
+                    }
+                    edits.push((point_range, to_insert));
+                    original_indent_columns.extend(original_indent_column);
+                }
+
+                editor.edit_with_block_indent(edits, original_indent_columns, cx);
+
+                // in line_mode vim will insert the new text on the next (or previous if before) line
+                // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
+                // otherwise vim will insert the next text at (or before) the current cursor position,
+                // the cursor will go to the last (or first, if is_multiline) inserted character.
+                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.replace_cursors_with(|map| {
+                        let mut cursors = Vec::new();
+                        for (anchor, line_mode, is_multiline) in &new_selections {
+                            let mut cursor = anchor.to_display_point(map);
+                            if *line_mode {
+                                if !before {
+                                    cursor =
+                                        movement::down(map, cursor, SelectionGoal::None, false).0;
+                                }
+                                cursor = movement::indented_line_beginning(map, cursor, true);
+                            } else if !is_multiline {
+                                cursor = movement::saturating_left(map, cursor)
+                            }
+                            cursors.push(cursor);
+                            if vim.state().mode == Mode::VisualBlock {
+                                break;
+                            }
+                        }
+
+                        cursors
+                    });
+                })
+            });
+        });
+        vim.switch_mode(Mode::Normal, true, cx);
+    });
+}
+
+#[cfg(test)]
+mod test {
+    use crate::{
+        state::Mode,
+        test::{NeovimBackedTestContext, VimTestContext},
+    };
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_paste(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        // single line
+        cx.set_shared_state(indoc! {"
+            The quick brown
+            fox ˇjumps over
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
+        cx.assert_shared_clipboard("jumps o").await;
+        cx.set_shared_state(indoc! {"
+            The quick brown
+            fox jumps oveˇr
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystroke("p").await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            fox jumps overjumps ˇo
+            the lazy dog"})
+            .await;
+
+        cx.set_shared_state(indoc! {"
+            The quick brown
+            fox jumps oveˇr
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystroke("shift-p").await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            fox jumps ovejumps ˇor
+            the lazy dog"})
+            .await;
+
+        // line mode
+        cx.set_shared_state(indoc! {"
+            The quick brown
+            fox juˇmps over
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["d", "d"]).await;
+        cx.assert_shared_clipboard("fox jumps over\n").await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            the laˇzy dog"})
+            .await;
+        cx.simulate_shared_keystroke("p").await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            the lazy dog
+            ˇfox jumps over"})
+            .await;
+        cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            ˇfox jumps over
+            the lazy dog
+            fox jumps over"})
+            .await;
+
+        // multiline, cursor to first character of pasted text.
+        cx.set_shared_state(indoc! {"
+            The quick brown
+            fox jumps ˇover
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
+        cx.assert_shared_clipboard("over\nthe lazy do").await;
+
+        cx.simulate_shared_keystroke("p").await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            fox jumps oˇover
+            the lazy dover
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            fox jumps ˇover
+            the lazy doover
+            the lazy dog"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        // copy in visual mode
+        cx.set_shared_state(indoc! {"
+                The quick brown
+                fox jˇumps over
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
+        cx.assert_shared_state(indoc! {"
+                The quick brown
+                fox ˇjumps over
+                the lazy dog"})
+            .await;
+        // paste in visual mode
+        cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+                The quick brown
+                fox jumps jumpˇs
+                the lazy dog"})
+            .await;
+        cx.assert_shared_clipboard("over").await;
+        // paste in visual line mode
+        cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            ˇover
+            fox jumps jumps
+            the lazy dog"})
+            .await;
+        cx.assert_shared_clipboard("over").await;
+        // paste in visual block mode
+        cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            oveˇrver
+            overox jumps jumps
+            overhe lazy dog"})
+            .await;
+
+        // copy in visual line mode
+        cx.set_shared_state(indoc! {"
+                The quick brown
+                fox juˇmps over
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
+        cx.assert_shared_state(indoc! {"
+                The quick brown
+                the laˇzy dog"})
+            .await;
+        // paste in visual mode
+        cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
+        cx.assert_shared_state(
+            &indoc! {"
+                The quick brown
+                the_
+                ˇfox jumps over
+                _dog"}
+            .replace("_", " "), // Hack for trailing whitespace
+        )
+        .await;
+        cx.assert_shared_clipboard("lazy").await;
+        cx.set_shared_state(indoc! {"
+            The quick brown
+            fox juˇmps over
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
+        cx.assert_shared_state(indoc! {"
+            The quick brown
+            the laˇzy dog"})
+            .await;
+        // paste in visual line mode
+        cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
+        cx.assert_shared_state(indoc! {"
+            ˇfox jumps over
+            the lazy dog"})
+            .await;
+        cx.assert_shared_clipboard("The quick brown\n").await;
+    }
+
+    #[gpui::test]
+    async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        // copy in visual block mode
+        cx.set_shared_state(indoc! {"
+            The ˇquick brown
+            fox jumps over
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
+            .await;
+        cx.assert_shared_clipboard("q\nj\nl").await;
+        cx.simulate_shared_keystrokes(["p"]).await;
+        cx.assert_shared_state(indoc! {"
+            The qˇquick brown
+            fox jjumps over
+            the llazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            The ˇq brown
+            fox jjjumps over
+            the lllazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
+            .await;
+
+        cx.set_shared_state(indoc! {"
+            The ˇquick brown
+            fox jumps over
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
+        cx.assert_shared_clipboard("q\nj").await;
+        cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            The qˇqick brown
+            fox jjmps over
+            the lzy dog"})
+            .await;
+
+        cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
+        cx.assert_shared_state(indoc! {"
+            ˇq
+            j
+            fox jjmps over
+            the lzy dog"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new_typescript(cx).await;
+
+        cx.set_state(
+            indoc! {"
+            class A {ˇ
+            }
+        "},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
+        cx.assert_state(
+            indoc! {"
+            class A {
+                a()ˇ{}
+            }
+            "},
+            Mode::Normal,
+        );
+        // cursor goes to the first non-blank character in the line;
+        cx.simulate_keystrokes(["y", "y", "p"]);
+        cx.assert_state(
+            indoc! {"
+            class A {
+                a(){}
+                ˇa(){}
+            }
+            "},
+            Mode::Normal,
+        );
+        // indentation is preserved when pasting
+        cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
+        cx.assert_state(
+            indoc! {"
+                ˇclass A {
+                    a(){}
+                class A {
+                    a(){}
+                }
+                "},
+            Mode::Normal,
+        );
+    }
+}

crates/vim/src/normal/scroll.rs 🔗

@@ -1,7 +1,9 @@
-use std::cmp::Ordering;
-
 use crate::Vim;
-use editor::{display_map::ToDisplayPoint, scroll::scroll_amount::ScrollAmount, Editor};
+use editor::{
+    display_map::ToDisplayPoint,
+    scroll::{scroll_amount::ScrollAmount, VERTICAL_SCROLL_MARGIN},
+    DisplayPoint, Editor,
+};
 use gpui::{actions, AppContext, ViewContext};
 use language::Bias;
 use workspace::Workspace;
@@ -53,13 +55,9 @@ fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmoun
 
 fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
     let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
+
     editor.scroll_screen(amount, cx);
     if should_move_cursor {
-        let selection_ordering = editor.newest_selection_on_screen(cx);
-        if selection_ordering.is_eq() {
-            return;
-        }
-
         let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
             visible_rows as u32
         } else {
@@ -69,21 +67,19 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
         let top_anchor = editor.scroll_manager.anchor().anchor;
 
         editor.change_selections(None, cx, |s| {
-            s.replace_cursors_with(|snapshot| {
-                let mut new_point = top_anchor.to_display_point(&snapshot);
-
-                match selection_ordering {
-                    Ordering::Less => {
-                        new_point = snapshot.clip_point(new_point, Bias::Right);
-                    }
-                    Ordering::Greater => {
-                        *new_point.row_mut() += visible_rows - 1;
-                        new_point = snapshot.clip_point(new_point, Bias::Left);
-                    }
-                    Ordering::Equal => unreachable!(),
-                }
+            s.move_heads_with(|map, head, goal| {
+                let top = top_anchor.to_display_point(map);
+                let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
+                let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
 
-                vec![new_point]
+                let new_head = if head.row() < min_row {
+                    map.clip_point(DisplayPoint::new(min_row, head.column()), Bias::Left)
+                } else if head.row() > max_row {
+                    map.clip_point(DisplayPoint::new(max_row, head.column()), Bias::Left)
+                } else {
+                    head
+                };
+                (new_head, goal)
             })
         });
     }

crates/vim/src/normal/search.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{actions, impl_actions, AppContext, ViewContext};
-use search::{buffer_search, BufferSearchBar, SearchOptions};
+use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
 use serde_derive::Deserialize;
 use workspace::{searchable::Direction, Pane, Workspace};
 
@@ -65,15 +65,13 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
                     cx.focus_self();
 
                     if query.is_empty() {
-                        search_bar.set_search_options(
-                            SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX,
-                            cx,
-                        );
+                        search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
+                        search_bar.activate_search_mode(SearchMode::Regex, cx);
                     }
-                    vim.state.search = SearchState {
+                    vim.workspace_state.search = SearchState {
                         direction,
                         count,
-                        initial_query: query,
+                        initial_query: query.clone(),
                     };
                 });
             }
@@ -83,7 +81,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
 
 // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
 fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
-    Vim::update(cx, |vim, _| vim.state.search = Default::default());
+    Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default());
     cx.propagate_action();
 }
 
@@ -93,8 +91,9 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                 search_bar.update(cx, |search_bar, cx| {
-                    let state = &mut vim.state.search;
+                    let state = &mut vim.workspace_state.search;
                     let mut count = state.count;
+                    let direction = state.direction;
 
                     // in the case that the query has changed, the search bar
                     // will have selected the next match already.
@@ -103,8 +102,8 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
                     {
                         count = count.saturating_sub(1)
                     }
-                    search_bar.select_match(state.direction, count, cx);
                     state.count = 1;
+                    search_bar.select_match(direction, count, cx);
                     search_bar.focus_editor(&Default::default(), cx);
                 });
             }

crates/vim/src/normal/substitute.rs 🔗

@@ -4,9 +4,9 @@ use language::Point;
 use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
 
 pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
-    let line_mode = vim.state.mode == Mode::Visual { line: true };
-    vim.switch_mode(Mode::Insert, true, cx);
+    let line_mode = vim.state().mode == Mode::VisualLine;
     vim.update_active_editor(cx, |editor, cx| {
+        editor.set_clip_at_line_ends(false, cx);
         editor.transact(cx, |editor, cx| {
             editor.change_selections(None, cx, |s| {
                 s.move_with(|map, selection| {
@@ -15,7 +15,10 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
                     }
                     if line_mode {
                         Motion::CurrentLine.expand_selection(map, selection, None, false);
-                        if let Some((point, _)) = Motion::FirstNonWhitespace.move_point(
+                        if let Some((point, _)) = (Motion::FirstNonWhitespace {
+                            display_lines: false,
+                        })
+                        .move_point(
                             map,
                             selection.start,
                             selection.goal,
@@ -32,6 +35,7 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
             editor.edit(edits, cx);
         });
     });
+    vim.switch_mode(Mode::Insert, true, cx);
 }
 
 #[cfg(test)]
@@ -52,7 +56,7 @@ mod test {
         cx.assert_editor_state("xˇbc\n");
 
         // supports a selection
-        cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false });
+        cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
         cx.assert_editor_state("a«bcˇ»\n");
         cx.simulate_keystrokes(["s", "x"]);
         cx.assert_editor_state("axˇ\n");

crates/vim/src/object.rs 🔗

@@ -62,9 +62,9 @@ pub fn init(cx: &mut AppContext) {
 }
 
 fn object(object: Object, cx: &mut WindowContext) {
-    match Vim::read(cx).state.mode {
+    match Vim::read(cx).state().mode {
         Mode::Normal => normal_object(object, cx),
-        Mode::Visual { .. } => visual_object(object, cx),
+        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx),
         Mode::Insert => {
             // Shouldn't execute a text object in insert mode. Ignoring
         }
@@ -72,6 +72,47 @@ fn object(object: Object, cx: &mut WindowContext) {
 }
 
 impl Object {
+    pub fn is_multiline(self) -> bool {
+        match self {
+            Object::Word { .. } | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes => {
+                false
+            }
+            Object::Sentence
+            | Object::Parentheses
+            | Object::AngleBrackets
+            | Object::CurlyBrackets
+            | Object::SquareBrackets => true,
+        }
+    }
+
+    pub fn always_expands_both_ways(self) -> bool {
+        match self {
+            Object::Word { .. } | Object::Sentence => false,
+            Object::Quotes
+            | Object::BackQuotes
+            | Object::DoubleQuotes
+            | Object::Parentheses
+            | Object::SquareBrackets
+            | Object::CurlyBrackets
+            | Object::AngleBrackets => true,
+        }
+    }
+
+    pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
+        match self {
+            Object::Word { .. } if current_mode == Mode::VisualLine => Mode::Visual,
+            Object::Word { .. } => current_mode,
+            Object::Sentence
+            | Object::Quotes
+            | Object::BackQuotes
+            | Object::DoubleQuotes
+            | Object::Parentheses
+            | Object::SquareBrackets
+            | Object::CurlyBrackets
+            | Object::AngleBrackets => Mode::Visual,
+        }
+    }
+
     pub fn range(
         self,
         map: &DisplaySnapshot,
@@ -87,13 +128,27 @@ impl Object {
                 }
             }
             Object::Sentence => sentence(map, relative_to, around),
-            Object::Quotes => surrounding_markers(map, relative_to, around, false, '\'', '\''),
-            Object::BackQuotes => surrounding_markers(map, relative_to, around, false, '`', '`'),
-            Object::DoubleQuotes => surrounding_markers(map, relative_to, around, false, '"', '"'),
-            Object::Parentheses => surrounding_markers(map, relative_to, around, true, '(', ')'),
-            Object::SquareBrackets => surrounding_markers(map, relative_to, around, true, '[', ']'),
-            Object::CurlyBrackets => surrounding_markers(map, relative_to, around, true, '{', '}'),
-            Object::AngleBrackets => surrounding_markers(map, relative_to, around, true, '<', '>'),
+            Object::Quotes => {
+                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
+            }
+            Object::BackQuotes => {
+                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
+            }
+            Object::DoubleQuotes => {
+                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
+            }
+            Object::Parentheses => {
+                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
+            }
+            Object::SquareBrackets => {
+                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
+            }
+            Object::CurlyBrackets => {
+                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
+            }
+            Object::AngleBrackets => {
+                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
+            }
         }
     }
 

crates/vim/src/state.rs 🔗

@@ -9,14 +9,16 @@ use crate::motion::Motion;
 pub enum Mode {
     Normal,
     Insert,
-    Visual { line: bool },
+    Visual,
+    VisualLine,
+    VisualBlock,
 }
 
 impl Mode {
     pub fn is_visual(&self) -> bool {
         match self {
             Mode::Normal | Mode::Insert => false,
-            Mode::Visual { .. } => true,
+            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => true,
         }
     }
 }
@@ -39,15 +41,20 @@ pub enum Operator {
     FindBackward { after: bool },
 }
 
-#[derive(Default)]
-pub struct VimState {
+#[derive(Default, Clone)]
+pub struct EditorState {
     pub mode: Mode,
+    pub last_mode: Mode,
     pub operator_stack: Vec<Operator>,
-    pub search: SearchState,
+}
 
+#[derive(Default, Clone)]
+pub struct WorkspaceState {
+    pub search: SearchState,
     pub last_find: Option<Motion>,
 }
 
+#[derive(Clone)]
 pub struct SearchState {
     pub direction: Direction,
     pub count: usize,
@@ -64,7 +71,7 @@ impl Default for SearchState {
     }
 }
 
-impl VimState {
+impl EditorState {
     pub fn cursor_shape(&self) -> CursorShape {
         match self.mode {
             Mode::Normal => {
@@ -74,7 +81,7 @@ impl VimState {
                     CursorShape::Underscore
                 }
             }
-            Mode::Visual { .. } => CursorShape::Block,
+            Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block,
             Mode::Insert => CursorShape::Bar,
         }
     }
@@ -87,9 +94,13 @@ impl VimState {
             )
     }
 
+    pub fn should_autoindent(&self) -> bool {
+        !(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock)
+    }
+
     pub fn clip_at_line_ends(&self) -> bool {
         match self.mode {
-            Mode::Insert | Mode::Visual { .. } => false,
+            Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => false,
             Mode::Normal => true,
         }
     }
@@ -101,7 +112,7 @@ impl VimState {
             "vim_mode",
             match self.mode {
                 Mode::Normal => "normal",
-                Mode::Visual { .. } => "visual",
+                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual",
                 Mode::Insert => "insert",
             },
         );

crates/vim/src/test.rs 🔗

@@ -1,7 +1,6 @@
 mod neovim_backed_binding_test_context;
 mod neovim_backed_test_context;
 mod neovim_connection;
-mod vim_binding_test_context;
 mod vim_test_context;
 
 use std::sync::Arc;
@@ -10,7 +9,6 @@ use command_palette::CommandPalette;
 use editor::DisplayPoint;
 pub use neovim_backed_binding_test_context::*;
 pub use neovim_backed_test_context::*;
-pub use vim_binding_test_context::*;
 pub use vim_test_context::*;
 
 use indoc::indoc;
@@ -241,7 +239,7 @@ async fn test_status_indicator(
     deterministic.run_until_parked();
     assert_eq!(
         cx.workspace(|_, cx| mode_indicator.read(cx).mode),
-        Some(Mode::Visual { line: false })
+        Some(Mode::Visual)
     );
 
     // hides if vim mode is disabled
@@ -261,3 +259,244 @@ async fn test_status_indicator(
         assert!(mode_indicator.read(cx).mode.is_some());
     });
 }
+
+#[gpui::test]
+async fn test_word_characters(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new_typescript(cx).await;
+    cx.set_state(
+        indoc! { "
+        class A {
+            #ˇgoop = 99;
+            $ˇgoop () { return this.#gˇoop };
+        };
+        console.log(new A().$gooˇp())
+    "},
+        Mode::Normal,
+    );
+    cx.simulate_keystrokes(["v", "i", "w"]);
+    cx.assert_state(
+        indoc! {"
+        class A {
+            «#goopˇ» = 99;
+            «$goopˇ» () { return this.«#goopˇ» };
+        };
+        console.log(new A().«$goopˇ»())
+    "},
+        Mode::Visual,
+    )
+}
+
+#[gpui::test]
+async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_wrap(12).await;
+    // tests line wrap as follows:
+    //  1: twelve char
+    //     twelve char
+    //  2: twelve char
+    cx.set_shared_state(indoc! { "
+        tˇwelve char twelve char
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! { "
+        twelve char twelve char
+        tˇwelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["k"]).await;
+    cx.assert_shared_state(indoc! { "
+        tˇwelve char twelve char
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["g", "j"]).await;
+    cx.assert_shared_state(indoc! { "
+        twelve char tˇwelve char
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["g", "j"]).await;
+    cx.assert_shared_state(indoc! { "
+        twelve char twelve char
+        tˇwelve char
+    "})
+        .await;
+
+    cx.simulate_shared_keystrokes(["g", "k"]).await;
+    cx.assert_shared_state(indoc! { "
+        twelve char tˇwelve char
+        twelve char
+    "})
+        .await;
+
+    cx.simulate_shared_keystrokes(["g", "^"]).await;
+    cx.assert_shared_state(indoc! { "
+        twelve char ˇtwelve char
+        twelve char
+    "})
+        .await;
+
+    cx.simulate_shared_keystrokes(["^"]).await;
+    cx.assert_shared_state(indoc! { "
+        ˇtwelve char twelve char
+        twelve char
+    "})
+        .await;
+
+    cx.simulate_shared_keystrokes(["g", "$"]).await;
+    cx.assert_shared_state(indoc! { "
+        twelve charˇ twelve char
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["$"]).await;
+    cx.assert_shared_state(indoc! { "
+        twelve char twelve chaˇr
+        twelve char
+    "})
+        .await;
+
+    cx.set_shared_state(indoc! { "
+        tˇwelve char twelve char
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["enter"]).await;
+    cx.assert_shared_state(indoc! { "
+            twelve char twelve char
+            ˇtwelve char
+        "})
+        .await;
+
+    cx.set_shared_state(indoc! { "
+        twelve char
+        tˇwelve char twelve char
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["o", "o", "escape"]).await;
+    cx.assert_shared_state(indoc! { "
+        twelve char
+        twelve char twelve char
+        ˇo
+        twelve char
+    "})
+        .await;
+
+    cx.set_shared_state(indoc! { "
+        twelve char
+        tˇwelve char twelve char
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["shift-a", "a", "escape"])
+        .await;
+    cx.assert_shared_state(indoc! { "
+        twelve char
+        twelve char twelve charˇa
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["shift-i", "i", "escape"])
+        .await;
+    cx.assert_shared_state(indoc! { "
+        twelve char
+        ˇitwelve char twelve chara
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["shift-d"]).await;
+    cx.assert_shared_state(indoc! { "
+        twelve char
+        ˇ
+        twelve char
+    "})
+        .await;
+
+    cx.set_shared_state(indoc! { "
+        twelve char
+        twelve char tˇwelve char
+        twelve char
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["shift-o", "o", "escape"])
+        .await;
+    cx.assert_shared_state(indoc! { "
+        twelve char
+        ˇo
+        twelve char twelve char
+        twelve char
+    "})
+        .await;
+}
+
+#[gpui::test]
+async fn test_folds(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+    cx.set_neovim_option("foldmethod=manual").await;
+
+    cx.set_shared_state(indoc! { "
+        fn boop() {
+          ˇbarp()
+          bazp()
+        }
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"])
+        .await;
+
+    // visual display is now:
+    // fn boop () {
+    //  [FOLDED]
+    // }
+
+    // TODO: this should not be needed but currently zf does not
+    // return to normal mode.
+    cx.simulate_shared_keystrokes(["escape"]).await;
+
+    // skip over fold downward
+    cx.simulate_shared_keystrokes(["g", "g"]).await;
+    cx.assert_shared_state(indoc! { "
+        ˇfn boop() {
+          barp()
+          bazp()
+        }
+    "})
+        .await;
+
+    cx.simulate_shared_keystrokes(["j", "j"]).await;
+    cx.assert_shared_state(indoc! { "
+        fn boop() {
+          barp()
+          bazp()
+        ˇ}
+    "})
+        .await;
+
+    // skip over fold upward
+    cx.simulate_shared_keystrokes(["2", "k"]).await;
+    cx.assert_shared_state(indoc! { "
+        ˇfn boop() {
+          barp()
+          bazp()
+        }
+    "})
+        .await;
+
+    // yank the fold
+    cx.simulate_shared_keystrokes(["down", "y", "y"]).await;
+    cx.assert_shared_clipboard("  barp()\n  bazp()\n").await;
+
+    // re-open
+    cx.simulate_shared_keystrokes(["z", "o"]).await;
+    cx.assert_shared_state(indoc! { "
+        fn boop() {
+        ˇ  barp()
+          bazp()
+        }
+    "})
+        .await;
+}

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -1,9 +1,13 @@
 use indoc::indoc;
+use settings::SettingsStore;
 use std::ops::{Deref, DerefMut, Range};
 
 use collections::{HashMap, HashSet};
 use gpui::ContextHandle;
-use language::OffsetRangeExt;
+use language::{
+    language_settings::{AllLanguageSettings, SoftWrap},
+    OffsetRangeExt,
+};
 use util::test::{generate_marked_text, marked_text_offsets};
 
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
@@ -116,7 +120,7 @@ impl<'a> NeovimBackedTestContext<'a> {
 
     pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
         let mode = if marked_text.contains("»") {
-            Mode::Visual { line: false }
+            Mode::Visual
         } else {
             Mode::Normal
         };
@@ -127,16 +131,46 @@ impl<'a> NeovimBackedTestContext<'a> {
         context_handle
     }
 
+    pub async fn set_shared_wrap(&mut self, columns: u32) {
+        if columns < 12 {
+            panic!("nvim doesn't support columns < 12")
+        }
+        self.neovim.set_option("wrap").await;
+        self.neovim.set_option("columns=12").await;
+
+        self.update(|cx| {
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                    settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength);
+                    settings.defaults.preferred_line_length = Some(columns);
+                });
+            })
+        })
+    }
+
+    pub async fn set_neovim_option(&mut self, option: &str) {
+        self.neovim.set_option(option).await;
+    }
+
     pub async fn assert_shared_state(&mut self, marked_text: &str) {
         let neovim = self.neovim_state().await;
-        if neovim != marked_text {
-            let initial_state = self
-                .last_set_state
-                .as_ref()
-                .unwrap_or(&"N/A".to_string())
-                .clone();
-            panic!(
-                indoc! {"Test is incorrect (currently expected != neovim state)
+        let editor = self.editor_state();
+        if neovim == marked_text && neovim == editor {
+            return;
+        }
+        let initial_state = self
+            .last_set_state
+            .as_ref()
+            .unwrap_or(&"N/A".to_string())
+            .clone();
+
+        let message = if neovim != marked_text {
+            "Test is incorrect (currently expected != neovim_state)"
+        } else {
+            "Editor does not match nvim behaviour"
+        };
+        panic!(
+            indoc! {"{}
                 # initial state:
                 {}
                 # keystrokes:
@@ -147,20 +181,65 @@ impl<'a> NeovimBackedTestContext<'a> {
                 {}
                 # zed state:
                 {}"},
-                initial_state,
-                self.recent_keystrokes.join(" "),
-                marked_text,
-                neovim,
-                self.editor_state(),
-            )
+            message,
+            initial_state,
+            self.recent_keystrokes.join(" "),
+            marked_text,
+            neovim,
+            editor
+        )
+    }
+
+    pub async fn assert_shared_clipboard(&mut self, text: &str) {
+        let neovim = self.neovim.read_register('"').await;
+        let editor = self
+            .platform()
+            .read_from_clipboard()
+            .unwrap()
+            .text()
+            .clone();
+
+        if text == neovim && text == editor {
+            return;
         }
-        self.assert_editor_state(marked_text)
+
+        let message = if neovim != text {
+            "Test is incorrect (currently expected != neovim)"
+        } else {
+            "Editor does not match nvim behaviour"
+        };
+
+        let initial_state = self
+            .last_set_state
+            .as_ref()
+            .unwrap_or(&"N/A".to_string())
+            .clone();
+
+        panic!(
+            indoc! {"{}
+                # initial state:
+                {}
+                # keystrokes:
+                {}
+                # currently expected:
+                {}
+                # neovim clipboard:
+                {}
+                # zed clipboard:
+                {}"},
+            message,
+            initial_state,
+            self.recent_keystrokes.join(" "),
+            text,
+            neovim,
+            editor
+        )
     }
 
     pub async fn neovim_state(&mut self) -> String {
         generate_marked_text(
             self.neovim.text().await.as_str(),
-            &vec![self.neovim_selection().await],
+            &self.neovim_selections().await[..],
             true,
         )
     }
@@ -169,9 +248,12 @@ impl<'a> NeovimBackedTestContext<'a> {
         self.neovim.mode().await.unwrap()
     }
 
-    async fn neovim_selection(&mut self) -> Range<usize> {
-        let neovim_selection = self.neovim.selection().await;
-        neovim_selection.to_offset(&self.buffer_snapshot())
+    async fn neovim_selections(&mut self) -> Vec<Range<usize>> {
+        let neovim_selections = self.neovim.selections().await;
+        neovim_selections
+            .into_iter()
+            .map(|selection| selection.to_offset(&self.buffer_snapshot()))
+            .collect()
     }
 
     pub async fn assert_state_matches(&mut self) {

crates/vim/src/test/neovim_connection.rs 🔗

@@ -1,5 +1,8 @@
 #[cfg(feature = "neovim")]
-use std::ops::{Deref, DerefMut};
+use std::{
+    cmp,
+    ops::{Deref, DerefMut},
+};
 use std::{ops::Range, path::PathBuf};
 
 #[cfg(feature = "neovim")]
@@ -37,6 +40,8 @@ pub enum NeovimData {
     Put { state: String },
     Key(String),
     Get { state: String, mode: Option<Mode> },
+    ReadRegister { name: char, value: String },
+    SetOption { value: String },
 }
 
 pub struct NeovimConnection {
@@ -135,7 +140,7 @@ impl NeovimConnection {
 
     #[cfg(feature = "neovim")]
     pub async fn set_state(&mut self, marked_text: &str) {
-        let (text, selection) = parse_state(&marked_text);
+        let (text, selections) = parse_state(&marked_text);
 
         let nvim_buffer = self
             .nvim
@@ -167,6 +172,11 @@ impl NeovimConnection {
             .await
             .expect("Could not get neovim window");
 
+        if selections.len() != 1 {
+            panic!("must have one selection");
+        }
+        let selection = &selections[0];
+
         let cursor = selection.start;
         nvim_window
             .set_cursor((cursor.row as i64 + 1, cursor.column as i64))
@@ -213,6 +223,59 @@ impl NeovimConnection {
         );
     }
 
+    #[cfg(feature = "neovim")]
+    pub async fn set_option(&mut self, value: &str) {
+        self.nvim
+            .command_output(format!("set {}", value).as_str())
+            .await
+            .unwrap();
+
+        self.data.push_back(NeovimData::SetOption {
+            value: value.to_string(),
+        })
+    }
+
+    #[cfg(not(feature = "neovim"))]
+    pub async fn set_option(&mut self, value: &str) {
+        assert_eq!(
+            self.data.pop_front(),
+            Some(NeovimData::SetOption {
+                value: value.to_string(),
+            }),
+            "operation does not match recorded script. re-record with --features=neovim"
+        );
+    }
+
+    #[cfg(not(feature = "neovim"))]
+    pub async fn read_register(&mut self, register: char) -> String {
+        if let Some(NeovimData::Get { .. }) = self.data.front() {
+            self.data.pop_front();
+        };
+        if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() {
+            if name == register {
+                return value;
+            }
+        }
+
+        panic!("operation does not match recorded script. re-record with --features=neovim")
+    }
+
+    #[cfg(feature = "neovim")]
+    pub async fn read_register(&mut self, name: char) -> String {
+        let value = self
+            .nvim
+            .command_output(format!("echo getreg('{}')", name).as_str())
+            .await
+            .unwrap();
+
+        self.data.push_back(NeovimData::ReadRegister {
+            name,
+            value: value.clone(),
+        });
+
+        value
+    }
+
     #[cfg(feature = "neovim")]
     async fn read_position(&mut self, cmd: &str) -> u32 {
         self.nvim
@@ -224,7 +287,7 @@ impl NeovimConnection {
     }
 
     #[cfg(feature = "neovim")]
-    pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
+    pub async fn state(&mut self) -> (Option<Mode>, String, Vec<Range<Point>>) {
         let nvim_buffer = self
             .nvim
             .get_current_buf()
@@ -261,16 +324,51 @@ impl NeovimConnection {
         let mode = match nvim_mode_text.as_ref() {
             "i" => Some(Mode::Insert),
             "n" => Some(Mode::Normal),
-            "v" => Some(Mode::Visual { line: false }),
-            "V" => Some(Mode::Visual { line: true }),
+            "v" => Some(Mode::Visual),
+            "V" => Some(Mode::VisualLine),
+            "\x16" => Some(Mode::VisualBlock),
             _ => None,
         };
 
+        let mut selections = Vec::new();
         // Vim uses the index of the first and last character in the selection
         // Zed uses the index of the positions between the characters, so we need
         // to add one to the end in visual mode.
         match mode {
-            Some(Mode::Visual { .. }) => {
+            Some(Mode::VisualBlock) if selection_row != cursor_row => {
+                // in zed we fake a block selecrtion by using multiple cursors (one per line)
+                // this code emulates that.
+                // to deal with casees where the selection is not perfectly rectangular we extract
+                // the content of the selection via the "a register to get the shape correctly.
+                self.nvim.input("\"aygv").await.unwrap();
+                let content = self.nvim.command_output("echo getreg('a')").await.unwrap();
+                let lines = content.split("\n").collect::<Vec<_>>();
+                let top = cmp::min(selection_row, cursor_row);
+                let left = cmp::min(selection_col, cursor_col);
+                for row in top..=cmp::max(selection_row, cursor_row) {
+                    let content = if row - top >= lines.len() as u32 {
+                        ""
+                    } else {
+                        lines[(row - top) as usize]
+                    };
+                    let line_len = self
+                        .read_position(format!("echo strlen(getline({}))", row + 1).as_str())
+                        .await;
+
+                    if left > line_len {
+                        continue;
+                    }
+
+                    let start = Point::new(row, left);
+                    let end = Point::new(row, left + content.len() as u32);
+                    if cursor_col >= selection_col {
+                        selections.push(start..end)
+                    } else {
+                        selections.push(end..start)
+                    }
+                }
+            }
+            Some(Mode::Visual) | Some(Mode::VisualLine) | Some(Mode::VisualBlock) => {
                 if selection_col > cursor_col {
                     let selection_line_length =
                         self.read_position("echo strlen(getline(line('v')))").await;
@@ -290,38 +388,37 @@ impl NeovimConnection {
                         cursor_row += 1;
                     }
                 }
+                selections.push(
+                    Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col),
+                )
             }
-            Some(Mode::Insert) | Some(Mode::Normal) | None => {}
+            Some(Mode::Insert) | Some(Mode::Normal) | None => selections
+                .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)),
         }
 
-        let (start, end) = (
-            Point::new(selection_row, selection_col),
-            Point::new(cursor_row, cursor_col),
-        );
-
         let state = NeovimData::Get {
             mode,
-            state: encode_range(&text, start..end),
+            state: encode_ranges(&text, &selections),
         };
 
         if self.data.back() != Some(&state) {
             self.data.push_back(state.clone());
         }
 
-        (mode, text, start..end)
+        (mode, text, selections)
     }
 
     #[cfg(not(feature = "neovim"))]
-    pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
+    pub async fn state(&mut self) -> (Option<Mode>, String, Vec<Range<Point>>) {
         if let Some(NeovimData::Get { state: text, mode }) = self.data.front() {
-            let (text, range) = parse_state(text);
-            (*mode, text, range)
+            let (text, ranges) = parse_state(text);
+            (*mode, text, ranges)
         } else {
             panic!("operation does not match recorded script. re-record with --features=neovim");
         }
     }
 
-    pub async fn selection(&mut self) -> Range<Point> {
+    pub async fn selections(&mut self) -> Vec<Range<Point>> {
         self.state().await.2
     }
 
@@ -421,51 +518,62 @@ impl Handler for NvimHandler {
     }
 }
 
-fn parse_state(marked_text: &str) -> (String, Range<Point>) {
+fn parse_state(marked_text: &str) -> (String, Vec<Range<Point>>) {
     let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
-    let byte_range = ranges[0].clone();
-    let mut point_range = Point::zero()..Point::zero();
-    let mut ix = 0;
-    let mut position = Point::zero();
-    for c in text.chars().chain(['\0']) {
-        if ix == byte_range.start {
-            point_range.start = position;
-        }
-        if ix == byte_range.end {
-            point_range.end = position;
-        }
-        let len_utf8 = c.len_utf8();
-        ix += len_utf8;
-        if c == '\n' {
-            position.row += 1;
-            position.column = 0;
-        } else {
-            position.column += len_utf8 as u32;
-        }
-    }
-    (text, point_range)
+    let point_ranges = ranges
+        .into_iter()
+        .map(|byte_range| {
+            let mut point_range = Point::zero()..Point::zero();
+            let mut ix = 0;
+            let mut position = Point::zero();
+            for c in text.chars().chain(['\0']) {
+                if ix == byte_range.start {
+                    point_range.start = position;
+                }
+                if ix == byte_range.end {
+                    point_range.end = position;
+                }
+                let len_utf8 = c.len_utf8();
+                ix += len_utf8;
+                if c == '\n' {
+                    position.row += 1;
+                    position.column = 0;
+                } else {
+                    position.column += len_utf8 as u32;
+                }
+            }
+            point_range
+        })
+        .collect::<Vec<_>>();
+    (text, point_ranges)
 }
 
 #[cfg(feature = "neovim")]
-fn encode_range(text: &str, range: Range<Point>) -> String {
-    let mut byte_range = 0..0;
-    let mut ix = 0;
-    let mut position = Point::zero();
-    for c in text.chars().chain(['\0']) {
-        if position == range.start {
-            byte_range.start = ix;
-        }
-        if position == range.end {
-            byte_range.end = ix;
-        }
-        let len_utf8 = c.len_utf8();
-        ix += len_utf8;
-        if c == '\n' {
-            position.row += 1;
-            position.column = 0;
-        } else {
-            position.column += len_utf8 as u32;
-        }
-    }
-    util::test::generate_marked_text(text, &[byte_range], true)
+fn encode_ranges(text: &str, point_ranges: &Vec<Range<Point>>) -> String {
+    let byte_ranges = point_ranges
+        .into_iter()
+        .map(|range| {
+            let mut byte_range = 0..0;
+            let mut ix = 0;
+            let mut position = Point::zero();
+            for c in text.chars().chain(['\0']) {
+                if position == range.start {
+                    byte_range.start = ix;
+                }
+                if position == range.end {
+                    byte_range.end = ix;
+                }
+                let len_utf8 = c.len_utf8();
+                ix += len_utf8;
+                if c == '\n' {
+                    position.row += 1;
+                    position.column = 0;
+                } else {
+                    position.column += len_utf8 as u32;
+                }
+            }
+            byte_range
+        })
+        .collect::<Vec<_>>();
+    util::test::generate_marked_text(text, &byte_ranges[..], true)
 }

crates/vim/src/test/vim_binding_test_context.rs 🔗

@@ -1,64 +0,0 @@
-use std::ops::{Deref, DerefMut};
-
-use crate::*;
-
-use super::VimTestContext;
-
-pub struct VimBindingTestContext<'a, const COUNT: usize> {
-    cx: VimTestContext<'a>,
-    keystrokes_under_test: [&'static str; COUNT],
-    mode_before: Mode,
-    mode_after: Mode,
-}
-
-impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
-    pub fn new(
-        keystrokes_under_test: [&'static str; COUNT],
-        mode_before: Mode,
-        mode_after: Mode,
-        cx: VimTestContext<'a>,
-    ) -> Self {
-        Self {
-            cx,
-            keystrokes_under_test,
-            mode_before,
-            mode_after,
-        }
-    }
-
-    pub fn binding<const NEW_COUNT: usize>(
-        self,
-        keystrokes_under_test: [&'static str; NEW_COUNT],
-    ) -> VimBindingTestContext<'a, NEW_COUNT> {
-        VimBindingTestContext {
-            keystrokes_under_test,
-            cx: self.cx,
-            mode_before: self.mode_before,
-            mode_after: self.mode_after,
-        }
-    }
-
-    pub fn assert(&mut self, initial_state: &str, state_after: &str) {
-        self.cx.assert_binding(
-            self.keystrokes_under_test,
-            initial_state,
-            self.mode_before,
-            state_after,
-            self.mode_after,
-        )
-    }
-}
-
-impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
-    type Target = VimTestContext<'a>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.cx
-    }
-}
-
-impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.cx
-    }
-}

crates/vim/src/test/vim_test_context.rs 🔗

@@ -8,16 +8,24 @@ use search::{BufferSearchBar, ProjectSearchBar};
 
 use crate::{state::Operator, *};
 
-use super::VimBindingTestContext;
-
 pub struct VimTestContext<'a> {
     cx: EditorLspTestContext<'a>,
 }
 
 impl<'a> VimTestContext<'a> {
     pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
-        let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
+        let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await;
+        Self::new_with_lsp(lsp, enabled)
+    }
+
+    pub async fn new_typescript(cx: &'a mut gpui::TestAppContext) -> VimTestContext<'a> {
+        Self::new_with_lsp(
+            EditorLspTestContext::new_typescript(Default::default(), cx).await,
+            true,
+        )
+    }
 
+    pub fn new_with_lsp(mut cx: EditorLspTestContext<'a>, enabled: bool) -> VimTestContext<'a> {
         cx.update(|cx| {
             search::init(cx);
             crate::init(cx);
@@ -76,12 +84,12 @@ impl<'a> VimTestContext<'a> {
     }
 
     pub fn mode(&mut self) -> Mode {
-        self.cx.read(|cx| cx.global::<Vim>().state.mode)
+        self.cx.read(|cx| cx.global::<Vim>().state().mode)
     }
 
     pub fn active_operator(&mut self) -> Option<Operator> {
         self.cx
-            .read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
+            .read(|cx| cx.global::<Vim>().state().operator_stack.last().copied())
     }
 
     pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
@@ -92,6 +100,7 @@ impl<'a> VimTestContext<'a> {
                 vim.switch_mode(mode, true, cx);
             })
         });
+        self.cx.foreground().run_until_parked();
         context_handle
     }
 
@@ -115,14 +124,6 @@ impl<'a> VimTestContext<'a> {
         assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
         assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
     }
-
-    pub fn binding<const COUNT: usize>(
-        mut self,
-        keystrokes: [&'static str; COUNT],
-    ) -> VimBindingTestContext<'a, COUNT> {
-        let mode = self.mode();
-        VimBindingTestContext::new(keystrokes, mode, mode, self)
-    }
 }
 
 impl<'a> Deref for VimTestContext<'a> {

crates/vim/src/utils.rs 🔗

@@ -1,5 +1,6 @@
 use editor::{ClipboardSelection, Editor};
 use gpui::{AppContext, ClipboardItem};
+use language::Point;
 
 pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut AppContext) {
     let selections = editor.selections.all_adjusted(cx);
@@ -7,13 +8,35 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App
     let mut text = String::new();
     let mut clipboard_selections = Vec::with_capacity(selections.len());
     {
+        let mut is_first = true;
         for selection in selections.iter() {
-            let initial_len = text.len();
-            let start = selection.start;
+            let mut start = selection.start;
             let end = selection.end;
+            if is_first {
+                is_first = false;
+            } else {
+                text.push_str("\n");
+            }
+            let initial_len = text.len();
+
+            // if the file does not end with \n, and our line-mode selection ends on
+            // that line, we will have expanded the start of the selection to ensure it
+            // contains a newline (so that delete works as expected). We undo that change
+            // here.
+            let is_last_line = linewise
+                && end.row == buffer.max_buffer_row()
+                && buffer.max_point().column > 0
+                && start == Point::new(start.row, buffer.line_len(start.row));
+
+            if is_last_line {
+                start = Point::new(buffer.max_buffer_row(), 0);
+            }
             for chunk in buffer.text_for_range(start..end) {
                 text.push_str(chunk);
             }
+            if is_last_line {
+                text.push_str("\n");
+            }
             clipboard_selections.push(ClipboardSelection {
                 len: text.len() - initial_len,
                 is_entire_line: linewise,

crates/vim/src/vim.rs 🔗

@@ -12,21 +12,21 @@ mod utils;
 mod visual;
 
 use anyhow::Result;
-use collections::CommandPaletteFilter;
+use collections::{CommandPaletteFilter, HashMap};
 use editor::{movement, Editor, EditorMode, Event};
 use gpui::{
     actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
     Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
-use language::CursorShape;
+use language::{CursorShape, Selection, SelectionGoal};
 pub use mode_indicator::ModeIndicator;
 use motion::Motion;
 use normal::normal_replace;
 use serde::Deserialize;
 use settings::{Setting, SettingsStore};
-use state::{Mode, Operator, VimState};
+use state::{EditorState, Mode, Operator, WorkspaceState};
 use std::sync::Arc;
-use visual::visual_replace;
+use visual::{visual_block_motion, visual_replace};
 use workspace::{self, Workspace};
 
 struct VimModeSetting(bool);
@@ -127,7 +127,9 @@ pub struct Vim {
     active_editor: Option<WeakViewHandle<Editor>>,
     editor_subscription: Option<Subscription>,
     enabled: bool,
-    state: VimState,
+    editor_states: HashMap<usize, EditorState>,
+    workspace_state: WorkspaceState,
+    default_state: EditorState,
 }
 
 impl Vim {
@@ -143,13 +145,13 @@ impl Vim {
     }
 
     fn set_active_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut WindowContext) {
-        self.active_editor = Some(editor.downgrade());
+        self.active_editor = Some(editor.clone().downgrade());
         self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
             Event::SelectionsChanged { local: true } => {
                 let editor = editor.read(cx);
                 if editor.leader_replica_id().is_none() {
-                    let newest_empty = editor.selections.newest::<usize>(cx).is_empty();
-                    local_selections_changed(newest_empty, cx);
+                    let newest = editor.selections.newest::<usize>(cx);
+                    local_selections_changed(newest, cx);
                 }
             }
             Event::InputIgnored { text } => {
@@ -163,8 +165,11 @@ impl Vim {
             let editor_mode = editor.mode();
             let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
 
-            if editor_mode == EditorMode::Full && !newest_selection_empty {
-                self.switch_mode(Mode::Visual { line: false }, true, cx);
+            if editor_mode == EditorMode::Full
+                && !newest_selection_empty
+                && self.state().mode == Mode::Normal
+            {
+                self.switch_mode(Mode::Visual, true, cx);
             }
         }
 
@@ -181,9 +186,14 @@ impl Vim {
     }
 
     fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
-        let last_mode = self.state.mode;
-        self.state.mode = mode;
-        self.state.operator_stack.clear();
+        let state = self.state();
+        let last_mode = state.mode;
+        let prior_mode = state.last_mode;
+        self.update_state(|state| {
+            state.last_mode = last_mode;
+            state.mode = mode;
+            state.operator_stack.clear();
+        });
 
         cx.emit_global(VimEvent::ModeChanged { mode });
 
@@ -196,11 +206,33 @@ impl Vim {
 
         // Adjust selections
         self.update_active_editor(cx, |editor, cx| {
+            if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
+            {
+                visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal)))
+            }
+
             editor.change_selections(None, cx, |s| {
+                // we cheat with visual block mode and use multiple cursors.
+                // the cost of this cheat is we need to convert back to a single
+                // cursor whenever vim would.
+                if last_mode == Mode::VisualBlock
+                    && (mode != Mode::VisualBlock && mode != Mode::Insert)
+                {
+                    let tail = s.oldest_anchor().tail();
+                    let head = s.newest_anchor().head();
+                    s.select_anchor_ranges(vec![tail..head]);
+                } else if last_mode == Mode::Insert
+                    && prior_mode == Mode::VisualBlock
+                    && mode != Mode::VisualBlock
+                {
+                    let pos = s.first_anchor().head();
+                    s.select_anchor_ranges(vec![pos..pos])
+                }
+
                 s.move_with(|map, selection| {
                     if last_mode.is_visual() && !mode.is_visual() {
                         let mut point = selection.head();
-                        if !selection.reversed {
+                        if !selection.reversed && !selection.is_empty() {
                             point = movement::left(map, selection.head());
                         }
                         selection.collapse_to(point, selection.goal)
@@ -215,7 +247,7 @@ impl Vim {
     }
 
     fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
-        self.state.operator_stack.push(operator);
+        self.update_state(|state| state.operator_stack.push(operator));
         self.sync_vim_settings(cx);
     }
 
@@ -228,9 +260,13 @@ impl Vim {
         }
     }
 
+    fn maybe_pop_operator(&mut self) -> Option<Operator> {
+        self.update_state(|state| state.operator_stack.pop())
+    }
+
     fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator {
-        let popped_operator = self.state.operator_stack.pop()
-            .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
+        let popped_operator = self.update_state( |state| state.operator_stack.pop()
+        )            .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
         self.sync_vim_settings(cx);
         popped_operator
     }
@@ -244,12 +280,12 @@ impl Vim {
     }
 
     fn clear_operator(&mut self, cx: &mut WindowContext) {
-        self.state.operator_stack.clear();
+        self.update_state(|state| state.operator_stack.clear());
         self.sync_vim_settings(cx);
     }
 
     fn active_operator(&self) -> Option<Operator> {
-        self.state.operator_stack.last().copied()
+        self.state().operator_stack.last().copied()
     }
 
     fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) {
@@ -260,17 +296,21 @@ impl Vim {
         match Vim::read(cx).active_operator() {
             Some(Operator::FindForward { before }) => {
                 let find = Motion::FindForward { before, text };
-                Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
+                Vim::update(cx, |vim, _| {
+                    vim.workspace_state.last_find = Some(find.clone())
+                });
                 motion::motion(find, cx)
             }
             Some(Operator::FindBackward { after }) => {
                 let find = Motion::FindBackward { after, text };
-                Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
+                Vim::update(cx, |vim, _| {
+                    vim.workspace_state.last_find = Some(find.clone())
+                });
                 motion::motion(find, cx)
             }
-            Some(Operator::Replace) => match Vim::read(cx).state.mode {
+            Some(Operator::Replace) => match Vim::read(cx).state().mode {
                 Mode::Normal => normal_replace(text, cx),
-                Mode::Visual { .. } => visual_replace(text, cx),
+                Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
                 _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
             },
             _ => {}
@@ -280,7 +320,6 @@ impl Vim {
     fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
         if self.enabled != enabled {
             self.enabled = enabled;
-            self.state = Default::default();
 
             cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
                 if self.enabled {
@@ -307,8 +346,29 @@ impl Vim {
         }
     }
 
+    pub fn state(&self) -> &EditorState {
+        if let Some(active_editor) = self.active_editor.as_ref() {
+            if let Some(state) = self.editor_states.get(&active_editor.id()) {
+                return state;
+            }
+        }
+
+        &self.default_state
+    }
+
+    pub fn update_state<T>(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T {
+        let mut state = self.state().clone();
+        let ret = func(&mut state);
+
+        if let Some(active_editor) = self.active_editor.as_ref() {
+            self.editor_states.insert(active_editor.id(), state);
+        }
+
+        ret
+    }
+
     fn sync_vim_settings(&self, cx: &mut WindowContext) {
-        let state = &self.state;
+        let state = self.state();
         let cursor_shape = state.cursor_shape();
 
         self.update_active_editor(cx, |editor, cx| {
@@ -317,7 +377,8 @@ impl Vim {
                 editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
                 editor.set_collapse_matches(true);
                 editor.set_input_enabled(!state.vim_controlled());
-                editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true });
+                editor.set_autoindent(state.should_autoindent());
+                editor.selections.line_mode = matches!(state.mode, Mode::VisualLine);
                 let context_layer = state.keymap_context_layer();
                 editor.set_keymap_context_layer::<Self>(context_layer, cx);
             } else {
@@ -333,6 +394,7 @@ impl Vim {
         editor.set_cursor_shape(CursorShape::Bar, cx);
         editor.set_clip_at_line_ends(false, cx);
         editor.set_input_enabled(true);
+        editor.set_autoindent(true);
         editor.selections.line_mode = false;
 
         // we set the VimEnabled context on all editors so that we
@@ -365,10 +427,14 @@ impl Setting for VimModeSetting {
     }
 }
 
-fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) {
+fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
-        if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty {
-            vim.switch_mode(Mode::Visual { line: false }, false, cx)
+        if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
+            if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
+                vim.switch_mode(Mode::VisualBlock, false, cx);
+            } else {
+                vim.switch_mode(Mode::Visual, false, cx)
+            }
         }
     })
 }

crates/vim/src/visual.rs 🔗

@@ -1,11 +1,14 @@
-use std::{borrow::Cow, sync::Arc};
+use std::{cmp, sync::Arc};
 
 use collections::HashMap;
 use editor::{
-    display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection,
+    display_map::{DisplaySnapshot, ToDisplayPoint},
+    movement,
+    scroll::autoscroll::Autoscroll,
+    Bias, DisplayPoint, Editor,
 };
 use gpui::{actions, AppContext, ViewContext, WindowContext};
-use language::{AutoindentMode, SelectionGoal};
+use language::{Selection, SelectionGoal};
 use workspace::Workspace;
 
 use crate::{
@@ -21,78 +24,191 @@ actions!(
     [
         ToggleVisual,
         ToggleVisualLine,
+        ToggleVisualBlock,
         VisualDelete,
         VisualYank,
-        VisualPaste,
         OtherEnd,
     ]
 );
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(toggle_visual);
-    cx.add_action(toggle_visual_line);
+    cx.add_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
+        toggle_mode(Mode::Visual, cx)
+    });
+    cx.add_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
+        toggle_mode(Mode::VisualLine, cx)
+    });
+    cx.add_action(
+        |_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
+            toggle_mode(Mode::VisualBlock, cx)
+        },
+    );
     cx.add_action(other_end);
     cx.add_action(delete);
     cx.add_action(yank);
-    cx.add_action(paste);
 }
 
 pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
-            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.move_with(|map, selection| {
-                    let was_reversed = selection.reversed;
-
-                    let mut current_head = selection.head();
-
-                    // our motions assume the current character is after the cursor,
-                    // but in (forward) visual mode the current character is just
-                    // before the end of the selection.
-
-                    // If the file ends with a newline (which is common) we don't do this.
-                    // so that if you go to the end of such a file you can use "up" to go
-                    // to the previous line and have it work somewhat as expected.
-                    if !selection.reversed
-                        && !selection.is_empty()
-                        && !(selection.end.column() == 0 && selection.end == map.max_point())
-                    {
-                        current_head = movement::left(map, selection.end)
+            if vim.state().mode == Mode::VisualBlock
+                && !matches!(
+                    motion,
+                    Motion::EndOfLine {
+                        display_lines: false
                     }
+                )
+            {
+                let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
+                visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
+                    motion.move_point(map, point, goal, times)
+                })
+            } else {
+                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.move_with(|map, selection| {
+                        let was_reversed = selection.reversed;
+                        let mut current_head = selection.head();
+
+                        // our motions assume the current character is after the cursor,
+                        // but in (forward) visual mode the current character is just
+                        // before the end of the selection.
+
+                        // If the file ends with a newline (which is common) we don't do this.
+                        // so that if you go to the end of such a file you can use "up" to go
+                        // to the previous line and have it work somewhat as expected.
+                        if !selection.reversed
+                            && !selection.is_empty()
+                            && !(selection.end.column() == 0 && selection.end == map.max_point())
+                        {
+                            current_head = movement::left(map, selection.end)
+                        }
 
-                    let Some((new_head, goal)) =
-                        motion.move_point(map, current_head, selection.goal, times) else { return };
-
-                    selection.set_head(new_head, goal);
+                        let Some((new_head, goal)) =
+                            motion.move_point(map, current_head, selection.goal, times)
+                        else {
+                            return;
+                        };
 
-                    // ensure the current character is included in the selection.
-                    if !selection.reversed {
-                        // TODO: maybe try clipping left for multi-buffers
-                        let next_point = movement::right(map, selection.end);
+                        selection.set_head(new_head, goal);
 
-                        if !(next_point.column() == 0 && next_point == map.max_point()) {
-                            selection.end = movement::right(map, selection.end)
+                        // ensure the current character is included in the selection.
+                        if !selection.reversed {
+                            let next_point = if vim.state().mode == Mode::VisualBlock {
+                                movement::saturating_right(map, selection.end)
+                            } else {
+                                movement::right(map, selection.end)
+                            };
+
+                            if !(next_point.column() == 0 && next_point == map.max_point()) {
+                                selection.end = next_point;
+                            }
                         }
-                    }
 
-                    // vim always ensures the anchor character stays selected.
-                    // if our selection has reversed, we need to move the opposite end
-                    // to ensure the anchor is still selected.
-                    if was_reversed && !selection.reversed {
-                        selection.start = movement::left(map, selection.start);
-                    } else if !was_reversed && selection.reversed {
-                        selection.end = movement::right(map, selection.end);
-                    }
+                        // vim always ensures the anchor character stays selected.
+                        // if our selection has reversed, we need to move the opposite end
+                        // to ensure the anchor is still selected.
+                        if was_reversed && !selection.reversed {
+                            selection.start = movement::left(map, selection.start);
+                        } else if !was_reversed && selection.reversed {
+                            selection.end = movement::right(map, selection.end);
+                        }
+                    })
                 });
-            });
+            }
         });
     });
 }
 
+pub fn visual_block_motion(
+    preserve_goal: bool,
+    editor: &mut Editor,
+    cx: &mut ViewContext<Editor>,
+    mut move_selection: impl FnMut(
+        &DisplaySnapshot,
+        DisplayPoint,
+        SelectionGoal,
+    ) -> Option<(DisplayPoint, SelectionGoal)>,
+) {
+    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+        let map = &s.display_map();
+        let mut head = s.newest_anchor().head().to_display_point(map);
+        let mut tail = s.oldest_anchor().tail().to_display_point(map);
+
+        let (start, end) = match s.newest_anchor().goal {
+            SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
+            SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
+            _ => (tail.column(), head.column()),
+        };
+        let goal = SelectionGoal::ColumnRange { start, end };
+
+        let was_reversed = tail.column() > head.column();
+        if !was_reversed && !preserve_goal {
+            head = movement::saturating_left(map, head);
+        }
+
+        let Some((new_head, _)) = move_selection(&map, head, goal) else {
+            return;
+        };
+        head = new_head;
+
+        let is_reversed = tail.column() > head.column();
+        if was_reversed && !is_reversed {
+            tail = movement::left(map, tail)
+        } else if !was_reversed && is_reversed {
+            tail = movement::right(map, tail)
+        }
+        if !is_reversed && !preserve_goal {
+            head = movement::saturating_right(map, head)
+        }
+
+        let columns = if is_reversed {
+            head.column()..tail.column()
+        } else if head.column() == tail.column() {
+            head.column()..(head.column() + 1)
+        } else {
+            tail.column()..head.column()
+        };
+
+        let mut selections = Vec::new();
+        let mut row = tail.row();
+
+        loop {
+            let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
+            let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
+            if columns.start <= map.line_len(row) {
+                let selection = Selection {
+                    id: s.new_selection_id(),
+                    start: start.to_point(map),
+                    end: end.to_point(map),
+                    reversed: is_reversed,
+                    goal: goal.clone(),
+                };
+
+                selections.push(selection);
+            }
+            if row == head.row() {
+                break;
+            }
+            if tail.row() > head.row() {
+                row -= 1
+            } else {
+                row += 1
+            }
+        }
+
+        s.select(selections);
+    })
+}
+
 pub fn visual_object(object: Object, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         if let Some(Operator::Object { around }) = vim.active_operator() {
             vim.pop_operator(cx);
+            let current_mode = vim.state().mode;
+            let target_mode = object.target_visual_mode(current_mode);
+            if target_mode != current_mode {
+                vim.switch_mode(target_mode, true, cx);
+            }
 
             vim.update_active_editor(cx, |editor, cx| {
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -108,20 +224,21 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) {
 
                         if let Some(range) = object.range(map, head, around) {
                             if !range.is_empty() {
-                                let expand_both_ways = if selection.is_empty() {
-                                    true
-                                // contains only one character
-                                } else if let Some((_, start)) =
-                                    map.reverse_chars_at(selection.end).next()
-                                {
-                                    selection.start == start
-                                } else {
-                                    false
-                                };
+                                let expand_both_ways =
+                                    if object.always_expands_both_ways() || selection.is_empty() {
+                                        true
+                                        // contains only one character
+                                    } else if let Some((_, start)) =
+                                        map.reverse_chars_at(selection.end).next()
+                                    {
+                                        selection.start == start
+                                    } else {
+                                        false
+                                    };
 
                                 if expand_both_ways {
-                                    selection.start = range.start;
-                                    selection.end = range.end;
+                                    selection.start = cmp::min(selection.start, range.start);
+                                    selection.end = cmp::max(selection.end, range.end);
                                 } else if selection.reversed {
                                     selection.start = range.start;
                                 } else {
@@ -136,28 +253,12 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) {
     });
 }
 
-pub fn toggle_visual(_: &mut Workspace, _: &ToggleVisual, cx: &mut ViewContext<Workspace>) {
-    Vim::update(cx, |vim, cx| match vim.state.mode {
-        Mode::Normal | Mode::Insert | Mode::Visual { line: true } => {
-            vim.switch_mode(Mode::Visual { line: false }, false, cx);
-        }
-        Mode::Visual { line: false } => {
-            vim.switch_mode(Mode::Normal, false, cx);
-        }
-    })
-}
-
-pub fn toggle_visual_line(
-    _: &mut Workspace,
-    _: &ToggleVisualLine,
-    cx: &mut ViewContext<Workspace>,
-) {
-    Vim::update(cx, |vim, cx| match vim.state.mode {
-        Mode::Normal | Mode::Insert | Mode::Visual { line: false } => {
-            vim.switch_mode(Mode::Visual { line: true }, false, cx);
-        }
-        Mode::Visual { line: true } => {
+fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        if vim.state().mode == mode {
             vim.switch_mode(Mode::Normal, false, cx);
+        } else {
+            vim.switch_mode(mode, false, cx);
         }
     })
 }
@@ -180,34 +281,39 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
             let mut original_columns: HashMap<_, _> = Default::default();
             let line_mode = editor.selections.line_mode;
 
-            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.move_with(|map, selection| {
-                    if line_mode {
-                        let mut position = selection.head();
-                        if !selection.reversed {
-                            position = movement::left(map, position);
+            editor.transact(cx, |editor, cx| {
+                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.move_with(|map, selection| {
+                        if line_mode {
+                            let mut position = selection.head();
+                            if !selection.reversed {
+                                position = movement::left(map, position);
+                            }
+                            original_columns.insert(selection.id, position.to_point(map).column);
                         }
-                        original_columns.insert(selection.id, position.to_point(map).column);
-                    }
-                    selection.goal = SelectionGoal::None;
+                        selection.goal = SelectionGoal::None;
+                    });
                 });
-            });
-            copy_selections_content(editor, line_mode, cx);
-            editor.insert("", cx);
+                copy_selections_content(editor, line_mode, cx);
+                editor.insert("", cx);
 
-            // Fixup cursor position after the deletion
-            editor.set_clip_at_line_ends(true, cx);
-            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.move_with(|map, selection| {
-                    let mut cursor = selection.head().to_point(map);
+                // Fixup cursor position after the deletion
+                editor.set_clip_at_line_ends(true, cx);
+                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.move_with(|map, selection| {
+                        let mut cursor = selection.head().to_point(map);
 
-                    if let Some(column) = original_columns.get(&selection.id) {
-                        cursor.column = *column
+                        if let Some(column) = original_columns.get(&selection.id) {
+                            cursor.column = *column
+                        }
+                        let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
+                        selection.collapse_to(cursor, selection.goal)
+                    });
+                    if vim.state().mode == Mode::VisualBlock {
+                        s.select_anchors(vec![s.first_anchor()])
                     }
-                    let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
-                    selection.collapse_to(cursor, selection.goal)
                 });
-            });
+            })
         });
         vim.switch_mode(Mode::Normal, true, cx);
     });
@@ -222,109 +328,8 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
                 s.move_with(|_, selection| {
                     selection.collapse_to(selection.start, SelectionGoal::None)
                 });
-            });
-        });
-        vim.switch_mode(Mode::Normal, true, cx);
-    });
-}
-
-pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>) {
-    Vim::update(cx, |vim, cx| {
-        vim.update_active_editor(cx, |editor, cx| {
-            editor.transact(cx, |editor, cx| {
-                if let Some(item) = cx.read_from_clipboard() {
-                    copy_selections_content(editor, editor.selections.line_mode, cx);
-                    let mut clipboard_text = Cow::Borrowed(item.text());
-                    if let Some(mut clipboard_selections) =
-                        item.metadata::<Vec<ClipboardSelection>>()
-                    {
-                        let (display_map, selections) = editor.selections.all_adjusted_display(cx);
-                        let all_selections_were_entire_line =
-                            clipboard_selections.iter().all(|s| s.is_entire_line);
-                        if clipboard_selections.len() != selections.len() {
-                            let mut newline_separated_text = String::new();
-                            let mut clipboard_selections =
-                                clipboard_selections.drain(..).peekable();
-                            let mut ix = 0;
-                            while let Some(clipboard_selection) = clipboard_selections.next() {
-                                newline_separated_text
-                                    .push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
-                                ix += clipboard_selection.len;
-                                if clipboard_selections.peek().is_some() {
-                                    newline_separated_text.push('\n');
-                                }
-                            }
-                            clipboard_text = Cow::Owned(newline_separated_text);
-                        }
-
-                        let mut new_selections = Vec::new();
-                        editor.buffer().update(cx, |buffer, cx| {
-                            let snapshot = buffer.snapshot(cx);
-                            let mut start_offset = 0;
-                            let mut edits = Vec::new();
-                            for (ix, selection) in selections.iter().enumerate() {
-                                let to_insert;
-                                let linewise;
-                                if let Some(clipboard_selection) = clipboard_selections.get(ix) {
-                                    let end_offset = start_offset + clipboard_selection.len;
-                                    to_insert = &clipboard_text[start_offset..end_offset];
-                                    linewise = clipboard_selection.is_entire_line;
-                                    start_offset = end_offset;
-                                } else {
-                                    to_insert = clipboard_text.as_str();
-                                    linewise = all_selections_were_entire_line;
-                                }
-
-                                let mut selection = selection.clone();
-                                if !selection.reversed {
-                                    let adjusted = selection.end;
-                                    // If the selection is empty, move both the start and end forward one
-                                    // character
-                                    if selection.is_empty() {
-                                        selection.start = adjusted;
-                                        selection.end = adjusted;
-                                    } else {
-                                        selection.end = adjusted;
-                                    }
-                                }
-
-                                let range = selection.map(|p| p.to_point(&display_map)).range();
-
-                                let new_position = if linewise {
-                                    edits.push((range.start..range.start, "\n"));
-                                    let mut new_position = range.start;
-                                    new_position.column = 0;
-                                    new_position.row += 1;
-                                    new_position
-                                } else {
-                                    range.start
-                                };
-
-                                new_selections.push(selection.map(|_| new_position));
-
-                                if linewise && to_insert.ends_with('\n') {
-                                    edits.push((
-                                        range.clone(),
-                                        &to_insert[0..to_insert.len().saturating_sub(1)],
-                                    ))
-                                } else {
-                                    edits.push((range.clone(), to_insert));
-                                }
-
-                                if linewise {
-                                    edits.push((range.end..range.end, "\n"));
-                                }
-                            }
-                            drop(snapshot);
-                            buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
-                        });
-
-                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                            s.select(new_selections)
-                        });
-                    } else {
-                        editor.insert(&clipboard_text, cx);
-                    }
+                if vim.state().mode == Mode::VisualBlock {
+                    s.select_anchors(vec![s.first_anchor()])
                 }
             });
         });
@@ -394,7 +399,7 @@ mod test {
             the lazy dog"
         })
         .await;
-        let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
+        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 
         // entering visual mode should select the character
         // under cursor
@@ -403,7 +408,7 @@ mod test {
             fox jumps over
             the lazy dog"})
             .await;
-        cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
+        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 
         // forwards motions should extend the selection
         cx.simulate_shared_keystrokes(["w", "j"]).await;
@@ -433,7 +438,7 @@ mod test {
             b
             "})
             .await;
-        let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
+        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
         cx.simulate_shared_keystrokes(["v"]).await;
         cx.assert_shared_state(indoc! {"
             a
@@ -441,7 +446,7 @@ mod test {
             ˇ»b
         "})
             .await;
-        cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
+        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
 
         // toggles off again
         cx.simulate_shared_keystrokes(["v"]).await;
@@ -513,7 +518,7 @@ mod test {
             b
             ˇ"})
             .await;
-        let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
+        let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
         cx.simulate_shared_keystrokes(["shift-v"]).await;
         cx.assert_shared_state(indoc! {"
             a
@@ -521,7 +526,7 @@ mod test {
             ˇ"})
             .await;
         assert_eq!(cx.mode(), cx.neovim_mode().await);
-        cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
+        cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
         cx.simulate_shared_keystrokes(["x"]).await;
         cx.assert_shared_state(indoc! {"
             a
@@ -566,38 +571,41 @@ mod test {
 
     #[gpui::test]
     async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["shift-v", "x"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
                 The quˇick brown
                 fox jumps over
                 the lazy dog"})
             .await;
-        // Test pasting code copied on delete
-        cx.simulate_shared_keystroke("p").await;
+        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
         cx.assert_state_matches().await;
 
-        cx.assert_all(indoc! {"
-                The quick brown
-                fox juˇmps over
-                the laˇzy dog"})
-            .await;
-        let mut cx = cx.binding(["shift-v", "j", "x"]);
-        cx.assert(indoc! {"
-                The quˇick brown
-                fox jumps over
-                the lazy dog"})
-            .await;
         // Test pasting code copied on delete
         cx.simulate_shared_keystroke("p").await;
         cx.assert_state_matches().await;
 
-        cx.assert_all(indoc! {"
+        cx.set_shared_state(indoc! {"
                 The quick brown
-                fox juˇmps over
+                fox jumps over
                 the laˇzy dog"})
             .await;
+        cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
+        cx.assert_state_matches().await;
+        cx.assert_shared_clipboard("the lazy dog\n").await;
+
+        for marked_text in cx.each_marked_position(indoc! {"
+                        The quˇick brown
+                        fox jumps over
+                        the lazy dog"})
+        {
+            cx.set_shared_state(&marked_text).await;
+            cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
+            cx.assert_state_matches().await;
+            // Test pasting code copied on delete
+            cx.simulate_shared_keystroke("p").await;
+            cx.assert_state_matches().await;
+        }
 
         cx.set_shared_state(indoc! {"
             The ˇlong line
@@ -611,144 +619,290 @@ mod test {
 
     #[gpui::test]
     async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["v", "w", "y"]);
-        cx.assert("The quick ˇbrown", "The quick ˇbrown");
-        cx.assert_clipboard_content(Some("brown"));
-        let mut cx = cx.binding(["v", "w", "j", "y"]);
-        cx.assert(
-            indoc! {"
-                The ˇquick brown
-                fox jumps over
-                the lazy dog"},
-            indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("The quick ˇbrown").await;
+        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
+        cx.assert_shared_state("The quick ˇbrown").await;
+        cx.assert_shared_clipboard("brown").await;
+
+        cx.set_shared_state(indoc! {"
                 The ˇquick brown
                 fox jumps over
-                the lazy dog"},
-        );
-        cx.assert_clipboard_content(Some(indoc! {"
-            quick brown
-            fox jumps o"}));
-        cx.assert(
-            indoc! {"
-                The quick brown
-                fox jumps over
-                the ˇlazy dog"},
-            indoc! {"
-                The quick brown
-                fox jumps over
-                the ˇlazy dog"},
-        );
-        cx.assert_clipboard_content(Some("lazy d"));
-        cx.assert(
-            indoc! {"
-                The quick brown
-                fox jumps ˇover
-                the lazy dog"},
-            indoc! {"
-                The quick brown
-                fox jumps ˇover
-                the lazy dog"},
-        );
-        cx.assert_clipboard_content(Some(indoc! {"
-                over
-                t"}));
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
+        cx.assert_shared_state(indoc! {"
+                    The ˇquick brown
+                    fox jumps over
+                    the lazy dog"})
+            .await;
+        cx.assert_shared_clipboard(indoc! {"
+                quick brown
+                fox jumps o"})
+            .await;
+
+        cx.set_shared_state(indoc! {"
+                    The quick brown
+                    fox jumps over
+                    the ˇlazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
+        cx.assert_shared_state(indoc! {"
+                    The quick brown
+                    fox jumps over
+                    the ˇlazy dog"})
+            .await;
+        cx.assert_shared_clipboard("lazy d").await;
+        cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
+        cx.assert_shared_clipboard("the lazy dog\n").await;
+
         let mut cx = cx.binding(["v", "b", "k", "y"]);
-        cx.assert(
-            indoc! {"
-                The ˇquick brown
-                fox jumps over
-                the lazy dog"},
-            indoc! {"
-                ˇThe quick brown
-                fox jumps over
-                the lazy dog"},
-        );
+        cx.set_shared_state(indoc! {"
+                    The ˇquick brown
+                    fox jumps over
+                    the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
+        cx.assert_shared_state(indoc! {"
+                    ˇThe quick brown
+                    fox jumps over
+                    the lazy dog"})
+            .await;
         cx.assert_clipboard_content(Some("The q"));
-        cx.assert(
-            indoc! {"
-                The quick brown
-                fox jumps over
-                the ˇlazy dog"},
-            indoc! {"
-                The quick brown
-                ˇfox jumps over
-                the lazy dog"},
-        );
-        cx.assert_clipboard_content(Some(indoc! {"
+    }
+
+    #[gpui::test]
+    async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {
+            "The ˇquick brown
+             fox jumps over
+             the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["ctrl-v"]).await;
+        cx.assert_shared_state(indoc! {
+            "The «qˇ»uick brown
             fox jumps over
-            the l"}));
-        cx.assert(
-            indoc! {"
-                The quick brown
-                fox jumps ˇover
-                the lazy dog"},
-            indoc! {"
-                The ˇquick brown
-                fox jumps over
-                the lazy dog"},
-        );
-        cx.assert_clipboard_content(Some(indoc! {"
-            quick brown
-            fox jumps o"}));
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["2", "down"]).await;
+        cx.assert_shared_state(indoc! {
+            "The «qˇ»uick brown
+            fox «jˇ»umps over
+            the «lˇ»azy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["e"]).await;
+        cx.assert_shared_state(indoc! {
+            "The «quicˇ»k brown
+            fox «jumpˇ»s over
+            the «lazyˇ» dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["^"]).await;
+        cx.assert_shared_state(indoc! {
+            "«ˇThe q»uick brown
+            «ˇfox j»umps over
+            «ˇthe l»azy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["$"]).await;
+        cx.assert_shared_state(indoc! {
+            "The «quick brownˇ»
+            fox «jumps overˇ»
+            the «lazy dogˇ»"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["shift-f", " "]).await;
+        cx.assert_shared_state(indoc! {
+            "The «quickˇ» brown
+            fox «jumpsˇ» over
+            the «lazy ˇ»dog"
+        })
+        .await;
+
+        // toggling through visual mode works as expected
+        cx.simulate_shared_keystrokes(["v"]).await;
+        cx.assert_shared_state(indoc! {
+            "The «quick brown
+            fox jumps over
+            the lazy ˇ»dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["ctrl-v"]).await;
+        cx.assert_shared_state(indoc! {
+            "The «quickˇ» brown
+            fox «jumpsˇ» over
+            the «lazy ˇ»dog"
+        })
+        .await;
+
+        cx.set_shared_state(indoc! {
+            "The ˇquick
+             brown
+             fox
+             jumps over the
+
+             lazy dog
+            "
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
+            .await;
+        cx.assert_shared_state(indoc! {
+            "The«ˇ q»uick
+            bro«ˇwn»
+            foxˇ
+            jumps over the
+
+            lazy dog
+            "
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["down"]).await;
+        cx.assert_shared_state(indoc! {
+            "The «qˇ»uick
+            brow«nˇ»
+            fox
+            jump«sˇ» over the
+
+            lazy dog
+            "
+        })
+        .await;
+        cx.simulate_shared_keystroke("left").await;
+        cx.assert_shared_state(indoc! {
+            "The«ˇ q»uick
+            bro«ˇwn»
+            foxˇ
+            jum«ˇps» over the
+
+            lazy dog
+            "
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
+        cx.assert_shared_state(indoc! {
+            "Theˇouick
+            broo
+            foxo
+            jumo over the
+
+            lazy dog
+            "
+        })
+        .await;
+
+        //https://github.com/zed-industries/community/issues/1950
+        cx.set_shared_state(indoc! {
+            "Theˇ quick brown
+
+            fox jumps over
+            the lazy dog
+            "
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"])
+            .await;
+        cx.assert_shared_state(indoc! {
+            "The «qˇ»uick brown
+
+            fox «jˇ»umps over
+            the lazy dog
+            "
+        })
+        .await;
+    }
+
+    #[gpui::test]
+    async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {
+            "ˇThe quick brown
+            fox jumps over
+            the lazy dog
+            "
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
+        cx.assert_shared_state(indoc! {
+            "«Tˇ»he quick brown
+            «fˇ»ox jumps over
+            «tˇ»he lazy dog
+            ˇ"
+        })
+        .await;
+
+        cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
+            .await;
+        cx.assert_shared_state(indoc! {
+            "ˇkThe quick brown
+            kfox jumps over
+            kthe lazy dog
+            k"
+        })
+        .await;
+
+        cx.set_shared_state(indoc! {
+            "ˇThe quick brown
+            fox jumps over
+            the lazy dog
+            "
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
+        cx.assert_shared_state(indoc! {
+            "«Tˇ»he quick brown
+            «fˇ»ox jumps over
+            «tˇ»he lazy dog
+            ˇ"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
+        cx.assert_shared_state(indoc! {
+            "ˇkhe quick brown
+            kox jumps over
+            khe lazy dog
+            k"
+        })
+        .await;
     }
 
     #[gpui::test]
-    async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
+    async fn test_visual_object(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("hello (in [parˇens] o)").await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
+        cx.simulate_shared_keystrokes(["a", "]"]).await;
+        cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
+        assert_eq!(cx.mode(), Mode::Visual);
+        cx.simulate_shared_keystrokes(["i", "("]).await;
+        cx.assert_shared_state("hello («in [parens] oˇ»)").await;
+
+        cx.set_shared_state("hello in a wˇord again.").await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
+            .await;
+        cx.assert_shared_state("hello in a w«ordˇ» again.").await;
+        assert_eq!(cx.mode(), Mode::VisualBlock);
+        cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
+        cx.assert_shared_state("«ˇhello in a word» again.").await;
+        assert_eq!(cx.mode(), Mode::Visual);
+    }
+
+    #[gpui::test]
+    async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
-        cx.set_state(
-            indoc! {"
-                The quick brown
-                fox «jumpsˇ» over
-                the lazy dog"},
-            Mode::Visual { line: false },
-        );
-        cx.simulate_keystroke("y");
-        cx.set_state(
-            indoc! {"
-                The quick brown
-                fox jumpˇs over
-                the lazy dog"},
-            Mode::Normal,
-        );
-        cx.simulate_keystroke("p");
-        cx.assert_state(
-            indoc! {"
-                The quick brown
-                fox jumpsjumpˇs over
-                the lazy dog"},
-            Mode::Normal,
-        );
 
-        cx.set_state(
-            indoc! {"
-                The quick brown
-                fox ju«mˇ»ps over
-                the lazy dog"},
-            Mode::Visual { line: true },
-        );
-        cx.simulate_keystroke("d");
-        cx.assert_state(
-            indoc! {"
-                The quick brown
-                the laˇzy dog"},
-            Mode::Normal,
-        );
-        cx.set_state(
-            indoc! {"
-                The quick brown
-                the «lazyˇ» dog"},
-            Mode::Visual { line: false },
-        );
-        cx.simulate_keystroke("p");
-        cx.assert_state(
-            &indoc! {"
-                The quick brown
-                the_
-                ˇfox jumps over
-                dog"}
-            .replace("_", " "), // Hack for trailing whitespace
-            Mode::Normal,
-        );
+        cx.set_state("aˇbc", Mode::Normal);
+        cx.simulate_keystrokes(["ctrl-v"]);
+        assert_eq!(cx.mode(), Mode::VisualBlock);
+        cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
+        assert_eq!(cx.mode(), Mode::VisualBlock);
     }
 }

crates/vim/test_data/test_enter_visual_line_mode.json 🔗

@@ -1,15 +1,15 @@
 {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
 {"Key":"shift-v"}
-{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":true}}}}
+{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualLine"}}
 {"Key":"x"}
 {"Get":{"state":"fox ˇjumps over\nthe lazy dog","mode":"Normal"}}
 {"Put":{"state":"a\nˇ\nb"}}
 {"Key":"shift-v"}
-{"Get":{"state":"a\n«\nˇ»b","mode":{"Visual":{"line":true}}}}
+{"Get":{"state":"a\n«\nˇ»b","mode":"VisualLine"}}
 {"Key":"x"}
 {"Get":{"state":"a\nˇb","mode":"Normal"}}
 {"Put":{"state":"a\nb\nˇ"}}
 {"Key":"shift-v"}
-{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":true}}}}
+{"Get":{"state":"a\nb\nˇ","mode":"VisualLine"}}
 {"Key":"x"}
 {"Get":{"state":"a\nˇb","mode":"Normal"}}

crates/vim/test_data/test_enter_visual_mode.json 🔗

@@ -1,20 +1,20 @@
 {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
 {"Key":"v"}
-{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"Visual"}}
 {"Key":"w"}
 {"Key":"j"}
-{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":"Visual"}}
 {"Key":"escape"}
 {"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}}
 {"Key":"v"}
 {"Key":"k"}
 {"Key":"b"}
-{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":"Visual"}}
 {"Put":{"state":"a\nˇ\nb\n"}}
 {"Key":"v"}
-{"Get":{"state":"a\n«\nˇ»b\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"a\n«\nˇ»b\n","mode":"Visual"}}
 {"Key":"v"}
 {"Get":{"state":"a\nˇ\nb\n","mode":"Normal"}}
 {"Put":{"state":"a\nb\nˇ"}}
 {"Key":"v"}
-{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"a\nb\nˇ","mode":"Visual"}}

crates/vim/test_data/test_folds.json 🔗

@@ -0,0 +1,23 @@
+{"SetOption":{"value":"foldmethod=manual"}}
+{"Put":{"state":"fn boop() {\n  ˇbarp()\n  bazp()\n}\n"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"z"}
+{"Key":"f"}
+{"Key":"escape"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇfn boop() {\n  barp()\n  bazp()\n}\n","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"j"}
+{"Get":{"state":"fn boop() {\n  barp()\n  bazp()\nˇ}\n","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"k"}
+{"Get":{"state":"ˇfn boop() {\n  barp()\n  bazp()\n}\n","mode":"Normal"}}
+{"Key":"down"}
+{"Key":"y"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"  barp()\n  bazp()\n"}}
+{"Key":"z"}
+{"Key":"o"}
+{"Get":{"state":"fn boop() {\nˇ  barp()\n  bazp()\n}\n","mode":"Normal"}}

crates/vim/test_data/test_multiline_surrounding_character_objects.json 🔗

@@ -2,9 +2,9 @@
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"{"}
-{"Get":{"state":"func empty(a string) bool {\n«   if a == \"\" {\n      return true\n   }\n   return false\nˇ»}","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"func empty(a string) bool {\n«   if a == \"\" {\n      return true\n   }\n   return false\nˇ»}","mode":"Visual"}}
 {"Put":{"state":"func empty(a string) bool {\n     if a == \"\" {\n         ˇreturn true\n     }\n     return false\n}"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"{"}
-{"Get":{"state":"func empty(a string) bool {\n     if a == \"\" {\n«         return true\nˇ»     }\n     return false\n}","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"func empty(a string) bool {\n     if a == \"\" {\n«         return true\nˇ»     }\n     return false\n}","mode":"Visual"}}

crates/vim/test_data/test_p.json 🔗

@@ -1,13 +0,0 @@
-{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
-{"Key":"d"}
-{"Key":"d"}
-{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
-{"Key":"p"}
-{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
-{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
-{"Key":"v"}
-{"Key":"w"}
-{"Key":"y"}
-{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
-{"Key":"p"}
-{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_paste.json 🔗

@@ -0,0 +1,31 @@
+{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"jumps o"}}
+{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
+{"Key":"shift-p"}
+{"Get":{"state":"The quick brown\nfox jumps ovejumps ˇor\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"d"}
+{"Key":"d"}
+{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"shift-p"}
+{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog\nfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"j"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"over\nthe lazy do"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps oˇover\nthe lazy dover\nthe lazy dog","mode":"Normal"}}
+{"Key":"u"}
+{"Key":"shift-p"}
+{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy doover\nthe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_paste_visual.json 🔗

@@ -0,0 +1,42 @@
+{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"y"}
+{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"w"}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps jumpˇs\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"over"}}
+{"Key":"up"}
+{"Key":"shift-v"}
+{"Key":"shift-p"}
+{"Get":{"state":"ˇover\nfox jumps jumps\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"over"}}
+{"Key":"ctrl-v"}
+{"Key":"down"}
+{"Key":"down"}
+{"Key":"p"}
+{"Get":{"state":"oveˇrver\noverox jumps jumps\noverhe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"shift-v"}
+{"Key":"p"}
+{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"The quick brown\n"}}

crates/vim/test_data/test_paste_visual_block.json 🔗

@@ -0,0 +1,31 @@
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"ctrl-v"}
+{"Key":"2"}
+{"Key":"j"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"q\nj\nl"}}
+{"Key":"p"}
+{"Get":{"state":"The qˇquick brown\nfox jjumps over\nthe llazy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"shift-p"}
+{"Get":{"state":"The ˇq brown\nfox jjjumps over\nthe lllazy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"shift-p"}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"q\nj"}}
+{"Key":"l"}
+{"Key":"ctrl-v"}
+{"Key":"2"}
+{"Key":"j"}
+{"Key":"shift-p"}
+{"Get":{"state":"The qˇqick brown\nfox jjmps over\nthe lzy dog","mode":"Normal"}}
+{"Key":"shift-v"}
+{"Key":"p"}
+{"Get":{"state":"ˇq\nj\nfox jjmps over\nthe lzy dog","mode":"Normal"}}

crates/vim/test_data/test_visual_block_insert.json 🔗

@@ -0,0 +1,18 @@
+{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}}
+{"Key":"ctrl-v"}
+{"Key":"9"}
+{"Key":"down"}
+{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}}
+{"Key":"shift-i"}
+{"Key":"k"}
+{"Key":"escape"}
+{"Get":{"state":"ˇkThe quick brown\nkfox jumps over\nkthe lazy dog\nk","mode":"Normal"}}
+{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}}
+{"Key":"ctrl-v"}
+{"Key":"9"}
+{"Key":"down"}
+{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Key":"escape"}
+{"Get":{"state":"ˇkhe quick brown\nkox jumps over\nkhe lazy dog\nk","mode":"Normal"}}

crates/vim/test_data/test_visual_block_mode.json 🔗

@@ -0,0 +1,38 @@
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"ctrl-v"}
+{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualBlock"}}
+{"Key":"2"}
+{"Key":"down"}
+{"Get":{"state":"The «qˇ»uick brown\nfox «jˇ»umps over\nthe «lˇ»azy dog","mode":"VisualBlock"}}
+{"Key":"e"}
+{"Get":{"state":"The «quicˇ»k brown\nfox «jumpˇ»s over\nthe «lazyˇ» dog","mode":"VisualBlock"}}
+{"Key":"^"}
+{"Get":{"state":"«ˇThe q»uick brown\n«ˇfox j»umps over\n«ˇthe l»azy dog","mode":"VisualBlock"}}
+{"Key":"$"}
+{"Get":{"state":"The «quick brownˇ»\nfox «jumps overˇ»\nthe «lazy dogˇ»","mode":"VisualBlock"}}
+{"Key":"shift-f"}
+{"Key":" "}
+{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}}
+{"Key":"v"}
+{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy ˇ»dog","mode":"Visual"}}
+{"Key":"ctrl-v"}
+{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}}
+{"Put":{"state":"The ˇquick\nbrown\nfox\njumps over the\n\nlazy dog\n"}}
+{"Key":"ctrl-v"}
+{"Key":"down"}
+{"Key":"down"}
+{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njumps over the\n\nlazy dog\n","mode":"VisualBlock"}}
+{"Key":"down"}
+{"Get":{"state":"The «qˇ»uick\nbrow«nˇ»\nfox\njump«sˇ» over the\n\nlazy dog\n","mode":"VisualBlock"}}
+{"Key":"left"}
+{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njum«ˇps» over the\n\nlazy dog\n","mode":"VisualBlock"}}
+{"Key":"s"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}}
+{"Put":{"state":"Theˇ quick brown\n\nfox jumps over\nthe lazy dog\n"}}
+{"Key":"l"}
+{"Key":"ctrl-v"}
+{"Key":"j"}
+{"Key":"j"}
+{"Get":{"state":"The «qˇ»uick brown\n\nfox «jˇ»umps over\nthe lazy dog\n","mode":"VisualBlock"}}

crates/vim/test_data/test_visual_delete.json 🔗

@@ -1,7 +1,7 @@
 {"Put":{"state":"The quick ˇbrown"}}
 {"Key":"v"}
 {"Key":"w"}
-{"Get":{"state":"The quick «brownˇ»","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»","mode":"Visual"}}
 {"Put":{"state":"The quick ˇbrown"}}
 {"Key":"v"}
 {"Key":"w"}

crates/vim/test_data/test_visual_line_delete.json 🔗

@@ -4,14 +4,11 @@
 {"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}}
 {"Key":"p"}
 {"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}}
-{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
-{"Key":"shift-v"}
-{"Key":"x"}
-{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
 {"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
 {"Key":"shift-v"}
 {"Key":"x"}
 {"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}}
 {"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
 {"Key":"shift-v"}
 {"Key":"j"}
@@ -19,16 +16,6 @@
 {"Get":{"state":"the laˇzy dog","mode":"Normal"}}
 {"Key":"p"}
 {"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}}
-{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
-{"Key":"shift-v"}
-{"Key":"j"}
-{"Key":"x"}
-{"Get":{"state":"The quˇick brown","mode":"Normal"}}
-{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
-{"Key":"shift-v"}
-{"Key":"j"}
-{"Key":"x"}
-{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
 {"Put":{"state":"The ˇlong line\nshould not\ncrash\n"}}
 {"Key":"shift-v"}
 {"Key":"$"}

crates/vim/test_data/test_visual_object.json 🔗

@@ -0,0 +1,19 @@
+{"Put":{"state":"hello (in [parˇens] o)"}}
+{"Key":"ctrl-v"}
+{"Key":"l"}
+{"Key":"a"}
+{"Key":"]"}
+{"Get":{"state":"hello (in «[parens]ˇ» o)","mode":"Visual"}}
+{"Key":"i"}
+{"Key":"("}
+{"Get":{"state":"hello («in [parens] oˇ»)","mode":"Visual"}}
+{"Put":{"state":"hello in a wˇord again."}}
+{"Key":"ctrl-v"}
+{"Key":"l"}
+{"Key":"i"}
+{"Key":"w"}
+{"Get":{"state":"hello in a w«ordˇ» again.","mode":"VisualBlock"}}
+{"Key":"o"}
+{"Key":"a"}
+{"Key":"s"}
+{"Get":{"state":"«ˇhello in a word» again.","mode":"VisualBlock"}}

crates/vim/test_data/test_visual_paste.json 🔗

@@ -0,0 +1,26 @@
+{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"y"}
+{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jjumpˇsumps over\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"k"}
+{"Key":"shift-v"}
+{"Key":"p"}
+{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_visual_word_object.json 🔗

@@ -1,236 +1,236 @@
 {"Put":{"state":"The quick ˇbrown\nfox"}}
 {"Key":"v"}
-{"Get":{"state":"The quick «bˇ»rown\nfox","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «bˇ»rown\nfox","mode":"Visual"}}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick «brownˇ»\nfox","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»\nfox","mode":"Visual"}}
 {"Put":{"state":"The quick ˇbrown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick browˇn   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brownˇ   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown«   ˇ»\nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown«   ˇ»\nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox ˇjumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox juˇmps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumpsˇ over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps« ˇ»over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps« ˇ»over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dogˇ  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog«  ˇ»\n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog«  ˇ»\n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \nˇ\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n«\nˇ»\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n«\nˇ»\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\nˇ\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n«\nˇ»\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n«\nˇ»\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\nˇ\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n«\nˇ»The-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n«\nˇ»The-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThˇe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«Theˇ»-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«Theˇ»-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nTheˇ-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe«-ˇ»quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe«-ˇ»quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-ˇquick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-«quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-«quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quˇick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-«quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-«quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quickˇ brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick« ˇ»brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick« ˇ»brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick ˇbrown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick «brownˇ» \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick «brownˇ» \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brownˇ \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown« ˇ»\n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown« ˇ»\n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \nˇ  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n«  ˇ»\n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n«  ˇ»\n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \nˇ  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n«  ˇ»\n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n«  ˇ»\n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \nˇ  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n«  ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n«  ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumpˇs over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dogˇ \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \nˇ\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}}
 {"Put":{"state":"The quick ˇbrown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick browˇn   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick «brownˇ»   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brownˇ   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown«   ˇ»\nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown«   ˇ»\nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox ˇjumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox juˇmps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox «jumpsˇ» over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumpsˇ over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps« ˇ»over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps« ˇ»over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dogˇ  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog«  ˇ»\n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog«  ˇ»\n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \nˇ\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n«\nˇ»\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n«\nˇ»\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\nˇ\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n«\nˇ»\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n«\nˇ»\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\nˇ\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n«\nˇ»The-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n«\nˇ»The-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThˇe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nTheˇ-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-ˇquick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quˇick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\n«The-quickˇ» brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quickˇ brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick« ˇ»brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick« ˇ»brown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick ˇbrown \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick «brownˇ» \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick «brownˇ» \n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brownˇ \n  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown« ˇ»\n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown« ˇ»\n  \n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \nˇ  \n  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n«  ˇ»\n  \n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n«  ˇ»\n  \n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \nˇ  \n  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n«  ˇ»\n  fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n«  ˇ»\n  fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \nˇ  fox-jumps over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n«  ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n«  ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumpˇs over\nthe lazy dog \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dogˇ \n\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}}
 {"Put":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \nˇ\n"}}
 {"Key":"v"}
 {"Key":"i"}
 {"Key":"shift-w"}
-{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}}
+{"Get":{"state":"The quick brown   \nfox jumps over\nthe lazy dog  \n\n\n\nThe-quick brown \n  \n  \n  fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}}

crates/vim/test_data/test_visual_yank.json 🔗

@@ -0,0 +1,29 @@
+{"Put":{"state":"The quick ˇbrown"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"y"}
+{"Get":{"state":"The quick ˇbrown","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"brown"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"y"}
+{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"quick brown\nfox jumps o"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"y"}
+{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"lazy d"}}
+{"Key":"shift-v"}
+{"Key":"y"}
+{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"y"}
+{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_wrapped_lines.json 🔗

@@ -0,0 +1,50 @@
+{"SetOption":{"value":"wrap"}}
+{"SetOption":{"value":"columns=12"}}
+{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}}
+{"Key":"j"}
+{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}}
+{"Key":"k"}
+{"Get":{"state":"tˇwelve char twelve char\ntwelve char\n","mode":"Normal"}}
+{"Key":"g"}
+{"Key":"j"}
+{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}}
+{"Key":"g"}
+{"Key":"j"}
+{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}}
+{"Key":"g"}
+{"Key":"k"}
+{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}}
+{"Key":"g"}
+{"Key":"^"}
+{"Get":{"state":"twelve char ˇtwelve char\ntwelve char\n","mode":"Normal"}}
+{"Key":"^"}
+{"Get":{"state":"ˇtwelve char twelve char\ntwelve char\n","mode":"Normal"}}
+{"Key":"g"}
+{"Key":"$"}
+{"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}}
+{"Key":"$"}
+{"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}}
+{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}}
+{"Key":"enter"}
+{"Get":{"state":"twelve char twelve char\nˇtwelve char\n","mode":"Normal"}}
+{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}}
+{"Key":"o"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Get":{"state":"twelve char\ntwelve char twelve char\nˇo\ntwelve char\n","mode":"Normal"}}
+{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}}
+{"Key":"shift-a"}
+{"Key":"a"}
+{"Key":"escape"}
+{"Get":{"state":"twelve char\ntwelve char twelve charˇa\ntwelve char\n","mode":"Normal"}}
+{"Key":"shift-i"}
+{"Key":"i"}
+{"Key":"escape"}
+{"Get":{"state":"twelve char\nˇitwelve char twelve chara\ntwelve char\n","mode":"Normal"}}
+{"Key":"shift-d"}
+{"Get":{"state":"twelve char\nˇ\ntwelve char\n","mode":"Normal"}}
+{"Put":{"state":"twelve char\ntwelve char tˇwelve char\ntwelve char\n"}}
+{"Key":"shift-o"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}}

crates/welcome/src/welcome.rs 🔗

@@ -232,7 +232,7 @@ impl Item for WelcomePage {
         Some("Welcome to Zed!".into())
     }
 
-    fn tab_content<T: View>(
+    fn tab_content<T: 'static>(
         &self,
         _detail: Option<usize>,
         style: &theme::Tab,

crates/workspace/Cargo.toml 🔗

@@ -22,6 +22,7 @@ test-support = [
 db = { path = "../db" }
 call = { path = "../call" }
 client = { path = "../client" }
+channel = { path = "../channel" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
 drag_and_drop = { path = "../drag_and_drop" }

crates/workspace/src/dock.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{StatusItemView, Workspace};
+use crate::{StatusItemView, Workspace, WorkspaceBounds};
 use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
@@ -13,20 +13,30 @@ pub trait Panel: View {
     fn position_is_valid(&self, position: DockPosition) -> bool;
     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
     fn size(&self, cx: &WindowContext) -> f32;
-    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>);
-    fn icon_path(&self) -> &'static str;
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
+    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
     fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
     fn icon_label(&self, _: &WindowContext) -> Option<String> {
         None
     }
     fn should_change_position_on_event(_: &Self::Event) -> bool;
-    fn should_zoom_in_on_event(_: &Self::Event) -> bool;
-    fn should_zoom_out_on_event(_: &Self::Event) -> bool;
-    fn is_zoomed(&self, cx: &WindowContext) -> bool;
-    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>);
-    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>);
-    fn should_activate_on_event(_: &Self::Event) -> bool;
-    fn should_close_on_event(_: &Self::Event) -> bool;
+    fn should_zoom_in_on_event(_: &Self::Event) -> bool {
+        false
+    }
+    fn should_zoom_out_on_event(_: &Self::Event) -> bool {
+        false
+    }
+    fn is_zoomed(&self, _cx: &WindowContext) -> bool {
+        false
+    }
+    fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
+    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
+    fn should_activate_on_event(_: &Self::Event) -> bool {
+        false
+    }
+    fn should_close_on_event(_: &Self::Event) -> bool {
+        false
+    }
     fn has_focus(&self, cx: &WindowContext) -> bool;
     fn is_focus_event(_: &Self::Event) -> bool;
 }
@@ -40,8 +50,8 @@ pub trait PanelHandle {
     fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
     fn set_active(&self, active: bool, cx: &mut WindowContext);
     fn size(&self, cx: &WindowContext) -> f32;
-    fn set_size(&self, size: f32, cx: &mut WindowContext);
-    fn icon_path(&self, cx: &WindowContext) -> &'static str;
+    fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
+    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
     fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
     fn icon_label(&self, cx: &WindowContext) -> Option<String>;
     fn has_focus(&self, cx: &WindowContext) -> bool;
@@ -72,7 +82,7 @@ where
         self.read(cx).size(cx)
     }
 
-    fn set_size(&self, size: f32, cx: &mut WindowContext) {
+    fn set_size(&self, size: Option<f32>, cx: &mut WindowContext) {
         self.update(cx, |this, cx| this.set_size(size, cx))
     }
 
@@ -88,8 +98,8 @@ where
         self.update(cx, |this, cx| this.set_active(active, cx))
     }
 
-    fn icon_path(&self, cx: &WindowContext) -> &'static str {
-        self.read(cx).icon_path()
+    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
+        self.read(cx).icon_path(cx)
     }
 
     fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
@@ -363,7 +373,7 @@ impl Dock {
         }
     }
 
-    pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext<Self>) {
+    pub fn resize_active_panel(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
         if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
             entry.panel.set_size(size, cx);
             cx.notify();
@@ -376,7 +386,7 @@ impl Dock {
                 .into_any()
                 .contained()
                 .with_style(self.style(cx))
-                .resizable(
+                .resizable::<WorkspaceBounds>(
                     self.position.to_resize_handle_side(),
                     active_entry.panel.size(cx),
                     |_, _, _| {},
@@ -413,7 +423,7 @@ impl View for Dock {
             ChildView::new(active_entry.panel.as_any(), cx)
                 .contained()
                 .with_style(style)
-                .resizable(
+                .resizable::<WorkspaceBounds>(
                     self.position.to_resize_handle_side(),
                     active_entry.panel.size(cx),
                     |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
@@ -480,8 +490,9 @@ impl View for PanelButtons {
             .map(|item| (item.panel.clone(), item.context_menu.clone()))
             .collect::<Vec<_>>();
         Flex::row()
-            .with_children(panels.into_iter().enumerate().map(
+            .with_children(panels.into_iter().enumerate().filter_map(
                 |(panel_ix, (view, context_menu))| {
+                    let icon_path = view.icon_path(cx)?;
                     let is_active = is_open && panel_ix == active_ix;
                     let (tooltip, tooltip_action) = if is_active {
                         (
@@ -495,92 +506,95 @@ impl View for PanelButtons {
                     } else {
                         view.icon_tooltip(cx)
                     };
-                    Stack::new()
-                        .with_child(
-                            MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
-                                let style = button_style.in_state(is_active);
-                                let style = style.style_for(state);
-                                Flex::row()
-                                    .with_child(
-                                        Svg::new(view.icon_path(cx))
-                                            .with_color(style.icon_color)
-                                            .constrained()
-                                            .with_width(style.icon_size)
-                                            .aligned(),
-                                    )
-                                    .with_children(if let Some(label) = view.icon_label(cx) {
-                                        Some(
-                                            Label::new(label, style.label.text.clone())
-                                                .contained()
-                                                .with_style(style.label.container)
+                    Some(
+                        Stack::new()
+                            .with_child(
+                                MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
+                                    let style = button_style.in_state(is_active);
+
+                                    let style = style.style_for(state);
+                                    Flex::row()
+                                        .with_child(
+                                            Svg::new(icon_path)
+                                                .with_color(style.icon_color)
+                                                .constrained()
+                                                .with_width(style.icon_size)
                                                 .aligned(),
                                         )
-                                    } else {
-                                        None
-                                    })
-                                    .constrained()
-                                    .with_height(style.icon_size)
-                                    .contained()
-                                    .with_style(style.container)
-                            })
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_click(MouseButton::Left, {
-                                let tooltip_action =
-                                    tooltip_action.as_ref().map(|action| action.boxed_clone());
-                                move |_, this, cx| {
-                                    if let Some(tooltip_action) = &tooltip_action {
-                                        let window = cx.window();
-                                        let view_id = this.workspace.id();
-                                        let tooltip_action = tooltip_action.boxed_clone();
-                                        cx.spawn(|_, mut cx| async move {
-                                            window.dispatch_action(
-                                                view_id,
-                                                &*tooltip_action,
-                                                &mut cx,
-                                            );
+                                        .with_children(if let Some(label) = view.icon_label(cx) {
+                                            Some(
+                                                Label::new(label, style.label.text.clone())
+                                                    .contained()
+                                                    .with_style(style.label.container)
+                                                    .aligned(),
+                                            )
+                                        } else {
+                                            None
                                         })
-                                        .detach();
-                                    }
-                                }
-                            })
-                            .on_click(MouseButton::Right, {
-                                let view = view.clone();
-                                let menu = context_menu.clone();
-                                move |_, _, cx| {
-                                    const POSITIONS: [DockPosition; 3] = [
-                                        DockPosition::Left,
-                                        DockPosition::Right,
-                                        DockPosition::Bottom,
-                                    ];
-
-                                    menu.update(cx, |menu, cx| {
-                                        let items = POSITIONS
-                                            .into_iter()
-                                            .filter(|position| {
-                                                *position != dock_position
-                                                    && view.position_is_valid(*position, cx)
-                                            })
-                                            .map(|position| {
-                                                let view = view.clone();
-                                                ContextMenuItem::handler(
-                                                    format!("Dock {}", position.to_label()),
-                                                    move |cx| view.set_position(position, cx),
-                                                )
+                                        .constrained()
+                                        .with_height(style.icon_size)
+                                        .contained()
+                                        .with_style(style.container)
+                                })
+                                .with_cursor_style(CursorStyle::PointingHand)
+                                .on_click(MouseButton::Left, {
+                                    let tooltip_action =
+                                        tooltip_action.as_ref().map(|action| action.boxed_clone());
+                                    move |_, this, cx| {
+                                        if let Some(tooltip_action) = &tooltip_action {
+                                            let window = cx.window();
+                                            let view_id = this.workspace.id();
+                                            let tooltip_action = tooltip_action.boxed_clone();
+                                            cx.spawn(|_, mut cx| async move {
+                                                window.dispatch_action(
+                                                    view_id,
+                                                    &*tooltip_action,
+                                                    &mut cx,
+                                                );
                                             })
-                                            .collect();
-                                        menu.show(Default::default(), menu_corner, items, cx);
-                                    })
-                                }
-                            })
-                            .with_tooltip::<Self>(
-                                panel_ix,
-                                tooltip,
-                                tooltip_action,
-                                tooltip_style.clone(),
-                                cx,
-                            ),
-                        )
-                        .with_child(ChildView::new(&context_menu, cx))
+                                            .detach();
+                                        }
+                                    }
+                                })
+                                .on_click(MouseButton::Right, {
+                                    let view = view.clone();
+                                    let menu = context_menu.clone();
+                                    move |_, _, cx| {
+                                        const POSITIONS: [DockPosition; 3] = [
+                                            DockPosition::Left,
+                                            DockPosition::Right,
+                                            DockPosition::Bottom,
+                                        ];
+
+                                        menu.update(cx, |menu, cx| {
+                                            let items = POSITIONS
+                                                .into_iter()
+                                                .filter(|position| {
+                                                    *position != dock_position
+                                                        && view.position_is_valid(*position, cx)
+                                                })
+                                                .map(|position| {
+                                                    let view = view.clone();
+                                                    ContextMenuItem::handler(
+                                                        format!("Dock {}", position.to_label()),
+                                                        move |cx| view.set_position(position, cx),
+                                                    )
+                                                })
+                                                .collect();
+                                            menu.show(Default::default(), menu_corner, items, cx);
+                                        })
+                                    }
+                                })
+                                .with_tooltip::<Self>(
+                                    panel_ix,
+                                    tooltip,
+                                    tooltip_action,
+                                    tooltip_style.clone(),
+                                    cx,
+                                ),
+                            )
+                            .with_child(ChildView::new(&context_menu, cx)),
+                    )
                 },
             ))
             .contained()
@@ -686,12 +700,12 @@ pub mod test {
             self.size
         }
 
-        fn set_size(&mut self, size: f32, _: &mut ViewContext<Self>) {
-            self.size = size;
+        fn set_size(&mut self, size: Option<f32>, _: &mut ViewContext<Self>) {
+            self.size = size.unwrap_or(300.);
         }
 
-        fn icon_path(&self) -> &'static str {
-            "icons/test_panel.svg"
+        fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
+            Some("icons/test_panel.svg")
         }
 
         fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {

crates/workspace/src/item.rs 🔗

@@ -101,7 +101,7 @@ pub trait Item: View {
     fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<str>> {
         None
     }
-    fn tab_content<V: View>(
+    fn tab_content<V: 'static>(
         &self,
         detail: Option<usize>,
         style: &theme::Tab,
@@ -158,9 +158,7 @@ pub trait Item: View {
     fn should_update_tab_on_event(_: &Self::Event) -> bool {
         false
     }
-    fn is_edit_event(_: &Self::Event) -> bool {
-        false
-    }
+
     fn act_as_type<'a>(
         &'a self,
         type_id: TypeId,
@@ -205,7 +203,7 @@ pub trait Item: View {
     fn show_toolbar(&self) -> bool {
         true
     }
-    fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
+    fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
         None
     }
 }
@@ -623,7 +621,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
     }
 
     fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
-        self.read(cx).pixel_position_of_cursor()
+        self.read(cx).pixel_position_of_cursor(cx)
     }
 }
 
@@ -674,7 +672,7 @@ pub trait FollowableItem: Item {
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
     fn from_state_proto(
         pane: ViewHandle<Pane>,
-        project: ModelHandle<Project>,
+        project: ViewHandle<Workspace>,
         id: ViewId,
         state: &mut Option<proto::view::Variant>,
         cx: &mut AppContext,
@@ -943,7 +941,7 @@ pub mod test {
             })
         }
 
-        fn tab_content<V: View>(
+        fn tab_content<V: 'static>(
             &self,
             detail: Option<usize>,
             _: &theme::Tab,

crates/workspace/src/pane.rs 🔗

@@ -222,6 +222,56 @@ impl TabBarContextMenu {
     }
 }
 
+#[allow(clippy::too_many_arguments)]
+fn nav_button<A: Action, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>)>(
+    svg_path: &'static str,
+    style: theme::Interactive<theme::IconButton>,
+    nav_button_height: f32,
+    tooltip_style: TooltipStyle,
+    enabled: bool,
+    on_click: F,
+    tooltip_action: A,
+    action_name: &str,
+    cx: &mut ViewContext<Pane>,
+) -> AnyElement<Pane> {
+    MouseEventHandler::new::<A, _>(0, cx, |state, _| {
+        let style = if enabled {
+            style.style_for(state)
+        } else {
+            style.disabled_style()
+        };
+        Svg::new(svg_path)
+            .with_color(style.color)
+            .constrained()
+            .with_width(style.icon_width)
+            .aligned()
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_width(style.button_width)
+            .with_height(nav_button_height)
+            .aligned()
+            .top()
+    })
+    .with_cursor_style(if enabled {
+        CursorStyle::PointingHand
+    } else {
+        CursorStyle::default()
+    })
+    .on_click(MouseButton::Left, move |_, toolbar, cx| {
+        on_click(toolbar, cx)
+    })
+    .with_tooltip::<A>(
+        0,
+        action_name.to_string(),
+        Some(Box::new(tooltip_action)),
+        tooltip_style,
+        cx,
+    )
+    .contained()
+    .into_any_named("nav button")
+}
+
 impl Pane {
     pub fn new(
         workspace: WeakViewHandle<Workspace>,
@@ -253,7 +303,7 @@ impl Pane {
                 pane: handle.clone(),
                 next_timestamp,
             }))),
-            toolbar: cx.add_view(|_| Toolbar::new(Some(handle))),
+            toolbar: cx.add_view(|_| Toolbar::new()),
             tab_bar_context_menu: TabBarContextMenu {
                 kind: TabBarContextMenuKind::New,
                 handle: context_menu,
@@ -265,7 +315,7 @@ impl Pane {
             has_focus: false,
             can_drop: Rc::new(|_, _| true),
             can_split: true,
-            render_tab_bar_buttons: Rc::new(|pane, cx| {
+            render_tab_bar_buttons: Rc::new(move |pane, cx| {
                 Flex::row()
                     // New menu
                     .with_child(Self::render_tab_bar_button(
@@ -1571,8 +1621,70 @@ impl View for Pane {
                                 },
                             ),
                         );
+                        let tooltip_style = theme.tooltip.clone();
+                        let tab_bar_theme = theme.workspace.tab_bar.clone();
+
+                        let nav_button_height = tab_bar_theme.height;
+                        let button_style = tab_bar_theme.nav_button;
+                        let border_for_nav_buttons = tab_bar_theme
+                            .tab_style(false, false)
+                            .container
+                            .border
+                            .clone();
 
                         let mut tab_row = Flex::row()
+                            .with_child(nav_button(
+                                "icons/arrow_left_16.svg",
+                                button_style.clone(),
+                                nav_button_height,
+                                tooltip_style.clone(),
+                                self.can_navigate_backward(),
+                                {
+                                    move |pane, cx| {
+                                        if let Some(workspace) = pane.workspace.upgrade(cx) {
+                                            let pane = cx.weak_handle();
+                                            cx.window_context().defer(move |cx| {
+                                                workspace.update(cx, |workspace, cx| {
+                                                    workspace
+                                                        .go_back(pane, cx)
+                                                        .detach_and_log_err(cx)
+                                                })
+                                            })
+                                        }
+                                    }
+                                },
+                                super::GoBack,
+                                "Go Back",
+                                cx,
+                            ))
+                            .with_child(
+                                nav_button(
+                                    "icons/arrow_right_16.svg",
+                                    button_style.clone(),
+                                    nav_button_height,
+                                    tooltip_style,
+                                    self.can_navigate_forward(),
+                                    {
+                                        move |pane, cx| {
+                                            if let Some(workspace) = pane.workspace.upgrade(cx) {
+                                                let pane = cx.weak_handle();
+                                                cx.window_context().defer(move |cx| {
+                                                    workspace.update(cx, |workspace, cx| {
+                                                        workspace
+                                                            .go_forward(pane, cx)
+                                                            .detach_and_log_err(cx)
+                                                    })
+                                                })
+                                            }
+                                        }
+                                    },
+                                    super::GoForward,
+                                    "Go Forward",
+                                    cx,
+                                )
+                                .contained()
+                                .with_border(border_for_nav_buttons),
+                            )
                             .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
 
                         if self.has_focus {
@@ -1864,12 +1976,12 @@ impl NavHistoryState {
     }
 }
 
-pub struct PaneBackdrop<V: View> {
+pub struct PaneBackdrop<V> {
     child_view: usize,
     child: AnyElement<V>,
 }
 
-impl<V: View> PaneBackdrop<V> {
+impl<V> PaneBackdrop<V> {
     pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
         PaneBackdrop {
             child,
@@ -1878,7 +1990,7 @@ impl<V: View> PaneBackdrop<V> {
     }
 }
 
-impl<V: View> Element<V> for PaneBackdrop<V> {
+impl<V: 'static> Element<V> for PaneBackdrop<V> {
     type LayoutState = ();
 
     type PaintState = ();

crates/workspace/src/pane/dragged_item_receiver.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     geometry::{rect::RectF, vector::Vector2F},
     platform::MouseButton,
     scene::MouseUp,
-    AppContext, Element, EventContext, MouseState, Quad, View, ViewContext, WeakViewHandle,
+    AppContext, Element, EventContext, MouseState, Quad, ViewContext, WeakViewHandle,
 };
 use project::ProjectEntryId;
 
@@ -42,7 +42,11 @@ where
     let mut handler = MouseEventHandler::above::<Tag, _>(region_id, cx, |state, cx| {
         // Observing hovered will cause a render when the mouse enters regardless
         // of if mouse position was accessed before
-        let drag_position = if state.hovered() { drag_position } else { None };
+        let drag_position = if state.dragging() {
+            drag_position
+        } else {
+            None
+        };
         Stack::new()
             .with_child(render_child(state, cx))
             .with_children(drag_position.map(|drag_position| {
@@ -107,7 +111,7 @@ where
     handler
 }
 
-pub fn handle_dropped_item<V: View>(
+pub fn handle_dropped_item<V: 'static>(
     event: MouseUp,
     workspace: WeakViewHandle<Workspace>,
     pane: &WeakViewHandle<Pane>,

crates/workspace/src/pane_group.rs 🔗

@@ -742,8 +742,8 @@ mod element {
 
                 while proposed_current_pixel_change.abs() > 0. {
                     let Some(current_ix) = successors.next() else {
-                            break;
-                        };
+                        break;
+                    };
 
                     let next_target_size = f32::max(
                         size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,

crates/workspace/src/toolbar.rs 🔗

@@ -1,7 +1,7 @@
-use crate::{ItemHandle, Pane};
+use crate::ItemHandle;
 use gpui::{
-    elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyElement, AnyViewHandle,
-    AppContext, Entity, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    elements::*, AnyElement, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle,
+    WindowContext,
 };
 
 pub trait ToolbarItemView: View {
@@ -25,7 +25,7 @@ pub trait ToolbarItemView: View {
     /// Number of times toolbar's height will be repeated to get the effective height.
     /// Useful when multiple rows one under each other are needed.
     /// The rows have the same width and act as a whole when reacting to resizes and similar events.
-    fn row_count(&self) -> usize {
+    fn row_count(&self, _cx: &ViewContext<Self>) -> usize {
         1
     }
 }
@@ -54,7 +54,6 @@ pub struct Toolbar {
     active_item: Option<Box<dyn ItemHandle>>,
     hidden: bool,
     can_navigate: bool,
-    pane: Option<WeakViewHandle<Pane>>,
     items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
 }
 
@@ -82,10 +81,7 @@ impl View for Toolbar {
 
                 ToolbarItemLocation::PrimaryLeft { flex } => {
                     primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-                    let left_item = ChildView::new(item.as_any(), cx)
-                        .aligned()
-                        .contained()
-                        .with_margin_right(spacing);
+                    let left_item = ChildView::new(item.as_any(), cx).aligned();
                     if let Some((flex, expanded)) = flex {
                         primary_left_items.push(left_item.flex(flex, expanded).into_any());
                     } else {
@@ -95,11 +91,7 @@ impl View for Toolbar {
 
                 ToolbarItemLocation::PrimaryRight { flex } => {
                     primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
-                    let right_item = ChildView::new(item.as_any(), cx)
-                        .aligned()
-                        .contained()
-                        .with_margin_left(spacing)
-                        .flex_float();
+                    let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float();
                     if let Some((flex, expanded)) = flex {
                         primary_right_items.push(right_item.flex(flex, expanded).into_any());
                     } else {
@@ -118,76 +110,10 @@ impl View for Toolbar {
             }
         }
 
-        let pane = self.pane.clone();
-        let mut enable_go_backward = false;
-        let mut enable_go_forward = false;
-        if let Some(pane) = pane.and_then(|pane| pane.upgrade(cx)) {
-            let pane = pane.read(cx);
-            enable_go_backward = pane.can_navigate_backward();
-            enable_go_forward = pane.can_navigate_forward();
-        }
-
         let container_style = theme.container;
         let height = theme.height * primary_items_row_count as f32;
-        let nav_button_height = theme.height;
-        let button_style = theme.nav_button;
-        let tooltip_style = theme::current(cx).tooltip.clone();
-
-        let mut primary_items = Flex::row();
-        if self.can_navigate {
-            primary_items.add_child(nav_button(
-                "icons/arrow_left_16.svg",
-                button_style,
-                nav_button_height,
-                tooltip_style.clone(),
-                enable_go_backward,
-                spacing,
-                {
-                    move |toolbar, cx| {
-                        if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
-                        {
-                            if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
-                                let pane = pane.downgrade();
-                                cx.window_context().defer(move |cx| {
-                                    workspace.update(cx, |workspace, cx| {
-                                        workspace.go_back(pane, cx).detach_and_log_err(cx);
-                                    });
-                                })
-                            }
-                        }
-                    }
-                },
-                super::GoBack,
-                "Go Back",
-                cx,
-            ));
-            primary_items.add_child(nav_button(
-                "icons/arrow_right_16.svg",
-                button_style,
-                nav_button_height,
-                tooltip_style,
-                enable_go_forward,
-                spacing,
-                {
-                    move |toolbar, cx| {
-                        if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
-                        {
-                            if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
-                                let pane = pane.downgrade();
-                                cx.window_context().defer(move |cx| {
-                                    workspace.update(cx, |workspace, cx| {
-                                        workspace.go_forward(pane, cx).detach_and_log_err(cx);
-                                    });
-                                })
-                            }
-                        }
-                    }
-                },
-                super::GoForward,
-                "Go Forward",
-                cx,
-            ));
-        }
+
+        let mut primary_items = Flex::row().with_spacing(spacing);
         primary_items.extend(primary_left_items);
         primary_items.extend(primary_right_items);
 
@@ -210,63 +136,65 @@ impl View for Toolbar {
     }
 }
 
-#[allow(clippy::too_many_arguments)]
-fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
-    svg_path: &'static str,
-    style: theme::Interactive<theme::IconButton>,
-    nav_button_height: f32,
-    tooltip_style: TooltipStyle,
-    enabled: bool,
-    spacing: f32,
-    on_click: F,
-    tooltip_action: A,
-    action_name: &'static str,
-    cx: &mut ViewContext<Toolbar>,
-) -> AnyElement<Toolbar> {
-    MouseEventHandler::new::<A, _>(0, cx, |state, _| {
-        let style = if enabled {
-            style.style_for(state)
-        } else {
-            style.disabled_style()
-        };
-        Svg::new(svg_path)
-            .with_color(style.color)
-            .constrained()
-            .with_width(style.icon_width)
-            .aligned()
-            .contained()
-            .with_style(style.container)
-            .constrained()
-            .with_width(style.button_width)
-            .with_height(nav_button_height)
-            .aligned()
-            .top()
-    })
-    .with_cursor_style(if enabled {
-        CursorStyle::PointingHand
-    } else {
-        CursorStyle::default()
-    })
-    .on_click(MouseButton::Left, move |_, toolbar, cx| {
-        on_click(toolbar, cx)
-    })
-    .with_tooltip::<A>(
-        0,
-        action_name,
-        Some(Box::new(tooltip_action)),
-        tooltip_style,
-        cx,
-    )
-    .contained()
-    .with_margin_right(spacing)
-    .into_any_named("nav button")
-}
-
+// <<<<<<< HEAD
+// =======
+// #[allow(clippy::too_many_arguments)]
+// fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
+//     svg_path: &'static str,
+//     style: theme::Interactive<theme::IconButton>,
+//     nav_button_height: f32,
+//     tooltip_style: TooltipStyle,
+//     enabled: bool,
+//     spacing: f32,
+//     on_click: F,
+//     tooltip_action: A,
+//     action_name: &'static str,
+//     cx: &mut ViewContext<Toolbar>,
+// ) -> AnyElement<Toolbar> {
+//     MouseEventHandler::new::<A, _>(0, cx, |state, _| {
+//         let style = if enabled {
+//             style.style_for(state)
+//         } else {
+//             style.disabled_style()
+//         };
+//         Svg::new(svg_path)
+//             .with_color(style.color)
+//             .constrained()
+//             .with_width(style.icon_width)
+//             .aligned()
+//             .contained()
+//             .with_style(style.container)
+//             .constrained()
+//             .with_width(style.button_width)
+//             .with_height(nav_button_height)
+//             .aligned()
+//             .top()
+//     })
+//     .with_cursor_style(if enabled {
+//         CursorStyle::PointingHand
+//     } else {
+//         CursorStyle::default()
+//     })
+//     .on_click(MouseButton::Left, move |_, toolbar, cx| {
+//         on_click(toolbar, cx)
+//     })
+//     .with_tooltip::<A>(
+//         0,
+//         action_name,
+//         Some(Box::new(tooltip_action)),
+//         tooltip_style,
+//         cx,
+//     )
+//     .contained()
+//     .with_margin_right(spacing)
+//     .into_any_named("nav button")
+// }
+
+// >>>>>>> 139cbbfd3aebd0863a7d51b0c12d748764cf0b2e
 impl Toolbar {
-    pub fn new(pane: Option<WeakViewHandle<Pane>>) -> Self {
+    pub fn new() -> Self {
         Self {
             active_item: None,
-            pane,
             items: Default::default(),
             hidden: false,
             can_navigate: true,
@@ -362,7 +290,7 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
     }
 
     fn row_count(&self, cx: &WindowContext) -> usize {
-        self.read(cx).row_count()
+        self.read_with(cx, |this, cx| this.row_count(cx))
     }
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -12,6 +12,7 @@ mod workspace_settings;
 
 use anyhow::{anyhow, Context, Result};
 use call::ActiveCall;
+use channel::ChannelStore;
 use client::{
     proto::{self, PeerId},
     Client, TypedEnvelope, UserStore,
@@ -344,7 +345,7 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
 
 type FollowableItemBuilder = fn(
     ViewHandle<Pane>,
-    ModelHandle<Project>,
+    ViewHandle<Workspace>,
     ViewId,
     &mut Option<proto::view::Variant>,
     &mut AppContext,
@@ -361,8 +362,8 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
         builders.insert(
             TypeId::of::<I>(),
             (
-                |pane, project, id, state, cx| {
-                    I::from_state_proto(pane, project, id, state, cx).map(|task| {
+                |pane, workspace, id, state, cx| {
+                    I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
                         cx.foreground()
                             .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
                     })
@@ -400,8 +401,9 @@ pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
 
 pub struct AppState {
     pub languages: Arc<LanguageRegistry>,
-    pub client: Arc<client::Client>,
-    pub user_store: ModelHandle<client::UserStore>,
+    pub client: Arc<Client>,
+    pub user_store: ModelHandle<UserStore>,
+    pub channel_store: ModelHandle<ChannelStore>,
     pub fs: Arc<dyn fs::Fs>,
     pub build_window_options:
         fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
@@ -424,6 +426,8 @@ impl AppState {
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = Client::new(http_client.clone(), cx);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+        let channel_store =
+            cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
 
         theme::init((), cx);
         client::init(&client, cx);
@@ -434,6 +438,7 @@ impl AppState {
             fs,
             languages,
             user_store,
+            channel_store,
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             build_window_options: |_, _, _| Default::default(),
             background_actions: || &[],
@@ -549,6 +554,8 @@ struct FollowerState {
     items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
 }
 
+enum WorkspaceBounds {}
+
 impl Workspace {
     pub fn new(
         workspace_id: WorkspaceId,
@@ -2307,8 +2314,12 @@ impl Workspace {
         item_id_to_move: usize,
         cx: &mut ViewContext<Self>,
     ) {
-        let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
-        let Some(from) = from.upgrade(cx) else { return; };
+        let Some(pane_to_split) = pane_to_split.upgrade(cx) else {
+            return;
+        };
+        let Some(from) = from.upgrade(cx) else {
+            return;
+        };
 
         let new_pane = self.add_pane(cx);
         self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
@@ -2841,7 +2852,13 @@ impl Workspace {
         views: Vec<proto::View>,
         cx: &mut AsyncAppContext,
     ) -> Result<()> {
-        let project = this.read_with(cx, |this, _| this.project.clone())?;
+        let this = this
+            .upgrade(cx)
+            .ok_or_else(|| anyhow!("workspace dropped"))?;
+        let project = this
+            .read_with(cx, |this, _| this.project.clone())
+            .ok_or_else(|| anyhow!("window dropped"))?;
+
         let replica_id = project
             .read_with(cx, |project, _| {
                 project
@@ -2867,12 +2884,11 @@ impl Workspace {
                 let id = ViewId::from_proto(id.clone())?;
                 let mut variant = view.variant.clone();
                 if variant.is_none() {
-                    Err(anyhow!("missing variant"))?;
+                    Err(anyhow!("missing view variant"))?;
                 }
                 for build_item in &item_builders {
-                    let task = cx.update(|cx| {
-                        build_item(pane.clone(), project.clone(), id, &mut variant, cx)
-                    });
+                    let task = cx
+                        .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx));
                     if let Some(task) = task {
                         item_tasks.push(task);
                         leader_view_ids.push(id);
@@ -2900,7 +2916,7 @@ impl Workspace {
                 }
 
                 Some(())
-            })?;
+            });
         }
         Ok(())
     }
@@ -3403,10 +3419,16 @@ impl Workspace {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+        let client = project.read(cx).client();
+        let user_store = project.read(cx).user_store();
+
+        let channel_store =
+            cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
         let app_state = Arc::new(AppState {
             languages: project.read(cx).languages().clone(),
-            client: project.read(cx).client(),
-            user_store: project.read(cx).user_store(),
+            client,
+            user_store,
+            channel_store,
             fs: project.read(cx).fs().clone(),
             build_window_options: |_, _, _| Default::default(),
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
@@ -3750,14 +3772,23 @@ impl View for Workspace {
                                         )
                                     }))
                                     .with_children(self.modal.as_ref().map(|modal| {
-                                        ChildView::new(modal.view.as_any(), cx)
-                                            .contained()
-                                            .with_style(theme.workspace.modal)
-                                            .aligned()
-                                            .top()
+                                        // Prevent clicks within the modal from falling
+                                        // through to the rest of the workspace.
+                                        enum ModalBackground {}
+                                        MouseEventHandler::new::<ModalBackground, _>(
+                                            0,
+                                            cx,
+                                            |_, cx| ChildView::new(modal.view.as_any(), cx),
+                                        )
+                                        .on_click(MouseButton::Left, |_, _, _| {})
+                                        .contained()
+                                        .with_style(theme.workspace.modal)
+                                        .aligned()
+                                        .top()
                                     }))
                                     .with_children(self.render_notifications(&theme.workspace, cx)),
                             ))
+                            .provide_resize_bounds::<WorkspaceBounds>()
                             .flex(1.0, true),
                     )
                     .with_child(ChildView::new(&self.status_bar, cx))
@@ -4841,7 +4872,9 @@ mod tests {
                 panel_1.size(cx)
             );
 
-            left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
+            left_dock.update(cx, |left_dock, cx| {
+                left_dock.resize_active_panel(Some(1337.), cx)
+            });
             assert_eq!(
                 workspace
                     .right_dock()

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.100.0"
+version = "0.103.0"
 publish = false
 
 [lib]
@@ -21,10 +21,12 @@ activity_indicator = { path = "../activity_indicator" }
 auto_update = { path = "../auto_update" }
 breadcrumbs = { path = "../breadcrumbs" }
 call = { path = "../call" }
+channel = { path = "../channel" }
 cli = { path = "../cli" }
 collab_ui = { path = "../collab_ui" }
 collections = { path = "../collections" }
 command_palette = { path = "../command_palette" }
+component_test = { path = "../component_test" }
 context_menu = { path = "../context_menu" }
 client = { path = "../client" }
 clock = { path = "../clock" }
@@ -54,10 +56,11 @@ plugin_runtime = { path = "../plugin_runtime",optional = true }
 project = { path = "../project" }
 project_panel = { path = "../project_panel" }
 project_symbols = { path = "../project_symbols" }
+quick_action_bar = { path = "../quick_action_bar" }
 recent_projects = { path = "../recent_projects" }
 rpc = { path = "../rpc" }
 settings = { path = "../settings" }
-staff_mode = { path = "../staff_mode" }
+feature_flags = { path = "../feature_flags" }
 sum_tree = { path = "../sum_tree" }
 text = { path = "../text" }
 terminal_view = { path = "../terminal_view" }
@@ -92,7 +95,7 @@ postage.workspace = true
 rand.workspace = true
 regex.workspace = true
 rsa = "0.4"
-rust-embed = { version = "6.3", features = ["include-exclude"] }
+rust-embed.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true

crates/zed/resources/zed.entitlements 🔗

@@ -18,11 +18,7 @@
 	<true/>
 	<key>com.apple.security.personal-information.photos-library</key>
 	<true/>
-	<key>com.apple.security.cs.allow-dyld-environment-variables</key>
-	<true/>
-	<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
-	<true/>
-	<key>com.apple.security.cs.disable-library-validation</key>
-	<true/>
+	<!-- <key>com.apple.security.cs.disable-library-validation</key>
+	<true/> -->
 </dict>
 </plist>

crates/zed/src/languages/c.rs 🔗

@@ -293,7 +293,7 @@ mod tests {
         let language = crate::languages::language("c", tree_sitter_c::language(), None).await;
 
         cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
+            let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx);
 
             // empty function
             buffer.edit([(0..0, "int main() {}")], None, cx);

crates/zed/src/languages/cpp/config.toml 🔗

@@ -1,5 +1,5 @@
 name = "C++"
-path_suffixes = ["cc", "cpp", "h", "hpp"]
+path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"]
 line_comment = "// "
 autoclose_before = ";:.,=}])>"
 brackets = [

crates/zed/src/languages/javascript/config.toml 🔗

@@ -1,5 +1,5 @@
 name = "JavaScript"
-path_suffixes = ["js", "jsx", "mjs"]
+path_suffixes = ["js", "jsx", "mjs", "cjs"]
 first_line_pattern = '^#!.*\bnode\b'
 line_comment = "// "
 autoclose_before = ";:.,=}])>"
@@ -13,6 +13,7 @@ brackets = [
     { start = "`", end = "`", close = true, newline = false, not_in = ["comment", "string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
 ]
+word_characters = ["$", "#"]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
 
 [overrides.element]

crates/zed/src/languages/json.rs 🔗

@@ -1,6 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use collections::HashMap;
+use feature_flags::FeatureFlagAppExt;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
 use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
@@ -9,7 +10,6 @@ use node_runtime::NodeRuntime;
 use serde_json::json;
 use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
 use smol::fs;
-use staff_mode::StaffMode;
 use std::{
     any::Any,
     ffi::OsString,
@@ -108,7 +108,7 @@ impl LspAdapter for JsonLspAdapter {
         cx: &mut AppContext,
     ) -> BoxFuture<'static, serde_json::Value> {
         let action_names = cx.all_action_names().collect::<Vec<_>>();
-        let staff_mode = cx.default_global::<StaffMode>().0;
+        let staff_mode = cx.is_staff();
         let language_names = &self.languages.language_names();
         let settings_schema = cx.global::<SettingsStore>().json_schema(
             &SettingsJsonSchemaParams {

crates/zed/src/languages/python.rs 🔗

@@ -93,7 +93,9 @@ impl LspAdapter for PythonLspAdapter {
         // to allow our own fuzzy score to be used to break ties.
         //
         // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
-        let Some(sort_text) = &mut item.sort_text else { return };
+        let Some(sort_text) = &mut item.sort_text else {
+            return;
+        };
         let mut parts = sort_text.split('.');
         let Some(first) = parts.next() else { return };
         let Some(second) = parts.next() else { return };
@@ -212,7 +214,7 @@ mod tests {
         });
 
         cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
+            let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx);
             let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
                 let ix = buffer.len();
                 buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);

crates/zed/src/languages/python/config.toml 🔗

@@ -1,5 +1,5 @@
 name = "Python"
-path_suffixes = ["py", "pyi"]
+path_suffixes = ["py", "pyi", "mpy"]
 first_line_pattern = '^#!.*\bpython[0-9.]*\b'
 line_comment = "# "
 autoclose_before = ";:.,=}])>"

crates/zed/src/languages/rust.rs 🔗

@@ -478,7 +478,7 @@ mod tests {
         let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await;
 
         cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
+            let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx);
 
             // indent between braces
             buffer.set_text("fn a() {}", cx);

crates/zed/src/languages/tsx/config.toml 🔗

@@ -12,6 +12,7 @@ brackets = [
     { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+word_characters = ["#", "$"]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
 
 [overrides.element]

crates/zed/src/languages/typescript.rs 🔗

@@ -362,8 +362,9 @@ mod tests {
         "#
         .unindent();
 
-        let buffer =
-            cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx));
+        let buffer = cx.add_model(|cx| {
+            language::Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)
+        });
         let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
         assert_eq!(
             outline

crates/zed/src/languages/typescript/config.toml 🔗

@@ -1,5 +1,5 @@
 name = "TypeScript"
-path_suffixes = ["ts"]
+path_suffixes = ["ts", "cts", "d.cts", "d.mts", "mts"]
 line_comment = "// "
 autoclose_before = ";:.,=}])>"
 brackets = [
@@ -12,3 +12,4 @@ brackets = [
     { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+word_characters = ["#", "$"]

crates/zed/src/main.rs 🔗

@@ -3,6 +3,7 @@
 
 use anyhow::{anyhow, Context, Result};
 use backtrace::Backtrace;
+use channel::ChannelStore;
 use cli::{
     ipc::{self, IpcSender},
     CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
@@ -30,7 +31,7 @@ use std::{
     env,
     ffi::OsStr,
     fs::OpenOptions,
-    io::Write as _,
+    io::{IsTerminal, Write as _},
     os::unix::prelude::OsStrExt,
     panic,
     path::{Path, PathBuf},
@@ -52,8 +53,6 @@ use uuid::Uuid;
 use welcome::{show_welcome_experience, FIRST_OPEN};
 
 use fs::RealFs;
-#[cfg(debug_assertions)]
-use staff_mode::StaffMode;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
 use workspace::AppState;
 use zed::{
@@ -121,7 +120,10 @@ fn main() {
         cx.set_global(*RELEASE_CHANNEL);
 
         #[cfg(debug_assertions)]
-        cx.set_global(StaffMode(true));
+        {
+            use feature_flags::FeatureFlagAppExt;
+            cx.set_staff(true);
+        }
 
         let mut store = SettingsStore::default();
         store
@@ -140,6 +142,8 @@ fn main() {
 
         languages::init(languages.clone(), node_runtime.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
+        let channel_store =
+            cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
 
         cx.set_global(client.clone());
 
@@ -155,6 +159,7 @@ fn main() {
         outline::init(cx);
         project_symbols::init(cx);
         project_panel::init(Assets, cx);
+        channel::init(&client);
         diagnostics::init(cx);
         search::init(cx);
         semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
@@ -162,6 +167,7 @@ fn main() {
         terminal_view::init(cx);
         copilot::init(http.clone(), node_runtime, cx);
         ai::init(cx);
+        component_test::init(cx);
 
         cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
         cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
@@ -181,6 +187,7 @@ fn main() {
             languages,
             client: client.clone(),
             user_store,
+            channel_store,
             fs,
             build_window_options,
             initialize_workspace,
@@ -628,8 +635,7 @@ async fn load_login_shell_environment() -> Result<()> {
 }
 
 fn stdout_is_a_pty() -> bool {
-    std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none()
-        && unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 }
+    std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal()
 }
 
 fn collect_path_args() -> Vec<PathBuf> {

crates/zed/src/zed.rs 🔗

@@ -10,7 +10,7 @@ use anyhow::Context;
 use assets::Assets;
 use breadcrumbs::Breadcrumbs;
 pub use client;
-use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
+use collab_ui::CollabTitlebarItem; // TODO: Add back toggle collab ui shortcut
 use collections::VecDeque;
 pub use editor;
 use editor::{Editor, MultiBuffer};
@@ -30,6 +30,7 @@ use gpui::{
 pub use lsp;
 pub use project;
 use project_panel::ProjectPanel;
+use quick_action_bar::QuickActionBar;
 use search::{BufferSearchBar, ProjectSearchBar};
 use serde::Deserialize;
 use serde_json::to_string_pretty;
@@ -85,20 +86,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
             cx.toggle_full_screen();
         },
     );
-    cx.add_action(
-        |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
-            if let Some(item) = workspace
-                .titlebar_item()
-                .and_then(|item| item.downcast::<CollabTitlebarItem>())
-            {
-                cx.defer(move |_, cx| {
-                    item.update(cx, |item, cx| {
-                        item.toggle_contacts_popover(&Default::default(), cx);
-                    });
-                });
-            }
-        },
-    );
     cx.add_global_action(quit);
     cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
     cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
@@ -220,6 +207,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
             workspace.toggle_panel_focus::<ProjectPanel>(cx);
         },
     );
+    cx.add_action(
+        |workspace: &mut Workspace,
+         _: &collab_ui::collab_panel::ToggleFocus,
+         cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
+        },
+    );
     cx.add_action(
         |workspace: &mut Workspace,
          _: &terminal_panel::ToggleFocus,
@@ -269,7 +263,11 @@ pub fn initialize_workspace(
                                 let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
                                 toolbar.add_item(breadcrumbs, cx);
                                 let buffer_search_bar = cx.add_view(BufferSearchBar::new);
-                                toolbar.add_item(buffer_search_bar, cx);
+                                toolbar.add_item(buffer_search_bar.clone(), cx);
+                                let quick_action_bar = cx.add_view(|_| {
+                                    QuickActionBar::new(buffer_search_bar, workspace)
+                                });
+                                toolbar.add_item(quick_action_bar, cx);
                                 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
                                 toolbar.add_item(project_search_bar, cx);
                                 let submit_feedback_button =
@@ -338,9 +336,14 @@ pub fn initialize_workspace(
         let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
         let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
         let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
-        let (project_panel, terminal_panel, assistant_panel) =
-            futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
-
+        let channels_panel =
+            collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
+        let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
+            project_panel,
+            terminal_panel,
+            assistant_panel,
+            channels_panel
+        )?;
         workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
             workspace.add_panel_with_extra_event_handler(
@@ -358,6 +361,7 @@ pub fn initialize_workspace(
             );
             workspace.add_panel(terminal_panel, cx);
             workspace.add_panel(assistant_panel, cx);
+            workspace.add_panel(channels_panel, cx);
 
             if !was_deserialized
                 && workspace
@@ -1703,6 +1707,8 @@ mod tests {
             .remove_file(Path::new("/root/a/file2"), Default::default())
             .await
             .unwrap();
+        cx.foreground().run_until_parked();
+
         workspace
             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
             .await
@@ -2382,6 +2388,7 @@ mod tests {
             language::init(cx);
             editor::init(cx);
             project_panel::init_settings(cx);
+            collab_ui::init(&app_state, cx);
             pane::init(cx);
             project_panel::init((), cx);
             terminal_view::init(cx);

rust-toolchain.toml 🔗

@@ -1,4 +1,4 @@
 [toolchain]
-channel = "1.71"
+channel = "1.72"
 components = [ "rustfmt" ]
 targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]

script/bundle 🔗

@@ -5,11 +5,29 @@ set -e
 build_flag="--release"
 target_dir="release"
 open_result=false
+local_only=false
+overwrite_local_app=false
+bundle_name=""
+
+# Function for displaying help info
+help_info() {
+  echo "
+Usage: ${0##*/} [options] [bundle_name]
+Build the application bundle.
+
+Options:
+  -d    Compile in debug mode and print the app bundle's path.
+  -l    Compile for local architecture only and copy bundle to /Applications.
+  -o    Open the resulting DMG or the app itself in local mode.
+  -f    Overwrite the local app bundle if it exists.
+  -h    Display this help and exit.
+  "
+}
 
 # If -o option is specified, the folder of the resulting dmg will be opened in finder
 # If -d is specified, Zed will be compiled in debug mode and the application's path printed
 # If -od or -do is specified Zed will be bundled in debug and the application will be run.
-while getopts 'od' flag
+while getopts 'dlfoh' flag
 do
     case "${flag}" in
         o) open_result=true;;
@@ -17,9 +35,21 @@ do
             build_flag="";
             target_dir="debug"
             ;;
+        l) local_only=true;;
+        f) overwrite_local_app=true;;
+        h)
+           help_info
+           exit 0
+           ;;
     esac
 done
 
+shift $((OPTIND-1))
+
+if [ "$1" ]; then
+    bundle_name=$1
+fi
+
 export ZED_BUNDLE=true
 export MACOSX_DEPLOYMENT_TARGET=10.15.7
 
@@ -33,14 +63,24 @@ rustup target add wasm32-wasi
 # Deal with versions of macOS that don't include libstdc++ headers
 export CXXFLAGS="-stdlib=libc++"
 
-echo "Compiling zed binary for aarch64-apple-darwin"
-cargo build ${build_flag} --package zed --target aarch64-apple-darwin
-echo "Compiling zed binary for x86_64-apple-darwin"
-cargo build ${build_flag} --package zed --target x86_64-apple-darwin
-echo "Compiling cli binary for aarch64-apple-darwin"
-cargo build ${build_flag} --package cli --target aarch64-apple-darwin
-echo "Compiling cli binary for x86_64-apple-darwin"
-cargo build ${build_flag} --package cli --target x86_64-apple-darwin
+version_info=$(rustc --version --verbose)
+host_line=$(echo "$version_info" | grep host)
+local_target_triple=${host_line#*: }
+
+if [ "$local_only" = true ]; then
+    echo "Building for local target only."
+    cargo build ${build_flag} --package zed
+    cargo build ${build_flag} --package cli
+else
+    echo "Compiling zed binary for aarch64-apple-darwin"
+    cargo build ${build_flag} --package zed --target aarch64-apple-darwin
+    echo "Compiling zed binary for x86_64-apple-darwin"
+    cargo build ${build_flag} --package zed --target x86_64-apple-darwin
+    echo "Compiling cli binary for aarch64-apple-darwin"
+    cargo build ${build_flag} --package cli --target aarch64-apple-darwin
+    echo "Compiling cli binary for x86_64-apple-darwin"
+    cargo build ${build_flag} --package cli --target x86_64-apple-darwin
+fi
 
 echo "Creating application bundle"
 pushd crates/zed
@@ -50,27 +90,34 @@ sed \
     -i .backup \
     "s/package.metadata.bundle-${channel}/package.metadata.bundle/" \
     Cargo.toml
-app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs)
+
+if [ "$local_only" = true ]; then
+    app_path=$(cargo bundle ${build_flag} --select-workspace-root | xargs)
+else
+    app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs)
+fi
 
 mv Cargo.toml.backup Cargo.toml
 popd
 echo "Bundled ${app_path}"
 
-echo "Creating fat binaries"
-lipo \
-    -create \
-    target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \
-    -output \
-    "${app_path}/Contents/MacOS/zed"
-lipo \
-    -create \
-    target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \
-    -output \
-    "${app_path}/Contents/MacOS/cli"
+if [ "$local_only" = false ]; then
+    echo "Creating fat binaries"
+    lipo \
+        -create \
+        target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \
+        -output \
+        "${app_path}/Contents/MacOS/zed"
+    lipo \
+        -create \
+        target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \
+        -output \
+        "${app_path}/Contents/MacOS/cli"
+fi
 
 echo "Copying WebRTC.framework into the frameworks folder"
 mkdir "${app_path}/Contents/Frameworks"
-cp -R target/x86_64-apple-darwin/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/"
+cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/"
 
 if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
     echo "Signing bundle with Apple-issued certificate"
@@ -99,31 +146,55 @@ if [ "$target_dir" = "debug" ]; then
     exit 0
 fi
 
-dmg_target_directory="target/${target_dir}"
-dmg_source_directory="${dmg_target_directory}/dmg"
-dmg_file_path="${dmg_target_directory}/Zed.dmg"
-
-echo "Creating DMG"
-rm -rf ${dmg_source_directory}
-mkdir -p ${dmg_source_directory}
-mv "${app_path}" "${dmg_source_directory}"
-
-ln -s /Applications ${dmg_source_directory}
-hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}"
-# If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`.
-# This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now.
-rm ${dmg_source_directory}/Applications
+if [ "$local_only" = true ]; then
+    # If bundle_name is not set or empty, use the basename of $app_path
+    if [ -z "$bundle_name" ]; then
+        bundle_name=$(basename "$app_path")
+    else
+        # If bundle_name doesn't end in .app, append it
+        if [[ "$bundle_name" != *.app ]]; then
+            bundle_name="$bundle_name.app"
+        fi
+    fi
 
-echo "Adding license agreement to DMG"
-npm install --global dmg-license minimist
-dmg-license script/eula/eula.json "${dmg_file_path}"
+    if [ "$overwrite_local_app" = true ]; then
+        rm -rf "/Applications/$bundle_name"
+    fi
+    mv "$app_path" "/Applications/$bundle_name"
 
-if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
-    echo "Notarizing DMG with Apple"
-    npm install -g notarize-cli
-    npx notarize-cli --file "${dmg_file_path}" --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD"
-fi
+    if [ "$open_result" = true ]; then
+        open "/Applications/$bundle_name"
+    else
+        echo "Installed application bundle:"
+        echo "/Applications/$bundle_name"
+    fi
+else
+    echo "Creating DMG"
+    dmg_target_directory="target/${target_dir}"
+    dmg_source_directory="${dmg_target_directory}/dmg"
+    dmg_file_path="${dmg_target_directory}/Zed.dmg"
+
+    rm -rf ${dmg_source_directory}
+    mkdir -p ${dmg_source_directory}
+    mv "${app_path}" "${dmg_source_directory}"
+
+    ln -s /Applications ${dmg_source_directory}
+    hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}"
+    # If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`.
+    # This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now.
+    rm ${dmg_source_directory}/Applications
+
+    echo "Adding license agreement to DMG"
+    npm install --global dmg-license minimist
+    dmg-license script/eula/eula.json "${dmg_file_path}"
+
+    if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
+        echo "Notarizing DMG with Apple"
+        npm install -g notarize-cli
+        npx notarize-cli --file "${dmg_file_path}" --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD"
+    fi
 
-if [ "$open_result" = true ]; then
-    open $dmg_target_directory
+    if [ "$open_result" = true ]; then
+        open $dmg_target_directory
+    fi
 fi

script/lib/bump-version.sh 🔗

@@ -12,7 +12,7 @@ if [[ -n $(git status --short --untracked-files=no) ]]; then
   exit 1
 fi
 
-which cargo-set-version > /dev/null || cargo install cargo-edit
+which cargo-set-version > /dev/null || cargo install cargo-edit --features vendored-openssl
 which jq > /dev/null || brew install jq
 cargo set-version --package $package --bump $version_increment
 cargo check --quiet

script/start-local-collaboration 🔗

@@ -53,6 +53,6 @@ sleep 0.5
 
 # Start the two Zed child processes. Open the given paths with the first instance.
 trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
-ZED_IMPERSONATE=${username_1} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ &
+ZED_IMPERSONATE=${ZED_IMPERSONATE:=${username_1}} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ &
 SECOND=true ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed &
 wait

script/zed-with-local-servers 🔗

@@ -1,3 +1,6 @@
 #!/bin/bash
 
-ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:3000 cargo run $@
+: "${ZED_IMPERSONATE:=as-cii}"
+export ZED_IMPERSONATE
+
+ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:8080 cargo run $@

styles/.eslintrc.js 🔗

@@ -28,6 +28,7 @@ module.exports = {
     },
     rules: {
         "linebreak-style": ["error", "unix"],
+        "@typescript-eslint/no-explicit-any": "off",
         semi: ["error", "never"],
     },
 }

styles/src/build_themes.ts 🔗

@@ -21,9 +21,7 @@ function clear_themes(theme_directory: string) {
     }
 }
 
-const all_themes: Theme[] = themes.map((theme) =>
-    create_theme(theme)
-)
+const all_themes: Theme[] = themes.map((theme) => create_theme(theme))
 
 function write_themes(themes: Theme[], output_directory: string) {
     clear_themes(output_directory)
@@ -34,10 +32,7 @@ function write_themes(themes: Theme[], output_directory: string) {
         const style_tree = app()
         const style_tree_json = JSON.stringify(style_tree, null, 2)
         const temp_path = path.join(temp_directory, `${theme.name}.json`)
-        const out_path = path.join(
-            output_directory,
-            `${theme.name}.json`
-        )
+        const out_path = path.join(output_directory, `${theme.name}.json`)
         fs.writeFileSync(temp_path, style_tree_json)
         fs.renameSync(temp_path, out_path)
         console.log(`- ${out_path} created`)

styles/src/build_tokens.ts 🔗

@@ -83,8 +83,6 @@ function write_tokens(themes: Theme[], tokens_directory: string) {
     console.log(`- ${METADATA_FILE} created`)
 }
 
-const all_themes: Theme[] = themes.map((theme) =>
-    create_theme(theme)
-)
+const all_themes: Theme[] = themes.map((theme) => create_theme(theme))
 
 write_tokens(all_themes, TOKENS_DIRECTORY)

styles/src/common.ts 🔗

@@ -1,5 +1,6 @@
 import chroma from "chroma-js"
 export * from "./theme"
+export * from "./theme/theme_config"
 export { chroma }
 
 export const font_families = {

styles/src/component/button.ts 🔗

@@ -0,0 +1,127 @@
+import { font_sizes, useTheme } from "../common"
+import { Layer, Theme } from "../theme"
+import { TextStyle, background } from "../style_tree/components"
+
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace Button {
+    export type Options = {
+        layer: Layer
+        background: keyof Theme["lowest"]
+        color: keyof Theme["lowest"]
+        variant: Button.Variant
+        size: Button.Size
+        shape: Button.Shape
+        margin: {
+            top?: number
+            bottom?: number
+            left?: number
+            right?: number
+        }
+        states: {
+            enabled?: boolean
+            hovered?: boolean
+            pressed?: boolean
+            focused?: boolean
+            disabled?: boolean
+        }
+    }
+
+    export type ToggleableOptions = Options & {
+        active_background: keyof Theme["lowest"]
+        active_color: keyof Theme["lowest"]
+    }
+
+    /** Padding added to each side of a Shape.Rectangle button */
+    export const RECTANGLE_PADDING = 2
+    export const FONT_SIZE = font_sizes.sm
+    export const ICON_SIZE = 14
+    export const CORNER_RADIUS = 6
+
+    export const variant = {
+        Default: "filled",
+        Outline: "outline",
+        Ghost: "ghost",
+    } as const
+
+    export type Variant = (typeof variant)[keyof typeof variant]
+
+    export const shape = {
+        Rectangle: "rectangle",
+        Square: "square",
+    } as const
+
+    export type Shape = (typeof shape)[keyof typeof shape]
+
+    export const size = {
+        Small: "sm",
+        Medium: "md",
+    } as const
+
+    export type Size = (typeof size)[keyof typeof size]
+
+    export type BaseStyle = {
+        corder_radius: number
+        background: string | null
+        padding: {
+            top: number
+            bottom: number
+            left: number
+            right: number
+        }
+        margin: Button.Options["margin"]
+        button_height: number
+    }
+
+    export type LabelButtonStyle = BaseStyle & TextStyle
+    // export type IconButtonStyle = ButtonStyle
+
+    export const button_base = (
+        options: Partial<Button.Options> = {
+            variant: Button.variant.Default,
+            shape: Button.shape.Rectangle,
+            states: {
+                hovered: true,
+                pressed: true,
+            },
+        }
+    ): BaseStyle => {
+        const theme = useTheme()
+
+        const layer = options.layer ?? theme.middle
+        const color = options.color ?? "base"
+        const background_color =
+            options.variant === Button.variant.Ghost
+                ? null
+                : background(layer, options.background ?? color)
+
+        const m = {
+            top: options.margin?.top ?? 0,
+            bottom: options.margin?.bottom ?? 0,
+            left: options.margin?.left ?? 0,
+            right: options.margin?.right ?? 0,
+        }
+        const size = options.size || Button.size.Medium
+        const padding = 2
+
+        const base: BaseStyle = {
+            background: background_color,
+            corder_radius: Button.CORNER_RADIUS,
+            padding: {
+                top: padding,
+                bottom: padding,
+                left:
+                    options.shape === Button.shape.Rectangle
+                        ? padding + Button.RECTANGLE_PADDING
+                        : padding,
+                right:
+                    options.shape === Button.shape.Rectangle
+                        ? padding + Button.RECTANGLE_PADDING
+                        : padding,
+            },
+            margin: m,
+            button_height: 16,
+        }
+
+        return base
+    }
+}

styles/src/component/icon_button.ts 🔗

@@ -1,6 +1,7 @@
 import { interactive, toggleable } from "../element"
 import { background, foreground } from "../style_tree/components"
-import { useTheme, Theme } from "../theme"
+import { useTheme, Theme, Layer } from "../theme"
+import { Button } from "./button"
 
 export type Margin = {
     top: number
@@ -10,23 +11,36 @@ export type Margin = {
 }
 
 interface IconButtonOptions {
-    layer?:
-    | Theme["lowest"]
-    | Theme["middle"]
-    | Theme["highest"]
+    layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"]
     color?: keyof Theme["lowest"]
+    background_color?: keyof Theme["lowest"]
     margin?: Partial<Margin>
+    variant?: Button.Variant
+    size?: Button.Size
 }
 
 type ToggleableIconButtonOptions = IconButtonOptions & {
     active_color?: keyof Theme["lowest"]
+    active_background_color?: keyof Theme["lowest"]
+    active_layer?: Layer
+    active_variant?: Button.Variant
 }
 
-export function icon_button({ color, margin, layer }: IconButtonOptions) {
+export function icon_button(
+    { color, background_color, margin, layer, variant, size }: IconButtonOptions = {
+        variant: Button.variant.Default,
+        size: Button.size.Medium,
+    }
+) {
     const theme = useTheme()
 
     if (!color) color = "base"
 
+    const default_background =
+        variant === Button.variant.Ghost
+            ? null
+            : background(layer ?? theme.lowest, background_color ?? color)
+
     const m = {
         top: margin?.top ?? 0,
         bottom: margin?.bottom ?? 0,
@@ -34,51 +48,63 @@ export function icon_button({ color, margin, layer }: IconButtonOptions) {
         right: margin?.right ?? 0,
     }
 
+    const padding = {
+        top: size === Button.size.Small ? 2 : 2,
+        bottom: size === Button.size.Small ? 2 : 2,
+        left: size === Button.size.Small ? 2 : 4,
+        right: size === Button.size.Small ? 2 : 4,
+    }
+
     return interactive({
         base: {
             corner_radius: 6,
-            padding: {
-                top: 2,
-                bottom: 2,
-                left: 4,
-                right: 4,
-            },
+            padding: padding,
             margin: m,
             icon_width: 14,
             icon_height: 14,
-            button_width: 20,
-            button_height: 16,
+            button_width: size === Button.size.Small ? 16 : 20,
+            button_height: 14,
         },
         state: {
             default: {
-                background: background(layer ?? theme.lowest, color),
+                background: default_background,
                 color: foreground(layer ?? theme.lowest, color),
             },
             hovered: {
-                background: background(layer ?? theme.lowest, color, "hovered"),
+                background: background(layer ?? theme.lowest, background_color ?? color, "hovered"),
                 color: foreground(layer ?? theme.lowest, color, "hovered"),
             },
             clicked: {
-                background: background(layer ?? theme.lowest, color, "pressed"),
+                background: background(layer ?? theme.lowest, background_color ?? color, "pressed"),
                 color: foreground(layer ?? theme.lowest, color, "pressed"),
             },
         },
     })
 }
 
-export function toggleable_icon_button(
-    theme: Theme,
-    { color, active_color, margin }: ToggleableIconButtonOptions
-) {
+export function toggleable_icon_button({
+    color,
+    background_color,
+    active_color,
+    active_background_color,
+    active_variant,
+    margin,
+    variant,
+    size,
+    active_layer,
+}: ToggleableIconButtonOptions) {
     if (!color) color = "base"
 
     return toggleable({
         state: {
-            inactive: icon_button({ color, margin }),
+            inactive: icon_button({ color, background_color, margin, variant, size }),
             active: icon_button({
                 color: active_color ? active_color : color,
+                background_color: active_background_color ? active_background_color : background_color,
                 margin,
-                layer: theme.middle,
+                layer: active_layer,
+                variant: active_variant || variant,
+                size,
             }),
         },
     })

styles/src/component/index.ts 🔗

@@ -0,0 +1,6 @@
+export * from "./icon_button"
+export * from "./indicator"
+export * from "./input"
+export * from "./tab"
+export * from "./tab_bar_button"
+export * from "./text_button"

styles/src/component/indicator.ts 🔗

@@ -0,0 +1,15 @@
+import { foreground } from "../style_tree/components"
+import { Layer, StyleSets } from "../theme"
+
+export const indicator = ({
+    layer,
+    color,
+}: {
+    layer: Layer
+    color: StyleSets
+}) => ({
+    corner_radius: 4,
+    padding: 4,
+    margin: { top: 12, left: 12 },
+    background: foreground(layer, color),
+})

styles/src/component/input.ts 🔗

@@ -0,0 +1,23 @@
+import { useTheme } from "../common"
+import { background, border, text } from "../style_tree/components"
+
+export const input = () => {
+    const theme = useTheme()
+
+    return {
+        background: background(theme.highest),
+        corner_radius: 8,
+        min_width: 200,
+        max_width: 500,
+        placeholder_text: text(theme.highest, "mono", "disabled"),
+        selection: theme.players[0],
+        text: text(theme.highest, "mono", "default"),
+        border: border(theme.highest),
+        padding: {
+            top: 3,
+            bottom: 3,
+            left: 12,
+            right: 8,
+        },
+    }
+}

styles/src/component/tab.ts 🔗

@@ -0,0 +1,73 @@
+import { Layer } from "../common"
+import { interactive, toggleable } from "../element"
+import { Border, text } from "../style_tree/components"
+
+type TabProps = {
+    layer: Layer
+}
+
+export const tab = ({ layer }: TabProps) => {
+    const active_color = text(layer, "sans", "base").color
+    const inactive_border: Border = {
+        color: "#FFFFFF00",
+        width: 1,
+        bottom: true,
+        left: false,
+        right: false,
+        top: false,
+    }
+    const active_border: Border = {
+        ...inactive_border,
+        color: active_color,
+    }
+
+    const base = {
+        ...text(layer, "sans", "variant"),
+        padding: {
+            top: 8,
+            left: 8,
+            right: 8,
+            bottom: 6,
+        },
+        border: inactive_border,
+    }
+
+    const i = interactive({
+        state: {
+            default: {
+                ...base,
+            },
+            hovered: {
+                ...base,
+                ...text(layer, "sans", "base", "hovered"),
+            },
+            clicked: {
+                ...base,
+                ...text(layer, "sans", "base", "pressed"),
+            },
+        },
+    })
+
+    return toggleable({
+        base: i,
+        state: {
+            active: {
+                default: {
+                    ...i,
+                    ...text(layer, "sans", "base"),
+                    border: active_border,
+                },
+                hovered: {
+                    ...i,
+                    ...text(layer, "sans", "base", "hovered"),
+                    border: active_border,
+                },
+                clicked: {
+                    ...i,
+                    ...text(layer, "sans", "base", "pressed"),
+                    border: active_border,
+                },
+            },
+        },
+    })
+}

styles/src/component/tab_bar_button.ts 🔗

@@ -12,44 +12,47 @@ type TabBarButtonProps = TabBarButtonOptions & {
     state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
 }
 
-export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) {
+export function tab_bar_button(
+    theme: Theme,
+    { icon, color = "base" }: TabBarButtonProps
+) {
     const button_spacing = 8
 
-    return (
-        interactive({
-            base: {
-                icon: {
-                    color: foreground(theme.middle, color),
-                    asset: icon,
-                    dimensions: {
-                        width: 15,
-                        height: 15,
-                    },
+    return interactive({
+        base: {
+            icon: {
+                color: foreground(theme.middle, color),
+                asset: icon,
+                dimensions: {
+                    width: 15,
+                    height: 15,
                 },
-                container: {
-                    corner_radius: 4,
-                    padding: {
-                        top: 4, bottom: 4, left: 4, right: 4
-                    },
-                    margin: {
-                        left: button_spacing / 2,
-                        right: button_spacing / 2,
-                    },
+            },
+            container: {
+                corner_radius: 4,
+                padding: {
+                    top: 4,
+                    bottom: 4,
+                    left: 4,
+                    right: 4,
+                },
+                margin: {
+                    left: button_spacing / 2,
+                    right: button_spacing / 2,
                 },
             },
-            state: {
-                hovered: {
-                    container: {
-                        background: background(theme.middle, color, "hovered"),
-
-                    }
+        },
+        state: {
+            hovered: {
+                container: {
+                    background: background(theme.middle, color, "hovered"),
                 },
-                clicked: {
-                    container: {
-                        background: background(theme.middle, color, "pressed"),
-                    }
+            },
+            clicked: {
+                container: {
+                    background: background(theme.middle, color, "pressed"),
                 },
             },
-        })
-    )
+        },
+    })
 }

styles/src/component/text_button.ts 🔗

@@ -6,15 +6,15 @@ import {
     text,
 } from "../style_tree/components"
 import { useTheme, Theme } from "../theme"
+import { Button } from "./button"
 import { Margin } from "./icon_button"
 
 interface TextButtonOptions {
-    layer?:
-    | Theme["lowest"]
-    | Theme["middle"]
-    | Theme["highest"]
+    layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"]
+    variant?: Button.Variant
     color?: keyof Theme["lowest"]
     margin?: Partial<Margin>
+    disabled?: boolean
     text_properties?: TextProperties
 }
 
@@ -23,14 +23,21 @@ type ToggleableTextButtonOptions = TextButtonOptions & {
 }
 
 export function text_button({
+    variant = Button.variant.Default,
     color,
     layer,
     margin,
+    disabled,
     text_properties,
-}: TextButtonOptions) {
+}: TextButtonOptions = {}) {
     const theme = useTheme()
     if (!color) color = "base"
 
+    const background_color =
+        variant === Button.variant.Ghost
+            ? null
+            : background(layer ?? theme.lowest, color)
+
     const text_options: TextProperties = {
         size: "xs",
         weight: "normal",
@@ -59,31 +66,54 @@ export function text_button({
         },
         state: {
             default: {
-                background: background(layer ?? theme.lowest, color),
-                color: foreground(layer ?? theme.lowest, color),
-            },
-            hovered: {
-                background: background(layer ?? theme.lowest, color, "hovered"),
-                color: foreground(layer ?? theme.lowest, color, "hovered"),
-            },
-            clicked: {
-                background: background(layer ?? theme.lowest, color, "pressed"),
-                color: foreground(layer ?? theme.lowest, color, "pressed"),
+                background: background_color,
+                color: disabled
+                    ? foreground(layer ?? theme.lowest, "disabled")
+                    : foreground(layer ?? theme.lowest, color),
             },
+            hovered: disabled
+                ? {}
+                : {
+                    background: background(
+                        layer ?? theme.lowest,
+                        color,
+                        "hovered"
+                    ),
+                    color: foreground(
+                        layer ?? theme.lowest,
+                        color,
+                        "hovered"
+                    ),
+                },
+            clicked: disabled
+                ? {}
+                : {
+                    background: background(
+                        layer ?? theme.lowest,
+                        color,
+                        "pressed"
+                    ),
+                    color: foreground(
+                        layer ?? theme.lowest,
+                        color,
+                        "pressed"
+                    ),
+                },
         },
     })
 }
 
 export function toggleable_text_button(
     theme: Theme,
-    { color, active_color, margin }: ToggleableTextButtonOptions
+    { variant, color, active_color, margin }: ToggleableTextButtonOptions = {}
 ) {
     if (!color) color = "base"
 
     return toggleable({
         state: {
-            inactive: text_button({ color, margin }),
+            inactive: text_button({ variant, color, margin }),
             active: text_button({
+                variant,
                 color: active_color ? active_color : color,
                 margin,
                 layer: theme.middle,

styles/src/element/index.ts 🔗

@@ -1,4 +1,6 @@
 import { interactive, Interactive } from "./interactive"
-import { toggleable } from "./toggle"
+import { toggleable, Toggleable } from "./toggle"
 
-export { interactive, Interactive, toggleable }
+export * from "./padding"
+export * from "./margin"
+export { interactive, Interactive, toggleable, Toggleable }

styles/src/element/margin.ts 🔗

@@ -0,0 +1,41 @@
+type MarginOptions = {
+    all?: number
+    left?: number
+    right?: number
+    top?: number
+    bottom?: number
+}
+
+export type MarginStyle = {
+    top: number
+    bottom: number
+    left: number
+    right: number
+}
+
+export const margin_style = (options: MarginOptions): MarginStyle => {
+    const { all, top, bottom, left, right } = options
+
+    if (all !== undefined)
+        return {
+            top: all,
+            bottom: all,
+            left: all,
+            right: all,
+        }
+
+    if (
+        top === undefined &&
+        bottom === undefined &&
+        left === undefined &&
+        right === undefined
+    )
+        throw new Error("Margin must have at least one value")
+
+    return {
+        top: top || 0,
+        bottom: bottom || 0,
+        left: left || 0,
+        right: right || 0,
+    }
+}

styles/src/element/padding.ts 🔗

@@ -0,0 +1,41 @@
+type PaddingOptions = {
+    all?: number
+    left?: number
+    right?: number
+    top?: number
+    bottom?: number
+}
+
+export type PaddingStyle = {
+    top: number
+    bottom: number
+    left: number
+    right: number
+}
+
+export const padding_style = (options: PaddingOptions): PaddingStyle => {
+    const { all, top, bottom, left, right } = options
+
+    if (all !== undefined)
+        return {
+            top: all,
+            bottom: all,
+            left: all,
+            right: all,
+        }
+
+    if (
+        top === undefined &&
+        bottom === undefined &&
+        left === undefined &&
+        right === undefined
+    )
+        throw new Error("Padding must have at least one value")
+
+    return {
+        top: top || 0,
+        bottom: bottom || 0,
+        left: left || 0,
+        right: right || 0,
+    }
+}

styles/src/element/toggle.ts 🔗

@@ -3,7 +3,7 @@ import { DeepPartial } from "utility-types"
 
 type ToggleState = "inactive" | "active"
 
-type Toggleable<T> = Record<ToggleState, T>
+export type Toggleable<T> = Record<ToggleState, T>
 
 export const NO_INACTIVE_OR_BASE_ERROR =
     "A toggleable object must have an inactive state, or a base property."

styles/src/style_tree/app.ts 🔗

@@ -1,5 +1,3 @@
-import contact_finder from "./contact_finder"
-import contacts_popover from "./contacts_popover"
 import command_palette from "./command_palette"
 import project_panel from "./project_panel"
 import search from "./search"
@@ -14,7 +12,7 @@ import simple_message_notification from "./simple_message_notification"
 import project_shared_notification from "./project_shared_notification"
 import tooltip from "./tooltip"
 import terminal from "./terminal"
-import contact_list from "./contact_list"
+import collab_panel from "./collab_panel"
 import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
 import incoming_call_notification from "./incoming_call_notification"
 import welcome from "./welcome"
@@ -23,6 +21,7 @@ import assistant from "./assistant"
 import { titlebar } from "./titlebar"
 import editor from "./editor"
 import feedback from "./feedback"
+import component_test from "./component_test"
 import { useTheme } from "../common"
 
 export default function app(): any {
@@ -46,9 +45,7 @@ export default function app(): any {
         editor: editor(),
         project_diagnostics: project_diagnostics(),
         project_panel: project_panel(),
-        contacts_popover: contacts_popover(),
-        contact_finder: contact_finder(),
-        contact_list: contact_list(),
+        collab_panel: collab_panel(),
         toolbar_dropdown_menu: toolbar_dropdown_menu(),
         search: search(),
         shared_screen: shared_screen(),
@@ -57,6 +54,7 @@ export default function app(): any {
         tooltip: tooltip(),
         terminal: terminal(),
         assistant: assistant(),
-        feedback: feedback()
+        feedback: feedback(),
+        component_test: component_test(),
     }
 }

styles/src/style_tree/assistant.ts 🔗

@@ -1,5 +1,5 @@
 import { text, border, background, foreground, TextStyle } from "./components"
-import { Interactive, interactive } from "../element"
+import { Interactive, interactive, toggleable } from "../element"
 import { tab_bar_button } from "../component/tab_bar_button"
 import { StyleSets, useTheme } from "../theme"
 
@@ -8,50 +8,48 @@ type RoleCycleButton = TextStyle & {
 }
 // TODO: Replace these with zed types
 type RemainingTokens = TextStyle & {
-    background: string,
-    margin: { top: number, right: number },
+    background: string
+    margin: { top: number; right: number }
     padding: {
-        right: number,
-        left: number,
-        top: number,
-        bottom: number,
-    },
-    corner_radius: number,
+        right: number
+        left: number
+        top: number
+        bottom: number
+    }
+    corner_radius: number
 }
 
 export default function assistant(): any {
     const theme = useTheme()
 
-    const interactive_role = (color: StyleSets): Interactive<RoleCycleButton> => {
-        return (
-            interactive({
-                base: {
+    const interactive_role = (
+        color: StyleSets
+    ): Interactive<RoleCycleButton> => {
+        return interactive({
+            base: {
+                ...text(theme.highest, "sans", color, { size: "sm" }),
+            },
+            state: {
+                hovered: {
                     ...text(theme.highest, "sans", color, { size: "sm" }),
+                    background: background(theme.highest, color, "hovered"),
                 },
-                state: {
-                    hovered: {
-                        ...text(theme.highest, "sans", color, { size: "sm" }),
-                        background: background(theme.highest, color, "hovered"),
-                    },
-                    clicked: {
-                        ...text(theme.highest, "sans", color, { size: "sm" }),
-                        background: background(theme.highest, color, "pressed"),
-                    }
+                clicked: {
+                    ...text(theme.highest, "sans", color, { size: "sm" }),
+                    background: background(theme.highest, color, "pressed"),
                 },
-            })
-        )
+            },
+        })
     }
 
     const tokens_remaining = (color: StyleSets): RemainingTokens => {
-        return (
-            {
-                ...text(theme.highest, "mono", color, { size: "xs" }),
-                background: background(theme.highest, "on", "default"),
-                margin: { top: 12, right: 20 },
-                padding: { right: 4, left: 4, top: 1, bottom: 1 },
-                corner_radius: 6,
-            }
-        )
+        return {
+            ...text(theme.highest, "mono", color, { size: "xs" }),
+            background: background(theme.highest, "on", "default"),
+            margin: { top: 12, right: 20 },
+            padding: { right: 4, left: 4, top: 1, bottom: 1 },
+            corner_radius: 6,
+        }
     }
 
     return {
@@ -59,6 +57,85 @@ export default function assistant(): any {
             background: background(theme.highest),
             padding: { left: 12 },
         },
+        inline: {
+            background: background(theme.highest),
+            margin: { top: 3, bottom: 3 },
+            border: border(theme.lowest, "on", {
+                top: true,
+                bottom: true,
+                overlay: true,
+            }),
+            editor: {
+                text: text(theme.highest, "mono", "default", { size: "sm" }),
+                placeholder_text: text(theme.highest, "sans", "on", "disabled"),
+                selection: theme.players[0],
+            },
+            disabled_editor: {
+                text: text(theme.highest, "mono", "disabled", { size: "sm" }),
+                placeholder_text: text(theme.highest, "sans", "on", "disabled"),
+                selection: {
+                    cursor: text(theme.highest, "mono", "disabled").color,
+                    selection: theme.players[0].selection,
+                },
+            },
+            pending_edit_background: background(theme.highest, "positive"),
+            include_conversation: toggleable({
+                base: interactive({
+                    base: {
+                        icon_size: 12,
+                        color: foreground(theme.highest, "variant"),
+
+                        button_width: 12,
+                        background: background(theme.highest, "on"),
+                        corner_radius: 2,
+                        border: {
+                            width: 1., color: background(theme.highest, "on")
+                        },
+                        padding: {
+                            left: 4,
+                            right: 4,
+                            top: 4,
+                            bottom: 4,
+                        },
+                    },
+                    state: {
+                        hovered: {
+                            ...text(theme.highest, "mono", "variant", "hovered"),
+                            background: background(theme.highest, "on", "hovered"),
+                            border: {
+                                width: 1., color: background(theme.highest, "on", "hovered")
+                            },
+                        },
+                        clicked: {
+                            ...text(theme.highest, "mono", "variant", "pressed"),
+                            background: background(theme.highest, "on", "pressed"),
+                            border: {
+                                width: 1., color: background(theme.highest, "on", "pressed")
+                            },
+                        },
+                    },
+                }),
+                state: {
+                    active: {
+                        default: {
+                            icon_size: 12,
+                            button_width: 12,
+                            color: foreground(theme.highest, "variant"),
+                            background: background(theme.highest, "accent"),
+                            border: border(theme.highest, "accent"),
+                        },
+                        hovered: {
+                            background: background(theme.highest, "accent", "hovered"),
+                            border: border(theme.highest, "accent", "hovered"),
+                        },
+                        clicked: {
+                            background: background(theme.highest, "accent", "pressed"),
+                            border: border(theme.highest, "accent", "pressed"),
+                        },
+                    },
+                },
+            }),
+        },
         message_header: {
             margin: { bottom: 4, top: 4 },
             background: background(theme.highest),
@@ -93,7 +170,10 @@ export default function assistant(): any {
                 base: {
                     background: background(theme.middle),
                     padding: { top: 4, bottom: 4 },
-                    border: border(theme.middle, "default", { top: true, overlay: true }),
+                    border: border(theme.middle, "default", {
+                        top: true,
+                        overlay: true,
+                    }),
                 },
                 state: {
                     hovered: {
@@ -101,7 +181,7 @@ export default function assistant(): any {
                     },
                     clicked: {
                         background: background(theme.middle, "pressed"),
-                    }
+                    },
                 },
             }),
             saved_at: {

styles/src/style_tree/collab_modals.ts 🔗

@@ -0,0 +1,155 @@
+import { useTheme } from "../theme"
+import { background, border, foreground, text } from "./components"
+import picker from "./picker"
+import { input } from "../component/input"
+import contact_finder from "./contact_finder"
+import { tab } from "../component/tab"
+import { icon_button } from "../component/icon_button"
+
+export default function channel_modal(): any {
+    const theme = useTheme()
+
+    const SPACING = 12 as const
+    const BUTTON_OFFSET = 6 as const
+    const ITEM_HEIGHT = 36 as const
+
+    const contact_button = {
+        background: background(theme.middle, "variant"),
+        color: foreground(theme.middle, "variant"),
+        icon_width: 8,
+        button_width: 16,
+        corner_radius: 8,
+    }
+
+    const picker_style = picker()
+    delete picker_style.shadow
+    delete picker_style.border
+
+    const picker_input = input()
+
+    const member_icon_style = icon_button({
+        variant: "ghost",
+        size: "sm",
+    }).default
+
+    return {
+        contact_finder: contact_finder(),
+        tabbed_modal: {
+            tab_button: tab({ layer: theme.middle }),
+            row_height: ITEM_HEIGHT,
+            header: {
+                background: background(theme.lowest),
+                border: border(theme.middle, {
+                    bottom: true,
+                    top: false,
+                    left: false,
+                    right: false,
+                }),
+                padding: {
+                    top: SPACING,
+                    left: SPACING - BUTTON_OFFSET,
+                    right: SPACING - BUTTON_OFFSET,
+                },
+                corner_radii: {
+                    top_right: 12,
+                    top_left: 12,
+                },
+            },
+            body: {
+                background: background(theme.middle),
+                padding: {
+                    top: SPACING - 4,
+                    left: SPACING,
+                    right: SPACING,
+                    bottom: SPACING,
+                },
+                corner_radii: {
+                    bottom_right: 12,
+                    bottom_left: 12,
+                },
+            },
+            modal: {
+                background: background(theme.middle),
+                shadow: theme.modal_shadow,
+                corner_radius: 12,
+                padding: {
+                    bottom: 0,
+                    left: 0,
+                    right: 0,
+                    top: 0,
+                },
+            },
+            // FIXME: due to a bug in the picker's size calculation, this must be 600
+            max_height: 600,
+            max_width: 540,
+            title: {
+                ...text(theme.middle, "sans", "on", { size: "lg" }),
+                padding: {
+                    left: BUTTON_OFFSET,
+                },
+            },
+            picker: {
+                empty_container: {},
+                item: {
+                    ...picker_style.item,
+                    margin: { left: SPACING, right: SPACING },
+                },
+                no_matches: picker_style.no_matches,
+                input_editor: picker_input,
+                empty_input_editor: picker_input,
+                header: picker_style.header,
+                footer: picker_style.footer,
+            },
+        },
+        channel_modal: {
+            // This is used for the icons that are rendered to the right of channel Members in both UIs
+            member_icon: member_icon_style,
+            // This is used for the icons that are rendered to the right of channel invites in both UIs
+            invitee_icon: member_icon_style,
+            remove_member_button: {
+                ...text(theme.middle, "sans", { size: "xs" }),
+                background: background(theme.middle),
+                padding: {
+                    left: 7,
+                    right: 7,
+                },
+            },
+            cancel_invite_button: {
+                ...text(theme.middle, "sans", { size: "xs" }),
+                background: background(theme.middle),
+            },
+            member_tag: {
+                ...text(theme.middle, "sans", { size: "xs" }),
+                border: border(theme.middle, "active"),
+                background: background(theme.middle),
+                margin: {
+                    left: 8,
+                },
+                padding: {
+                    left: 4,
+                    right: 4,
+                },
+            },
+            contact_avatar: {
+                corner_radius: 10,
+                width: 18,
+            },
+            contact_username: {
+                padding: {
+                    left: 8,
+                },
+            },
+            contact_button: {
+                ...contact_button,
+                hover: {
+                    background: background(theme.middle, "variant", "hovered"),
+                },
+            },
+            disabled_contact_button: {
+                ...contact_button,
+                background: background(theme.middle, "disabled"),
+                color: foreground(theme.middle, "disabled"),
+            },
+        },
+    }
+}

styles/src/style_tree/collab_panel.ts 🔗

@@ -0,0 +1,410 @@
+import {
+    background,
+    border,
+    border_color,
+    foreground,
+    text,
+} from "./components"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+import collab_modals from "./collab_modals"
+import { icon_button, toggleable_icon_button } from "../component/icon_button"
+import { indicator } from "../component/indicator"
+
+export default function contacts_panel(): any {
+    const theme = useTheme()
+
+    const CHANNEL_SPACING = 4 as const
+    const NAME_MARGIN = 6 as const
+    const SPACING = 12 as const
+    const INDENT_SIZE = 8 as const
+    const ITEM_HEIGHT = 28 as const
+
+    const layer = theme.middle
+
+    const contact_button = {
+        background: background(layer, "on"),
+        color: foreground(layer, "on"),
+        icon_width: 14,
+        button_width: 16,
+        corner_radius: 8,
+    }
+
+    const project_row = {
+        guest_avatar_spacing: 4,
+        height: 24,
+        guest_avatar: {
+            corner_radius: 8,
+            width: 14,
+        },
+        name: {
+            ...text(layer, "sans", { size: "sm" }),
+            margin: {
+                left: NAME_MARGIN,
+                right: 4,
+            },
+        },
+        guests: {
+            margin: {
+                left: NAME_MARGIN,
+                right: NAME_MARGIN,
+            },
+        },
+        padding: {
+            left: SPACING,
+            right: SPACING,
+        },
+    }
+
+    const icon_style = {
+        color: foreground(layer, "variant"),
+        width: 14,
+    }
+
+    const header_icon_button = toggleable_icon_button({
+        variant: "ghost",
+        size: "sm",
+        active_layer: theme.lowest,
+    })
+
+    const subheader_row = toggleable({
+        base: interactive({
+            base: {
+                ...text(layer, "sans", { size: "sm" }),
+                padding: {
+                    left: SPACING,
+                    right: SPACING,
+                },
+            },
+            state: {
+                hovered: {
+                    background: background(layer, "hovered"),
+                },
+                clicked: {
+                    background: background(layer, "pressed"),
+                },
+            },
+        }),
+        state: {
+            active: {
+                default: {
+                    ...text(theme.lowest, "sans", { size: "sm" }),
+                    background: background(theme.lowest),
+                },
+                clicked: {
+                    background: background(layer, "pressed"),
+                },
+            },
+        },
+    })
+
+    const filter_input = {
+        background: background(layer, "on"),
+        corner_radius: 6,
+        text: text(layer, "sans", "base"),
+        placeholder_text: text(layer, "sans", "base", "disabled", {
+            size: "xs",
+        }),
+        selection: theme.players[0],
+        border: border(layer, "on"),
+        padding: {
+            bottom: 4,
+            left: 8,
+            right: 8,
+            top: 4,
+        },
+        margin: {
+            left: SPACING,
+            right: SPACING,
+        },
+    }
+
+    const item_row = toggleable({
+        base: interactive({
+            base: {
+                padding: {
+                    left: SPACING,
+                    right: SPACING,
+                },
+            },
+            state: {
+                clicked: {
+                    background: background(layer, "pressed"),
+                },
+            },
+        }),
+        state: {
+            inactive: {
+                hovered: {
+                    background: background(layer, "hovered"),
+                },
+            },
+            active: {
+                default: {
+                    ...text(theme.lowest, "sans", { size: "sm" }),
+                    background: background(theme.lowest),
+                },
+                clicked: {
+                    background: background(layer, "pressed"),
+                },
+            },
+        },
+    })
+
+    return {
+        ...collab_modals(),
+        disclosure: {
+            button: icon_button({ variant: "ghost", size: "sm" }),
+            spacing: CHANNEL_SPACING,
+        },
+        log_in_button: interactive({
+            base: {
+                background: background(theme.middle),
+                border: border(theme.middle, "active"),
+                corner_radius: 4,
+                margin: {
+                    top: 4,
+                    left: 16,
+                    right: 16,
+                },
+                padding: {
+                    top: 3,
+                    bottom: 3,
+                    left: 7,
+                    right: 7,
+                },
+                ...text(theme.middle, "sans", "default", { size: "sm" }),
+            },
+            state: {
+                hovered: {
+                    ...text(theme.middle, "sans", "default", { size: "sm" }),
+                    background: background(theme.middle, "hovered"),
+                    border: border(theme.middle, "active"),
+                },
+                clicked: {
+                    ...text(theme.middle, "sans", "default", { size: "sm" }),
+                    background: background(theme.middle, "pressed"),
+                    border: border(theme.middle, "active"),
+                },
+            },
+        }),
+        background: background(layer),
+        padding: {
+            top: SPACING,
+        },
+        user_query_editor: filter_input,
+        channel_hash: icon_style,
+        user_query_editor_height: 33,
+        add_contact_button: header_icon_button,
+        add_channel_button: header_icon_button,
+        leave_call_button: header_icon_button,
+        row_height: ITEM_HEIGHT,
+        channel_indent: INDENT_SIZE * 2 + 2,
+        section_icon_size: 14,
+        header_row: {
+            ...text(layer, "sans", { size: "sm", weight: "bold" }),
+            margin: { top: SPACING },
+            padding: {
+                left: SPACING,
+                right: SPACING,
+            },
+        },
+        subheader_row,
+        leave_call: interactive({
+            base: {
+                background: background(layer),
+                border: border(layer),
+                corner_radius: 6,
+                margin: {
+                    top: 1,
+                },
+                padding: {
+                    top: 1,
+                    bottom: 1,
+                    left: 7,
+                    right: 7,
+                },
+                ...text(layer, "sans", "variant", { size: "xs" }),
+            },
+            state: {
+                hovered: {
+                    ...text(layer, "sans", "hovered", { size: "xs" }),
+                    background: background(layer, "hovered"),
+                    border: border(layer, "hovered"),
+                },
+            },
+        }),
+        contact_row: toggleable({
+            base: interactive({
+                base: {
+                    padding: {
+                        left: SPACING,
+                        right: SPACING,
+                    },
+                },
+                state: {
+                    clicked: {
+                        background: background(layer, "pressed"),
+                    },
+                },
+            }),
+            state: {
+                inactive: {
+                    hovered: {
+                        background: background(layer, "hovered"),
+                    },
+                },
+                active: {
+                    default: {
+                        ...text(theme.lowest, "sans", { size: "sm" }),
+                        background: background(theme.lowest),
+                    },
+                    clicked: {
+                        background: background(layer, "pressed"),
+                    },
+                },
+            },
+        }),
+        channel_row: item_row,
+        channel_name: {
+            ...text(layer, "sans", { size: "sm" }),
+            margin: {
+                left: CHANNEL_SPACING,
+            },
+        },
+        list_empty_label_container: {
+            margin: {
+                left: NAME_MARGIN,
+            },
+        },
+        list_empty_icon: {
+            color: foreground(layer, "variant"),
+            width: 14,
+        },
+        list_empty_state: toggleable({
+            base: interactive({
+                base: {
+                    ...text(layer, "sans", "variant", { size: "sm" }),
+                    padding: {
+                        top: SPACING / 2,
+                        bottom: SPACING / 2,
+                        left: SPACING,
+                        right: SPACING,
+                    },
+                },
+                state: {
+                    clicked: {
+                        background: background(layer, "pressed"),
+                    },
+                },
+            }),
+            state: {
+                inactive: {
+                    hovered: {
+                        background: background(layer, "hovered"),
+                    },
+                },
+                active: {
+                    default: {
+                        ...text(theme.lowest, "sans", { size: "sm" }),
+                        background: background(theme.lowest),
+                    },
+                    clicked: {
+                        background: background(layer, "pressed"),
+                    },
+                },
+            },
+        }),
+        contact_avatar: {
+            corner_radius: 10,
+            width: 20,
+        },
+        channel_avatar: {
+            corner_radius: 10,
+            width: 20,
+        },
+        extra_participant_label: {
+            corner_radius: 10,
+            padding: {
+                left: 10,
+                right: 4,
+            },
+            background: background(layer, "hovered"),
+            ...text(layer, "sans", "hovered", { size: "xs" }),
+        },
+        contact_status_free: indicator({ layer, color: "positive" }),
+        contact_status_busy: indicator({ layer, color: "negative" }),
+        contact_username: {
+            ...text(layer, "sans", { size: "sm" }),
+            margin: {
+                left: NAME_MARGIN,
+            },
+        },
+        contact_button_spacing: NAME_MARGIN,
+        contact_button: icon_button({
+            variant: "ghost",
+            color: "variant",
+            size: "sm",
+        }),
+        disabled_button: {
+            ...contact_button,
+            background: background(layer, "on"),
+            color: foreground(layer, "on"),
+        },
+        calling_indicator: {
+            ...text(layer, "sans", "variant", { size: "xs" }),
+        },
+        tree_branch: toggleable({
+            base: interactive({
+                base: {
+                    color: border_color(layer),
+                    width: 1,
+                },
+                state: {
+                    hovered: {
+                        color: border_color(layer),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: {
+                        color: border_color(layer),
+                    },
+                },
+            },
+        }),
+        project_row: toggleable({
+            base: interactive({
+                base: {
+                    ...project_row,
+                    icon: {
+                        margin: { left: NAME_MARGIN },
+                        color: foreground(layer, "variant"),
+                        width: 14,
+                    },
+                    name: {
+                        ...project_row.name,
+                        ...text(layer, "sans", { size: "sm" }),
+                    },
+                },
+                state: {
+                    hovered: {
+                        background: background(layer, "hovered"),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: { background: background(theme.lowest) },
+                },
+            },
+        }),
+        face_overlap: 8,
+        channel_editor: {
+            padding: {
+                left: NAME_MARGIN,
+            },
+        },
+    }
+}

styles/src/style_tree/component_test.ts 🔗

@@ -0,0 +1,26 @@
+import { useTheme } from "../common"
+import { text_button } from "../component/text_button"
+import { icon_button } from "../component/icon_button"
+import { text } from "./components"
+import { toggleable } from "../element"
+
+export default function contacts_panel(): any {
+    const theme = useTheme()
+
+    return {
+        button: text_button({}),
+        toggle: toggleable({
+            base: text_button({}),
+            state: {
+                active: {
+                    ...text_button({ color: "accent" }),
+                },
+            },
+        }),
+        disclosure: {
+            ...text(theme.lowest, "sans", "base"),
+            button: icon_button({ variant: "ghost" }),
+            spacing: 4,
+        },
+    }
+}

styles/src/style_tree/contact_finder.ts 🔗

@@ -1,11 +1,11 @@
-import picker from "./picker"
+// import picker from "./picker"
 import { background, border, foreground, text } from "./components"
 import { useTheme } from "../theme"
 
 export default function contact_finder(): any {
     const theme = useTheme()
 
-    const side_margin = 6
+    // const side_margin = 6
     const contact_button = {
         background: background(theme.middle, "variant"),
         color: foreground(theme.middle, "variant"),
@@ -14,42 +14,42 @@ export default function contact_finder(): any {
         corner_radius: 8,
     }
 
-    const picker_style = picker()
-    const picker_input = {
-        background: background(theme.middle, "on"),
-        corner_radius: 6,
-        text: text(theme.middle, "mono"),
-        placeholder_text: text(theme.middle, "mono", "on", "disabled", {
-            size: "xs",
-        }),
-        selection: theme.players[0],
-        border: border(theme.middle),
-        padding: {
-            bottom: 4,
-            left: 8,
-            right: 8,
-            top: 4,
-        },
-        margin: {
-            left: side_margin,
-            right: side_margin,
-        },
-    }
+    // const picker_style = picker()
+    // const picker_input = {
+    //     background: background(theme.middle, "on"),
+    //     corner_radius: 6,
+    //     text: text(theme.middle, "mono"),
+    //     placeholder_text: text(theme.middle, "mono", "on", "disabled", {
+    //         size: "xs",
+    //     }),
+    //     selection: theme.players[0],
+    //     border: border(theme.middle),
+    //     padding: {
+    //         bottom: 4,
+    //         left: 8,
+    //         right: 8,
+    //         top: 4,
+    //     },
+    //     margin: {
+    //         left: side_margin,
+    //         right: side_margin,
+    //     },
+    // }
 
     return {
-        picker: {
-            empty_container: {},
-            item: {
-                ...picker_style.item,
-                margin: { left: side_margin, right: side_margin },
-            },
-            no_matches: picker_style.no_matches,
-            input_editor: picker_input,
-            empty_input_editor: picker_input,
-            header: picker_style.header,
-            footer: picker_style.footer,
-        },
-        row_height: 28,
+        // picker: {
+        //     empty_container: {},
+        //     item: {
+        //         ...picker_style.item,
+        //         margin: { left: side_margin, right: side_margin },
+        //     },
+        //     no_matches: picker_style.no_matches,
+        //     input_editor: picker_input,
+        //     empty_input_editor: picker_input,
+        //     header: picker_style.header,
+        //     footer: picker_style.footer,
+        // },
+        // row_height: 28,
         contact_avatar: {
             corner_radius: 10,
             width: 18,

styles/src/style_tree/contact_list.ts 🔗

@@ -1,247 +0,0 @@
-import {
-    background,
-    border,
-    border_color,
-    foreground,
-    text,
-} from "./components"
-import { interactive, toggleable } from "../element"
-import { useTheme } from "../theme"
-export default function contacts_panel(): any {
-    const theme = useTheme()
-
-    const name_margin = 8
-    const side_padding = 12
-
-    const layer = theme.middle
-
-    const contact_button = {
-        background: background(layer, "on"),
-        color: foreground(layer, "on"),
-        icon_width: 8,
-        button_width: 16,
-        corner_radius: 8,
-    }
-    const project_row = {
-        guest_avatar_spacing: 4,
-        height: 24,
-        guest_avatar: {
-            corner_radius: 8,
-            width: 14,
-        },
-        name: {
-            ...text(layer, "mono", { size: "sm" }),
-            margin: {
-                left: name_margin,
-                right: 6,
-            },
-        },
-        guests: {
-            margin: {
-                left: name_margin,
-                right: name_margin,
-            },
-        },
-        padding: {
-            left: side_padding,
-            right: side_padding,
-        },
-    }
-
-    return {
-        background: background(layer),
-        padding: { top: 12 },
-        user_query_editor: {
-            background: background(layer, "on"),
-            corner_radius: 6,
-            text: text(layer, "mono", "on"),
-            placeholder_text: text(layer, "mono", "on", "disabled", {
-                size: "xs",
-            }),
-            selection: theme.players[0],
-            border: border(layer, "on"),
-            padding: {
-                bottom: 4,
-                left: 8,
-                right: 8,
-                top: 4,
-            },
-            margin: {
-                left: 6,
-            },
-        },
-        user_query_editor_height: 33,
-        add_contact_button: {
-            margin: { left: 6, right: 12 },
-            color: foreground(layer, "on"),
-            button_width: 28,
-            icon_width: 16,
-        },
-        row_height: 28,
-        section_icon_size: 8,
-        header_row: toggleable({
-            base: interactive({
-                base: {
-                    ...text(layer, "mono", { size: "sm" }),
-                    margin: { top: 14 },
-                    padding: {
-                        left: side_padding,
-                        right: side_padding,
-                    },
-                    background: background(layer, "default"), // posiewic: breaking change
-                },
-                state: {
-                    hovered: {
-                        background: background(layer, "hovered"),
-                    },
-                    clicked: {
-                        background: background(layer, "pressed"),
-                    },
-                }, // hack, we want headerRow to be interactive for whatever reason. It probably shouldn't be interactive in the first place.
-            }),
-            state: {
-                active: {
-                    default: {
-                        ...text(layer, "mono", "active", { size: "sm" }),
-                        background: background(layer, "active"),
-                    },
-                    hovered: {
-                        background: background(layer, "hovered"),
-                    },
-                    clicked: {
-                        background: background(layer, "pressed"),
-                    },
-                },
-            },
-        }),
-        leave_call: interactive({
-            base: {
-                background: background(layer),
-                border: border(layer),
-                corner_radius: 6,
-                margin: {
-                    top: 1,
-                },
-                padding: {
-                    top: 1,
-                    bottom: 1,
-                    left: 7,
-                    right: 7,
-                },
-                ...text(layer, "sans", "variant", { size: "xs" }),
-            },
-            state: {
-                hovered: {
-                    ...text(layer, "sans", "hovered", { size: "xs" }),
-                    background: background(layer, "hovered"),
-                    border: border(layer, "hovered"),
-                },
-            },
-        }),
-        contact_row: {
-            inactive: {
-                default: {
-                    padding: {
-                        left: side_padding,
-                        right: side_padding,
-                    },
-                },
-            },
-            active: {
-                default: {
-                    background: background(layer, "active"),
-                    padding: {
-                        left: side_padding,
-                        right: side_padding,
-                    },
-                },
-            },
-        },
-        contact_avatar: {
-            corner_radius: 10,
-            width: 18,
-        },
-        contact_status_free: {
-            corner_radius: 4,
-            padding: 4,
-            margin: { top: 12, left: 12 },
-            background: foreground(layer, "positive"),
-        },
-        contact_status_busy: {
-            corner_radius: 4,
-            padding: 4,
-            margin: { top: 12, left: 12 },
-            background: foreground(layer, "negative"),
-        },
-        contact_username: {
-            ...text(layer, "mono", { size: "sm" }),
-            margin: {
-                left: name_margin,
-            },
-        },
-        contact_button_spacing: name_margin,
-        contact_button: interactive({
-            base: { ...contact_button },
-            state: {
-                hovered: {
-                    background: background(layer, "hovered"),
-                },
-            },
-        }),
-        disabled_button: {
-            ...contact_button,
-            background: background(layer, "on"),
-            color: foreground(layer, "on"),
-        },
-        calling_indicator: {
-            ...text(layer, "mono", "variant", { size: "xs" }),
-        },
-        tree_branch: toggleable({
-            base: interactive({
-                base: {
-                    color: border_color(layer),
-                    width: 1,
-                },
-                state: {
-                    hovered: {
-                        color: border_color(layer),
-                    },
-                },
-            }),
-            state: {
-                active: {
-                    default: {
-                        color: border_color(layer),
-                    },
-                },
-            },
-        }),
-        project_row: toggleable({
-            base: interactive({
-                base: {
-                    ...project_row,
-                    background: background(layer),
-                    icon: {
-                        margin: { left: name_margin },
-                        color: foreground(layer, "variant"),
-                        width: 12,
-                    },
-                    name: {
-                        ...project_row.name,
-                        ...text(layer, "mono", { size: "sm" }),
-                    },
-                },
-                state: {
-                    hovered: {
-                        background: background(layer, "hovered"),
-                    },
-                },
-            }),
-            state: {
-                active: {
-                    default: { background: background(layer, "active") },
-                },
-            },
-        }),
-    }
-}

styles/src/style_tree/contacts_popover.ts 🔗

@@ -3,14 +3,4 @@ import { background, border } from "./components"
 
 export default function contacts_popover(): any {
     const theme = useTheme()
-
-    return {
-        background: background(theme.middle),
-        corner_radius: 6,
-        padding: { top: 6, bottom: 6 },
-        shadow: theme.popover_shadow,
-        border: border(theme.middle),
-        width: 300,
-        height: 400,
-    }
 }

styles/src/style_tree/context_menu.ts 🔗

@@ -31,16 +31,6 @@ export default function context_menu(): any {
                 state: {
                     hovered: {
                         background: background(theme.middle, "hovered"),
-                        label: text(theme.middle, "sans", "hovered", {
-                            size: "sm",
-                        }),
-                        keystroke: {
-                            ...text(theme.middle, "sans", "hovered", {
-                                size: "sm",
-                                weight: "bold",
-                            }),
-                            padding: { left: 3, right: 3 },
-                        },
                     },
                     clicked: {
                         background: background(theme.middle, "pressed"),

styles/src/style_tree/editor.ts 🔗

@@ -184,6 +184,7 @@ export default function editor(): any {
             theme.players[6],
             theme.players[7],
         ],
+        absent_selection: theme.players[7],
         autocomplete: {
             background: background(theme.middle),
             corner_radius: 8,
@@ -309,7 +310,7 @@ export default function editor(): any {
                     ? with_opacity(theme.ramps.green(0.5).hex(), 0.8)
                     : with_opacity(theme.ramps.green(0.4).hex(), 0.8),
             },
-            selections: foreground(layer, "accent")
+            selections: foreground(layer, "accent"),
         },
         composition_mark: {
             underline: {

styles/src/style_tree/feedback.ts 🔗

@@ -37,7 +37,7 @@ export default function feedback(): any {
                     ...text(theme.highest, "mono", "on", "disabled"),
                     background: background(theme.highest, "on", "disabled"),
                     border: border(theme.highest, "on", "disabled"),
-                }
+                },
             },
         }),
         button_margin: 8,

styles/src/style_tree/project_panel.ts 🔗

@@ -64,17 +64,17 @@ export default function project_panel(): any {
         const unselected_default_style = merge(
             base_properties,
             unselected?.default ?? {},
-            {},
+            {}
         )
         const unselected_hovered_style = merge(
             base_properties,
             { background: background(theme.middle, "hovered") },
-            unselected?.hovered ?? {},
+            unselected?.hovered ?? {}
         )
         const unselected_clicked_style = merge(
             base_properties,
             { background: background(theme.middle, "pressed") },
-            unselected?.clicked ?? {},
+            unselected?.clicked ?? {}
         )
         const selected_default_style = merge(
             base_properties,
@@ -82,7 +82,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.default ?? {},
+            selected_style?.default ?? {}
         )
         const selected_hovered_style = merge(
             base_properties,
@@ -90,7 +90,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest, "hovered"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.hovered ?? {},
+            selected_style?.hovered ?? {}
         )
         const selected_clicked_style = merge(
             base_properties,
@@ -98,7 +98,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest, "pressed"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.clicked ?? {},
+            selected_style?.clicked ?? {}
         )
 
         return toggleable({
@@ -175,7 +175,7 @@ export default function project_panel(): any {
                 default: {
                     icon_color: foreground(theme.middle, "variant"),
                 },
-            },
+            }
         ),
         cut_entry: entry(
             {
@@ -190,7 +190,7 @@ export default function project_panel(): any {
                         size: "sm",
                     }),
                 },
-            },
+            }
         ),
         filename_editor: {
             background: background(theme.middle, "on"),

styles/src/style_tree/search.ts 🔗

@@ -2,9 +2,23 @@ import { with_opacity } from "../theme/color"
 import { background, border, foreground, text } from "./components"
 import { interactive, toggleable } from "../element"
 import { useTheme } from "../theme"
+import { text_button } from "../component/text_button"
+
+const search_results = () => {
+    const theme = useTheme()
+
+    return {
+        // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
+        match_background: with_opacity(
+            foreground(theme.highest, "accent"),
+            0.4
+        ),
+    }
+}
 
 export default function search(): any {
     const theme = useTheme()
+    const SEARCH_ROW_SPACING = 12
 
     // Search input
     const editor = {
@@ -17,13 +31,13 @@ export default function search(): any {
         text: text(theme.highest, "mono", "default"),
         border: border(theme.highest),
         margin: {
-            right: 12,
+            right: SEARCH_ROW_SPACING,
         },
         padding: {
-            top: 3,
-            bottom: 3,
-            left: 12,
-            right: 8,
+            top: 4,
+            bottom: 4,
+            left: 10,
+            right: 4,
         },
     }
 
@@ -34,84 +48,163 @@ export default function search(): any {
     }
 
     return {
-        // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
-        match_background: with_opacity(
-            foreground(theme.highest, "accent"),
-            0.4
-        ),
+        padding: { top: 0, bottom: 0 },
+
         option_button: toggleable({
             base: interactive({
                 base: {
-                    ...text(theme.highest, "mono", "on"),
+                    icon_width: 14,
+                    button_width: 32,
+                    color: foreground(theme.highest, "variant"),
                     background: background(theme.highest, "on"),
-                    corner_radius: 6,
-                    border: border(theme.highest, "on"),
-                    margin: {
-                        right: 4,
+                    corner_radius: 2,
+                    margin: { right: 2 },
+                    border: {
+                        width: 1,
+                        color: background(theme.highest, "on"),
                     },
                     padding: {
-                        bottom: 2,
-                        left: 10,
-                        right: 10,
-                        top: 2,
+                        left: 4,
+                        right: 4,
+                        top: 4,
+                        bottom: 4,
                     },
                 },
                 state: {
                     hovered: {
-                        ...text(theme.highest, "mono", "on", "hovered"),
+                        ...text(theme.highest, "mono", "variant", "hovered"),
                         background: background(theme.highest, "on", "hovered"),
-                        border: border(theme.highest, "on", "hovered"),
+                        border: {
+                            width: 1,
+                            color: background(theme.highest, "on", "hovered"),
+                        },
                     },
                     clicked: {
-                        ...text(theme.highest, "mono", "on", "pressed"),
+                        ...text(theme.highest, "mono", "variant", "pressed"),
                         background: background(theme.highest, "on", "pressed"),
-                        border: border(theme.highest, "on", "pressed"),
+                        border: {
+                            width: 1,
+                            color: background(theme.highest, "on", "pressed"),
+                        },
                     },
                 },
             }),
             state: {
                 active: {
                     default: {
-                        ...text(theme.highest, "mono", "accent"),
+                        icon_width: 14,
+                        button_width: 32,
+                        color: foreground(theme.highest, "variant"),
+                        background: background(theme.highest, "accent"),
+                        border: border(theme.highest, "accent"),
                     },
                     hovered: {
-                        ...text(theme.highest, "mono", "accent", "hovered"),
+                        background: background(
+                            theme.highest,
+                            "accent",
+                            "hovered"
+                        ),
+                        border: border(theme.highest, "accent", "hovered"),
                     },
                     clicked: {
-                        ...text(theme.highest, "mono", "accent", "pressed"),
+                        background: background(
+                            theme.highest,
+                            "accent",
+                            "pressed"
+                        ),
+                        border: border(theme.highest, "accent", "pressed"),
                     },
                 },
             },
         }),
-        action_button: interactive({
-            base: {
-                ...text(theme.highest, "mono", "on"),
-                background: background(theme.highest, "on"),
-                corner_radius: 6,
-                border: border(theme.highest, "on"),
-                margin: {
-                    right: 4,
+        option_button_component: toggleable({
+            base: interactive({
+                base: {
+                    icon_size: 14,
+                    color: foreground(theme.highest, "variant"),
+
+                    button_width: 32,
+                    background: background(theme.highest, "on"),
+                    corner_radius: 2,
+                    margin: { right: 2 },
+                    border: {
+                        width: 1,
+                        color: background(theme.highest, "on"),
+                    },
+                    padding: {
+                        left: 4,
+                        right: 4,
+                        top: 4,
+                        bottom: 4,
+                    },
                 },
-                padding: {
-                    bottom: 2,
-                    left: 10,
-                    right: 10,
-                    top: 2,
+                state: {
+                    hovered: {
+                        ...text(theme.highest, "mono", "variant", "hovered"),
+                        background: background(theme.highest, "on", "hovered"),
+                        border: {
+                            width: 1,
+                            color: background(theme.highest, "on", "hovered"),
+                        },
+                    },
+                    clicked: {
+                        ...text(theme.highest, "mono", "variant", "pressed"),
+                        background: background(theme.highest, "on", "pressed"),
+                        border: {
+                            width: 1,
+                            color: background(theme.highest, "on", "pressed"),
+                        },
+                    },
                 },
-            },
+            }),
             state: {
-                hovered: {
-                    ...text(theme.highest, "mono", "on", "hovered"),
-                    background: background(theme.highest, "on", "hovered"),
-                    border: border(theme.highest, "on", "hovered"),
-                },
-                clicked: {
-                    ...text(theme.highest, "mono", "on", "pressed"),
-                    background: background(theme.highest, "on", "pressed"),
-                    border: border(theme.highest, "on", "pressed"),
+                active: {
+                    default: {
+                        icon_size: 14,
+                        button_width: 32,
+                        color: foreground(theme.highest, "variant"),
+                        background: background(theme.highest, "accent"),
+                        border: border(theme.highest, "accent"),
+                    },
+                    hovered: {
+                        background: background(
+                            theme.highest,
+                            "accent",
+                            "hovered"
+                        ),
+                        border: border(theme.highest, "accent", "hovered"),
+                    },
+                    clicked: {
+                        background: background(
+                            theme.highest,
+                            "accent",
+                            "pressed"
+                        ),
+                        border: border(theme.highest, "accent", "pressed"),
+                    },
                 },
             },
         }),
+        // Search tool buttons
+        // HACK: This is not how disabled elements should be created
+        // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled
+        action_button: toggleable({
+            state: {
+                inactive: text_button({
+                    variant: "ghost",
+                    layer: theme.highest,
+                    disabled: true,
+                    margin: { right: SEARCH_ROW_SPACING },
+                    text_properties: { size: "sm" },
+                }),
+                active: text_button({
+                    variant: "ghost",
+                    layer: theme.highest,
+                    margin: { right: SEARCH_ROW_SPACING },
+                    text_properties: { size: "sm" },
+                }),
+            },
+        }),
         editor,
         invalid_editor: {
             ...editor,
@@ -123,15 +216,15 @@ export default function search(): any {
             border: border(theme.highest, "negative"),
         },
         match_index: {
-            ...text(theme.highest, "mono", "variant"),
+            ...text(theme.highest, "mono", { size: "sm" }),
             padding: {
-                left: 6,
+                right: SEARCH_ROW_SPACING,
             },
         },
         option_button_group: {
             padding: {
-                left: 12,
-                right: 12,
+                left: SEARCH_ROW_SPACING,
+                right: SEARCH_ROW_SPACING,
             },
         },
         include_exclude_inputs: {
@@ -140,28 +233,171 @@ export default function search(): any {
                 right: 6,
             },
         },
-        results_status: {
+        major_results_status: {
             ...text(theme.highest, "mono", "on"),
-            size: 18,
+            size: 15,
         },
-        dismiss_button: interactive({
-            base: {
-                color: foreground(theme.highest, "variant"),
-                icon_width: 12,
-                button_width: 14,
-                padding: {
-                    left: 10,
-                    right: 10,
+        minor_results_status: {
+            ...text(theme.highest, "mono", "variant"),
+            size: 13,
+        },
+        // Input Icon
+        editor_icon: {
+            icon: {
+                color: foreground(theme.highest, "disabled"),
+                asset: "icons/magnifying_glass.svg",
+                dimensions: {
+                    width: 14,
+                    height: 14,
                 },
             },
-            state: {
-                hovered: {
-                    color: foreground(theme.highest, "hovered"),
+            container: {
+                margin: { right: 4 },
+                padding: { left: 1, right: 1 },
+            },
+        },
+        // Toggle group buttons - Text | Regex | Semantic
+        mode_button: toggleable({
+            base: interactive({
+                base: {
+                    ...text(theme.highest, "mono", "variant", { size: "sm" }),
+                    background: background(theme.highest, "variant"),
+
+                    border: {
+                        ...border(theme.highest, "on"),
+                        left: false,
+                        right: false,
+                    },
+                    margin: {
+                        top: 1,
+                        bottom: 1,
+                    },
+                    padding: {
+                        left: 10,
+                        right: 10,
+                    },
+                    corner_radius: 6,
+                },
+                state: {
+                    hovered: {
+                        ...text(theme.highest, "mono", "variant", "hovered", {
+                            size: "sm",
+                        }),
+                        background: background(
+                            theme.highest,
+                            "variant",
+                            "hovered"
+                        ),
+                        border: border(theme.highest, "on", "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.highest, "mono", "variant", "pressed", {
+                            size: "sm",
+                        }),
+                        background: background(
+                            theme.highest,
+                            "variant",
+                            "pressed"
+                        ),
+                        border: border(theme.highest, "on", "pressed"),
+                    },
                 },
-                clicked: {
-                    color: foreground(theme.highest, "pressed"),
+            }),
+            state: {
+                active: {
+                    default: {
+                        ...text(theme.highest, "mono", "on", { size: "sm" }),
+                        background: background(theme.highest, "on"),
+                    },
+                    hovered: {
+                        ...text(theme.highest, "mono", "on", "hovered", {
+                            size: "sm",
+                        }),
+                        background: background(theme.highest, "on", "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.highest, "mono", "on", "pressed", {
+                            size: "sm",
+                        }),
+                        background: background(theme.highest, "on", "pressed"),
+                    },
                 },
             },
         }),
+        // Next/Previous Match buttons
+        // HACK: This is not how disabled elements should be created
+        // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled
+        nav_button: toggleable({
+            state: {
+                inactive: interactive({
+                    base: {
+                        background: background(theme.highest, "disabled"),
+                        text: text(theme.highest, "mono", "disabled"),
+                        corner_radius: 6,
+                        border: {
+                            ...border(theme.highest, "disabled"),
+                            left: false,
+                            right: false,
+                        },
+                        margin: {
+                            top: 1,
+                            bottom: 1,
+                        },
+                        padding: {
+                            left: 10,
+                            right: 10,
+                        },
+                    },
+                    state: {
+                        hovered: {},
+                    },
+                }),
+                active: interactive({
+                    base: {
+                        text: text(theme.highest, "mono", "on"),
+                        background: background(theme.highest, "on"),
+                        corner_radius: 6,
+                        border: {
+                            ...border(theme.highest, "on"),
+                            left: false,
+                            right: false,
+                        },
+                        margin: {
+                            top: 1,
+                            bottom: 1,
+                        },
+                        padding: {
+                            left: 10,
+                            right: 10,
+                        },
+                    },
+                    state: {
+                        hovered: {
+                            ...text(theme.highest, "mono", "on", "hovered"),
+                            background: background(
+                                theme.highest,
+                                "on",
+                                "hovered"
+                            ),
+                            border: border(theme.highest, "on", "hovered"),
+                        },
+                        clicked: {
+                            ...text(theme.highest, "mono", "on", "pressed"),
+                            background: background(
+                                theme.highest,
+                                "on",
+                                "pressed"
+                            ),
+                            border: border(theme.highest, "on", "pressed"),
+                        },
+                    },
+                }),
+            },
+        }),
+        search_bar_row_height: 32,
+        search_row_spacing: 8,
+        option_button_height: 22,
+        modes_container: {},
+        ...search_results(),
     }
 }

styles/src/style_tree/status_bar.ts 🔗

@@ -28,16 +28,18 @@ export default function status_bar(): any {
             right: 6,
         },
         border: border(layer, { top: true, overlay: true }),
-        cursor_position: text(layer, "sans", "variant", { size: "xs" }),
+        cursor_position: text(layer, "sans", "base", { size: "xs" }),
         vim_mode_indicator: {
             margin: { left: 6 },
-            ...text(layer, "mono", "variant", { size: "xs" }),
+            ...text(layer, "mono", "base", { size: "xs" }),
         },
         active_language: text_button({
-            color: "variant"
+            color: "base",
         }),
-        auto_update_progress_message: text(layer, "sans", "variant", { size: "xs" }),
-        auto_update_done_message: text(layer, "sans", "variant", { size: "xs" }),
+        auto_update_progress_message: text(layer, "sans", "base", {
+            size: "xs",
+        }),
+        auto_update_done_message: text(layer, "sans", "base", { size: "xs" }),
         lsp_status: interactive({
             base: {
                 ...diagnostic_status_container,
@@ -64,43 +66,45 @@ export default function status_bar(): any {
         diagnostic_summary: interactive({
             base: {
                 height: 20,
-                icon_width: 16,
+                icon_width: 14,
                 icon_spacing: 2,
                 summary_spacing: 6,
                 text: text(layer, "sans", { size: "sm" }),
-                icon_color_ok: foreground(layer, "variant"),
+                icon_color_ok: foreground(layer, "base"),
                 icon_color_warning: foreground(layer, "warning"),
                 icon_color_error: foreground(layer, "negative"),
                 container_ok: {
                     corner_radius: 6,
-                    padding: { top: 3, bottom: 3, left: 7, right: 7 },
-                },
-                container_warning: {
-                    ...diagnostic_status_container,
-                    background: background(layer, "warning"),
-                    border: border(layer, "warning"),
-                },
-                container_error: {
-                    ...diagnostic_status_container,
-                    background: background(layer, "negative"),
-                    border: border(layer, "negative"),
+                    padding: { top: 2, bottom: 2, left: 6, right: 6 },
                 },
+                container_warning: diagnostic_status_container,
+                container_error: diagnostic_status_container
             },
             state: {
                 hovered: {
                     icon_color_ok: foreground(layer, "on"),
                     container_ok: {
-                        background: background(layer, "on", "hovered"),
+                        background: background(layer, "hovered")
                     },
                     container_warning: {
-                        background: background(layer, "warning", "hovered"),
-                        border: border(layer, "warning", "hovered"),
+                        background: background(layer, "hovered")
                     },
                     container_error: {
-                        background: background(layer, "negative", "hovered"),
-                        border: border(layer, "negative", "hovered"),
+                        background: background(layer, "hovered")
                     },
                 },
+                clicked: {
+                    icon_color_ok: foreground(layer, "on"),
+                    container_ok: {
+                        background: background(layer, "pressed")
+                    },
+                    container_warning: {
+                        background: background(layer, "pressed")
+                    },
+                    container_error: {
+                        background: background(layer, "pressed")
+                    }
+                }
             },
         }),
         panel_buttons: {
@@ -111,8 +115,9 @@ export default function status_bar(): any {
                 base: interactive({
                     base: {
                         ...status_container,
-                        icon_size: 16,
-                        icon_color: foreground(layer, "variant"),
+                        icon_size: 14,
+                        icon_color: foreground(layer, "base"),
+                        background: background(layer, "default"),
                         label: {
                             margin: { left: 6 },
                             ...text(layer, "sans", { size: "xs" }),
@@ -120,23 +125,25 @@ export default function status_bar(): any {
                     },
                     state: {
                         hovered: {
-                            icon_color: foreground(layer, "hovered"),
-                            background: background(layer, "variant"),
+                            background: background(layer, "hovered"),
+                        },
+                        clicked: {
+                            background: background(layer, "pressed"),
                         },
                     },
                 }),
                 state: {
                     active: {
                         default: {
-                            icon_color: foreground(layer, "active"),
-                            background: background(layer, "active"),
+                            icon_color: foreground(layer, "accent", "default"),
+                            background: background(layer, "default"),
                         },
                         hovered: {
-                            icon_color: foreground(layer, "hovered"),
+                            icon_color: foreground(layer, "accent", "hovered"),
                             background: background(layer, "hovered"),
                         },
                         clicked: {
-                            icon_color: foreground(layer, "pressed"),
+                            icon_color: foreground(layer, "accent", "pressed"),
                             background: background(layer, "pressed"),
                         },
                     },

styles/src/style_tree/tab_bar.ts 🔗

@@ -84,6 +84,27 @@ export default function tab_bar(): any {
             bottom: false,
         },
     }
+    const nav_button = interactive({
+        base: {
+            color: foreground(theme.highest, "on"),
+            icon_width: 12,
+
+            button_width: active_pane_active_tab.height,
+            border: border(theme.lowest, "on", {
+                bottom: true,
+                overlay: true,
+            }),
+        },
+        state: {
+            hovered: {
+                color: foreground(theme.highest, "on", "hovered"),
+                background: background(theme.highest, "on", "hovered"),
+            },
+            disabled: {
+                color: foreground(theme.highest, "on", "disabled"),
+            },
+        },
+    })
 
     const dragged_tab = {
         ...active_pane_active_tab,
@@ -141,5 +162,6 @@ export default function tab_bar(): any {
                 right: false,
             },
         },
+        nav_button: nav_button,
     }
 }

styles/src/style_tree/titlebar.ts 🔗

@@ -1,8 +1,6 @@
-import { icon_button, toggleable_icon_button } from "../component/icon_button"
-import { toggleable_text_button } from "../component/text_button"
+import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component"
 import { interactive, toggleable } from "../element"
-import { useTheme } from "../theme"
-import { with_opacity } from "../theme/color"
+import { useTheme, with_opacity } from "../theme"
 import { background, border, foreground, text } from "./components"
 
 const ITEM_SPACING = 8
@@ -34,16 +32,17 @@ function call_controls() {
     }
 
     return {
-        toggle_microphone_button: toggleable_icon_button(theme, {
+        toggle_microphone_button: toggleable_icon_button({
             margin: {
                 ...margin_y,
                 left: space.group,
                 right: space.half_item,
             },
             active_color: "negative",
+            active_background_color: "negative",
         }),
 
-        toggle_speakers_button: toggleable_icon_button(theme, {
+        toggle_speakers_button: toggleable_icon_button({
             margin: {
                 ...margin_y,
                 left: space.half_item,
@@ -51,13 +50,14 @@ function call_controls() {
             },
         }),
 
-        screen_share_button: toggleable_icon_button(theme, {
+        screen_share_button: toggleable_icon_button({
             margin: {
                 ...margin_y,
                 left: space.half_item,
                 right: space.group,
             },
             active_color: "accent",
+            active_background_color: "accent",
         }),
 
         muted: foreground(theme.lowest, "negative"),
@@ -178,15 +178,17 @@ export function titlebar(): any {
             left: 80,
             right: 0,
         },
-
-        // Project
-        project_name_divider: text(theme.lowest, "sans", "variant"),
+        menu: {
+            width: 300,
+            height: 400,
+        },
 
         project_menu_button: toggleable_text_button(theme, {
-            color: 'base',
+            color: "base"
         }),
+
         git_menu_button: toggleable_text_button(theme, {
-            color: 'variant',
+            color: "variant",
         }),
 
         // Collaborators
@@ -259,7 +261,7 @@ export function titlebar(): any {
 
         ...call_controls(),
 
-        toggle_contacts_button: toggleable_icon_button(theme, {
+        toggle_contacts_button: toggleable_icon_button({
             margin: {
                 left: ITEM_SPACING,
             },

styles/src/style_tree/toolbar.ts 🔗

@@ -0,0 +1,38 @@
+import { useTheme } from "../common"
+import { toggleable_icon_button } from "../component/icon_button"
+import { interactive } from "../element"
+import { background, border, foreground, text } from "./components"
+
+export const toolbar = () => {
+    const theme = useTheme()
+
+    return {
+        height: 32,
+        padding: { left: 4, right: 4, top: 4, bottom: 4 },
+        background: background(theme.highest),
+        border: border(theme.highest, { bottom: true }),
+        item_spacing: 4,
+        toggleable_tool: toggleable_icon_button({
+            margin: { left: 4 },
+            variant: "ghost",
+            active_color: "accent",
+        }),
+        breadcrumb_height: 24,
+        breadcrumbs: interactive({
+            base: {
+                ...text(theme.highest, "sans", "variant"),
+                corner_radius: 6,
+                padding: {
+                    left: 6,
+                    right: 6,
+                },
+            },
+            state: {
+                hovered: {
+                    color: foreground(theme.highest, "on", "hovered"),
+                    background: background(theme.highest, "on", "hovered"),
+                },
+            },
+        }),
+    }
+}

styles/src/style_tree/workspace.ts 🔗

@@ -12,6 +12,7 @@ import tabBar from "./tab_bar"
 import { interactive } from "../element"
 import { titlebar } from "./titlebar"
 import { useTheme } from "../theme"
+import { toolbar } from "./toolbar"
 
 export default function workspace(): any {
     const theme = useTheme()
@@ -127,47 +128,7 @@ export default function workspace(): any {
         },
         status_bar: statusBar(),
         titlebar: titlebar(),
-        toolbar: {
-            height: 34,
-            background: background(theme.highest),
-            border: border(theme.highest, { bottom: true }),
-            item_spacing: 8,
-            nav_button: interactive({
-                base: {
-                    color: foreground(theme.highest, "on"),
-                    icon_width: 12,
-                    button_width: 24,
-                    corner_radius: 6,
-                },
-                state: {
-                    hovered: {
-                        color: foreground(theme.highest, "on", "hovered"),
-                        background: background(theme.highest, "on", "hovered"),
-                    },
-                    disabled: {
-                        color: foreground(theme.highest, "on", "disabled"),
-                    },
-                },
-            }),
-            padding: { left: 8, right: 8, top: 4, bottom: 4 },
-        },
-        breadcrumb_height: 24,
-        breadcrumbs: interactive({
-            base: {
-                ...text(theme.highest, "sans", "variant"),
-                corner_radius: 6,
-                padding: {
-                    left: 6,
-                    right: 6,
-                },
-            },
-            state: {
-                hovered: {
-                    color: foreground(theme.highest, "on", "hovered"),
-                    background: background(theme.highest, "on", "hovered"),
-                },
-            },
-        }),
+        toolbar: toolbar(),
         disconnected_overlay: {
             ...text(theme.lowest, "sans"),
             background: with_opacity(background(theme.lowest), 0.8),

styles/src/theme/create_theme.ts 🔗

@@ -1,4 +1,4 @@
-import chroma, { Scale, Color } from "chroma-js"
+import { Scale, Color } from "chroma-js"
 import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
 export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
 import {
@@ -13,16 +13,16 @@ export interface Theme {
     is_light: boolean
 
     /**
-    * App background, other elements that should sit directly on top of the background.
-    */
+     * App background, other elements that should sit directly on top of the background.
+     */
     lowest: Layer
     /**
-    * Panels, tabs, other UI surfaces that sit on top of the background.
-    */
+     * Panels, tabs, other UI surfaces that sit on top of the background.
+     */
     middle: Layer
     /**
-    * Editors like code buffers, conversation editors, etc.
-    */
+     * Editors like code buffers, conversation editors, etc.
+     */
     highest: Layer
 
     ramps: RampSet
@@ -206,7 +206,10 @@ function build_color_family(ramps: RampSet): ColorFamily {
     for (const ramp in ramps) {
         const ramp_value = ramps[ramp as keyof RampSet]
 
-        const lightnessValues = [ramp_value(0).get('hsl.l') * 100, ramp_value(1).get('hsl.l') * 100]
+        const lightnessValues = [
+            ramp_value(0).get("hsl.l") * 100,
+            ramp_value(1).get("hsl.l") * 100,
+        ]
         const low = Math.min(...lightnessValues)
         const high = Math.max(...lightnessValues)
         const range = high - low

styles/src/theme/index.ts 🔗

@@ -23,3 +23,4 @@ export * from "./create_theme"
 export * from "./ramps"
 export * from "./syntax"
 export * from "./theme_config"
+export * from "./color"

styles/src/theme/tokens/theme.ts 🔗

@@ -4,11 +4,7 @@ import {
     SingleOtherToken,
     TokenTypes,
 } from "@tokens-studio/types"
-import {
-    Shadow,
-    SyntaxHighlightStyle,
-    ThemeSyntax,
-} from "../create_theme"
+import { Shadow, SyntaxHighlightStyle, ThemeSyntax } from "../create_theme"
 import { LayerToken, layer_token } from "./layer"
 import { PlayersToken, players_token } from "./players"
 import { color_token } from "./token"

styles/tsconfig.json 🔗

@@ -21,10 +21,7 @@
         "experimentalDecorators": true,
         "strictPropertyInitialization": false,
         "skipLibCheck": true,
-        "useUnknownInCatchVariables": false,
-        "baseUrl": "."
+        "useUnknownInCatchVariables": false
     },
-    "exclude": [
-        "node_modules"
-    ]
+    "exclude": ["node_modules"]
 }

test.rs 🔗

@@ -0,0 +1,5670 @@
+#![feature(prelude_import)]
+#![allow(dead_code, unused_variables)]
+#[prelude_import]
+use std::prelude::rust_2021::*;
+#[macro_use]
+extern crate std;
+use color::black;
+use components::button;
+use element::Element;
+use frame::frame;
+use gpui::{
+    geometry::{rect::RectF, vector::vec2f},
+    platform::WindowOptions,
+};
+use log::LevelFilter;
+use simplelog::SimpleLogger;
+use themes::{rose_pine, ThemeColors};
+use view::view;
+mod adapter {
+    use crate::element::{LayoutContext, PaintContext};
+    use gpui::{geometry::rect::RectF, LayoutEngine};
+    use util::ResultExt;
+    use crate::element::AnyElement;
+    pub struct Adapter<V>(pub(crate) AnyElement<V>);
+    impl<V: 'static> gpui::Element<V> for Adapter<V> {
+        type LayoutState = Option<LayoutEngine>;
+        type PaintState = ();
+        fn layout(
+            &mut self,
+            constraint: gpui::SizeConstraint,
+            view: &mut V,
+            cx: &mut LayoutContext<V>,
+        ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+            cx.push_layout_engine(LayoutEngine::new());
+            let node = self.0.layout(view, cx).log_err();
+            if let Some(node) = node {
+                let layout_engine = cx.layout_engine().unwrap();
+                layout_engine.compute_layout(node, constraint.max).log_err();
+            }
+            let layout_engine = cx.pop_layout_engine();
+            if true {
+                if !layout_engine.is_some() {
+                    ::core::panicking::panic("assertion failed: layout_engine.is_some()")
+                }
+            }
+            (constraint.max, layout_engine)
+        }
+        fn paint(
+            &mut self,
+            scene: &mut gpui::SceneBuilder,
+            bounds: RectF,
+            visible_bounds: RectF,
+            layout_engine: &mut Option<LayoutEngine>,
+            view: &mut V,
+            legacy_cx: &mut gpui::PaintContext<V>,
+        ) -> Self::PaintState {
+            legacy_cx.push_layout_engine(layout_engine.take().unwrap());
+            let mut cx = PaintContext::new(legacy_cx, scene);
+            self.0.paint(view, &mut cx).log_err();
+            *layout_engine = legacy_cx.pop_layout_engine();
+            if true {
+                if !layout_engine.is_some() {
+                    ::core::panicking::panic("assertion failed: layout_engine.is_some()")
+                }
+            }
+        }
+        fn rect_for_text_range(
+            &self,
+            range_utf16: std::ops::Range<usize>,
+            bounds: RectF,
+            visible_bounds: RectF,
+            layout: &Self::LayoutState,
+            paint: &Self::PaintState,
+            view: &V,
+            cx: &gpui::ViewContext<V>,
+        ) -> Option<RectF> {
+            ::core::panicking::panic("not yet implemented")
+        }
+        fn debug(
+            &self,
+            bounds: RectF,
+            layout: &Self::LayoutState,
+            paint: &Self::PaintState,
+            view: &V,
+            cx: &gpui::ViewContext<V>,
+        ) -> gpui::serde_json::Value {
+            ::core::panicking::panic("not yet implemented")
+        }
+    }
+}
+mod color {
+    #![allow(dead_code)]
+    use std::{num::ParseIntError, ops::Range};
+    use smallvec::SmallVec;
+    pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
+        let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
+        let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
+        let b = (hex & 0xFF) as f32 / 255.0;
+        Rgba { r, g, b, a: 1.0 }.into()
+    }
+    pub struct Rgba {
+        pub r: f32,
+        pub g: f32,
+        pub b: f32,
+        pub a: f32,
+    }
+    #[automatically_derived]
+    impl ::core::clone::Clone for Rgba {
+        #[inline]
+        fn clone(&self) -> Rgba {
+            let _: ::core::clone::AssertParamIsClone<f32>;
+            *self
+        }
+    }
+    #[automatically_derived]
+    impl ::core::marker::Copy for Rgba {}
+    #[automatically_derived]
+    impl ::core::default::Default for Rgba {
+        #[inline]
+        fn default() -> Rgba {
+            Rgba {
+                r: ::core::default::Default::default(),
+                g: ::core::default::Default::default(),
+                b: ::core::default::Default::default(),
+                a: ::core::default::Default::default(),
+            }
+        }
+    }
+    #[automatically_derived]
+    impl ::core::fmt::Debug for Rgba {
+        fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
+            ::core::fmt::Formatter::debug_struct_field4_finish(
+                f,
+                "Rgba",
+                "r",
+                &self.r,
+                "g",
+                &self.g,
+                "b",
+                &self.b,
+                "a",
+                &&self.a,
+            )
+        }
+    }
+    pub trait Lerp {
+        fn lerp(&self, level: f32) -> Hsla;
+    }
+    impl Lerp for Range<Hsla> {
+        fn lerp(&self, level: f32) -> Hsla {
+            let level = level.clamp(0., 1.);
+            Hsla {
+                h: self.start.h + (level * (self.end.h - self.start.h)),
+                s: self.start.s + (level * (self.end.s - self.start.s)),
+                l: self.start.l + (level * (self.end.l - self.start.l)),
+                a: self.start.a + (level * (self.end.a - self.start.a)),
+            }
+        }
+    }
+    impl From<gpui::color::Color> for Rgba {
+        fn from(value: gpui::color::Color) -> Self {
+            Self {
+                r: value.0.r as f32 / 255.0,
+                g: value.0.g as f32 / 255.0,
+                b: value.0.b as f32 / 255.0,
+                a: value.0.a as f32 / 255.0,
+            }
+        }
+    }
+    impl From<Hsla> for Rgba {
+        fn from(color: Hsla) -> Self {
+            let h = color.h;
+            let s = color.s;
+            let l = color.l;
+            let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
+            let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
+            let m = l - c / 2.0;
+            let cm = c + m;
+            let xm = x + m;
+            let (r, g, b) = match (h * 6.0).floor() as i32 {
+                0 | 6 => (cm, xm, m),
+                1 => (xm, cm, m),
+                2 => (m, cm, xm),
+                3 => (m, xm, cm),
+                4 => (xm, m, cm),
+                _ => (cm, m, xm),
+            };
+            Rgba { r, g, b, a: color.a }
+        }
+    }
+    impl TryFrom<&'_ str> for Rgba {
+        type Error = ParseIntError;
+        fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
+            let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0;
+            let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0;
+            let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0;
+            let a = if value.len() > 7 {
+                u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0
+            } else {
+                1.0
+            };
+            Ok(Rgba { r, g, b, a })
+        }
+    }
+    impl Into<gpui::color::Color> for Rgba {
+        fn into(self) -> gpui::color::Color {
+            gpui::color::rgba(self.r, self.g, self.b, self.a)
+        }
+    }
+    pub struct Hsla {
+        pub h: f32,
+        pub s: f32,
+        pub l: f32,
+        pub a: f32,
+    }
+    #[automatically_derived]
+    impl ::core::default::Default for Hsla {
+        #[inline]
+        fn default() -> Hsla {
+            Hsla {
+                h: ::core::default::Default::default(),
+                s: ::core::default::Default::default(),
+                l: ::core::default::Default::default(),
+                a: ::core::default::Default::default(),
+            }
+        }
+    }
+    #[automatically_derived]
+    impl ::core::marker::Copy for Hsla {}
+    #[automatically_derived]
+    impl ::core::clone::Clone for Hsla {
+        #[inline]
+        fn clone(&self) -> Hsla {
+            let _: ::core::clone::AssertParamIsClone<f32>;
+            *self
+        }
+    }
+    #[automatically_derived]
+    impl ::core::fmt::Debug for Hsla {
+        fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
+            ::core::fmt::Formatter::debug_struct_field4_finish(
+                f,
+                "Hsla",
+                "h",
+                &self.h,
+                "s",
+                &self.s,
+                "l",
+                &self.l,
+                "a",
+                &&self.a,
+            )
+        }
+    }
+    #[automatically_derived]
+    impl ::core::marker::StructuralPartialEq for Hsla {}
+    #[automatically_derived]
+    impl ::core::cmp::PartialEq for Hsla {
+        #[inline]
+        fn eq(&self, other: &Hsla) -> bool {
+            self.h == other.h && self.s == other.s && self.l == other.l
+                && self.a == other.a
+        }
+    }
+    pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
+        Hsla {
+            h: h.clamp(0., 1.),
+            s: s.clamp(0., 1.),
+            l: l.clamp(0., 1.),
+            a: a.clamp(0., 1.),
+        }
+    }
+    pub fn black() -> Hsla {
+        Hsla { h: 0., s: 0., l: 0., a: 1. }
+    }
+    impl From<Rgba> for Hsla {
+        fn from(color: Rgba) -> Self {
+            let r = color.r;
+            let g = color.g;
+            let b = color.b;
+            let max = r.max(g.max(b));
+            let min = r.min(g.min(b));
+            let delta = max - min;
+            let l = (max + min) / 2.0;
+            let s = if l == 0.0 || l == 1.0 {
+                0.0
+            } else if l < 0.5 {
+                delta / (2.0 * l)
+            } else {
+                delta / (2.0 - 2.0 * l)
+            };
+            let h = if delta == 0.0 {
+                0.0
+            } else if max == r {
+                ((g - b) / delta).rem_euclid(6.0) / 6.0
+            } else if max == g {
+                ((b - r) / delta + 2.0) / 6.0
+            } else {
+                ((r - g) / delta + 4.0) / 6.0
+            };
+            Hsla { h, s, l, a: color.a }
+        }
+    }
+    impl Hsla {
+        /// Scales the saturation and lightness by the given values, clamping at 1.0.
+        pub fn scale_sl(mut self, s: f32, l: f32) -> Self {
+            self.s = (self.s * s).clamp(0., 1.);
+            self.l = (self.l * l).clamp(0., 1.);
+            self
+        }
+        /// Increases the saturation of the color by a certain amount, with a max
+        /// value of 1.0.
+        pub fn saturate(mut self, amount: f32) -> Self {
+            self.s += amount;
+            self.s = self.s.clamp(0.0, 1.0);
+            self
+        }
+        /// Decreases the saturation of the color by a certain amount, with a min
+        /// value of 0.0.
+        pub fn desaturate(mut self, amount: f32) -> Self {
+            self.s -= amount;
+            self.s = self.s.max(0.0);
+            if self.s < 0.0 {
+                self.s = 0.0;
+            }
+            self
+        }
+        /// Brightens the color by increasing the lightness by a certain amount,
+        /// with a max value of 1.0.
+        pub fn brighten(mut self, amount: f32) -> Self {
+            self.l += amount;
+            self.l = self.l.clamp(0.0, 1.0);
+            self
+        }
+        /// Darkens the color by decreasing the lightness by a certain amount,
+        /// with a max value of 0.0.
+        pub fn darken(mut self, amount: f32) -> Self {
+            self.l -= amount;
+            self.l = self.l.clamp(0.0, 1.0);
+            self
+        }
+    }
+    impl From<gpui::color::Color> for Hsla {
+        fn from(value: gpui::color::Color) -> Self {
+            Rgba::from(value).into()
+        }
+    }
+    impl Into<gpui::color::Color> for Hsla {
+        fn into(self) -> gpui::color::Color {
+            Rgba::from(self).into()
+        }
+    }
+    pub struct ColorScale {
+        colors: SmallVec<[Hsla; 2]>,
+        positions: SmallVec<[f32; 2]>,
+    }
+    pub fn scale<I, C>(colors: I) -> ColorScale
+    where
+        I: IntoIterator<Item = C>,
+        C: Into<Hsla>,
+    {
+        let mut scale = ColorScale {
+            colors: colors.into_iter().map(Into::into).collect(),
+            positions: SmallVec::new(),
+        };
+        let num_colors: f32 = scale.colors.len() as f32 - 1.0;
+        scale
+            .positions = (0..scale.colors.len())
+            .map(|i| i as f32 / num_colors)
+            .collect();
+        scale
+    }
+    impl ColorScale {
+        fn at(&self, t: f32) -> Hsla {
+            if true {
+                if !(0.0 <= t && t <= 1.0) {
+                    {
+                        ::core::panicking::panic_fmt(
+                            format_args!(
+                                "t value {0} is out of range. Expected value in range 0.0 to 1.0",
+                                t,
+                            ),
+                        );
+                    }
+                }
+            }
+            let position = match self
+                .positions
+                .binary_search_by(|a| a.partial_cmp(&t).unwrap())
+            {
+                Ok(index) | Err(index) => index,
+            };
+            let lower_bound = position.saturating_sub(1);
+            let upper_bound = position.min(self.colors.len() - 1);
+            let lower_color = &self.colors[lower_bound];
+            let upper_color = &self.colors[upper_bound];
+            match upper_bound.checked_sub(lower_bound) {
+                Some(0) | None => *lower_color,
+                Some(_) => {
+                    let interval_t = (t - self.positions[lower_bound])
+                        / (self.positions[upper_bound] - self.positions[lower_bound]);
+                    let h = lower_color.h + interval_t * (upper_color.h - lower_color.h);
+                    let s = lower_color.s + interval_t * (upper_color.s - lower_color.s);
+                    let l = lower_color.l + interval_t * (upper_color.l - lower_color.l);
+                    let a = lower_color.a + interval_t * (upper_color.a - lower_color.a);
+                    Hsla { h, s, l, a }
+                }
+            }
+        }
+    }
+}
+mod components {
+    use crate::{
+        element::{Element, ElementMetadata},
+        frame, text::ArcCow, themes::rose_pine,
+    };
+    use gpui::{platform::MouseButton, ViewContext};
+    use playground_macros::Element;
+    use std::{marker::PhantomData, rc::Rc};
+    struct ButtonHandlers<V, D> {
+        click: Option<Rc<dyn Fn(&mut V, &D, &mut ViewContext<V>)>>,
+    }
+    impl<V, D> Default for ButtonHandlers<V, D> {
+        fn default() -> Self {
+            Self { click: None }
+        }
+    }
+    #[element_crate = "crate"]
+    pub struct Button<V: 'static, D: 'static> {
+        metadata: ElementMetadata<V>,
+        handlers: ButtonHandlers<V, D>,
+        label: Option<ArcCow<'static, str>>,
+        icon: Option<ArcCow<'static, str>>,
+        data: Rc<D>,
+        view_type: PhantomData<V>,
+    }
+    impl<V: 'static, D: 'static> crate::element::Element<V> for Button<V, D> {
+        type Layout = crate::element::AnyElement<V>;
+        fn declared_style(&mut self) -> &mut crate::style::OptionalStyle {
+            &mut self.metadata.style
+        }
+        fn handlers_mut(&mut self) -> &mut Vec<crate::element::EventHandler<V>> {
+            &mut self.metadata.handlers
+        }
+        fn layout(
+            &mut self,
+            view: &mut V,
+            cx: &mut crate::element::LayoutContext<V>,
+        ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
+            let mut element = self.render(view, cx).into_any();
+            let node_id = element.layout(view, cx)?;
+            Ok((node_id, element))
+        }
+        fn paint<'a>(
+            &mut self,
+            layout: crate::element::Layout<'a, Self::Layout>,
+            view: &mut V,
+            cx: &mut crate::element::PaintContext<V>,
+        ) -> anyhow::Result<()> {
+            layout.from_element.paint(view, cx)?;
+            Ok(())
+        }
+    }
+    impl<V: 'static, D: 'static> crate::element::IntoElement<V> for Button<V, D> {
+        type Element = Self;
+        fn into_element(self) -> Self {
+            self
+        }
+    }
+    impl<V: 'static> Button<V, ()> {
+        fn new() -> Self {
+            Self {
+                metadata: Default::default(),
+                handlers: ButtonHandlers::default(),
+                label: None,
+                icon: None,
+                data: Rc::new(()),
+                view_type: PhantomData,
+            }
+        }
+        pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
+            Button {
+                metadata: Default::default(),
+                handlers: ButtonHandlers::default(),
+                label: self.label,
+                icon: self.icon,
+                data: Rc::new(data),
+                view_type: PhantomData,
+            }
+        }
+    }
+    impl<V: 'static, D: 'static> Button<V, D> {
+        pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
+            self.label = Some(label.into());
+            self
+        }
+        pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
+            self.icon = Some(icon.into());
+            self
+        }
+        pub fn click(
+            self,
+            handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static,
+        ) -> Self {
+            let data = self.data.clone();
+            Element::click(
+                self,
+                MouseButton::Left,
+                move |view, _, cx| {
+                    handler(view, data.as_ref(), cx);
+                },
+            )
+        }
+    }
+    pub fn button<V>() -> Button<V, ()> {
+        Button::new()
+    }
+    impl<V: 'static, D: 'static> Button<V, D> {
+        fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
+            let button = frame()
+                .fill(rose_pine::dawn().error(0.5))
+                .h_4()
+                .children(self.label.clone());
+            if let Some(handler) = self.handlers.click.clone() {
+                let data = self.data.clone();
+                button
+                    .mouse_down(
+                        MouseButton::Left,
+                        move |view, event, cx| { handler(view, data.as_ref(), cx) },
+                    )
+            } else {
+                button
+            }
+        }
+    }
+}
+mod element {
+    use crate::{
+        adapter::Adapter, color::Hsla, hoverable::Hoverable,
+        style::{Display, Fill, OptionalStyle, Overflow, Position},
+    };
+    use anyhow::Result;
+    pub use gpui::LayoutContext;
+    use gpui::{
+        geometry::{DefinedLength, Length, OptionalPoint},
+        platform::{MouseButton, MouseButtonEvent},
+        EngineLayout, EventContext, RenderContext, ViewContext,
+    };
+    use playground_macros::tailwind_lengths;
+    use std::{
+        any::{Any, TypeId},
+        cell::Cell, rc::Rc,
+    };
+    pub use crate::paint_context::PaintContext;
+    pub use taffy::tree::NodeId;
+    pub struct Layout<'a, E: ?Sized> {
+        pub from_engine: EngineLayout,
+        pub from_element: &'a mut E,
+    }
+    pub struct ElementMetadata<V> {
+        pub style: OptionalStyle,
+        pub handlers: Vec<EventHandler<V>>,
+    }
+    pub struct EventHandler<V> {
+        handler: Rc<dyn Fn(&mut V, &dyn Any, &mut EventContext<V>)>,
+        event_type: TypeId,
+        outside_bounds: bool,
+    }
+    impl<V> Clone for EventHandler<V> {
+        fn clone(&self) -> Self {
+            Self {
+                handler: self.handler.clone(),
+                event_type: self.event_type,
+                outside_bounds: self.outside_bounds,
+            }
+        }
+    }
+    impl<V> Default for ElementMetadata<V> {
+        fn default() -> Self {
+            Self {
+                style: OptionalStyle::default(),
+                handlers: Vec::new(),
+            }
+        }
+    }
+    pub trait Element<V: 'static>: 'static {
+        type Layout: 'static;
+        fn declared_style(&mut self) -> &mut OptionalStyle;
+        fn computed_style(&mut self) -> &OptionalStyle {
+            self.declared_style()
+        }
+        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>>;
+        fn layout(
+            &mut self,
+            view: &mut V,
+            cx: &mut LayoutContext<V>,
+        ) -> Result<(NodeId, Self::Layout)>;
+        fn paint<'a>(
+            &mut self,
+            layout: Layout<Self::Layout>,
+            view: &mut V,
+            cx: &mut PaintContext<V>,
+        ) -> Result<()>;
+        /// Convert to a dynamically-typed element suitable for layout and paint.
+        fn into_any(self) -> AnyElement<V>
+        where
+            Self: 'static + Sized,
+        {
+            AnyElement {
+                element: Box::new(self) as Box<dyn ElementObject<V>>,
+                layout: None,
+            }
+        }
+        fn adapt(self) -> Adapter<V>
+        where
+            Self: Sized,
+            Self: Element<V>,
+        {
+            Adapter(self.into_any())
+        }
+        fn click(
+            self,
+            button: MouseButton,
+            handler: impl Fn(&mut V, &MouseButtonEvent, &mut ViewContext<V>) + 'static,
+        ) -> Self
+        where
+            Self: Sized,
+        {
+            let pressed: Rc<Cell<bool>> = Default::default();
+            self.mouse_down(
+                    button,
+                    {
+                        let pressed = pressed.clone();
+                        move |_, _, _| {
+                            pressed.set(true);
+                        }
+                    },
+                )
+                .mouse_up_outside(
+                    button,
+                    {
+                        let pressed = pressed.clone();
+                        move |_, _, _| {
+                            pressed.set(false);
+                        }
+                    },
+                )
+                .mouse_up(
+                    button,
+                    move |view, event, event_cx| {
+                        if pressed.get() {
+                            pressed.set(false);
+                            handler(view, event, event_cx);
+                        }
+                    },
+                )
+        }
+        fn mouse_down(
+            mut self,
+            button: MouseButton,
+            handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+        ) -> Self
+        where
+            Self: Sized,
+        {
+            self.handlers_mut()
+                .push(EventHandler {
+                    handler: Rc::new(move |view, event, event_cx| {
+                        let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+                        if event.button == button && event.is_down {
+                            handler(view, event, event_cx);
+                        }
+                    }),
+                    event_type: TypeId::of::<MouseButtonEvent>(),
+                    outside_bounds: false,
+                });
+            self
+        }
+        fn mouse_down_outside(
+            mut self,
+            button: MouseButton,
+            handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+        ) -> Self
+        where
+            Self: Sized,
+        {
+            self.handlers_mut()
+                .push(EventHandler {
+                    handler: Rc::new(move |view, event, event_cx| {
+                        let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+                        if event.button == button && event.is_down {
+                            handler(view, event, event_cx);
+                        }
+                    }),
+                    event_type: TypeId::of::<MouseButtonEvent>(),
+                    outside_bounds: true,
+                });
+            self
+        }
+        fn mouse_up(
+            mut self,
+            button: MouseButton,
+            handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+        ) -> Self
+        where
+            Self: Sized,
+        {
+            self.handlers_mut()
+                .push(EventHandler {
+                    handler: Rc::new(move |view, event, event_cx| {
+                        let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+                        if event.button == button && !event.is_down {
+                            handler(view, event, event_cx);
+                        }
+                    }),
+                    event_type: TypeId::of::<MouseButtonEvent>(),
+                    outside_bounds: false,
+                });
+            self
+        }
+        fn mouse_up_outside(
+            mut self,
+            button: MouseButton,
+            handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+        ) -> Self
+        where
+            Self: Sized,
+        {
+            self.handlers_mut()
+                .push(EventHandler {
+                    handler: Rc::new(move |view, event, event_cx| {
+                        let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
+                        if event.button == button && !event.is_down {
+                            handler(view, event, event_cx);
+                        }
+                    }),
+                    event_type: TypeId::of::<MouseButtonEvent>(),
+                    outside_bounds: true,
+                });
+            self
+        }
+        fn block(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().display = Some(Display::Block);
+            self
+        }
+        fn flex(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().display = Some(Display::Flex);
+            self
+        }
+        fn grid(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().display = Some(Display::Grid);
+            self
+        }
+        fn overflow_visible(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self
+                .declared_style()
+                .overflow = OptionalPoint {
+                x: Some(Overflow::Visible),
+                y: Some(Overflow::Visible),
+            };
+            self
+        }
+        fn overflow_hidden(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self
+                .declared_style()
+                .overflow = OptionalPoint {
+                x: Some(Overflow::Hidden),
+                y: Some(Overflow::Hidden),
+            };
+            self
+        }
+        fn overflow_scroll(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self
+                .declared_style()
+                .overflow = OptionalPoint {
+                x: Some(Overflow::Scroll),
+                y: Some(Overflow::Scroll),
+            };
+            self
+        }
+        fn overflow_x_visible(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().overflow.x = Some(Overflow::Visible);
+            self
+        }
+        fn overflow_x_hidden(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().overflow.x = Some(Overflow::Hidden);
+            self
+        }
+        fn overflow_x_scroll(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().overflow.x = Some(Overflow::Scroll);
+            self
+        }
+        fn overflow_y_visible(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().overflow.y = Some(Overflow::Visible);
+            self
+        }
+        fn overflow_y_hidden(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().overflow.y = Some(Overflow::Hidden);
+            self
+        }
+        fn overflow_y_scroll(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().overflow.y = Some(Overflow::Scroll);
+            self
+        }
+        fn relative(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().position = Some(Position::Relative);
+            self
+        }
+        fn absolute(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().position = Some(Position::Absolute);
+            self
+        }
+        fn inset_0(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Pixels(0.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_px(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Pixels(1.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_0_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.125).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_1(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.25).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_1_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.375).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_2(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.5).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_2_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.625).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_3(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.75).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_3_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.875).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_4(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.25).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_6(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.5).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_7(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.75).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_8(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_9(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.25).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_10(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.5).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_11(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.75).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_12(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(3.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_14(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(3.5).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_16(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(4.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_20(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(5.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_24(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(6.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_28(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(7.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_32(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(8.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_36(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(9.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_40(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(10.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_44(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(11.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_48(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(12.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_52(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(13.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_56(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(14.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_60(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(15.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_64(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(16.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_72(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(18.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_80(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(20.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_96(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(24.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_half(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_1_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_2_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_1_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(25.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_2_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_3_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(75.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_1_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(20.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_2_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(40.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_3_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(60.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_4_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(80.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_1_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(16.666667).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_2_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_3_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_4_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_5_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(83.333333).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_1_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(8.333333).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_2_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(16.666667).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_3_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(25.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_4_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_5_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(41.666667).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_6_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_7_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(58.333333).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_8_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_9_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(75.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_10_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(83.333333).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_11_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(91.666667).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn inset_full(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(100.).into();
+            {
+                let inset = self
+                    .computed_style()
+                    .inset
+                    .get_or_insert_with(Default::default);
+                inset.top = length;
+                inset.right = length;
+                inset.bottom = length;
+                inset.left = length;
+                self
+            }
+        }
+        fn w(mut self, width: impl Into<Length>) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().size.width = Some(width.into());
+            self
+        }
+        fn w_auto(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().size.width = Some(Length::Auto);
+            self
+        }
+        fn w_0(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Pixels(0.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_px(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Pixels(1.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_0_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.125).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_1(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.25).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_1_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.375).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_2(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.5).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_2_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.625).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_3(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.75).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_3_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.875).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_4(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.25).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_6(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.5).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_7(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.75).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_8(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_9(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.25).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_10(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.5).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_11(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.75).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_12(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(3.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_14(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(3.5).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_16(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(4.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_20(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(5.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_24(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(6.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_28(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(7.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_32(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(8.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_36(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(9.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_40(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(10.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_44(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(11.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_48(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(12.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_52(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(13.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_56(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(14.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_60(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(15.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_64(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(16.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_72(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(18.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_80(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(20.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_96(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(24.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_half(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_1_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_2_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_1_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(25.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_2_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_3_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(75.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_1_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(20.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_2_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(40.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_3_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(60.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_4_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(80.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_1_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(16.666667).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_2_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_3_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_4_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_5_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(83.333333).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_1_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(8.333333).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_2_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(16.666667).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_3_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(25.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_4_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_5_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(41.666667).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_6_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_7_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(58.333333).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_8_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_9_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(75.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_10_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(83.333333).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_11_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(91.666667).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn w_full(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(100.).into();
+            {
+                self.declared_style().size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_0(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Pixels(0.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_px(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Pixels(1.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_0_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.125).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_1(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.25).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_1_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.375).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_2(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.5).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_2_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.625).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_3(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.75).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_3_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.875).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_4(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.25).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_6(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.5).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_7(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.75).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_8(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_9(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.25).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_10(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.5).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_11(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.75).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_12(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(3.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_14(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(3.5).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_16(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(4.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_20(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(5.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_24(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(6.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_28(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(7.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_32(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(8.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_36(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(9.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_40(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(10.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_44(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(11.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_48(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(12.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_52(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(13.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_56(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(14.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_60(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(15.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_64(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(16.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_72(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(18.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_80(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(20.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_96(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(24.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_half(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_1_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_2_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_1_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(25.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_2_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_3_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(75.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_1_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(20.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_2_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(40.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_3_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(60.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_4_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(80.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_1_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(16.666667).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_2_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_3_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_4_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_5_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(83.333333).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_1_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(8.333333).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_2_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(16.666667).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_3_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(25.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_4_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_5_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(41.666667).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_6_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_7_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(58.333333).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_8_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_9_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(75.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_10_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(83.333333).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_11_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(91.666667).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn min_w_full(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(100.).into();
+            {
+                self.declared_style().min_size.width = Some(length);
+                self
+            }
+        }
+        fn h(mut self, height: impl Into<Length>) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().size.height = Some(height.into());
+            self
+        }
+        fn h_auto(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().size.height = Some(Length::Auto);
+            self
+        }
+        fn h_0(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Pixels(0.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_px(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Pixels(1.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_0_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(0.125).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_1(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(0.25).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_1_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(0.375).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_2(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(0.5).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_2_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(0.625).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_3(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(0.75).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_3_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(0.875).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_4(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(1.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(1.25).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_6(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(1.5).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_7(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(1.75).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_8(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(2.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_9(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(2.25).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_10(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(2.5).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_11(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(2.75).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_12(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(3.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_14(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(3.5).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_16(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(4.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_20(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(5.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_24(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(6.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_28(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(7.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_32(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(8.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_36(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(9.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_40(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(10.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_44(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(11.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_48(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(12.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_52(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(13.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_56(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(14.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_60(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(15.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_64(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(16.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_72(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(18.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_80(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(20.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_96(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Rems(24.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_half(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_1_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_2_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_1_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(25.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_2_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_3_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(75.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_1_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(20.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_2_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(40.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_3_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(60.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_4_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(80.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_1_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(16.666667).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_2_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_3_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_4_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_5_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(83.333333).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_1_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(8.333333).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_2_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(16.666667).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_3_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(25.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_4_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_5_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(41.666667).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_6_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_7_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(58.333333).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_8_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_9_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(75.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_10_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(83.333333).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_11_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(91.666667).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn h_full(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let height = DefinedLength::Percent(100.).into();
+            {
+                self.declared_style().size.height = Some(height);
+                self
+            }
+        }
+        fn min_h_0(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Pixels(0.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_px(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Pixels(1.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_0_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.125).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_1(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.25).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_1_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.375).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_2(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.5).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_2_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.625).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_3(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.75).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_3_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(0.875).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_4(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_5(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.25).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_6(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.5).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_7(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(1.75).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_8(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_9(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.25).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_10(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.5).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_11(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(2.75).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_12(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(3.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_14(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(3.5).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_16(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(4.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_20(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(5.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_24(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(6.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_28(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(7.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_32(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(8.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_36(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(9.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_40(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(10.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_44(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(11.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_48(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(12.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_52(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(13.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_56(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(14.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_60(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(15.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_64(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(16.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_72(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(18.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_80(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(20.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_96(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Rems(24.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_half(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_1_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_2_3rd(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_1_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(25.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_2_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_3_4th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(75.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_1_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(20.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_2_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(40.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_3_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(60.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_4_5th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(80.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_1_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(16.666667).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_2_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_3_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_4_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_5_6th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(83.333333).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_1_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(8.333333).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_2_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(16.666667).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_3_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(25.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_4_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(33.333333).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_5_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(41.666667).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_6_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(50.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_7_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(58.333333).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_8_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(66.666667).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_9_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(75.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_10_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(83.333333).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_11_12th(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(91.666667).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn min_h_full(mut self) -> Self
+        where
+            Self: Sized,
+        {
+            let length = DefinedLength::Percent(100.).into();
+            {
+                self.declared_style().min_size.height = Some(length);
+                self
+            }
+        }
+        fn hoverable(self) -> Hoverable<V, Self>
+        where
+            Self: Sized,
+        {
+            Hoverable::new(self)
+        }
+        fn fill(mut self, fill: impl Into<Fill>) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().fill = Some(Some(fill.into()));
+            self
+        }
+        fn text_color(mut self, color: impl Into<Hsla>) -> Self
+        where
+            Self: Sized,
+        {
+            self.declared_style().text_color = Some(Some(color.into()));
+            self
+        }
+    }
+    trait ElementObject<V> {
+        fn style(&mut self) -> &mut OptionalStyle;
+        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>>;
+        fn layout(
+            &mut self,
+            view: &mut V,
+            cx: &mut LayoutContext<V>,
+        ) -> Result<(NodeId, Box<dyn Any>)>;
+        fn paint(
+            &mut self,
+            layout: Layout<dyn Any>,
+            view: &mut V,
+            cx: &mut PaintContext<V>,
+        ) -> Result<()>;
+    }
+    impl<V: 'static, E: Element<V>> ElementObject<V> for E {
+        fn style(&mut self) -> &mut OptionalStyle {
+            Element::declared_style(self)
+        }
+        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
+            Element::handlers_mut(self)
+        }
+        fn layout(
+            &mut self,
+            view: &mut V,
+            cx: &mut LayoutContext<V>,
+        ) -> Result<(NodeId, Box<dyn Any>)> {
+            let (node_id, layout) = self.layout(view, cx)?;
+            let layout = Box::new(layout) as Box<dyn Any>;
+            Ok((node_id, layout))
+        }
+        fn paint(
+            &mut self,
+            layout: Layout<dyn Any>,
+            view: &mut V,
+            cx: &mut PaintContext<V>,
+        ) -> Result<()> {
+            let layout = Layout {
+                from_engine: layout.from_engine,
+                from_element: layout.from_element.downcast_mut::<E::Layout>().unwrap(),
+            };
+            self.paint(layout, view, cx)
+        }
+    }
+    /// A dynamically typed element.
+    pub struct AnyElement<V> {
+        element: Box<dyn ElementObject<V>>,
+        layout: Option<(NodeId, Box<dyn Any>)>,
+    }
+    impl<V: 'static> AnyElement<V> {
+        pub fn layout(
+            &mut self,
+            view: &mut V,
+            cx: &mut LayoutContext<V>,
+        ) -> Result<NodeId> {
+            let pushed_text_style = self.push_text_style(cx);
+            let (node_id, layout) = self.element.layout(view, cx)?;
+            self.layout = Some((node_id, layout));
+            if pushed_text_style {
+                cx.pop_text_style();
+            }
+            Ok(node_id)
+        }
+        pub fn push_text_style(&mut self, cx: &mut impl RenderContext) -> bool {
+            let text_style = self.element.style().text_style();
+            if let Some(text_style) = text_style {
+                let mut current_text_style = cx.text_style();
+                text_style.apply(&mut current_text_style);
+                cx.push_text_style(current_text_style);
+                true
+            } else {
+                false
+            }
+        }
+        pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) -> Result<()> {
+            let pushed_text_style = self.push_text_style(cx);
+            let (layout_node_id, element_layout) = self
+                .layout
+                .as_mut()
+                .expect("paint called before layout");
+            let layout = Layout {
+                from_engine: cx
+                    .layout_engine()
+                    .unwrap()
+                    .computed_layout(*layout_node_id)
+                    .expect(
+                        "you can currently only use playground elements within an adapter",
+                    ),
+                from_element: element_layout.as_mut(),
+            };
+            let style = self.element.style();
+            let fill_color = style.fill.flatten().and_then(|fill| fill.color());
+            if let Some(fill_color) = fill_color {
+                cx.scene
+                    .push_quad(gpui::scene::Quad {
+                        bounds: layout.from_engine.bounds,
+                        background: Some(fill_color.into()),
+                        border: Default::default(),
+                        corner_radii: Default::default(),
+                    });
+            }
+            for event_handler in self.element.handlers_mut().iter().cloned() {
+                let EngineLayout { order, bounds } = layout.from_engine;
+                let view_id = cx.view_id();
+                let view_event_handler = event_handler.handler.clone();
+                cx.scene
+                    .interactive_regions
+                    .push(gpui::scene::InteractiveRegion {
+                        order,
+                        bounds,
+                        outside_bounds: event_handler.outside_bounds,
+                        event_handler: Rc::new(move |view, event, window_cx, view_id| {
+                            let mut view_context = ViewContext::mutable(
+                                window_cx,
+                                view_id,
+                            );
+                            let mut event_context = EventContext::new(&mut view_context);
+                            view_event_handler(
+                                view.downcast_mut().unwrap(),
+                                event,
+                                &mut event_context,
+                            );
+                        }),
+                        event_type: event_handler.event_type,
+                        view_id,
+                    });
+            }
+            self.element.paint(layout, view, cx)?;
+            if pushed_text_style {
+                cx.pop_text_style();
+            }
+            Ok(())
+        }
+    }
+    impl<V: 'static> Element<V> for AnyElement<V> {
+        type Layout = ();
+        fn declared_style(&mut self) -> &mut OptionalStyle {
+            self.element.style()
+        }
+        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
+            self.element.handlers_mut()
+        }
+        fn layout(
+            &mut self,
+            view: &mut V,
+            cx: &mut LayoutContext<V>,
+        ) -> Result<(NodeId, Self::Layout)> {
+            Ok((self.layout(view, cx)?, ()))
+        }
+        fn paint(
+            &mut self,
+            layout: Layout<()>,
+            view: &mut V,
+            cx: &mut PaintContext<V>,
+        ) -> Result<()> {
+            self.paint(view, cx)
+        }
+    }
+    pub trait IntoElement<V: 'static> {
+        type Element: Element<V>;
+        fn into_element(self) -> Self::Element;
+        fn into_any_element(self) -> AnyElement<V>
+        where
+            Self: Sized,
+        {
+            self.into_element().into_any()
+        }
+    }
+}
+mod frame {
+    use crate::{
+        element::{
+            AnyElement, Element, EventHandler, IntoElement, Layout, LayoutContext,
+            NodeId, PaintContext,
+        },
+        style::{OptionalStyle, Style},
+    };
+    use anyhow::{anyhow, Result};
+    use gpui::LayoutNodeId;
+    use playground_macros::IntoElement;
+    #[element_crate = "crate"]
+    pub struct Frame<V: 'static> {
+        style: OptionalStyle,
+        handlers: Vec<EventHandler<V>>,
+        children: Vec<AnyElement<V>>,
+    }
+    impl<V: 'static> crate::element::IntoElement<V> for Frame<V> {
+        type Element = Self;
+        fn into_element(self) -> Self {
+            self
+        }
+    }
+    pub fn frame<V>() -> Frame<V> {
+        Frame {
+            style: OptionalStyle::default(),
+            handlers: Vec::new(),
+            children: Vec::new(),
+        }
+    }
+    impl<V: 'static> Element<V> for Frame<V> {
+        type Layout = ();
+        fn declared_style(&mut self) -> &mut OptionalStyle {
+            &mut self.style
+        }
+        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
+            &mut self.handlers
+        }
+        fn layout(
+            &mut self,
+            view: &mut V,
+            cx: &mut LayoutContext<V>,
+        ) -> Result<(NodeId, Self::Layout)> {
+            let child_layout_node_ids = self
+                .children
+                .iter_mut()
+                .map(|child| child.layout(view, cx))
+                .collect::<Result<Vec<LayoutNodeId>>>()?;
+            let rem_size = cx.rem_pixels();
+            let style: Style = self.style.into();
+            let node_id = cx
+                .layout_engine()
+                .ok_or_else(|| ::anyhow::__private::must_use({
+                    let error = ::anyhow::__private::format_err(
+                        format_args!("no layout engine"),
+                    );
+                    error
+                }))?
+                .add_node(style.to_taffy(rem_size), child_layout_node_ids)?;
+            Ok((node_id, ()))
+        }
+        fn paint(
+            &mut self,
+            layout: Layout<()>,
+            view: &mut V,
+            cx: &mut PaintContext<V>,
+        ) -> Result<()> {
+            for child in &mut self.children {
+                child.paint(view, cx)?;
+            }
+            Ok(())
+        }
+    }
+    impl<V: 'static> Frame<V> {
+        pub fn child(mut self, child: impl IntoElement<V>) -> Self {
+            self.children.push(child.into_any_element());
+            self
+        }
+        pub fn children<I, E>(mut self, children: I) -> Self
+        where
+            I: IntoIterator<Item = E>,
+            E: IntoElement<V>,
+        {
+            self.children.extend(children.into_iter().map(|e| e.into_any_element()));
+            self
+        }
+    }
+}
+mod hoverable {
+    use std::{cell::Cell, marker::PhantomData, rc::Rc};
+    use gpui::{
+        geometry::{rect::RectF, vector::Vector2F},
+        scene::MouseMove, EngineLayout,
+    };
+    use crate::{element::Element, style::{OptionalStyle, Style}};
+    pub struct Hoverable<V, E> {
+        hover_style: OptionalStyle,
+        computed_style: Option<Style>,
+        view_type: PhantomData<V>,
+        child: E,
+    }
+    impl<V, E> Hoverable<V, E> {
+        pub fn new(child: E) -> Self {
+            Self {
+                hover_style: OptionalStyle::default(),
+                computed_style: None,
+                view_type: PhantomData,
+                child,
+            }
+        }
+    }
+    impl<V: 'static, E: Element<V>> Element<V> for Hoverable<V, E> {
+        type Layout = E::Layout;
+        fn declared_style(&mut self) -> &mut OptionalStyle {
+            &mut self.hover_style
+        }
+        fn computed_style(&mut self) -> &OptionalStyle {
+            ::core::panicking::panic("not yet implemented")
+        }
+        fn handlers_mut(&mut self) -> &mut Vec<crate::element::EventHandler<V>> {
+            self.child.handlers_mut()
+        }
+        fn layout(
+            &mut self,
+            view: &mut V,
+            cx: &mut gpui::LayoutContext<V>,
+        ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
+            self.child.layout(view, cx)
+        }
+        fn paint<'a>(
+            &mut self,
+            layout: crate::element::Layout<Self::Layout>,
+            view: &mut V,
+            cx: &mut crate::element::PaintContext<V>,
+        ) -> anyhow::Result<()> {
+            let EngineLayout { bounds, order } = layout.from_engine;
+            let window_bounds = RectF::new(Vector2F::zero(), cx.window_size());
+            let was_hovered = Rc::new(Cell::new(false));
+            self.child.paint(layout, view, cx)?;
+            cx.draw_interactive_region(
+                order,
+                window_bounds,
+                false,
+                move |view, event: &MouseMove, cx| {
+                    let is_hovered = bounds.contains_point(cx.mouse_position());
+                    if is_hovered != was_hovered.get() {
+                        was_hovered.set(is_hovered);
+                        cx.repaint();
+                    }
+                },
+            );
+            Ok(())
+        }
+    }
+}
+mod paint_context {
+    use std::{any::TypeId, rc::Rc};
+    use derive_more::{Deref, DerefMut};
+    use gpui::{geometry::rect::RectF, EventContext, RenderContext, ViewContext};
+    pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext};
+    pub use taffy::tree::NodeId;
+    pub struct PaintContext<'a, 'b, 'c, 'd, V> {
+        #[deref]
+        #[deref_mut]
+        pub(crate) legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
+        pub(crate) scene: &'d mut gpui::SceneBuilder,
+    }
+    impl<'a, 'b, 'c, 'd, V> ::core::ops::Deref for PaintContext<'a, 'b, 'c, 'd, V> {
+        type Target = &'d mut LegacyPaintContext<'a, 'b, 'c, V>;
+        #[inline]
+        fn deref(&self) -> &Self::Target {
+            &self.legacy_cx
+        }
+    }
+    impl<'a, 'b, 'c, 'd, V> ::core::ops::DerefMut for PaintContext<'a, 'b, 'c, 'd, V> {
+        #[inline]
+        fn deref_mut(&mut self) -> &mut Self::Target {
+            &mut self.legacy_cx
+        }
+    }
+    impl<V> RenderContext for PaintContext<'_, '_, '_, '_, V> {
+        fn text_style(&self) -> gpui::fonts::TextStyle {
+            self.legacy_cx.text_style()
+        }
+        fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
+            self.legacy_cx.push_text_style(style)
+        }
+        fn pop_text_style(&mut self) {
+            self.legacy_cx.pop_text_style()
+        }
+    }
+    impl<'a, 'b, 'c, 'd, V: 'static> PaintContext<'a, 'b, 'c, 'd, V> {
+        pub fn new(
+            legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
+            scene: &'d mut gpui::SceneBuilder,
+        ) -> Self {
+            Self { legacy_cx, scene }
+        }
+        pub fn draw_interactive_region<E: 'static>(
+            &mut self,
+            order: u32,
+            bounds: RectF,
+            outside_bounds: bool,
+            event_handler: impl Fn(&mut V, &E, &mut EventContext<V>) + 'static,
+        ) {
+            self.scene
+                .interactive_regions
+                .push(gpui::scene::InteractiveRegion {
+                    order,
+                    bounds,
+                    outside_bounds,
+                    event_handler: Rc::new(move |view, event, window_cx, view_id| {
+                        let mut view_context = ViewContext::mutable(window_cx, view_id);
+                        let mut event_context = EventContext::new(&mut view_context);
+                        event_handler(
+                            view.downcast_mut().unwrap(),
+                            event.downcast_ref().unwrap(),
+                            &mut event_context,
+                        );
+                    }),
+                    event_type: TypeId::of::<E>(),
+                    view_id: self.view_id(),
+                });
+        }
+    }
+}
+mod style {
+    use crate::color::Hsla;
+    use gpui::geometry::{
+        DefinedLength, Edges, Length, OptionalEdges, OptionalPoint, OptionalSize, Point,
+        Size,
+    };
+    use optional::Optional;
+    pub use taffy::style::{
+        AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap,
+        JustifyContent, Overflow, Position,
+    };
+    pub struct Style {
+        /// What layout strategy should be used?
+        pub display: Display,
+        /// How children overflowing their container should affect layout
+        #[optional]
+        pub overflow: Point<Overflow>,
+        /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
+        pub scrollbar_width: f32,
+        /// What should the `position` value of this struct use as a base offset?
+        pub position: Position,
+        /// How should the position of this element be tweaked relative to the layout defined?
+        pub inset: Edges<Length>,
+        /// Sets the initial size of the item
+        #[optional]
+        pub size: Size<Length>,
+        /// Controls the minimum size of the item
+        #[optional]
+        pub min_size: Size<Length>,
+        /// Controls the maximum size of the item
+        #[optional]
+        pub max_size: Size<Length>,
+        /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
+        pub aspect_ratio: Option<f32>,
+        /// How large should the margin be on each side?
+        #[optional]
+        pub margin: Edges<Length>,
+        /// How large should the padding be on each side?
+        pub padding: Edges<DefinedLength>,
+        /// How large should the border be on each side?
+        pub border: Edges<DefinedLength>,
+        /// How this node's children aligned in the cross/block axis?
+        pub align_items: Option<AlignItems>,
+        /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
+        pub align_self: Option<AlignSelf>,
+        /// How should content contained within this item be aligned in the cross/block axis
+        pub align_content: Option<AlignContent>,
+        /// How should contained within this item be aligned in the main/inline axis
+        pub justify_content: Option<JustifyContent>,
+        /// How large should the gaps between items in a flex container be?
+        pub gap: Size<DefinedLength>,
+        /// Which direction does the main axis flow in?
+        pub flex_direction: FlexDirection,
+        /// Should elements wrap, or stay in a single line?
+        pub flex_wrap: FlexWrap,
+        /// Sets the initial main axis size of the item
+        pub flex_basis: Length,
+        /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive.
+        pub flex_grow: f32,
+        /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive.
+        pub flex_shrink: f32,
+        /// The fill color of this element
+        pub fill: Option<Fill>,
+        /// The color of text within this element. Cascades to children unless overridden.
+        pub text_color: Option<Hsla>,
+    }
+    #[automatically_derived]
+    impl ::core::clone::Clone for Style {
+        #[inline]
+        fn clone(&self) -> Style {
+            Style {
+                display: ::core::clone::Clone::clone(&self.display),
+                overflow: ::core::clone::Clone::clone(&self.overflow),
+                scrollbar_width: ::core::clone::Clone::clone(&self.scrollbar_width),
+                position: ::core::clone::Clone::clone(&self.position),
+                inset: ::core::clone::Clone::clone(&self.inset),
+                size: ::core::clone::Clone::clone(&self.size),
+                min_size: ::core::clone::Clone::clone(&self.min_size),
+                max_size: ::core::clone::Clone::clone(&self.max_size),
+                aspect_ratio: ::core::clone::Clone::clone(&self.aspect_ratio),
+                margin: ::core::clone::Clone::clone(&self.margin),
+                padding: ::core::clone::Clone::clone(&self.padding),
+                border: ::core::clone::Clone::clone(&self.border),
+                align_items: ::core::clone::Clone::clone(&self.align_items),
+                align_self: ::core::clone::Clone::clone(&self.align_self),
+                align_content: ::core::clone::Clone::clone(&self.align_content),
+                justify_content: ::core::clone::Clone::clone(&self.justify_content),
+                gap: ::core::clone::Clone::clone(&self.gap),
+                flex_direction: ::core::clone::Clone::clone(&self.flex_direction),
+                flex_wrap: ::core::clone::Clone::clone(&self.flex_wrap),
+                flex_basis: ::core::clone::Clone::clone(&self.flex_basis),
+                flex_grow: ::core::clone::Clone::clone(&self.flex_grow),
+                flex_shrink: ::core::clone::Clone::clone(&self.flex_shrink),
+                fill: ::core::clone::Clone::clone(&self.fill),
+                text_color: ::core::clone::Clone::clone(&self.text_color),
+            }
+        }
+    }
+    pub struct OptionalStyle {
+        pub display: Option<Display>,
+        pub overflow: OptionalPoint<Overflow>,
+        pub scrollbar_width: Option<f32>,
+        pub position: Option<Position>,
+        pub inset: Option<Edges<Length>>,
+        pub size: OptionalSize<Length>,
+        pub min_size: OptionalSize<Length>,
+        pub max_size: OptionalSize<Length>,
+        pub aspect_ratio: Option<Option<f32>>,
+        pub margin: OptionalEdges<Length>,
+        pub padding: Option<Edges<DefinedLength>>,
+        pub border: Option<Edges<DefinedLength>>,
+        pub align_items: Option<Option<AlignItems>>,
+        pub align_self: Option<Option<AlignSelf>>,
+        pub align_content: Option<Option<AlignContent>>,
+        pub justify_content: Option<Option<JustifyContent>>,
+        pub gap: Option<Size<DefinedLength>>,
+        pub flex_direction: Option<FlexDirection>,
+        pub flex_wrap: Option<FlexWrap>,
+        pub flex_basis: Option<Length>,
+        pub flex_grow: Option<f32>,
+        pub flex_shrink: Option<f32>,
+        pub fill: Option<Option<Fill>>,
+        pub text_color: Option<Option<Hsla>>,
+    }
+    #[automatically_derived]
+    impl ::core::default::Default for OptionalStyle {
+        #[inline]
+        fn default() -> OptionalStyle {
+            OptionalStyle {
+                display: ::core::default::Default::default(),
+                overflow: ::core::default::Default::default(),
+                scrollbar_width: ::core::default::Default::default(),
+                position: ::core::default::Default::default(),
+                inset: ::core::default::Default::default(),
+                size: ::core::default::Default::default(),
+                min_size: ::core::default::Default::default(),
+                max_size: ::core::default::Default::default(),
+                aspect_ratio: ::core::default::Default::default(),
+                margin: ::core::default::Default::default(),
+                padding: ::core::default::Default::default(),
+                border: ::core::default::Default::default(),
+                align_items: ::core::default::Default::default(),
+                align_self: ::core::default::Default::default(),
+                align_content: ::core::default::Default::default(),
+                justify_content: ::core::default::Default::default(),
+                gap: ::core::default::Default::default(),
+                flex_direction: ::core::default::Default::default(),
+                flex_wrap: ::core::default::Default::default(),
+                flex_basis: ::core::default::Default::default(),
+                flex_grow: ::core::default::Default::default(),
+                flex_shrink: ::core::default::Default::default(),
+                fill: ::core::default::Default::default(),
+                text_color: ::core::default::Default::default(),
+            }
+        }
+    }
+    #[automatically_derived]
+    impl ::core::clone::Clone for OptionalStyle {
+        #[inline]
+        fn clone(&self) -> OptionalStyle {
+            OptionalStyle {
+                display: ::core::clone::Clone::clone(&self.display),
+                overflow: ::core::clone::Clone::clone(&self.overflow),
+                scrollbar_width: ::core::clone::Clone::clone(&self.scrollbar_width),
+                position: ::core::clone::Clone::clone(&self.position),
+                inset: ::core::clone::Clone::clone(&self.inset),
+                size: ::core::clone::Clone::clone(&self.size),
+                min_size: ::core::clone::Clone::clone(&self.min_size),
+                max_size: ::core::clone::Clone::clone(&self.max_size),
+                aspect_ratio: ::core::clone::Clone::clone(&self.aspect_ratio),
+                margin: ::core::clone::Clone::clone(&self.margin),
+                padding: ::core::clone::Clone::clone(&self.padding),
+                border: ::core::clone::Clone::clone(&self.border),
+                align_items: ::core::clone::Clone::clone(&self.align_items),
+                align_self: ::core::clone::Clone::clone(&self.align_self),
+                align_content: ::core::clone::Clone::clone(&self.align_content),
+                justify_content: ::core::clone::Clone::clone(&self.justify_content),
+                gap: ::core::clone::Clone::clone(&self.gap),
+                flex_direction: ::core::clone::Clone::clone(&self.flex_direction),
+                flex_wrap: ::core::clone::Clone::clone(&self.flex_wrap),
+                flex_basis: ::core::clone::Clone::clone(&self.flex_basis),
+                flex_grow: ::core::clone::Clone::clone(&self.flex_grow),
+                flex_shrink: ::core::clone::Clone::clone(&self.flex_shrink),
+                fill: ::core::clone::Clone::clone(&self.fill),
+                text_color: ::core::clone::Clone::clone(&self.text_color),
+            }
+        }
+    }
+    impl Optional for OptionalStyle {
+        type Base = Style;
+        fn assign(&self, base: &mut Self::Base) {
+            if let Some(value) = self.display.clone() {
+                base.display = value;
+            }
+            if let Some(value) = self.overflow.clone() {
+                base.overflow = value;
+            }
+            if let Some(value) = self.scrollbar_width.clone() {
+                base.scrollbar_width = value;
+            }
+            if let Some(value) = self.position.clone() {
+                base.position = value;
+            }
+            if let Some(value) = self.inset.clone() {
+                base.inset = value;
+            }
+            if let Some(value) = self.size.clone() {
+                base.size = value;
+            }
+            if let Some(value) = self.min_size.clone() {
+                base.min_size = value;
+            }
+            if let Some(value) = self.max_size.clone() {
+                base.max_size = value;
+            }
+            if let Some(value) = self.aspect_ratio.clone() {
+                base.aspect_ratio = value;
+            }
+            if let Some(value) = self.margin.clone() {
+                base.margin = value;
+            }
+            if let Some(value) = self.padding.clone() {
+                base.padding = value;
+            }
+            if let Some(value) = self.border.clone() {
+                base.border = value;
+            }
+            if let Some(value) = self.align_items.clone() {
+                base.align_items = value;
+            }
+            if let Some(value) = self.align_self.clone() {
+                base.align_self = value;
+            }
+            if let Some(value) = self.align_content.clone() {
+                base.align_content = value;
+            }
+            if let Some(value) = self.justify_content.clone() {
+                base.justify_content = value;
+            }
+            if let Some(value) = self.gap.clone() {
+                base.gap = value;
+            }
+            if let Some(value) = self.flex_direction.clone() {
+                base.flex_direction = value;
+            }
+            if let Some(value) = self.flex_wrap.clone() {
+                base.flex_wrap = value;
+            }
+            if let Some(value) = self.flex_basis.clone() {
+                base.flex_basis = value;
+            }
+            if let Some(value) = self.flex_grow.clone() {
+                base.flex_grow = value;
+            }
+            if let Some(value) = self.flex_shrink.clone() {
+                base.flex_shrink = value;
+            }
+            if let Some(value) = self.fill.clone() {
+                base.fill = value;
+            }
+            if let Some(value) = self.text_color.clone() {
+                base.text_color = value;
+            }
+        }
+    }
+    impl From<OptionalStyle> for Style
+    where
+        Style: Default,
+    {
+        fn from(wrapper: OptionalStyle) -> Self {
+            let mut base = Self::default();
+            wrapper.assign(&mut base);
+            base
+        }
+    }
+    impl Style {
+        pub const DEFAULT: Style = Style {
+            display: Display::DEFAULT,
+            overflow: Point {
+                x: Overflow::Visible,
+                y: Overflow::Visible,
+            },
+            scrollbar_width: 0.0,
+            position: Position::Relative,
+            inset: Edges::auto(),
+            margin: Edges::<Length>::zero(),
+            padding: Edges::<DefinedLength>::zero(),
+            border: Edges::<DefinedLength>::zero(),
+            size: Size::auto(),
+            min_size: Size::auto(),
+            max_size: Size::auto(),
+            aspect_ratio: None,
+            gap: Size::zero(),
+            align_items: None,
+            align_self: None,
+            align_content: None,
+            justify_content: None,
+            flex_direction: FlexDirection::Row,
+            flex_wrap: FlexWrap::NoWrap,
+            flex_grow: 0.0,
+            flex_shrink: 1.0,
+            flex_basis: Length::Auto,
+            fill: None,
+            text_color: None,
+        };
+        pub fn new() -> Self {
+            Self::DEFAULT.clone()
+        }
+        pub fn to_taffy(&self, rem_size: f32) -> taffy::style::Style {
+            taffy::style::Style {
+                display: self.display,
+                overflow: self.overflow.clone().into(),
+                scrollbar_width: self.scrollbar_width,
+                position: self.position,
+                inset: self.inset.to_taffy(rem_size),
+                size: self.size.to_taffy(rem_size),
+                min_size: self.min_size.to_taffy(rem_size),
+                max_size: self.max_size.to_taffy(rem_size),
+                aspect_ratio: self.aspect_ratio,
+                margin: self.margin.to_taffy(rem_size),
+                padding: self.padding.to_taffy(rem_size),
+                border: self.border.to_taffy(rem_size),
+                align_items: self.align_items,
+                align_self: self.align_self,
+                align_content: self.align_content,
+                justify_content: self.justify_content,
+                gap: self.gap.to_taffy(rem_size),
+                flex_direction: self.flex_direction,
+                flex_wrap: self.flex_wrap,
+                flex_basis: self.flex_basis.to_taffy(rem_size).into(),
+                flex_grow: self.flex_grow,
+                flex_shrink: self.flex_shrink,
+                ..Default::default()
+            }
+        }
+    }
+    impl Default for Style {
+        fn default() -> Self {
+            Self::DEFAULT.clone()
+        }
+    }
+    impl OptionalStyle {
+        pub fn text_style(&self) -> Option<OptionalTextStyle> {
+            self.text_color.map(|color| OptionalTextStyle { color })
+        }
+    }
+    pub struct OptionalTextStyle {
+        color: Option<Hsla>,
+    }
+    impl OptionalTextStyle {
+        pub fn apply(&self, style: &mut gpui::fonts::TextStyle) {
+            if let Some(color) = self.color {
+                style.color = color.into();
+            }
+        }
+    }
+    pub enum Fill {
+        Color(Hsla),
+    }
+    #[automatically_derived]
+    impl ::core::clone::Clone for Fill {
+        #[inline]
+        fn clone(&self) -> Fill {
+            match self {
+                Fill::Color(__self_0) => {
+                    Fill::Color(::core::clone::Clone::clone(__self_0))
+                }
+            }
+        }
+    }
+    impl Fill {
+        pub fn color(&self) -> Option<Hsla> {
+            match self {
+                Fill::Color(color) => Some(*color),
+            }
+        }
+    }
+    impl Default for Fill {
+        fn default() -> Self {
+            Self::Color(Hsla::default())
+        }
+    }
+    impl From<Hsla> for Fill {
+        fn from(color: Hsla) -> Self {
+            Self::Color(color)
+        }
+    }
+}
+mod text {
+    use crate::{
+        element::{Element, ElementMetadata, EventHandler, IntoElement},
+        style::Style,
+    };
+    use gpui::{geometry::Size, text_layout::LineLayout, RenderContext};
+    use parking_lot::Mutex;
+    use std::sync::Arc;
+    impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
+        type Element = Text<V>;
+        fn into_element(self) -> Self::Element {
+            Text {
+                text: self.into(),
+                metadata: Default::default(),
+            }
+        }
+    }
+    pub struct Text<V> {
+        text: ArcCow<'static, str>,
+        metadata: ElementMetadata<V>,
+    }
+    impl<V: 'static> Element<V> for Text<V> {
+        type Layout = Arc<Mutex<Option<TextLayout>>>;
+        fn declared_style(&mut self) -> &mut crate::style::OptionalStyle {
+            &mut self.metadata.style
+        }
+        fn layout(
+            &mut self,
+            view: &mut V,
+            cx: &mut gpui::LayoutContext<V>,
+        ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
+            let rem_size = cx.rem_pixels();
+            let fonts = cx.platform().fonts();
+            let text_style = cx.text_style();
+            let line_height = cx.font_cache().line_height(text_style.font_size);
+            let layout_engine = cx.layout_engine().expect("no layout engine present");
+            let text = self.text.clone();
+            let layout = Arc::new(Mutex::new(None));
+            let style: Style = self.metadata.style.into();
+            let node_id = layout_engine
+                .add_measured_node(
+                    style.to_taffy(rem_size),
+                    {
+                        let layout = layout.clone();
+                        move |params| {
+                            let line_layout = fonts
+                                .layout_line(
+                                    text.as_ref(),
+                                    text_style.font_size,
+                                    &[(text.len(), text_style.to_run())],
+                                );
+                            let size = Size {
+                                width: line_layout.width,
+                                height: line_height,
+                            };
+                            layout
+                                .lock()
+                                .replace(TextLayout {
+                                    line_layout: Arc::new(line_layout),
+                                    line_height,
+                                });
+                            size
+                        }
+                    },
+                )?;
+            Ok((node_id, layout))
+        }
+        fn paint<'a>(
+            &mut self,
+            layout: crate::element::Layout<Arc<Mutex<Option<TextLayout>>>>,
+            view: &mut V,
+            cx: &mut crate::element::PaintContext<V>,
+        ) -> anyhow::Result<()> {
+            let element_layout_lock = layout.from_element.lock();
+            let element_layout = element_layout_lock
+                .as_ref()
+                .expect("layout has not been performed");
+            let line_layout = element_layout.line_layout.clone();
+            let line_height = element_layout.line_height;
+            drop(element_layout_lock);
+            let text_style = cx.text_style();
+            let line = gpui::text_layout::Line::new(
+                line_layout,
+                &[(self.text.len(), text_style.to_run())],
+            );
+            line.paint(
+                cx.scene,
+                layout.from_engine.bounds.origin(),
+                layout.from_engine.bounds,
+                line_height,
+                cx.legacy_cx,
+            );
+            Ok(())
+        }
+        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
+            &mut self.metadata.handlers
+        }
+    }
+    pub struct TextLayout {
+        line_layout: Arc<LineLayout>,
+        line_height: f32,
+    }
+    pub enum ArcCow<'a, T: ?Sized> {
+        Borrowed(&'a T),
+        Owned(Arc<T>),
+    }
+    impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
+        fn clone(&self) -> Self {
+            match self {
+                Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
+                Self::Owned(owned) => Self::Owned(owned.clone()),
+            }
+        }
+    }
+    impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
+        fn from(s: &'a T) -> Self {
+            Self::Borrowed(s)
+        }
+    }
+    impl<T> From<Arc<T>> for ArcCow<'_, T> {
+        fn from(s: Arc<T>) -> Self {
+            Self::Owned(s)
+        }
+    }
+    impl From<String> for ArcCow<'_, str> {
+        fn from(value: String) -> Self {
+            Self::Owned(value.into())
+        }
+    }
+    impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
+        type Target = T;
+        fn deref(&self) -> &Self::Target {
+            match self {
+                ArcCow::Borrowed(s) => s,
+                ArcCow::Owned(s) => s.as_ref(),
+            }
+        }
+    }
+    impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
+        fn as_ref(&self) -> &T {
+            match self {
+                ArcCow::Borrowed(borrowed) => borrowed,
+                ArcCow::Owned(owned) => owned.as_ref(),
+            }
+        }
+    }
+}
+mod themes {
+    use crate::color::{Hsla, Lerp};
+    use std::ops::Range;
+    pub mod rose_pine {
+        use std::ops::Range;
+        use crate::{
+            color::{hsla, rgb, Hsla},
+            ThemeColors,
+        };
+        pub struct RosePineThemes {
+            pub default: RosePinePalette,
+            pub dawn: RosePinePalette,
+            pub moon: RosePinePalette,
+        }
+        pub struct RosePinePalette {
+            pub base: Hsla,
+            pub surface: Hsla,
+            pub overlay: Hsla,
+            pub muted: Hsla,
+            pub subtle: Hsla,
+            pub text: Hsla,
+            pub love: Hsla,
+            pub gold: Hsla,
+            pub rose: Hsla,
+            pub pine: Hsla,
+            pub foam: Hsla,
+            pub iris: Hsla,
+            pub highlight_low: Hsla,
+            pub highlight_med: Hsla,
+            pub highlight_high: Hsla,
+        }
+        #[automatically_derived]
+        impl ::core::clone::Clone for RosePinePalette {
+            #[inline]
+            fn clone(&self) -> RosePinePalette {
+                let _: ::core::clone::AssertParamIsClone<Hsla>;
+                *self
+            }
+        }
+        #[automatically_derived]
+        impl ::core::marker::Copy for RosePinePalette {}
+        #[automatically_derived]
+        impl ::core::fmt::Debug for RosePinePalette {
+            fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
+                let names: &'static _ = &[
+                    "base",
+                    "surface",
+                    "overlay",
+                    "muted",
+                    "subtle",
+                    "text",
+                    "love",
+                    "gold",
+                    "rose",
+                    "pine",
+                    "foam",
+                    "iris",
+                    "highlight_low",
+                    "highlight_med",
+                    "highlight_high",
+                ];
+                let values: &[&dyn ::core::fmt::Debug] = &[
+                    &self.base,
+                    &self.surface,
+                    &self.overlay,
+                    &self.muted,
+                    &self.subtle,
+                    &self.text,
+                    &self.love,
+                    &self.gold,
+                    &self.rose,
+                    &self.pine,
+                    &self.foam,
+                    &self.iris,
+                    &self.highlight_low,
+                    &self.highlight_med,
+                    &&self.highlight_high,
+                ];
+                ::core::fmt::Formatter::debug_struct_fields_finish(
+                    f,
+                    "RosePinePalette",
+                    names,
+                    values,
+                )
+            }
+        }
+        impl RosePinePalette {
+            pub fn default() -> RosePinePalette {
+                RosePinePalette {
+                    base: rgb(0x191724),
+                    surface: rgb(0x1f1d2e),
+                    overlay: rgb(0x26233a),
+                    muted: rgb(0x6e6a86),
+                    subtle: rgb(0x908caa),
+                    text: rgb(0xe0def4),
+                    love: rgb(0xeb6f92),
+                    gold: rgb(0xf6c177),
+                    rose: rgb(0xebbcba),
+                    pine: rgb(0x31748f),
+                    foam: rgb(0x9ccfd8),
+                    iris: rgb(0xc4a7e7),
+                    highlight_low: rgb(0x21202e),
+                    highlight_med: rgb(0x403d52),
+                    highlight_high: rgb(0x524f67),
+                }
+            }
+            pub fn moon() -> RosePinePalette {
+                RosePinePalette {
+                    base: rgb(0x232136),
+                    surface: rgb(0x2a273f),
+                    overlay: rgb(0x393552),
+                    muted: rgb(0x6e6a86),
+                    subtle: rgb(0x908caa),
+                    text: rgb(0xe0def4),
+                    love: rgb(0xeb6f92),
+                    gold: rgb(0xf6c177),
+                    rose: rgb(0xea9a97),
+                    pine: rgb(0x3e8fb0),
+                    foam: rgb(0x9ccfd8),
+                    iris: rgb(0xc4a7e7),
+                    highlight_low: rgb(0x2a283e),
+                    highlight_med: rgb(0x44415a),
+                    highlight_high: rgb(0x56526e),
+                }
+            }
+            pub fn dawn() -> RosePinePalette {
+                RosePinePalette {
+                    base: rgb(0xfaf4ed),
+                    surface: rgb(0xfffaf3),
+                    overlay: rgb(0xf2e9e1),
+                    muted: rgb(0x9893a5),
+                    subtle: rgb(0x797593),
+                    text: rgb(0x575279),
+                    love: rgb(0xb4637a),
+                    gold: rgb(0xea9d34),
+                    rose: rgb(0xd7827e),
+                    pine: rgb(0x286983),
+                    foam: rgb(0x56949f),
+                    iris: rgb(0x907aa9),
+                    highlight_low: rgb(0xf4ede8),
+                    highlight_med: rgb(0xdfdad9),
+                    highlight_high: rgb(0xcecacd),
+                }
+            }
+        }
+        pub fn default() -> ThemeColors {
+            theme_colors(&RosePinePalette::default())
+        }
+        pub fn moon() -> ThemeColors {
+            theme_colors(&RosePinePalette::moon())
+        }
+        pub fn dawn() -> ThemeColors {
+            theme_colors(&RosePinePalette::dawn())
+        }
+        fn theme_colors(p: &RosePinePalette) -> ThemeColors {
+            ThemeColors {
+                base: scale_sl(p.base, (0.8, 0.8), (1.2, 1.2)),
+                surface: scale_sl(p.surface, (0.8, 0.8), (1.2, 1.2)),
+                overlay: scale_sl(p.overlay, (0.8, 0.8), (1.2, 1.2)),
+                muted: scale_sl(p.muted, (0.8, 0.8), (1.2, 1.2)),
+                subtle: scale_sl(p.subtle, (0.8, 0.8), (1.2, 1.2)),
+                text: scale_sl(p.text, (0.8, 0.8), (1.2, 1.2)),
+                highlight_low: scale_sl(p.highlight_low, (0.8, 0.8), (1.2, 1.2)),
+                highlight_med: scale_sl(p.highlight_med, (0.8, 0.8), (1.2, 1.2)),
+                highlight_high: scale_sl(p.highlight_high, (0.8, 0.8), (1.2, 1.2)),
+                success: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
+                warning: scale_sl(p.gold, (0.8, 0.8), (1.2, 1.2)),
+                error: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
+                inserted: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
+                deleted: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
+                modified: scale_sl(p.rose, (0.8, 0.8), (1.2, 1.2)),
+            }
+        }
+        /// Produces a range by multiplying the saturation and lightness of the base color by the given
+        /// start and end factors.
+        fn scale_sl(
+            base: Hsla,
+            (start_s, start_l): (f32, f32),
+            (end_s, end_l): (f32, f32),
+        ) -> Range<Hsla> {
+            let start = hsla(base.h, base.s * start_s, base.l * start_l, base.a);
+            let end = hsla(base.h, base.s * end_s, base.l * end_l, base.a);
+            Range { start, end }
+        }
+    }
+    pub struct ThemeColors {
+        pub base: Range<Hsla>,
+        pub surface: Range<Hsla>,
+        pub overlay: Range<Hsla>,
+        pub muted: Range<Hsla>,
+        pub subtle: Range<Hsla>,
+        pub text: Range<Hsla>,
+        pub highlight_low: Range<Hsla>,
+        pub highlight_med: Range<Hsla>,
+        pub highlight_high: Range<Hsla>,
+        pub success: Range<Hsla>,
+        pub warning: Range<Hsla>,
+        pub error: Range<Hsla>,
+        pub inserted: Range<Hsla>,
+        pub deleted: Range<Hsla>,
+        pub modified: Range<Hsla>,
+    }
+    impl ThemeColors {
+        pub fn base(&self, level: f32) -> Hsla {
+            self.base.lerp(level)
+        }
+        pub fn surface(&self, level: f32) -> Hsla {
+            self.surface.lerp(level)
+        }
+        pub fn overlay(&self, level: f32) -> Hsla {
+            self.overlay.lerp(level)
+        }
+        pub fn muted(&self, level: f32) -> Hsla {
+            self.muted.lerp(level)
+        }
+        pub fn subtle(&self, level: f32) -> Hsla {
+            self.subtle.lerp(level)
+        }
+        pub fn text(&self, level: f32) -> Hsla {
+            self.text.lerp(level)
+        }
+        pub fn highlight_low(&self, level: f32) -> Hsla {
+            self.highlight_low.lerp(level)
+        }
+        pub fn highlight_med(&self, level: f32) -> Hsla {
+            self.highlight_med.lerp(level)
+        }
+        pub fn highlight_high(&self, level: f32) -> Hsla {
+            self.highlight_high.lerp(level)
+        }
+        pub fn success(&self, level: f32) -> Hsla {
+            self.success.lerp(level)
+        }
+        pub fn warning(&self, level: f32) -> Hsla {
+            self.warning.lerp(level)
+        }
+        pub fn error(&self, level: f32) -> Hsla {
+            self.error.lerp(level)
+        }
+        pub fn inserted(&self, level: f32) -> Hsla {
+            self.inserted.lerp(level)
+        }
+        pub fn deleted(&self, level: f32) -> Hsla {
+            self.deleted.lerp(level)
+        }
+        pub fn modified(&self, level: f32) -> Hsla {
+            self.modified.lerp(level)
+        }
+    }
+}
+mod view {
+    use crate::element::{AnyElement, Element};
+    use gpui::{Element as _, ViewContext};
+    pub fn view<F, E>(mut render: F) -> ViewFn
+    where
+        F: 'static + FnMut(&mut ViewContext<ViewFn>) -> E,
+        E: Element<ViewFn>,
+    {
+        ViewFn(Box::new(move |cx| (render)(cx).into_any()))
+    }
+    pub struct ViewFn(Box<dyn FnMut(&mut ViewContext<ViewFn>) -> AnyElement<ViewFn>>);
+    impl gpui::Entity for ViewFn {
+        type Event = ();
+    }
+    impl gpui::View for ViewFn {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
+            (self.0)(cx).adapt().into_any()
+        }
+    }
+}
+fn main() {
+    SimpleLogger::init(LevelFilter::Info, Default::default())
+        .expect("could not initialize logger");
+    gpui::App::new(())
+        .unwrap()
+        .run(|cx| {
+            cx.add_window(
+                WindowOptions {
+                    bounds: gpui::platform::WindowBounds::Fixed(
+                        RectF::new(vec2f(0., 0.), vec2f(400., 300.)),
+                    ),
+                    center: true,
+                    ..Default::default()
+                },
+                |_| view(|_| playground(&rose_pine::moon())),
+            );
+            cx.platform().activate(true);
+        });
+}
+fn playground<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
+    frame()
+        .text_color(black())
+        .h_full()
+        .w_half()
+        .fill(theme.success(0.5))
+        .child(
+            button()
+                .label("Hello")
+                .click(|_, _, _| {
+                    ::std::io::_print(format_args!("click!\n"));
+                }),
+        )
+}