Merge remote-tracking branch 'origin/main' into ai-refactoring

Antonio Scandurra created

Change summary

.github/workflows/publish_collab_image.yml                             |    7 
Cargo.lock                                                             |  361 
Cargo.toml                                                             |   12 
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/keymaps/default.json                                            |    3 
assets/keymaps/textmate.json                                           |   19 
assets/keymaps/vim.json                                                |   59 
assets/settings/default.json                                           |   10 
crates/ai/src/ai.rs                                                    |    3 
crates/ai/src/assistant.rs                                             |   45 
crates/ai/src/assistant_settings.rs                                    |   33 
crates/audio/Cargo.toml                                                |    2 
crates/call/Cargo.toml                                                 |    1 
crates/call/src/call.rs                                                |   23 
crates/channel/Cargo.toml                                              |   51 
crates/channel/src/channel.rs                                          |   14 
crates/channel/src/channel_buffer.rs                                   |  197 
crates/channel/src/channel_store.rs                                    |  196 
crates/channel/src/channel_store_tests.rs                              |    3 
crates/client/Cargo.toml                                               |    1 
crates/client/src/client.rs                                            |    5 
crates/client/src/telemetry.rs                                         |    1 
crates/client/src/user.rs                                              |    4 
crates/collab/Cargo.toml                                               |    7 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql         |   41 
crates/collab/migrations/20230819154600_add_channel_buffers.sql        |   40 
crates/collab/src/db.rs                                                | 3915 
crates/collab/src/db/ids.rs                                            |  127 
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                                  |  243 
crates/collab/src/db/signup.rs                                         |   57 
crates/collab/src/db/tables.rs                                         |   24 
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                                 |   22 
crates/collab/src/db/tables/channel_buffer_collaborator.rs             |   43 
crates/collab/src/db/tables/channel_member.rs                          |    4 
crates/collab/src/db/tables/channel_path.rs                            |    2 
crates/collab/src/db/tables/contact.rs                                 |   28 
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                                    |    2 
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                                    |    2 
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                                          | 1651 
crates/collab/src/db/tests/buffer_tests.rs                             |  165 
crates/collab/src/db/tests/db_tests.rs                                 | 1573 
crates/collab/src/rpc.rs                                               |  133 
crates/collab/src/tests.rs                                             |    8 
crates/collab/src/tests/channel_buffer_tests.rs                        |  426 
crates/collab/src/tests/channel_tests.rs                               |   10 
crates/collab/src/tests/integration_tests.rs                           |    1 
crates/collab_ui/Cargo.toml                                            |    2 
crates/collab_ui/src/channel_view.rs                                   |  351 
crates/collab_ui/src/collab_panel.rs                                   |  425 
crates/collab_ui/src/collab_panel/channel_modal.rs                     |    3 
crates/collab_ui/src/collab_titlebar_item.rs                           |    2 
crates/collab_ui/src/collab_ui.rs                                      |   12 
crates/collab_ui/src/notifications.rs                                  |    5 
crates/component_test/Cargo.toml                                       |   18 
crates/component_test/src/component_test.rs                            |  121 
crates/diagnostics/src/diagnostics.rs                                  |    4 
crates/drag_and_drop/src/drag_and_drop.rs                              |   14 
crates/editor/src/editor.rs                                            |   94 
crates/editor/src/editor_tests.rs                                      |    6 
crates/editor/src/element.rs                                           |   56 
crates/editor/src/items.rs                                             |   36 
crates/editor/src/movement.rs                                          |   46 
crates/editor/src/multi_buffer.rs                                      |   20 
crates/editor/src/scroll.rs                                            |    3 
crates/editor/src/selections_collection.rs                             |   20 
crates/editor/src/test/editor_lsp_test_context.rs                      |   31 
crates/feedback/src/feedback_editor.rs                                 |    2 
crates/gpui/Cargo.toml                                                 |    2 
crates/gpui/examples/components.rs                                     |   14 
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                                                 |  390 
crates/gpui/src/app/window.rs                                          |  233 
crates/gpui/src/color.rs                                               |   62 
crates/gpui/src/elements.rs                                            |   79 
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                                  |  278 
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                                       |   49 
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                                  |   10 
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                                    |    8 
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/platform.rs                                            |    2 
crates/gpui/src/platform/event.rs                                      |   20 
crates/gpui/src/platform/mac/event.rs                                  |    2 
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                                              |   10 
crates/gpui_macros/Cargo.toml                                          |    3 
crates/gpui_macros/src/gpui_macros.rs                                  |   54 
crates/install_cli/src/install_cli.rs                                  |    2 
crates/language/src/buffer.rs                                          |   80 
crates/language/src/buffer_tests.rs                                    |   69 
crates/language/src/language.rs                                        |    5 
crates/language/src/proto.rs                                           |    1 
crates/language/src/syntax_map.rs                                      |   10 
crates/language_tools/src/lsp_log.rs                                   |    2 
crates/language_tools/src/syntax_tree_view.rs                          |    3 
crates/project/src/project.rs                                          |   26 
crates/project/src/search.rs                                           |   27 
crates/project_panel/src/project_panel.rs                              |    2 
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/rpc/Cargo.toml                                                  |    2 
crates/rpc/proto/zed.proto                                             |   45 
crates/rpc/src/proto.rs                                                |   17 
crates/search/src/buffer_search.rs                                     |    7 
crates/search/src/mode.rs                                              |   20 
crates/search/src/project_search.rs                                    |  326 
crates/search/src/search.rs                                            |   24 
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                            |  449 
crates/semantic_index/src/semantic_index_tests.rs                      |    7 
crates/settings/src/settings.rs                                        |    2 
crates/settings/src/settings_store.rs                                  |    3 
crates/sum_tree/src/tree_map.rs                                        |   12 
crates/terminal_view/src/terminal_element.rs                           |    1 
crates/terminal_view/src/terminal_view.rs                              |    2 
crates/text/src/text.rs                                                |    2 
crates/theme/src/components.rs                                         |  317 
crates/theme/src/theme.rs                                              |   30 
crates/theme/src/ui.rs                                                 |   16 
crates/vim/src/editor_events.rs                                        |    8 
crates/vim/src/mode_indicator.rs                                       |   12 
crates/vim/src/motion.rs                                               |   37 
crates/vim/src/normal.rs                                               |  189 
crates/vim/src/normal/case.rs                                          |    6 
crates/vim/src/normal/change.rs                                        |    9 
crates/vim/src/normal/paste.rs                                         |  468 
crates/vim/src/normal/scroll.rs                                        |   40 
crates/vim/src/normal/search.rs                                        |   11 
crates/vim/src/normal/substitute.rs                                    |    7 
crates/vim/src/object.rs                                               |   94 
crates/vim/src/state.rs                                                |   29 
crates/vim/src/test.rs                                                 |   30 
crates/vim/src/test/neovim_backed_test_context.rs                      |   97 
crates/vim/src/test/neovim_connection.rs                               |  206 
crates/vim/src/test/vim_binding_test_context.rs                        |   64 
crates/vim/src/test/vim_test_context.rs                                |   26 
crates/vim/src/utils.rs                                                |   27 
crates/vim/src/vim.rs                                                  |  124 
crates/vim/src/visual.rs                                               |  840 
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_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                       |   32 
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/welcome/src/welcome.rs                                          |    2 
crates/workspace/Cargo.toml                                            |    1 
crates/workspace/src/item.rs                                           |   14 
crates/workspace/src/pane.rs                                           |    6 
crates/workspace/src/pane/dragged_item_receiver.rs                     |   10 
crates/workspace/src/shared_screen.rs                                  |    2 
crates/workspace/src/workspace.rs                                      |   26 
crates/zed/Cargo.toml                                                  |    6 
crates/zed/resources/zed.entitlements                                  |    8 
crates/zed/src/languages/javascript/config.toml                        |    1 
crates/zed/src/languages/php/config.toml                               |    1 
crates/zed/src/languages/tsx/config.toml                               |    1 
crates/zed/src/languages/typescript/config.toml                        |    1 
crates/zed/src/main.rs                                                 |    7 
script/lib/bump-version.sh                                             |    2 
styles/src/component/icon_button.ts                                    |   14 
styles/src/style_tree/app.ts                                           |    5 
styles/src/style_tree/collab_panel.ts                                  |   39 
styles/src/style_tree/component_test.ts                                |   27 
styles/src/style_tree/context_menu.ts                                  |    2 
styles/src/style_tree/editor.ts                                        |    1 
test.rs                                                                | 5670 
258 files changed, 24,874 insertions(+), 7,617 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 🔗

@@ -36,11 +36,11 @@ dependencies = [
 
 [[package]]
 name = "addr2line"
-version = "0.20.0"
+version = "0.21.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
 dependencies = [
- "gimli 0.27.3",
+ "gimli 0.28.0",
 ]
 
 [[package]]
@@ -88,9 +88,9 @@ dependencies = [
 
 [[package]]
 name = "aho-corasick"
-version = "1.0.2"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a"
 dependencies = [
  "memchr",
 ]
@@ -146,7 +146,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.28",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -157,7 +157,7 @@ dependencies = [
  "alacritty_config",
  "alacritty_config_derive",
  "base64 0.13.1",
- "bitflags 2.3.3",
+ "bitflags 2.4.0",
  "home",
  "libc",
  "log",
@@ -250,9 +250,9 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
+checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
 
 [[package]]
 name = "anstyle-parse"
@@ -274,9 +274,9 @@ dependencies = [
 
 [[package]]
 name = "anstyle-wincon"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
+checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c"
 dependencies = [
  "anstyle",
  "windows-sys",
@@ -284,9 +284,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.72"
+version = "1.0.75"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
 
 [[package]]
 name = "arrayref"
@@ -343,7 +343,7 @@ dependencies = [
  "futures-core",
  "futures-io",
  "once_cell",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
  "tokio",
 ]
 
@@ -357,7 +357,7 @@ dependencies = [
  "futures-core",
  "futures-io",
  "memchr",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
 ]
 
 [[package]]
@@ -417,15 +417,15 @@ dependencies = [
  "polling",
  "rustix 0.37.23",
  "slab",
- "socket2",
+ "socket2 0.4.9",
  "waker-fn",
 ]
 
 [[package]]
 name = "async-lock"
-version = "2.7.0"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
 dependencies = [
  "event-listener",
 ]
@@ -488,7 +488,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.28",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -511,7 +511,7 @@ dependencies = [
  "log",
  "memchr",
  "once_cell",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
  "pin-utils",
  "slab",
  "wasm-bindgen-futures",
@@ -525,7 +525,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
 dependencies = [
  "async-stream-impl",
  "futures-core",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
 ]
 
 [[package]]
@@ -536,7 +536,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.28",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -573,13 +573,13 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.72"
+version = "0.1.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
+checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.28",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -592,7 +592,7 @@ dependencies = [
  "futures-io",
  "futures-util",
  "log",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
  "tungstenite 0.16.0",
 ]
 
@@ -687,12 +687,12 @@ dependencies = [
  "http",
  "http-body",
  "hyper",
- "itoa 1.0.9",
+ "itoa",
  "matchit",
  "memchr",
  "mime",
  "percent-encoding",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
  "serde",
  "serde_json",
  "serde_urlencoded",
@@ -733,7 +733,7 @@ dependencies = [
  "futures-util",
  "http",
  "mime",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
  "serde",
  "serde_json",
  "tokio",
@@ -745,16 +745,16 @@ dependencies = [
 
 [[package]]
 name = "backtrace"
-version = "0.3.68"
+version = "0.3.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
 dependencies = [
- "addr2line 0.20.0",
+ "addr2line 0.21.0",
  "cc",
  "cfg-if 1.0.0",
  "libc",
  "miniz_oxide 0.7.1",
- "object 0.31.1",
+ "object 0.32.0",
  "rustc-demangle",
 ]
 
@@ -837,7 +837,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.28",
+ "syn 2.0.29",
  "which",
 ]
 
@@ -864,9 +864,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.3.3"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
 dependencies = [
  "serde",
 ]
@@ -1002,7 +1002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
 dependencies = [
  "memchr",
- "regex-automata 0.3.4",
+ "regex-automata 0.3.6",
  "serde",
 ]
 
@@ -1069,6 +1069,7 @@ dependencies = [
  "anyhow",
  "async-broadcast",
  "audio",
+ "channel",
  "client",
  "collections",
  "fs",
@@ -1162,11 +1163,12 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
 
 [[package]]
 name = "cc"
-version = "1.0.79"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
 dependencies = [
  "jobserver",
+ "libc",
 ]
 
 [[package]]
@@ -1196,6 +1198,41 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "channel"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "collections",
+ "db",
+ "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",
+ "staff_mode",
+ "sum_tree",
+ "tempfile",
+ "text",
+ "thiserror",
+ "time 0.3.27",
+ "tiny_http",
+ "url",
+ "util",
+ "uuid 1.4.1",
+]
+
 [[package]]
 name = "chrono"
 version = "0.4.26"
@@ -1257,9 +1294,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.3.19"
+version = "4.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d"
+checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487"
 dependencies = [
  "clap_builder",
  "clap_derive 4.3.12",
@@ -1268,9 +1305,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.3.19"
+version = "4.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1"
+checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1300,7 +1337,7 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.28",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -1318,12 +1355,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"
@@ -1366,8 +1397,9 @@ dependencies = [
  "staff_mode",
  "sum_tree",
  "tempfile",
+ "text",
  "thiserror",
- "time 0.3.24",
+ "time 0.3.27",
  "tiny_http",
  "url",
  "util",
@@ -1421,7 +1453,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.17.0"
+version = "0.18.0"
 dependencies = [
  "anyhow",
  "async-tungstenite",
@@ -1430,8 +1462,11 @@ dependencies = [
  "axum-extra",
  "base64 0.13.1",
  "call",
+ "channel",
  "clap 3.2.25",
  "client",
+ "clock",
+ "collab_ui",
  "collections",
  "ctor",
  "dashmap",
@@ -1456,6 +1491,7 @@ dependencies = [
  "pretty_assertions",
  "project",
  "prometheus",
+ "prost 0.8.0",
  "rand 0.8.5",
  "reqwest",
  "rpc",
@@ -1468,8 +1504,9 @@ dependencies = [
  "settings",
  "sha-1 0.9.8",
  "sqlx",
+ "text",
  "theme",
- "time 0.3.24",
+ "time 0.3.27",
  "tokio",
  "tokio-tungstenite",
  "toml 0.5.11",
@@ -1490,6 +1527,7 @@ dependencies = [
  "anyhow",
  "auto_update",
  "call",
+ "channel",
  "client",
  "clock",
  "collections",
@@ -1500,6 +1538,7 @@ dependencies = [
  "futures 0.3.28",
  "fuzzy",
  "gpui",
+ "language",
  "log",
  "menu",
  "picker",
@@ -1568,6 +1607,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"
@@ -1996,7 +2048,7 @@ dependencies = [
  "openssl-probe",
  "openssl-sys",
  "schannel",
- "socket2",
+ "socket2 0.4.9",
  "winapi 0.3.9",
 ]
 
@@ -2017,9 +2069,9 @@ dependencies = [
 
 [[package]]
 name = "dashmap"
-version = "5.5.0"
+version = "5.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d"
+checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28"
 dependencies = [
  "cfg-if 1.0.0",
  "hashbrown 0.14.0",
@@ -2077,9 +2129,9 @@ dependencies = [
 
 [[package]]
 name = "deranged"
-version = "0.3.6"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01"
+checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
 dependencies = [
  "serde",
 ]
@@ -2097,6 +2149,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"
@@ -2258,9 +2319,9 @@ dependencies = [
 
 [[package]]
 name = "dyn-clone"
-version = "1.0.12"
+version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
+checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555"
 
 [[package]]
 name = "editor"
@@ -2323,9 +2384,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
 
 [[package]]
 name = "encoding_rs"
-version = "0.8.32"
+version = "0.8.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -2373,9 +2434,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "erased-serde"
-version = "0.3.28"
+version = "0.3.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070"
+checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a"
 dependencies = [
  "serde",
 ]
@@ -2538,13 +2599,13 @@ dependencies = [
 
 [[package]]
 name = "filetime"
-version = "0.2.21"
+version = "0.2.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
+checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
- "redox_syscall 0.2.16",
+ "redox_syscall 0.3.5",
  "windows-sys",
 ]
 
@@ -2556,9 +2617,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
 
 [[package]]
 name = "flate2"
-version = "1.0.26"
+version = "1.0.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
 dependencies = [
  "crc32fast",
  "miniz_oxide 0.7.1",
@@ -2699,7 +2760,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempfile",
- "time 0.3.24",
+ "time 0.3.27",
  "util",
 ]
 
@@ -2837,7 +2898,7 @@ dependencies = [
  "futures-io",
  "memchr",
  "parking",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
  "waker-fn",
 ]
 
@@ -2849,7 +2910,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.28",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -2878,7 +2939,7 @@ dependencies = [
  "futures-sink",
  "futures-task",
  "memchr",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
  "pin-utils",
  "slab",
  "tokio-io",
@@ -2956,9 +3017,9 @@ dependencies = [
 
 [[package]]
 name = "gimli"
-version = "0.27.3"
+version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
+checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
 
 [[package]]
 name = "git"
@@ -3001,11 +3062,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "globset"
-version = "0.4.12"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006"
+checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
 dependencies = [
- "aho-corasick 1.0.2",
+ "aho-corasick 1.0.4",
  "bstr",
  "fnv",
  "log",
@@ -3079,6 +3140,7 @@ dependencies = [
  "png",
  "postage",
  "rand 0.8.5",
+ "refineable",
  "resvg",
  "schemars",
  "seahash",
@@ -3090,7 +3152,8 @@ dependencies = [
  "smol",
  "sqlez",
  "sum_tree",
- "time 0.3.24",
+ "taffy",
+ "time 0.3.27",
  "tiny-skia",
  "usvg",
  "util",
@@ -3102,16 +3165,23 @@ dependencies = [
 name = "gpui_macros"
 version = "0.1.0"
 dependencies = [
+ "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"
+version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
+checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
 dependencies = [
  "bytes 1.4.0",
  "fnv",
@@ -3305,7 +3375,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
 dependencies = [
  "bytes 1.4.0",
  "fnv",
- "itoa 1.0.9",
+ "itoa",
 ]
 
 [[package]]
@@ -3316,7 +3386,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
 dependencies = [
  "bytes 1.4.0",
  "http",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
 ]
 
 [[package]]
@@ -3333,9 +3403,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
 
 [[package]]
 name = "httpdate"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
 
 [[package]]
 name = "human_bytes"
@@ -3364,9 +3434,9 @@ dependencies = [
  "http-body",
  "httparse",
  "httpdate",
- "itoa 1.0.9",
- "pin-project-lite 0.2.10",
- "socket2",
+ "itoa",
+ "pin-project-lite 0.2.12",
+ "socket2 0.4.9",
  "tokio",
  "tower-service",
  "tracing",
@@ -3380,7 +3450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
 dependencies = [
  "hyper",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
  "tokio",
  "tokio-io-timeout",
 ]
@@ -3598,7 +3668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
 dependencies = [
  "hermit-abi 0.3.2",
- "rustix 0.38.4",
+ "rustix 0.38.8",
  "windows-sys",
 ]
 
@@ -3638,12 +3708,6 @@ dependencies = [
  "either",
 ]
 
-[[package]]
-name = "itoa"
-version = "0.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
-
 [[package]]
 name = "itoa"
 version = "1.0.9"
@@ -3896,17 +3960,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"
@@ -4081,9 +4134,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.19"
+version = "0.4.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
 dependencies = [
  "serde",
  "value-bag",
@@ -4114,9 +4167,9 @@ dependencies = [
 
 [[package]]
 name = "lsp-types"
-version = "0.94.0"
+version = "0.94.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237"
+checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1"
 dependencies = [
  "bitflags 1.3.2",
  "serde",
@@ -4572,9 +4625,9 @@ dependencies = [
 
 [[package]]
 name = "num-bigint"
-version = "0.4.3"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
+checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
 dependencies = [
  "autocfg",
  "num-integer",
@@ -4730,9 +4783,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.31.1"
+version = "0.32.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1"
+checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe"
 dependencies = [
  "memchr",
 ]
@@ -4760,15 +4813,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"
@@ -4783,9 +4827,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "openssl"
-version = "0.10.55"
+version = "0.10.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
+checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e"
 dependencies = [
  "bitflags 1.3.2",
  "cfg-if 1.0.0",
@@ -4804,7 +4848,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.28",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -4815,9 +4859,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.90"
+version = "0.9.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
+checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac"
 dependencies = [
  "cc",
  "libc",
@@ -4952,7 +4996,7 @@ dependencies = [
  "libc",
  "redox_syscall 0.3.5",
  "smallvec",
- "windows-targets 0.48.1",
+ "windows-targets 0.48.5",
 ]
 
 [[package]]
@@ -5044,12 +5088,12 @@ dependencies = [
 
 [[package]]
 name = "petgraph"
-version = "0.6.3"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
+checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
 dependencies = [
  "fixedbitset",
- "indexmap 1.9.3",
+ "indexmap 2.0.0",
 ]
 
 [[package]]
@@ -5077,22 +5121,22 @@ checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
 
 [[package]]
 name = "pin-project"
-version = "1.1.2"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842"
+checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.1.2"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
+checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.28",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -5103,9 +5147,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777"
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.10"
+version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57"
+checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05"
 
 [[package]]
 name = "pin-utils"
@@ -5119,6 +5163,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"
@@ -5130,7 +5201,7 @@ dependencies = [
  "line-wrap",
  "quick-xml",
  "serde",
- "time 0.3.24",
+ "time 0.3.27",
 ]
 
 [[package]]
@@ -5195,7 +5266,7 @@ dependencies = [
  "concurrent-queue",
  "libc",
  "log",
- "pin-project-lite 0.2.10",
+ "pin-project-lite 0.2.12",
  "windows-sys",
 ]
 
@@ -5245,7 +5316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62"
 dependencies = [
  "proc-macro2",
- "syn 2.0.28",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -5585,9 +5656,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.32"
+version = "1.0.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
 dependencies = [
  "proc-macro2",
 ]

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",
@@ -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

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/keymaps/default.json 🔗

@@ -173,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",
@@ -543,6 +544,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",

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",
@@ -277,6 +287,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 +309,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 +373,7 @@
     }
   },
   {
-    "context": "Editor && vim_mode == visual && !VimWaiting",
+    "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject",
     "bindings": {
       "u": "editor::Undo",
       "o": "vim::OtherEnd",
@@ -373,10 +381,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 +413,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 🔗

@@ -126,7 +126,7 @@
     // Whether to show the collaboration panel button in the status bar.
     "button": true,
     // Where to dock channels panel. Can be 'left' or 'right'.
-    "dock": "right",
+    "dock": "left",
     // Default width of the channels panel.
     "default_width": 240
   },
@@ -138,7 +138,13 @@
     // 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,

crates/ai/src/ai.rs 🔗

@@ -4,6 +4,7 @@ mod streaming_diff;
 
 use anyhow::{anyhow, Result};
 pub use assistant::AssistantPanel;
+use assistant_settings::OpenAIModel;
 use chrono::{DateTime, Local};
 use collections::HashMap;
 use fs::Fs;
@@ -65,7 +66,7 @@ struct SavedConversation {
     messages: Vec<SavedMessage>,
     message_metadata: HashMap<MessageId, MessageMetadata>,
     summary: String,
-    model: String,
+    model: OpenAIModel,
 }
 
 impl SavedConversation {

crates/ai/src/assistant.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    assistant_settings::{AssistantDockPosition, AssistantSettings},
+    assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
     stream_completion,
     streaming_diff::{Hunk, StreamingDiff},
     MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role,
@@ -1314,7 +1314,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<()>>,
@@ -1334,7 +1334,6 @@ 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);
@@ -1353,6 +1352,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(),
@@ -1362,9 +1364,9 @@ impl Conversation {
             completion_count: Default::default(),
             pending_completions: Default::default(),
             token_count: None,
-            max_token_count: tiktoken_rs::model::get_context_size(model),
+            max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
             pending_token_count: Task::ready(None),
-            model: model.into(),
+            model: model.clone(),
             _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
             pending_save: Task::ready(Ok(())),
             path: None,
@@ -1458,7 +1460,7 @@ impl Conversation {
             completion_count: Default::default(),
             pending_completions: Default::default(),
             token_count: None,
-            max_token_count: tiktoken_rs::model::get_context_size(&model),
+            max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
             pending_token_count: Task::ready(None),
             model,
             _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
@@ -1512,13 +1514,16 @@ impl Conversation {
                 cx.background().timer(Duration::from_millis(200)).await;
                 let token_count = cx
                     .background()
-                    .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model, &messages) })
+                    .spawn(async move {
+                        tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages)
+                    })
                     .await?;
 
                 this.upgrade(&cx)
                     .ok_or_else(|| anyhow!("conversation was dropped"))?
                     .update(&mut cx, |this, cx| {
-                        this.max_token_count = tiktoken_rs::model::get_context_size(&this.model);
+                        this.max_token_count =
+                            tiktoken_rs::model::get_context_size(&this.model.full_name());
                         this.token_count = Some(token_count);
                         cx.notify()
                     });
@@ -1532,7 +1537,7 @@ impl Conversation {
         Some(self.max_token_count as isize - self.token_count? as isize)
     }
 
-    fn set_model(&mut self, model: String, cx: &mut ModelContext<Self>) {
+    fn set_model(&mut self, model: OpenAIModel, cx: &mut ModelContext<Self>) {
         self.model = model;
         self.count_remaining_tokens(cx);
         cx.notify();
@@ -1574,7 +1579,7 @@ impl Conversation {
                 }
             } else {
                 let request = OpenAIRequest {
-                    model: self.model.clone(),
+                    model: self.model.full_name().to_string(),
                     messages: self
                         .messages(cx)
                         .filter(|message| matches!(message.status, MessageStatus::Done))
@@ -1900,7 +1905,7 @@ impl Conversation {
                                 .into(),
                     }));
                 let request = OpenAIRequest {
-                    model: self.model.clone(),
+                    model: self.model.full_name().to_string(),
                     messages: messages.collect(),
                     stream: true,
                 };
@@ -2504,11 +2509,8 @@ impl ConversationEditor {
 
     fn cycle_model(&mut self, cx: &mut ViewContext<Self>) {
         self.conversation.update(cx, |conversation, cx| {
-            let new_model = match conversation.model.as_str() {
-                "gpt-4-0613" => "gpt-3.5-turbo-0613",
-                _ => "gpt-4-0613",
-            };
-            conversation.set_model(new_model.into(), cx);
+            let new_model = conversation.model.cycle();
+            conversation.set_model(new_model, cx);
         });
     }
 
@@ -2530,7 +2532,8 @@ impl ConversationEditor {
 
         MouseEventHandler::new::<Model, _>(0, cx, |state, cx| {
             let style = style.model.style_for(state);
-            Label::new(self.conversation.read(cx).model.clone(), style.text.clone())
+            let model_display_name = self.conversation.read(cx).model.short_name();
+            Label::new(model_display_name, style.text.clone())
                 .contained()
                 .with_style(style.container)
         })
@@ -2712,6 +2715,8 @@ mod tests {
 
     #[gpui::test]
     fn test_inserting_and_removing_messages(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        init(cx);
         let registry = Arc::new(LanguageRegistry::test());
         let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx));
         let buffer = conversation.read(cx).buffer.clone();
@@ -2838,6 +2843,8 @@ mod tests {
 
     #[gpui::test]
     fn test_message_splitting(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        init(cx);
         let registry = Arc::new(LanguageRegistry::test());
         let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx));
         let buffer = conversation.read(cx).buffer.clone();
@@ -2932,6 +2939,8 @@ mod tests {
 
     #[gpui::test]
     fn test_messages_for_offsets(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        init(cx);
         let registry = Arc::new(LanguageRegistry::test());
         let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx));
         let buffer = conversation.read(cx).buffer.clone();
@@ -3012,6 +3021,8 @@ mod tests {
 
     #[gpui::test]
     fn test_serialization(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        init(cx);
         let registry = Arc::new(LanguageRegistry::test());
         let conversation =
             cx.add_model(|cx| Conversation::new(Default::default(), registry.clone(), cx));

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 {
@@ -17,6 +48,7 @@ pub struct AssistantSettings {
     pub dock: AssistantDockPosition,
     pub default_width: f32,
     pub default_height: f32,
+    pub default_open_ai_model: OpenAIModel,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@@ -25,6 +57,7 @@ pub struct AssistantSettingsContent {
     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/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/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 🔗

@@ -7,9 +7,8 @@ use std::sync::Arc;
 use anyhow::{anyhow, Result};
 use audio::Audio;
 use call_settings::CallSettings;
-use client::{
-    proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
-};
+use channel::ChannelId;
+use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
 use postage::watch;
@@ -274,7 +273,7 @@ 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,
         })?;
@@ -406,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/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" }
+staff_mode = { path = "../staff_mode" }
+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/client/src/channel_store.rs → crates/channel/src/channel_store.rs 🔗

@@ -1,19 +1,14 @@
-use crate::Status;
-use crate::{Client, Subscription, User, UserStore};
-use anyhow::anyhow;
-use anyhow::Result;
-use collections::HashMap;
-use collections::HashSet;
-use futures::channel::mpsc;
-use futures::Future;
-use futures::StreamExt;
-use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
+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 type UserId = u64;
 
 pub struct ChannelStore {
     channels_by_id: HashMap<ChannelId, Arc<Channel>>,
@@ -23,6 +18,7 @@ pub struct ChannelStore {
     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,
@@ -57,6 +53,11 @@ pub enum ChannelMemberStatus {
     NotMember,
 }
 
+enum OpenedChannelBuffer {
+    Open(WeakModelHandle<ChannelBuffer>),
+    Loading(Shared<Task<Result<ModelHandle<ChannelBuffer>, Arc<anyhow::Error>>>>),
+}
+
 impl ChannelStore {
     pub fn new(
         client: Arc<Client>,
@@ -70,16 +71,14 @@ impl ChannelStore {
         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 matches!(status, Status::ConnectionLost | Status::SignedOut) {
+                if !status.is_connected() {
                     if let Some(this) = this.upgrade(&cx) {
                         this.update(&mut cx, |this, cx| {
-                            this.channels_by_id.clear();
-                            this.channel_invitations.clear();
-                            this.channel_participants.clear();
-                            this.channels_with_admin_privileges.clear();
-                            this.channel_paths.clear();
-                            this.outgoing_invites.clear();
-                            cx.notify();
+                            if matches!(status, Status::ConnectionLost | Status::SignedOut) {
+                                this.handle_disconnect(cx);
+                            } else {
+                                this.disconnect_buffers(cx);
+                            }
                         });
                     } else {
                         break;
@@ -87,6 +86,7 @@ impl ChannelStore {
                 }
             }
         });
+
         Self {
             channels_by_id: HashMap::default(),
             channel_invitations: Vec::default(),
@@ -94,6 +94,7 @@ impl ChannelStore {
             channel_participants: Default::default(),
             channels_with_admin_privileges: Default::default(),
             outgoing_invites: Default::default(),
+            opened_buffers: Default::default(),
             update_channels_tx,
             client,
             user_store,
@@ -114,6 +115,16 @@ impl ChannelStore {
         }
     }
 
+    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()
     }
@@ -141,6 +152,74 @@ impl ChannelStore {
         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) {
@@ -403,6 +482,27 @@ impl ChannelStore {
         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,
@@ -437,38 +537,44 @@ impl ChannelStore {
                     .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 in payload.channels {
-                if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) {
-                    // FIXME: We may be missing a path for this existing channel in certain cases
-                    let existing_channel = Arc::make_mut(existing_channel);
-                    existing_channel.name = channel.name;
-                    continue;
+                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);
+                        }
+                    }
                 }
+            }
 
-                self.channels_by_id.insert(
-                    channel.id,
-                    Arc::new(Channel {
-                        id: channel.id,
-                        name: channel.name,
-                    }),
-                );
-
-                if let Some(parent_id) = channel.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);
+            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;
                         }
-                        ix += 1;
+                    } else {
+                        self.channel_paths.push(vec![channel.id]);
                     }
-                } else {
-                    self.channel_paths.push(vec![channel.id]);
                 }
             }
 

crates/client/src/channel_store_tests.rs → crates/channel/src/channel_store_tests.rs 🔗

@@ -1,4 +1,7 @@
 use super::*;
+use client::{Client, UserStore};
+use gpui::{AppContext, ModelHandle};
+use rpc::proto;
 use util::http::FakeHttpClient;
 
 #[gpui::test]

crates/client/Cargo.toml 🔗

@@ -17,6 +17,7 @@ db = { path = "../db" }
 gpui = { path = "../gpui" }
 util = { path = "../util" }
 rpc = { path = "../rpc" }
+text = { path = "../text" }
 settings = { path = "../settings" }
 staff_mode = { path = "../staff_mode" }
 sum_tree = { path = "../sum_tree" }

crates/client/src/client.rs 🔗

@@ -1,10 +1,6 @@
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
-#[cfg(test)]
-mod channel_store_tests;
-
-pub mod channel_store;
 pub mod telemetry;
 pub mod user;
 
@@ -48,7 +44,6 @@ use util::channel::ReleaseChannel;
 use util::http::HttpClient;
 use util::{ResultExt, TryFutureExt};
 
-pub use channel_store::*;
 pub use rpc::*;
 pub use telemetry::ClickhouseEvent;
 pub use user::*;

crates/client/src/user.rs 🔗

@@ -10,9 +10,11 @@ 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>>,
 }

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.17.0"
+version = "0.18.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 🔗

@@ -208,3 +208,44 @@ CREATE TABLE "channel_members" (
 );
 
 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");

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/src/db.rs 🔗

@@ -1,3850 +1,117 @@
-mod access_token;
-mod channel;
-mod channel_member;
-mod channel_path;
-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::fmt::Write as _;
-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 (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 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 is_current_room_different_channel(
-        &self,
-        user_id: UserId,
-        channel_id: ChannelId,
-    ) -> Result<bool> {
-        self.transaction(|tx| async move {
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryAs {
-                ChannelId,
-            }
-
-            let channel_id_model: Option<ChannelId> = room_participant::Entity::find()
-                .select_only()
-                .column_as(room::Column::ChannelId, QueryAs::ChannelId)
-                .inner_join(room::Entity)
-                .filter(room_participant::Column::UserId.eq(user_id))
-                .into_values::<_, QueryAs>()
-                .one(&*tx)
-                .await?;
-
-            let result = channel_id_model
-                .map(|channel_id_model| channel_id_model != channel_id)
-                .unwrap_or(false);
-
-            Ok(result)
-        })
-        .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 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 (_, 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,
-            },
-        ))
-    }
-
-    // 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?;
-
-            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)
-    }
-
-    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
-    }
-
-    // channels
-
-    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
-    }
+#[cfg(test)]
+pub use tests::TestDb;
 
-    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
-    }
+mod ids;
+mod queries;
+mod tables;
 
-    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?;
+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};
 
-            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?;
+pub use ids::*;
+pub use sea_orm::ConnectOptions;
+pub use tables::user::Model as User;
 
-            if result.rows_affected == 0 {
-                Err(anyhow!("no such member"))?;
-            }
+pub struct Database {
+    options: ConnectOptions,
+    pool: DatabaseConnection,
+    rooms: DashMap<RoomId, Arc<Mutex<()>>>,
+    rng: Mutex<StdRng>,
+    executor: Executor,
+    #[cfg(test)]
+    runtime: Option<tokio::runtime::Runtime>,
+}
 
-            Ok(())
+// 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 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
+    #[cfg(test)]
+    pub fn reset(&self) {
+        self.rooms.clear();
     }
 
-    pub async fn get_channel_members_internal(
+    pub async fn migrate(
         &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)
-    }
+        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:?}"))?;
 
-    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(())
-    }
+        let mut connection = sqlx::AnyConnection::connect(self.options.get_url()).await?;
 
-    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)
+        connection.ensure_migrations_table().await?;
+        let applied_migrations: HashMap<_, _> = connection
+            .list_applied_migrations()
             .await?
-            .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?;
-        Ok(())
-    }
+            .into_iter()
+            .map(|m| (m.version, m))
+            .collect();
 
-    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);
+        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
+                        ))?;
                     }
                 }
-            }
-        }
-        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);
+                None => {
+                    let elapsed = connection.apply(&migration).await?;
+                    new_migrations.push((migration, elapsed));
                 }
             }
-            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
+        Ok(new_migrations)
     }
 
     async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>

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

@@ -0,0 +1,127 @@
+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);

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,243 @@
+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
+    }
+}

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,24 @@
+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 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 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/channel.rs → crates/collab/src/db/tables/channel.rs 🔗

@@ -1,4 +1,4 @@
-use super::ChannelId;
+use crate::db::ChannelId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
@@ -15,8 +15,12 @@ impl ActiveModelBehavior for ActiveModel {}
 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 {
@@ -31,8 +35,14 @@ impl Related<super::room::Entity> for Entity {
     }
 }
 
-// impl Related<super::follower::Entity> for Entity {
-//     fn to() -> RelationDef {
-//         Relation::Follower.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/channel_member.rs → crates/collab/src/db/tables/channel_member.rs 🔗

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

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

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

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/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,4 +1,4 @@
-use super::{ChannelId, RoomId};
+use crate::db::{ChannelId, RoomId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)]

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/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,1570 +1,143 @@
+mod buffer_tests;
+mod db_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,
-                    },
-                )
+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();
+
+        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();
-            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()
-            .unwrap();
-        assert_eq!(user.id, user_id1);
-        assert_eq!(&user.github_login, "login1");
-        assert_eq!(user.github_user_id, Some(101));
+            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
+        });
 
-        assert!(db
-            .get_or_create_user_by_github_account("non-existent-login", None, None)
-            .await
-            .unwrap()
-            .is_none());
+        db.runtime = Some(runtime);
 
-        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()));
+        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);
-
-        db.leave_room(ConnectionId { owner_id, id: 1 })
-            .await
-            .unwrap();
-        assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
-    }
-);
+                .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
+        });
 
-#[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%");
-}
+        db.runtime = Some(runtime);
 
-#[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());
-}
-
-test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
-    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_postgres,
-    test_joining_channels_sqlite,
-    db,
-    {
-        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_postgres,
-    test_channel_invites_sqlite,
-    db,
-    {
-        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_postgres,
-    test_channel_renames_sqlite,
-    db,
-    {
-        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
+#[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/rpc.rs 🔗

@@ -35,8 +35,8 @@ use lazy_static::lazy_static;
 use prometheus::{register_int_gauge, IntGauge};
 use rpc::{
     proto::{
-        self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
-        RequestMessage,
+        self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage,
+        LiveKitConnectionInfo, RequestMessage,
     },
     Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
 };
@@ -248,6 +248,9 @@ impl Server {
             .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)
@@ -851,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();
@@ -866,6 +873,8 @@ async fn connection_lost(
                 }
             }
             update_user_contacts(session.user_id, &session).await?;
+
+
         }
         _ = teardown.changed().fuse() => {}
     }
@@ -2478,6 +2487,104 @@ async fn join_channel(
     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
@@ -2803,6 +2910,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,14 +1,14 @@
 use crate::{
-    db::{NewUserParams, TestDb, UserId},
+    db::{tests::TestDb, NewUserParams, UserId},
     executor::Executor,
     rpc::{Server, CLEANUP_TIMEOUT},
     AppState,
 };
 use anyhow::anyhow;
 use call::{ActiveCall, Room};
+use channel::ChannelStore;
 use client::{
-    self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError,
-    UserStore,
+    self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
 };
 use collections::{HashMap, HashSet};
 use fs::FakeFs;
@@ -31,6 +31,7 @@ use std::{
 use util::http::FakeHttpClient;
 use workspace::Workspace;
 
+mod channel_buffer_tests;
 mod channel_tests;
 mod integration_tests;
 mod randomized_integration_tests;
@@ -210,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

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 🔗

@@ -3,7 +3,8 @@ use crate::{
     tests::{room_participants, RoomParticipants, TestServer},
 };
 use call::ActiveCall;
-use client::{ChannelId, ChannelMembership, ChannelStore, User};
+use channel::{ChannelId, ChannelMembership, ChannelStore};
+use client::User;
 use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
 use rpc::{proto, RECEIVE_TIMEOUT};
 use std::sync::Arc;
@@ -798,7 +799,7 @@ async fn test_lost_channel_creation(
 
     deterministic.run_until_parked();
 
-    // Sanity check
+    // Sanity check, B has the invitation
     assert_channel_invitations(
         client_b.channel_store(),
         cx_b,
@@ -810,6 +811,7 @@ async fn test_lost_channel_creation(
         }],
     );
 
+    // A creates a subchannel while the invite is still pending.
     let subchannel_id = client_a
         .channel_store()
         .update(cx_a, |channel_store, cx| {
@@ -840,7 +842,7 @@ async fn test_lost_channel_creation(
         ],
     );
 
-    // Accept the invite
+    // Client B accepts the invite
     client_b
         .channel_store()
         .update(cx_b, |channel_store, _| {
@@ -851,7 +853,7 @@ async fn test_lost_channel_creation(
 
     deterministic.run_until_parked();
 
-    // B should now see the channel
+    // Client B should now see the channel
     assert_channels(
         client_b.channel_store(),
         cx_b,

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

@@ -4163,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()

crates/collab_ui/Cargo.toml 🔗

@@ -26,6 +26,7 @@ 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" }
@@ -33,6 +34,7 @@ editor = { path = "../editor" }
 feedback = { path = "../feedback" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
+language = { path = "../language" }
 menu = { path = "../menu" }
 picker = { path = "../picker" }
 project = { path = "../project" }

crates/collab_ui/src/channel_view.rs 🔗

@@ -0,0 +1,351 @@
+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 {
+        true
+    }
+
+    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 🔗

@@ -4,10 +4,8 @@ mod panel_settings;
 
 use anyhow::Result;
 use call::ActiveCall;
-use client::{
-    proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore,
-};
-
+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};
@@ -16,16 +14,18 @@ use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
     actions,
     elements::{
-        Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState,
-        MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg,
+        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, ModelHandle,
+    serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
     Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
@@ -35,7 +35,7 @@ use serde_derive::{Deserialize, Serialize};
 use settings::SettingsStore;
 use staff_mode::StaffMode;
 use std::{borrow::Cow, mem, sync::Arc};
-use theme::IconButton;
+use theme::{components::ComponentExt, IconButton};
 use util::{iife, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
@@ -43,7 +43,10 @@ use workspace::{
     Workspace,
 };
 
-use crate::face_pile::FacePile;
+use crate::{
+    channel_view::{self, ChannelView},
+    face_pile::FacePile,
+};
 use channel_modal::ChannelModal;
 
 use self::contact_finder::ContactFinder;
@@ -53,6 +56,11 @@ 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,
@@ -73,7 +81,21 @@ struct RenameChannel {
     channel_id: u64,
 }
 
-actions!(collab_panel, [ToggleFocus, Remove, Secondary]);
+#[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,
@@ -82,16 +104,19 @@ impl_actions!(
         NewChannel,
         InviteMembers,
         ManageMembers,
-        RenameChannel
+        RenameChannel,
+        ToggleCollapse,
+        OpenChannelBuffer
     ]
 );
 
-const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
+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);
@@ -105,6 +130,10 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
     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)]
@@ -147,6 +176,7 @@ pub struct CollabPanel {
     list_state: ListState<Self>,
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
+    collapsed_channels: Vec<ChannelId>,
     workspace: WeakViewHandle<Workspace>,
     context_menu_on_selected: bool,
 }
@@ -154,6 +184,7 @@ pub struct CollabPanel {
 #[derive(Serialize, Deserialize)]
 struct SerializedChannelsPanel {
     width: Option<f32>,
+    collapsed_channels: Vec<ChannelId>,
 }
 
 #[derive(Debug)]
@@ -198,6 +229,9 @@ enum ListEntry {
         channel: Arc<Channel>,
         depth: usize,
     },
+    ChannelNotes {
+        channel_id: ChannelId,
+    },
     ChannelEditor {
         depth: usize,
     },
@@ -341,6 +375,12 @@ impl CollabPanel {
                                 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(),
@@ -398,6 +438,7 @@ impl CollabPanel {
                 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,
@@ -464,7 +505,7 @@ impl CollabPanel {
         cx.spawn(|mut cx| async move {
             let serialized_panel = if let Some(panel) = cx
                 .background()
-                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHANNELS_PANEL_KEY) })
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
                 .await
                 .log_err()
                 .flatten()
@@ -479,6 +520,7 @@ impl CollabPanel {
                 if let Some(serialized_panel) = serialized_panel {
                     panel.update(cx, |panel, cx| {
                         panel.width = serialized_panel.width;
+                        panel.collapsed_channels = serialized_panel.collapsed_channels;
                         cx.notify();
                     });
                 }
@@ -489,12 +531,16 @@ impl CollabPanel {
 
     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(
-                        CHANNELS_PANEL_KEY.into(),
-                        serde_json::to_string(&SerializedChannelsPanel { width })?,
+                        COLLABORATION_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedChannelsPanel {
+                            width,
+                            collapsed_channels,
+                        })?,
                     )
                     .await?;
                 anyhow::Ok(())
@@ -518,6 +564,10 @@ impl CollabPanel {
             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();
@@ -657,10 +707,24 @@ impl CollabPanel {
                         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) =>
@@ -963,25 +1027,19 @@ impl CollabPanel {
     ) -> AnyElement<Self> {
         enum JoinProject {}
 
-        let font_cache = cx.font_cache();
-        let host_avatar_height = theme
+        let host_avatar_width = 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, _| {
+        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
@@ -989,39 +1047,20 @@ impl CollabPanel {
                 .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(
-                    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: (0.).into(),
-                            });
-                            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: (0.).into(),
-                            });
-                        }))
+                    Svg::new("icons/file_icons/folder.svg")
+                        .with_color(theme.channel_hash.color)
                         .constrained()
-                        .with_width(host_avatar_height),
+                        .with_width(theme.channel_hash.width)
+                        .aligned()
+                        .left(),
                 )
                 .with_child(
                     Label::new(project_name, row.name.text.clone())
@@ -1196,7 +1235,7 @@ impl CollabPanel {
                 });
 
                 if let Some(name) = channel_name {
-                    Cow::Owned(format!("Current Call - #{}", name))
+                    Cow::Owned(format!("#{}", name))
                 } else {
                     Cow::Borrowed("Current Call")
                 }
@@ -1332,7 +1371,7 @@ impl CollabPanel {
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, this, cx| {
                     if can_collapse {
-                        this.toggle_expanded(section, cx);
+                        this.toggle_section_expanded(section, cx);
                     }
                 })
         }
@@ -1479,6 +1518,11 @@ impl CollabPanel {
         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)
@@ -1537,6 +1581,10 @@ impl CollabPanel {
         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)
@@ -1550,7 +1598,7 @@ impl CollabPanel {
         const FACEPILE_LIMIT: usize = 3;
 
         MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
-            Flex::row()
+            Flex::<Self>::row()
                 .with_child(
                     Svg::new("icons/hash.svg")
                         .with_color(theme.channel_hash.color)
@@ -1599,6 +1647,11 @@ impl CollabPanel {
                     }
                 })
                 .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()
@@ -1618,6 +1671,61 @@ impl CollabPanel {
         .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>,
@@ -1815,39 +1923,52 @@ impl CollabPanel {
         channel_id: u64,
         cx: &mut ViewContext<Self>,
     ) {
-        if self.channel_store.read(cx).is_user_admin(channel_id) {
-            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
-                });
+        self.context_menu_on_selected = position.is_none();
 
-                context_menu.show(
-                    position.unwrap_or_default(),
-                    if self.context_menu_on_selected {
-                        gpui::elements::AnchorCorner::TopRight
-                    } else {
-                        gpui::elements::AnchorCorner::BottomLeft
-                    },
-                    vec![
-                        ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
-                        ContextMenuItem::Separator,
-                        ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }),
-                        ContextMenuItem::Separator,
-                        ContextMenuItem::action("Rename", RenameChannel { channel_id }),
-                        ContextMenuItem::action("Manage", ManageMembers { channel_id }),
-                        ContextMenuItem::Separator,
-                        ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
-                    ],
-                    cx,
-                );
+        self.context_menu.update(cx, |context_menu, cx| {
+            context_menu.set_position_mode(if self.context_menu_on_selected {
+                OverlayPositionMode::Local
+            } else {
+                OverlayPositionMode::Window
             });
 
-            cx.notify();
-        }
+            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>) {
@@ -1912,7 +2033,7 @@ impl CollabPanel {
                         | Section::Online
                         | Section::Offline
                         | Section::ChannelInvites => {
-                            self.toggle_expanded(*section, cx);
+                            self.toggle_section_expanded(*section, cx);
                         }
                     },
                     ListEntry::Contact { contact, calling } => {
@@ -2000,7 +2121,7 @@ impl CollabPanel {
         }
     }
 
-    fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
+    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 {
@@ -2009,6 +2130,55 @@ impl CollabPanel {
         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))
@@ -2048,6 +2218,8 @@ impl CollabPanel {
     }
 
     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,
@@ -2103,6 +2275,21 @@ impl CollabPanel {
         }
     }
 
+    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;
@@ -2261,6 +2448,51 @@ impl CollabPanel {
     }
 }
 
+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"
@@ -2354,7 +2586,7 @@ impl View for CollabPanel {
                 .into_any()
         })
         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
-        .into_any_named("channels panel")
+        .into_any_named("collab panel")
     }
 }
 
@@ -2404,7 +2636,10 @@ impl Panel for CollabPanel {
     }
 
     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-        ("Channels Panel".to_string(), Some(Box::new(ToggleFocus)))
+        (
+            "Collaboration Panel".to_string(),
+            Some(Box::new(ToggleFocus)),
+        )
     }
 
     fn should_change_position_on_event(event: &Self::Event) -> bool {
@@ -2467,6 +2702,14 @@ impl PartialEq for ListEntry {
                     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;

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

@@ -1,4 +1,5 @@
-use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore};
+use channel::{ChannelId, ChannelMembership, ChannelStore};
+use client::{proto, User, UserId, UserStore};
 use context_menu::{ContextMenu, ContextMenuItem};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -1096,7 +1096,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,3 +1,4 @@
+pub mod channel_view;
 pub mod collab_panel;
 mod collab_titlebar_item;
 mod contact_notification;
@@ -49,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,
                 );
@@ -57,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,
                 );
@@ -73,11 +76,18 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
         let client = call.client();
         room.update(cx, |room, cx| {
             if room.is_muted(cx) {
-                ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, 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,
                 );

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/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/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/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,

crates/editor/src/editor.rs 🔗

@@ -559,6 +559,7 @@ 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>>,
@@ -577,6 +578,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,
@@ -1393,6 +1395,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,
@@ -1412,6 +1415,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(),
@@ -1590,10 +1594,31 @@ 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 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,
@@ -1722,7 +1747,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,
+            )
         });
     }
 
@@ -2093,12 +2143,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;
@@ -2120,7 +2170,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(
@@ -2197,7 +2247,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);
@@ -2657,7 +2707,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);
@@ -3037,7 +3086,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,
                     );
                 });
@@ -4732,6 +4781,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 {
@@ -4739,6 +4789,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);
@@ -4769,6 +4824,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;
@@ -4777,6 +4833,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);
@@ -4796,7 +4857,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 =
@@ -4804,18 +4865,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| {
@@ -4831,8 +4881,9 @@ impl Editor {
                             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];
+                                dbg!(start_offset, end_offset, &clipboard_text, &to_insert);
                                 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 {
@@ -8527,6 +8578,7 @@ fn build_style(
                 font_size,
                 font_properties,
                 underline: Default::default(),
+                soft_wrap: false,
             },
             placeholder_text: None,
             line_height_scalar,

crates/editor/src/editor_tests.rs 🔗

@@ -5237,6 +5237,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()
@@ -6383,7 +6384,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,
@@ -6478,7 +6479,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,
@@ -7528,6 +7529,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 🔗

@@ -62,6 +62,7 @@ struct SelectionLayout {
     head: DisplayPoint,
     cursor_shape: CursorShape,
     is_newest: bool,
+    is_local: bool,
     range: Range<DisplayPoint>,
     active_rows: Range<u32>,
 }
@@ -73,6 +74,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 +111,7 @@ impl SelectionLayout {
             head,
             cursor_shape,
             is_newest,
+            is_local,
             range,
             active_rows,
         }
@@ -605,7 +608,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 +763,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 +854,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 +874,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
@@ -1337,7 +1340,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;
@@ -2124,7 +2127,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 +2158,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 {
+                None
+            };
+
             // 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 +2177,7 @@ impl Element<Editor> for EditorElement {
                     cursor_shape,
                     &snapshot.display_snapshot,
                     false,
+                    false,
                 ));
         }
         selections.extend(remote_selections);
@@ -2191,6 +2201,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 +2217,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;
@@ -2591,7 +2609,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,

crates/editor/src/items.rs 🔗

@@ -49,11 +49,12 @@ 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 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!() };
 
@@ -561,7 +562,7 @@ impl Item for Editor {
         }
     }
 
-    fn tab_content<T: View>(
+    fn tab_content<T: 'static>(
         &self,
         detail: Option<usize>,
         style: &theme::Tab,
@@ -753,7 +754,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
     }
 
@@ -1028,7 +1029,7 @@ impl SearchableItem for Editor {
             if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
                 ranges.extend(
                     query
-                        .search(excerpt_buffer.as_rope())
+                        .search(excerpt_buffer, None)
                         .await
                         .into_iter()
                         .map(|range| {
@@ -1038,17 +1039,22 @@ impl SearchableItem for Editor {
             } else {
                 for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
                     let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
-                    let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
-                    ranges.extend(query.search(&rope).await.into_iter().map(|range| {
-                        let start = excerpt
-                            .buffer
-                            .anchor_after(excerpt_range.start + range.start);
-                        let end = excerpt
-                            .buffer
-                            .anchor_before(excerpt_range.start + range.end);
-                        buffer.anchor_in_excerpt(excerpt.id.clone(), start)
-                            ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
-                    }));
+                    ranges.extend(
+                        query
+                            .search(&excerpt.buffer, Some(excerpt_range.clone()))
+                            .await
+                            .into_iter()
+                            .map(|range| {
+                                let start = excerpt
+                                    .buffer
+                                    .anchor_after(excerpt_range.start + range.start);
+                                let end = excerpt
+                                    .buffer
+                                    .anchor_before(excerpt_range.start + range.end);
+                                buffer.anchor_in_excerpt(excerpt.id.clone(), start)
+                                    ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
+                            }),
+                    );
                 }
             }
             ranges

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;
@@ -176,14 +176,21 @@ pub fn line_end(
 }
 
 pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let raw_point = point.to_point(map);
+    let language = map.buffer_snapshot.language_at(raw_point);
+
     find_preceding_boundary(map, point, |left, right| {
-        (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
+        (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace())
+            || left == '\n'
     })
 }
 
 pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let raw_point = point.to_point(map);
+    let language = map.buffer_snapshot.language_at(raw_point);
     find_preceding_boundary(map, point, |left, right| {
-        let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
+        let is_word_start =
+            char_kind(language, left) != char_kind(language, right) && !right.is_whitespace();
         let is_subword_start =
             left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
         is_word_start || is_subword_start || left == '\n'
@@ -191,14 +198,20 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
 }
 
 pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let raw_point = point.to_point(map);
+    let language = map.buffer_snapshot.language_at(raw_point);
     find_boundary(map, point, |left, right| {
-        (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
+        (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace())
+            || right == '\n'
     })
 }
 
 pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let raw_point = point.to_point(map);
+    let language = map.buffer_snapshot.language_at(raw_point);
     find_boundary(map, point, |left, right| {
-        let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
+        let is_word_end =
+            (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace();
         let is_subword_end =
             left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
         is_word_end || is_subword_end || right == '\n'
@@ -385,10 +398,15 @@ pub fn find_boundary_in_line(
 }
 
 pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
+    let raw_point = point.to_point(map);
+    let language = map.buffer_snapshot.language_at(raw_point);
     let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
     let text = &map.buffer_snapshot;
-    let next_char_kind = text.chars_at(ix).next().map(char_kind);
-    let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
+    let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c));
+    let prev_char_kind = text
+        .reversed_chars_at(ix)
+        .next()
+        .map(|c| char_kind(language, c));
     prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
 }
 

crates/editor/src/multi_buffer.rs 🔗

@@ -1394,10 +1394,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
@@ -1408,7 +1405,9 @@ impl MultiBuffer {
             return false;
         }
 
-        if char.is_alphanumeric() || char == '_' {
+        let language = self.language_at(position.clone(), cx);
+
+        if char_kind(language.as_ref(), char) == CharKind::Word {
             return true;
         }
 
@@ -1913,13 +1912,16 @@ impl MultiBufferSnapshot {
         let mut end = start;
         let mut next_chars = self.chars_at(start).peekable();
         let mut prev_chars = self.reversed_chars_at(start).peekable();
+
+        let language = self.language_at(start);
+        let kind = |c| char_kind(language, c);
         let word_kind = cmp::max(
-            prev_chars.peek().copied().map(char_kind),
-            next_chars.peek().copied().map(char_kind),
+            prev_chars.peek().copied().map(kind),
+            next_chars.peek().copied().map(kind),
         );
 
         for ch in prev_chars {
-            if Some(char_kind(ch)) == word_kind && ch != '\n' {
+            if Some(kind(ch)) == word_kind && ch != '\n' {
                 start -= ch.len_utf8();
             } else {
                 break;
@@ -1927,7 +1929,7 @@ impl MultiBufferSnapshot {
         }
 
         for ch in next_chars {
-            if Some(char_kind(ch)) == word_kind && ch != '\n' {
+            if Some(kind(ch)) == word_kind && ch != '\n' {
                 end += ch.len_utf8();
             } else {
                 break;

crates/editor/src/scroll.rs 🔗

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

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/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/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 🔗

@@ -2,7 +2,7 @@ use button_component::Button;
 
 use gpui::{
     color::Color,
-    elements::{Component, ContainerStyle, Flex, Label, ParentElement},
+    elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent},
     fonts::{self, TextStyle},
     platform::WindowOptions,
     AnyElement, App, Element, Entity, View, ViewContext,
@@ -72,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| {
@@ -84,7 +84,7 @@ impl View for TestView {
                         inactive: ButtonStyle::fill(Color::red()),
                         active: ButtonStyle::fill(Color::green()),
                     })
-                    .into_element(),
+                    .element(),
                 )
                 .expanded()
                 .contained()
@@ -114,7 +114,7 @@ mod theme {
 // Component creation:
 mod toggleable_button {
     use gpui::{
-        elements::{Component, ContainerStyle, LabelStyle},
+        elements::{ContainerStyle, LabelStyle, StatefulComponent},
         scene::MouseClick,
         EventContext, View,
     };
@@ -156,7 +156,7 @@ mod toggleable_button {
         }
     }
 
-    impl<V: View> Component<V> for ToggleableButton<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))
@@ -171,7 +171,7 @@ mod toggleable_button {
 mod button_component {
 
     use gpui::{
-        elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler},
+        elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent},
         platform::MouseButton,
         scene::MouseClick,
         AnyElement, Element, EventContext, TypeTag, View, ViewContext,
@@ -212,7 +212,7 @@ mod button_component {
         }
     }
 
-    impl<V: View> Component<V> for Button<V> {
+    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;
 

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
     }
@@ -640,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)
@@ -649,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)
@@ -658,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(
@@ -699,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| {
@@ -898,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();
@@ -1382,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,
@@ -1659,6 +1656,9 @@ impl AppContext {
                             subscription_id,
                             callback,
                         ),
+                        Effect::RepaintWindow { window } => {
+                            self.handle_repaint_window_effect(window)
+                        }
                     }
                     self.pending_notifications.clear();
                 } else {
@@ -1896,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 {
@@ -2151,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>,
@@ -2255,6 +2264,9 @@ pub enum Effect {
         window: AnyWindowHandle,
         is_active: bool,
     },
+    RepaintWindow {
+        window: AnyWindowHandle,
+    },
     WindowActivationObservation {
         window: AnyWindowHandle,
         subscription_id: usize,
@@ -2448,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(),
         }
     }
 }
@@ -2543,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
     }
@@ -2878,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 {
@@ -2886,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,
@@ -2901,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,
@@ -2913,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,
@@ -3226,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
@@ -3295,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,
         }
@@ -3341,6 +3345,10 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         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,
@@ -3350,6 +3358,17 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
     }
 }
 
+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,
@@ -3428,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>,
@@ -3500,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 {
@@ -3527,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)
     }
@@ -3543,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 {
@@ -3573,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 {
@@ -3613,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)
     }
@@ -3629,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>
@@ -3661,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 {
@@ -3687,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)
     }
@@ -3703,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 {
@@ -3764,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> {
@@ -4031,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>()),
@@ -4069,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,
@@ -4149,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,
@@ -4160,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>()
     }
 
@@ -4238,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> {
@@ -4251,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)
     }
 
@@ -4275,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);
@@ -4293,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);
 
@@ -4429,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,
@@ -4440,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
@@ -4620,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>()))
@@ -4640,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 {
@@ -4680,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 {
@@ -4709,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(),
@@ -5263,6 +5350,7 @@ mod tests {
                     button: MouseButton::Left,
                     modifiers: Default::default(),
                     click_count: 1,
+                    is_down: true,
                 }),
                 false,
             );

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 🔗

@@ -34,7 +34,7 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
+    json, Action, Entity, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View,
     ViewContext, WeakViewHandle, WindowContext,
 };
 use anyhow::{anyhow, Result};
@@ -42,14 +42,19 @@ 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 {
-        V::ui_name()
+        type_name::<V>()
     }
 
     fn layout(
@@ -229,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,
@@ -249,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(
@@ -266,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,
@@ -287,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,
@@ -330,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 {
@@ -469,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()
     }
@@ -506,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);
     }
@@ -548,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 = ();
 
@@ -606,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 }
     }
@@ -679,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(())
         })
     }
@@ -719,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()));
     }
@@ -739,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 🔗

@@ -1,74 +1,281 @@
+use std::{any::Any, marker::PhantomData};
+
 use pathfinder_geometry::{rect::RectF, vector::Vector2F};
 
 use crate::{
-    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
 };
 
 use super::Empty;
 
-pub trait GeneralComponent {
-    fn render<V: View>(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
+/// 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;
 }
 
-pub trait StyleableComponent {
+/// 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: GeneralComponent;
+    type Output: Component;
 
     fn with_style(self, style: Self::Style) -> Self::Output;
 }
 
-impl GeneralComponent for () {
-    fn render<V: View>(self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
-        Empty::new().into_any()
+/// 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)
     }
 }
 
-impl StyleableComponent for () {
+/// 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 = ();
+
+    type Output = C;
 
     fn with_style(self, _: Self::Style) -> Self::Output {
-        ()
+        self.component
     }
 }
 
-pub trait Component<V: View> {
+/// 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 into_element(self) -> ComponentAdapter<V, Self>
+    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<V: View, C: GeneralComponent> Component<V> for C {
-    fn render(self, v: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V> {
-        self.render(v, cx)
+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
     }
 }
 
-pub struct ComponentAdapter<V: View, E> {
+/// 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>>,
-    #[cfg(debug_assertions)]
-    _component_name: &'static str,
+    phantom: PhantomData<V>,
 }
 
-impl<E, V: View> ComponentAdapter<V, E> {
+impl<E, V: 'static> ComponentAdapter<V, E> {
     pub fn new(e: E) -> Self {
         Self {
             component: Some(e),
             element: None,
-            #[cfg(debug_assertions)]
-            _component_name: std::any::type_name::<E>(),
+            phantom: PhantomData,
         }
     }
 }
 
-impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
+impl<V: 'static, C: StatefulComponent<V> + 'static> Element<V> for ComponentAdapter<V, C> {
     type LayoutState = ();
 
     type PaintState = ();
@@ -80,8 +287,12 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
         cx: &mut LayoutContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         if self.element.is_none() {
-            let component = self.component.take().unwrap();
-            self.element = Some(component.render(view, cx.view_context()));
+            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, ())
@@ -98,7 +309,7 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
     ) -> Self::PaintState {
         self.element
             .as_mut()
-            .unwrap()
+            .expect("Layout should always be called before paint")
             .paint(scene, bounds.origin(), visible_bounds, view, cx)
     }
 
@@ -114,8 +325,7 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
     ) -> Option<RectF> {
         self.element
             .as_ref()
-            .unwrap()
-            .rect_for_text_range(range_utf16, view, cx)
+            .and_then(|el| el.rect_for_text_range(range_utf16, view, cx))
     }
 
     fn debug(
@@ -126,16 +336,10 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
         view: &V,
         cx: &ViewContext<V>,
     ) -> serde_json::Value {
-        #[cfg(debug_assertions)]
-        let component_name = self._component_name;
-
-        #[cfg(not(debug_assertions))]
-        let component_name = "Unknown";
-
         serde_json::json!({
             "type": "ComponentAdapter",
-            "child": self.element.as_ref().unwrap().debug(view, cx),
-            "component_name": component_name
+            "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,8 @@ impl<V: View> Flex<V> {
         cx: &mut LayoutContext<V>,
     ) {
         let cross_axis = self.axis.invert();
-        for child in &mut self.children {
+        let last = self.children.len() - 1;
+        for (ix, child) in &mut self.children.iter_mut().enumerate() {
             if let Some(metadata) = child.metadata::<FlexParentData>() {
                 if let Some((flex, expanded)) = metadata.flex {
                     if expanded != layout_expanded {
@@ -93,6 +101,10 @@ impl<V: View> Flex<V> {
                     } else {
                         let space_per_flex = *remaining_space / *remaining_flex;
                         space_per_flex * flex
+                    } - if ix == 0 || ix == last {
+                        self.spacing / 2.
+                    } else {
+                        self.spacing
                     };
                     let child_min = if expanded { child_max } else { 0. };
                     let child_constraint = match self.axis {
@@ -115,13 +127,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 = ();
 
@@ -137,7 +149,8 @@ impl<V: View> Element<V> for Flex<V> {
 
         let cross_axis = self.axis.invert();
         let mut cross_axis_max: f32 = 0.0;
-        for child in &mut self.children {
+        let last = self.children.len().saturating_sub(1);
+        for (ix, child) in &mut self.children.iter_mut().enumerate() {
             let metadata = child.metadata::<FlexParentData>();
             contains_float |= metadata.map_or(false, |metadata| metadata.float);
 
@@ -155,7 +168,12 @@ impl<V: View> Element<V> for Flex<V> {
                     ),
                 };
                 let size = child.layout(child_constraint, view, cx);
-                fixed_space += size.along(self.axis);
+                fixed_space += size.along(self.axis)
+                    + if ix == 0 || ix == last {
+                        self.spacing / 2.
+                    } else {
+                        self.spacing
+                    };
                 cross_axis_max = cross_axis_max.max(size.along(cross_axis));
             }
         }
@@ -315,7 +333,8 @@ impl<V: View> Element<V> for Flex<V> {
             }
         }
 
-        for child in &mut self.children {
+        let last = self.children.len().saturating_sub(1);
+        for (ix, child) in &mut self.children.iter_mut().enumerate() {
             if remaining_space > 0. {
                 if let Some(metadata) = child.metadata::<FlexParentData>() {
                     if metadata.float {
@@ -353,9 +372,11 @@ impl<V: View> Element<V> for Flex<V> {
 
             child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
 
+            let spacing = if ix == last { 0. } else { self.spacing };
+
             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() + spacing, 0.0),
+                Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing),
             }
         }
 
@@ -401,12 +422,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 +449,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 🔗

@@ -59,7 +59,7 @@ where
         .and_then(|map| map.0.get(&tag))
 }
 
-pub struct Resizable<V: View> {
+pub struct Resizable<V: 'static> {
     child: AnyElement<V>,
     tag: TypeTag,
     handle_side: HandleSide,
@@ -69,7 +69,7 @@ pub struct Resizable<V: View> {
 
 const DEFAULT_HANDLE_SIZE: f32 = 4.0;
 
-impl<V: View> Resizable<V> {
+impl<V: 'static> Resizable<V> {
     pub fn new<Tag: 'static>(
         child: AnyElement<V>,
         handle_side: HandleSide,
@@ -97,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 = ();
 
@@ -219,12 +219,12 @@ impl<V: View> Element<V> for Resizable<V> {
 #[derive(Debug, Default)]
 struct ProviderMap(HashMap<TypeTag, (RectF, RectF)>);
 
-pub struct BoundsProvider<V: View, P> {
+pub struct BoundsProvider<V: 'static, P> {
     child: AnyElement<V>,
     phantom: std::marker::PhantomData<P>,
 }
 
-impl<V: View, P: 'static> BoundsProvider<V, P> {
+impl<V: 'static, P: 'static> BoundsProvider<V, P> {
     pub fn new(child: AnyElement<V>) -> Self {
         Self {
             child,

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, TypeTag, 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>>,
@@ -181,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/platform.rs 🔗

@@ -192,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/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 🔗

@@ -1,14 +1,14 @@
-use gpui::{elements::RenderElement, View, ViewContext};
-use gpui_macros::Element;
+use gpui::{elements::Empty, Element, 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!()
+    impl TestElement {
+        fn render<V: 'static>(&mut self, _: &mut V, _: &mut ViewContext<V>) -> impl Element<V> {
+            Empty::new()
         }
     }
 }

crates/gpui_macros/Cargo.toml 🔗

@@ -10,6 +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"

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/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 🔗

@@ -359,6 +359,14 @@ impl Buffer {
         )
     }
 
+    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,
+        )
+    }
+
     pub fn from_proto(
         replica_id: ReplicaId,
         message: proto::BufferState,
@@ -2165,27 +2173,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>) {
@@ -2193,13 +2220,16 @@ impl BufferSnapshot {
         let mut end = start;
         let mut next_chars = self.chars_at(start).peekable();
         let mut prev_chars = self.reversed_chars_at(start).peekable();
+
+        let language = self.language_at(start);
+        let kind = |c| char_kind(language, c);
         let word_kind = cmp::max(
-            prev_chars.peek().copied().map(char_kind),
-            next_chars.peek().copied().map(char_kind),
+            prev_chars.peek().copied().map(kind),
+            next_chars.peek().copied().map(kind),
         );
 
         for ch in prev_chars {
-            if Some(char_kind(ch)) == word_kind && ch != '\n' {
+            if Some(kind(ch)) == word_kind && ch != '\n' {
                 start -= ch.len_utf8();
             } else {
                 break;
@@ -2207,7 +2237,7 @@ impl BufferSnapshot {
         }
 
         for ch in next_chars {
-            if Some(char_kind(ch)) == word_kind && ch != '\n' {
+            if Some(kind(ch)) == word_kind && ch != '\n' {
                 end += ch.len_utf8();
             } else {
                 break;
@@ -3004,14 +3034,18 @@ pub fn contiguous_ranges(
     })
 }
 
-pub fn char_kind(c: char) -> CharKind {
+pub fn char_kind(language: Option<&Arc<Language>>, c: char) -> CharKind {
     if c.is_whitespace() {
-        CharKind::Whitespace
+        return CharKind::Whitespace;
     } else if c.is_alphanumeric() || c == '_' {
-        CharKind::Word
-    } else {
-        CharKind::Punctuation
+        return CharKind::Word;
+    }
+    if let Some(language) = language {
+        if language.config.word_characters.contains(&c) {
+            return CharKind::Word;
+        }
     }
+    CharKind::Punctuation
 }
 
 /// Find all of the ranges of whitespace that occur at the ends of lines

crates/language/src/buffer_tests.rs 🔗

@@ -1631,7 +1631,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| {
@@ -1718,6 +1718,73 @@ 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, text.clone(), cx).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, |_| {});

crates/language/src/language.rs 🔗

@@ -11,7 +11,7 @@ mod buffer_tests;
 
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
 use futures::{
     channel::oneshot,
     future::{BoxFuture, Shared},
@@ -344,6 +344,8 @@ pub struct LanguageConfig {
     pub block_comment: Option<(Arc<str>, Arc<str>)>,
     #[serde(default)]
     pub overrides: HashMap<String, LanguageConfigOverride>,
+    #[serde(default)]
+    pub word_characters: HashSet<char>,
 }
 
 #[derive(Debug, Default)]
@@ -411,6 +413,7 @@ impl Default for LanguageConfig {
             block_comment: Default::default(),
             overrides: Default::default(),
             collapsed_placeholder: Default::default(),
+            word_characters: Default::default(),
         }
     }
 }

crates/language/src/proto.rs 🔗

@@ -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,
 }
@@ -1279,7 +1279,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 +1699,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_tools/src/lsp_log.rs 🔗

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

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/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;
@@ -250,6 +250,7 @@ enum ProjectClientState {
 pub struct Collaborator {
     pub peer_id: proto::PeerId,
     pub replica_id: ReplicaId,
+    pub user_id: UserId,
 }
 
 #[derive(Clone, Debug, PartialEq)]
@@ -281,6 +282,7 @@ pub enum Event {
         old_peer_id: proto::PeerId,
         new_peer_id: proto::PeerId,
     },
+    CollaboratorJoined(proto::PeerId),
     CollaboratorLeft(proto::PeerId),
     RefreshInlayHints,
 }
@@ -4454,10 +4456,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| {
@@ -5170,7 +5182,7 @@ impl Project {
                                         snapshot.file().map(|file| file.path().as_ref()),
                                     ) {
                                         query
-                                            .search(snapshot.as_rope())
+                                            .search(&snapshot, None)
                                             .await
                                             .iter()
                                             .map(|range| {
@@ -5920,6 +5932,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();
@@ -7746,6 +7759,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/search.rs 🔗

@@ -3,7 +3,7 @@ use anyhow::{Context, Result};
 use client::proto;
 use globset::{Glob, GlobMatcher};
 use itertools::Itertools;
-use language::{char_kind, Rope};
+use language::{char_kind, BufferSnapshot};
 use regex::{Regex, RegexBuilder};
 use smol::future::yield_now;
 use std::{
@@ -39,6 +39,7 @@ pub enum SearchQuery {
         case_sensitive: bool,
         inner: SearchInputs,
     },
+
     Regex {
         regex: Regex,
 
@@ -214,12 +215,24 @@ impl SearchQuery {
         }
     }
 
-    pub async fn search(&self, rope: &Rope) -> Vec<Range<usize>> {
+    pub async fn search(
+        &self,
+        buffer: &BufferSnapshot,
+        subrange: Option<Range<usize>>,
+    ) -> Vec<Range<usize>> {
         const YIELD_INTERVAL: usize = 20000;
 
         if self.as_str().is_empty() {
             return Default::default();
         }
+        let language = buffer.language_at(0);
+        let rope = if let Some(range) = subrange {
+            buffer.as_rope().slice(range)
+        } else {
+            buffer.as_rope().clone()
+        };
+
+        let kind = |c| char_kind(language, c);
 
         let mut matches = Vec::new();
         match self {
@@ -236,10 +249,10 @@ impl SearchQuery {
 
                     let mat = mat.unwrap();
                     if *whole_word {
-                        let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind);
-                        let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap());
-                        let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap());
-                        let next_kind = rope.chars_at(mat.end()).next().map(char_kind);
+                        let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind);
+                        let start_kind = kind(rope.chars_at(mat.start()).next().unwrap());
+                        let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap());
+                        let next_kind = rope.chars_at(mat.end()).next().map(kind);
                         if Some(start_kind) == prev_kind || Some(end_kind) == next_kind {
                             continue;
                         }
@@ -247,6 +260,7 @@ impl SearchQuery {
                     matches.push(mat.start()..mat.end())
                 }
             }
+
             Self::Regex {
                 regex, multiline, ..
             } => {
@@ -284,6 +298,7 @@ impl SearchQuery {
                 }
             }
         }
+
         matches
     }
 

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,

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/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 🔗

@@ -142,6 +142,13 @@ message Envelope {
         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;
     }
 }
 
@@ -411,6 +418,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;
@@ -540,6 +557,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;
@@ -948,6 +970,22 @@ message RenameChannel {
     string name = 2;
 }
 
+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;
@@ -1082,6 +1120,7 @@ message View {
 
     oneof variant {
         Editor editor = 3;
+        ChannelView channel_view = 4;
     }
 
     message Editor {
@@ -1094,6 +1133,11 @@ message View {
         float scroll_x = 7;
         float scroll_y = 8;
     }
+
+    message ChannelView {
+        uint64 channel_id = 1;
+        Editor editor = 2;
+    }
 }
 
 message Collaborator {
@@ -1144,7 +1188,6 @@ enum GitStatus {
     Conflict = 2;
 }
 
-
 message BufferState {
     uint64 id = 1;
     optional File file = 2;

crates/rpc/src/proto.rs 🔗

@@ -248,7 +248,13 @@ messages!(
     (GetPrivateUserInfo, Foreground),
     (GetPrivateUserInfoResponse, Foreground),
     (GetChannelMembers, Foreground),
-    (GetChannelMembersResponse, Foreground)
+    (GetChannelMembersResponse, Foreground),
+    (JoinChannelBuffer, Foreground),
+    (JoinChannelBufferResponse, Foreground),
+    (LeaveChannelBuffer, Background),
+    (UpdateChannelBuffer, Foreground),
+    (RemoveChannelBufferCollaborator, Foreground),
+    (AddChannelBufferCollaborator, Foreground),
 );
 
 request_messages!(
@@ -315,6 +321,8 @@ request_messages!(
     (UpdateParticipantLocation, Ack),
     (UpdateProject, Ack),
     (UpdateWorktree, Ack),
+    (JoinChannelBuffer, JoinChannelBufferResponse),
+    (LeaveChannelBuffer, Ack)
 );
 
 entity_messages!(
@@ -370,6 +378,13 @@ entity_messages!(
     UpdateDiffBase
 );
 
+entity_messages!(
+    channel_id,
+    UpdateChannelBuffer,
+    RemoveChannelBufferCollaborator,
+    AddChannelBufferCollaborator
+);
+
 const KIB: usize = 1024;
 const MIB: usize = KIB * 1024;
 const MAX_BUFFER_LEN: usize = MIB;

crates/search/src/buffer_search.rs 🔗

@@ -523,6 +523,11 @@ impl BufferSearchBar {
     }
 
     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;
         }
@@ -797,7 +802,7 @@ impl BufferSearchBar {
         }
     }
     fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
-        self.activate_search_mode(next_mode(&self.current_mode), cx);
+        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;

crates/search/src/mode.rs 🔗

@@ -1,11 +1,12 @@
 use gpui::Action;
 
-use crate::{ActivateRegexMode, ActivateTextMode};
+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,
 }
 
@@ -19,6 +20,7 @@ impl SearchMode {
     pub(crate) fn label(&self) -> &'static str {
         match self {
             SearchMode::Text => "Text",
+            SearchMode::Semantic => "Semantic",
             SearchMode::Regex => "Regex",
         }
     }
@@ -26,6 +28,7 @@ impl SearchMode {
     pub(crate) fn region_id(&self) -> usize {
         match self {
             SearchMode::Text => 3,
+            SearchMode::Semantic => 4,
             SearchMode::Regex => 5,
         }
     }
@@ -33,6 +36,7 @@ impl SearchMode {
     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",
         }
     }
@@ -40,6 +44,7 @@ impl SearchMode {
     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),
         }
     }
@@ -48,6 +53,7 @@ impl SearchMode {
         match self {
             SearchMode::Regex => true,
             SearchMode::Text => true,
+            SearchMode::Semantic => true,
         }
     }
 
@@ -61,14 +67,22 @@ impl SearchMode {
     pub(crate) fn button_side(&self) -> Option<Side> {
         match self {
             SearchMode::Text => Some(Side::Left),
+            SearchMode::Semantic => None,
             SearchMode::Regex => Some(Side::Right),
         }
     }
 }
 
-pub(crate) fn next_mode(mode: &SearchMode) -> SearchMode {
+pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {
+    let next_text_state = if semantic_enabled {
+        SearchMode::Semantic
+    } else {
+        SearchMode::Regex
+    };
+
     match mode {
-        SearchMode::Text => SearchMode::Regex,
+        SearchMode::Text => next_text_state,
+        SearchMode::Semantic => SearchMode::Regex,
         SearchMode::Regex => SearchMode::Text,
     }
 }

crates/search/src/project_search.rs 🔗

@@ -2,10 +2,10 @@ use crate::{
     history::SearchHistory,
     mode::SearchMode,
     search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
-    CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch,
-    SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
+    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,
@@ -13,6 +13,8 @@ use editor::{
 };
 use futures::StreamExt;
 
+use gpui::platform::PromptLevel;
+
 use gpui::{
     actions, elements::*, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext,
     Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
@@ -20,10 +22,12 @@ use gpui::{
 };
 
 use menu::Confirm;
+use postage::stream::Stream;
 use project::{
-    search::{PathMatcher, SearchQuery},
+    search::{PathMatcher, SearchInputs, SearchQuery},
     Entry, Project,
 };
+use semantic_index::SemanticIndex;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -60,7 +64,7 @@ pub fn init(cx: &mut AppContext) {
     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.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);
@@ -114,6 +118,8 @@ pub struct ProjectSearchView {
     model: ModelHandle<ProjectSearch>,
     query_editor: ViewHandle<Editor>,
     results_editor: ViewHandle<Editor>,
+    semantic_state: Option<SemanticSearchState>,
+    semantic_permissioned: Option<bool>,
     search_options: SearchOptions,
     panels_with_errors: HashSet<InputPanel>,
     active_match_index: Option<usize>,
@@ -125,6 +131,12 @@ pub struct ProjectSearchView {
     current_mode: SearchMode,
 }
 
+struct SemanticSearchState {
+    file_count: usize,
+    outstanding_file_count: usize,
+    _progress_task: Task<()>,
+}
+
 pub struct ProjectSearchBar {
     active_project_search: Option<ViewHandle<ProjectSearchView>>,
     subscription: Option<Subscription>,
@@ -206,6 +218,60 @@ impl ProjectSearch {
         }));
         cx.notify();
     }
+
+    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(),
+                    inputs.as_str().to_owned(),
+                    10,
+                    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(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 (_task, mut match_ranges) = 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);
+                    }
+                    this.no_results = Some(false);
+                    cx.notify();
+                });
+            }
+
+            this.update(&mut cx, |this, cx| {
+                this.pending_search.take();
+                cx.notify();
+            });
+
+            None
+        }));
+        cx.notify();
+    }
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -245,10 +311,27 @@ impl View for ProjectSearchView {
             } 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 {
+                    format!(
+                        "Indexing: {} of {}...",
+                        semantic.file_count - semantic.outstanding_file_count,
+                        semantic.file_count
+                    )
+                } else {
+                    "Indexing complete".to_string()
+                }
+            } else {
+                "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()]
@@ -256,11 +339,19 @@ impl View for ProjectSearchView {
                     vec![]
                 }
             } else {
-                vec![
-                    "".to_owned(),
-                    "Include/exclude specific paths with the filter option.".to_owned(),
-                    "Matching exact word and/or casing is available too.".to_owned(),
-                ]
+                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 =
@@ -391,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,
@@ -408,10 +499,14 @@ impl Item for ProjectSearchView {
                     .with_margin_right(tab_theme.spacing),
             )
             .with_child({
-                let tab_name: Option<Cow<_>> =
-                    self.model.read(cx).active_query.as_ref().map(|query| {
-                        let query_text =
-                            util::truncate_and_trailoff(query.as_str(), MAX_TAB_TITLE_LEN);
+                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(
@@ -539,6 +634,50 @@ impl ProjectSearchView {
         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;
@@ -561,7 +700,61 @@ impl ProjectSearchView {
         self.current_mode = mode;
         self.active_match_index = None;
 
-        self.search(cx);
+        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();
     }
@@ -657,6 +850,8 @@ impl ProjectSearchView {
             model,
             query_editor,
             results_editor,
+            semantic_state: None,
+            semantic_permissioned: None,
             search_options: options,
             panels_with_errors: HashSet::new(),
             active_match_index: None,
@@ -670,6 +865,18 @@ impl ProjectSearchView {
         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,
@@ -687,6 +894,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)
         });
     }
@@ -745,8 +953,26 @@ impl ProjectSearchView {
     }
 
     fn search(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(query) = self.build_search_query(cx) {
-            self.model.update(cx, |model, cx| model.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));
+                }
+            }
         }
     }
 
@@ -946,7 +1172,8 @@ impl ProjectSearchBar {
             .and_then(|item| item.downcast::<ProjectSearchView>())
         {
             search_view.update(cx, |this, cx| {
-                let new_mode = crate::mode::next_mode(&this.current_mode);
+                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);
             })
@@ -1071,18 +1298,18 @@ impl ProjectSearchBar {
         }
     }
 
-    // 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 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() {
@@ -1195,7 +1422,8 @@ impl View for ProjectSearchBar {
                 },
                 cx,
             );
-
+            let search = _search.read(cx);
+            let is_semantic_disabled = search.semantic_state.is_none();
             let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| {
                 crate::search_bar::render_option_button_icon(
                     self.is_option_enabled(option, cx),
@@ -1209,17 +1437,17 @@ impl View for ProjectSearchBar {
                     cx,
                 )
             };
-            let case_sensitive = render_option_button_icon(
-                "icons/case_insensitive_12.svg",
-                SearchOptions::CASE_SENSITIVE,
-                cx,
-            );
+            let case_sensitive = is_semantic_disabled.then(|| {
+                render_option_button_icon(
+                    "icons/case_insensitive_12.svg",
+                    SearchOptions::CASE_SENSITIVE,
+                    cx,
+                )
+            });
 
-            let whole_word = render_option_button_icon(
-                "icons/word_search_12.svg",
-                SearchOptions::WHOLE_WORD,
-                cx,
-            );
+            let whole_word = is_semantic_disabled.then(|| {
+                render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx)
+            });
 
             let search = _search.read(cx);
             let icon_style = theme.search.editor_icon.clone();
@@ -1235,8 +1463,8 @@ impl View for ProjectSearchBar {
                 .with_child(
                     Flex::row()
                         .with_child(filter_button)
-                        .with_child(case_sensitive)
-                        .with_child(whole_word)
+                        .with_children(case_sensitive)
+                        .with_children(whole_word)
                         .flex(1., false)
                         .constrained()
                         .contained(),
@@ -1335,7 +1563,8 @@ impl View for ProjectSearchBar {
                 )
             };
             let is_active = search.active_match_index.is_some();
-
+            let semantic_index = SemanticIndex::enabled(cx)
+                .then(|| search_button_for_mode(SearchMode::Semantic, cx));
             let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
                 render_nav_button(
                     label,
@@ -1361,6 +1590,7 @@ impl View for ProjectSearchBar {
                 .with_child(
                     Flex::row()
                         .with_child(search_button_for_mode(SearchMode::Text, cx))
+                        .with_children(semantic_index)
                         .with_child(search_button_for_mode(SearchMode::Regex, cx))
                         .contained()
                         .with_style(theme.search.modes_container),
@@ -1406,6 +1636,12 @@ impl ToolbarItemView for ProjectSearchBar {
         self.subscription = None;
         self.active_project_search = None;
         if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
+            search.update(cx, |search, cx| {
+                if search.current_mode == SearchMode::Semantic {
+                    search.index_project(cx);
+                }
+            });
+
             self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
             self.active_project_search = Some(search);
             ToolbarItemLocation::PrimaryLeft {

crates/search/src/search.rs 🔗

@@ -2,15 +2,13 @@ use bitflags::bitflags;
 pub use buffer_search::BufferSearchBar;
 use gpui::{
     actions,
-    elements::{Component, StyleableComponent, TooltipStyle},
+    elements::{Component, SafeStylable, TooltipStyle},
     Action, AnyElement, AppContext, Element, View,
 };
 pub use mode::SearchMode;
 use project::search::SearchQuery;
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
-use theme::components::{
-    action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle,
-};
+use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle};
 
 pub mod buffer_search;
 mod history;
@@ -35,6 +33,7 @@ actions!(
         NextHistoryQuery,
         PreviousHistoryQuery,
         ActivateTextMode,
+        ActivateSemanticMode,
         ActivateRegexMode
     ]
 );
@@ -90,15 +89,12 @@ impl SearchOptions {
         tooltip_style: TooltipStyle,
         button_style: ToggleIconButtonStyle,
     ) -> AnyElement<V> {
-        ActionButton::new_dynamic(
-            self.to_toggle_action(),
-            format!("Toggle {}", self.label()),
-            tooltip_style,
-        )
-        .with_contents(Svg::new(self.icon()))
-        .toggleable(active)
-        .with_style(button_style)
-        .into_element()
-        .into_any()
+        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/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(
@@ -54,6 +55,22 @@ pub fn init(
         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,
@@ -92,15 +109,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()
@@ -126,6 +223,7 @@ impl ProjectState {
     }
 }
 
+#[derive(Clone)]
 pub struct PendingFile {
     worktree_db_id: i64,
     relative_path: PathBuf,
@@ -134,6 +232,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>,
@@ -380,6 +491,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();
+            }
         }
     }
 
@@ -389,6 +514,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,
@@ -397,9 +523,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,
         };
@@ -516,12 +676,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)
@@ -532,6 +792,7 @@ impl SemanticIndex {
                 }
             })
             .collect::<Vec<_>>();
+
         let worktree_db_ids = project
             .read(cx)
             .worktrees(cx)
@@ -540,15 +801,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)
@@ -558,6 +825,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);
@@ -568,34 +836,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
@@ -612,41 +880,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))
         })
     }
 
@@ -690,6 +1001,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
@@ -699,6 +1011,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)?;
 
@@ -773,6 +1090,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)
@@ -794,9 +1116,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/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/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_view/src/terminal_element.rs 🔗

@@ -567,6 +567,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_view.rs 🔗

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

crates/text/src/text.rs 🔗

@@ -12,7 +12,7 @@ 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;

crates/theme/src/components.rs 🔗

@@ -1,23 +1,143 @@
-use gpui::elements::StyleableComponent;
+use gpui::{elements::SafeStylable, Action};
 
 use crate::{Interactive, Toggleable};
 
-use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle};
+use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle};
 
-pub type ToggleIconButtonStyle = Toggleable<Interactive<ButtonStyle<SvgStyle>>>;
+pub type IconButtonStyle = Interactive<ButtonStyle<SvgStyle>>;
+pub type ToggleIconButtonStyle = Toggleable<IconButtonStyle>;
 
-pub trait ComponentExt<C: StyleableComponent> {
+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: StyleableComponent> ComponentExt<C> for 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::{GeneralComponent, StyleableComponent};
+    use gpui::elements::{Component, SafeStylable};
 
     use crate::Toggleable;
 
@@ -27,7 +147,7 @@ pub mod toggle {
         component: C,
     }
 
-    impl<C: StyleableComponent> Toggle<C, ()> {
+    impl<C: SafeStylable> Toggle<C, ()> {
         pub fn new(component: C, active: bool) -> Self {
             Toggle {
                 active,
@@ -37,7 +157,7 @@ pub mod toggle {
         }
     }
 
-    impl<C: StyleableComponent> StyleableComponent for Toggle<C, ()> {
+    impl<C: SafeStylable> SafeStylable for Toggle<C, ()> {
         type Style = Toggleable<C::Style>;
 
         type Output = Toggle<C, Self::Style>;
@@ -51,15 +171,11 @@ pub mod toggle {
         }
     }
 
-    impl<C: StyleableComponent> GeneralComponent for Toggle<C, Toggleable<C::Style>> {
-        fn render<V: gpui::View>(
-            self,
-            v: &mut V,
-            cx: &mut gpui::ViewContext<V>,
-        ) -> gpui::AnyElement<V> {
+    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(v, cx)
+                .render(cx)
         }
     }
 }
@@ -68,96 +184,103 @@ pub mod action_button {
     use std::borrow::Cow;
 
     use gpui::{
-        elements::{
-            ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle,
-        },
+        elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle},
         platform::{CursorStyle, MouseButton},
-        Action, Element, TypeTag, View,
+        Action, Element, TypeTag,
     };
     use schemars::JsonSchema;
     use serde_derive::Deserialize;
 
     use crate::Interactive;
 
-    pub struct ActionButton<C, S> {
-        action: Box<dyn Action>,
-        tooltip: Cow<'static, str>,
-        tooltip_style: TooltipStyle,
-        tag: TypeTag,
-        contents: C,
-        style: Interactive<S>,
-    }
-
     #[derive(Clone, Deserialize, Default, JsonSchema)]
     pub struct ButtonStyle<C> {
         #[serde(flatten)]
-        container: ContainerStyle,
-        button_width: Option<f32>,
-        button_height: Option<f32>,
+        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,
     }
 
-    impl ActionButton<(), ()> {
-        pub fn new_dynamic(
-            action: Box<dyn Action>,
-            tooltip: impl Into<Cow<'static, str>>,
-            tooltip_style: TooltipStyle,
-        ) -> Self {
+    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(),
-                style: Interactive::new_blank(),
-                tooltip: tooltip.into(),
-                tooltip_style,
                 action,
+                style: Interactive::new_blank(),
+                tooltip: None,
+                id: 0,
             }
         }
 
-        pub fn new<A: Action + Clone>(
-            action: A,
+        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::new_dynamic(Box::new(action), tooltip, tooltip_style)
+            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: StyleableComponent>(self, contents: C) -> ActionButton<C, ()> {
-            ActionButton {
+        pub fn with_contents<C: SafeStylable>(self, contents: C) -> Button<C, ()> {
+            Button {
                 action: self.action,
                 tag: self.tag,
                 style: self.style,
                 tooltip: self.tooltip,
-                tooltip_style: self.tooltip_style,
+                id: self.id,
                 contents,
             }
         }
     }
 
-    impl<C: StyleableComponent> StyleableComponent for ActionButton<C, ()> {
+    impl<C: SafeStylable> SafeStylable for Button<C, ()> {
         type Style = Interactive<ButtonStyle<C::Style>>;
-        type Output = ActionButton<C, ButtonStyle<C::Style>>;
+        type Output = Button<C, ButtonStyle<C::Style>>;
 
         fn with_style(self, style: Self::Style) -> Self::Output {
-            ActionButton {
+            Button {
                 action: self.action,
                 tag: self.tag,
                 contents: self.contents,
                 tooltip: self.tooltip,
-                tooltip_style: self.tooltip_style,
+                id: self.id,
                 style,
             }
         }
     }
 
-    impl<C: StyleableComponent> GeneralComponent for ActionButton<C, ButtonStyle<C::Style>> {
-        fn render<V: View>(self, v: &mut V, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
-            MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| {
+    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(v, cx)
+                    .render(cx)
                     .contained()
                     .with_style(style.container)
                     .constrained();
@@ -179,21 +302,21 @@ pub mod action_button {
                     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);
+                        window.dispatch_action(view, action.as_ref(), &mut cx)
                     })
                     .detach();
                 }
             })
             .with_cursor_style(CursorStyle::PointingHand)
-            .with_dynamic_tooltip(
-                self.tag,
-                0,
-                self.tooltip,
-                Some(self.action),
-                self.tooltip_style,
-                cx,
-            )
-            .into_any()
+            .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
         }
     }
 }
@@ -202,7 +325,7 @@ pub mod svg {
     use std::borrow::Cow;
 
     use gpui::{
-        elements::{GeneralComponent, StyleableComponent},
+        elements::{Component, Empty, SafeStylable},
         Element,
     };
     use schemars::JsonSchema;
@@ -225,6 +348,7 @@ pub mod svg {
             pub enum IconSize {
                 IconSize { icon_size: f32 },
                 Dimensions { width: f32, height: f32 },
+                IconDimensions { icon_width: f32, icon_height: f32 },
             }
 
             #[derive(Deserialize)]
@@ -248,6 +372,14 @@ pub mod svg {
                     icon_height: height,
                     color,
                 },
+                IconSize::IconDimensions {
+                    icon_width,
+                    icon_height,
+                } => SvgStyle {
+                    icon_width,
+                    icon_height,
+                    color,
+                },
             };
 
             Ok(result)
@@ -255,20 +387,27 @@ pub mod svg {
     }
 
     pub struct Svg<S> {
-        path: Cow<'static, str>,
+        path: Option<Cow<'static, str>>,
         style: S,
     }
 
     impl Svg<()> {
         pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
             Self {
-                path: path.into(),
+                path: Some(path.into()),
+                style: (),
+            }
+        }
+
+        pub fn optional(path: Option<impl Into<Cow<'static, str>>>) -> Self {
+            Self {
+                path: path.map(Into::into),
                 style: (),
             }
         }
     }
 
-    impl StyleableComponent for Svg<()> {
+    impl SafeStylable for Svg<()> {
         type Style = SvgStyle;
 
         type Output = Svg<SvgStyle>;
@@ -281,18 +420,19 @@ pub mod svg {
         }
     }
 
-    impl GeneralComponent for Svg<SvgStyle> {
-        fn render<V: gpui::View>(
-            self,
-            _: &mut V,
-            _: &mut gpui::ViewContext<V>,
-        ) -> gpui::AnyElement<V> {
-            gpui::elements::Svg::new(self.path)
-                .with_color(self.style.color)
-                .constrained()
-                .with_width(self.style.icon_width)
-                .with_height(self.style.icon_height)
-                .into_any()
+    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()
         }
     }
 }
@@ -301,7 +441,8 @@ pub mod label {
     use std::borrow::Cow;
 
     use gpui::{
-        elements::{GeneralComponent, LabelStyle, StyleableComponent},
+        elements::{Component, LabelStyle, SafeStylable},
+        fonts::TextStyle,
         Element,
     };
 
@@ -319,25 +460,21 @@ pub mod label {
         }
     }
 
-    impl StyleableComponent for Label<()> {
-        type Style = LabelStyle;
+    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: style.into(),
             }
         }
     }
 
-    impl GeneralComponent for Label<LabelStyle> {
-        fn render<V: gpui::View>(
-            self,
-            _: &mut V,
-            _: &mut gpui::ViewContext<V>,
-        ) -> gpui::AnyElement<V> {
+    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 🔗

@@ -3,7 +3,7 @@ mod theme_registry;
 mod theme_settings;
 pub mod ui;
 
-use components::ToggleIconButtonStyle;
+use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle};
 use gpui::{
     color::Color,
     elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@@ -14,7 +14,7 @@ use schemars::JsonSchema;
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use settings::SettingsStore;
-use std::{collections::HashMap, sync::Arc};
+use std::{collections::HashMap, ops::Deref, sync::Arc};
 use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle};
 
 pub use theme_registry::*;
@@ -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)]
@@ -221,6 +222,7 @@ pub struct CopilotAuthAuthorized {
 pub struct CollabPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
+    pub disclosure: DisclosureStyle<()>,
     pub list_empty_state: Toggleable<Interactive<ContainedText>>,
     pub list_empty_icon: Icon,
     pub list_empty_label_container: ContainerStyle,
@@ -259,6 +261,13 @@ pub struct CollabPanel {
     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>>,
@@ -747,6 +756,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,
@@ -890,6 +900,14 @@ 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 {
@@ -907,6 +925,14 @@ pub struct Toggleable<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 {

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()
@@ -117,11 +117,11 @@ impl IconStyle {
     }
 }
 
-pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
+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,
@@ -157,7 +157,7 @@ pub fn cta_button<Tag, L, V, F>(
 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, _| {
@@ -196,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/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 🔗

@@ -147,9 +147,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 +158,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 {
@@ -439,11 +439,12 @@ pub(crate) fn next_word_start(
     ignore_punctuation: bool,
     times: usize,
 ) -> DisplayPoint {
+    let language = map.buffer_snapshot.language_at(point.to_point(map));
     for _ in 0..times {
         let mut crossed_newline = false;
         point = movement::find_boundary(map, point, |left, right| {
-            let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
-            let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+            let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+            let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
             let at_newline = right == '\n';
 
             let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
@@ -463,11 +464,12 @@ fn next_word_end(
     ignore_punctuation: bool,
     times: usize,
 ) -> DisplayPoint {
+    let language = map.buffer_snapshot.language_at(point.to_point(map));
     for _ in 0..times {
         *point.column_mut() += 1;
         point = movement::find_boundary(map, point, |left, right| {
-            let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
-            let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+            let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+            let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
 
             left_kind != right_kind && left_kind != CharKind::Whitespace
         });
@@ -493,12 +495,13 @@ fn previous_word_start(
     ignore_punctuation: bool,
     times: usize,
 ) -> DisplayPoint {
+    let language = map.buffer_snapshot.language_at(point.to_point(map));
     for _ in 0..times {
         // This works even though find_preceding_boundary is called for every character in the line containing
         // cursor because the newline is checked only once.
         point = movement::find_preceding_boundary(map, point, |left, right| {
-            let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
-            let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+            let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+            let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
 
             (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
         });
@@ -508,6 +511,7 @@ fn previous_word_start(
 
 fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
     let mut last_point = DisplayPoint::new(from.row(), 0);
+    let language = map.buffer_snapshot.language_at(from.to_point(map));
     for (ch, point) in map.chars_at(last_point) {
         if ch == '\n' {
             return from;
@@ -515,7 +519,7 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi
 
         last_point = point;
 
-        if char_kind(ch) != CharKind::Whitespace {
+        if char_kind(language, ch) != CharKind::Whitespace {
             break;
         }
     }
@@ -651,7 +655,10 @@ 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)
+    first_non_whitespace(
+        map,
+        map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left),
+    )
 }
 
 #[cfg(test)]
@@ -799,4 +806,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,12 +1,13 @@
 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,
@@ -14,13 +15,11 @@ use crate::{
     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,
@@ -89,9 +87,8 @@ pub fn init(cx: &mut AppContext) {
             delete_motion(vim, Motion::EndOfLine, times, cx);
         })
     });
-    cx.add_action(paste);
-
     scroll::init(cx);
+    paste::init(cx);
 }
 
 pub fn normal_motion(
@@ -116,8 +113,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),
@@ -250,144 +247,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 +742,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 🔗

@@ -82,16 +82,19 @@ fn expand_changed_word_selection(
     ignore_punctuation: bool,
 ) -> bool {
     if times.is_none() || times.unwrap() == 1 {
+        let language = map
+            .buffer_snapshot
+            .language_at(selection.start.to_point(map));
         let in_word = map
             .chars_at(selection.head())
             .next()
-            .map(|(c, _)| char_kind(c) != CharKind::Whitespace)
+            .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace)
             .unwrap_or_default();
 
         if in_word {
             selection.end = movement::find_boundary(map, selection.end, |left, right| {
-                let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
-                let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+                let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+                let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
 
                 left_kind != right_kind && left_kind != CharKind::Whitespace
             });

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 🔗

@@ -68,10 +68,10 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
                         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(),
                     };
                 });
             }
@@ -81,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();
 }
 
@@ -91,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.
@@ -101,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| {
@@ -32,6 +32,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 +53,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(), '<', '>')
+            }
         }
     }
 
@@ -122,17 +177,18 @@ fn in_word(
     ignore_punctuation: bool,
 ) -> Option<Range<DisplayPoint>> {
     // Use motion::right so that we consider the character under the cursor when looking for the start
+    let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
     let start = movement::find_preceding_boundary_in_line(
         map,
         right(map, relative_to, 1),
         |left, right| {
-            char_kind(left).coerce_punctuation(ignore_punctuation)
-                != char_kind(right).coerce_punctuation(ignore_punctuation)
+            char_kind(language, left).coerce_punctuation(ignore_punctuation)
+                != char_kind(language, right).coerce_punctuation(ignore_punctuation)
         },
     );
     let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
-        char_kind(left).coerce_punctuation(ignore_punctuation)
-            != char_kind(right).coerce_punctuation(ignore_punctuation)
+        char_kind(language, left).coerce_punctuation(ignore_punctuation)
+            != char_kind(language, right).coerce_punctuation(ignore_punctuation)
     });
 
     Some(start..end)
@@ -155,10 +211,11 @@ fn around_word(
     relative_to: DisplayPoint,
     ignore_punctuation: bool,
 ) -> Option<Range<DisplayPoint>> {
+    let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
     let in_word = map
         .chars_at(relative_to)
         .next()
-        .map(|(c, _)| char_kind(c) != CharKind::Whitespace)
+        .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace)
         .unwrap_or(false);
 
     if in_word {
@@ -182,20 +239,21 @@ fn around_next_word(
     relative_to: DisplayPoint,
     ignore_punctuation: bool,
 ) -> Option<Range<DisplayPoint>> {
+    let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
     // Get the start of the word
     let start = movement::find_preceding_boundary_in_line(
         map,
         right(map, relative_to, 1),
         |left, right| {
-            char_kind(left).coerce_punctuation(ignore_punctuation)
-                != char_kind(right).coerce_punctuation(ignore_punctuation)
+            char_kind(language, left).coerce_punctuation(ignore_punctuation)
+                != char_kind(language, right).coerce_punctuation(ignore_punctuation)
         },
     );
 
     let mut word_found = false;
     let end = movement::find_boundary(map, relative_to, |left, right| {
-        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
-        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+        let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+        let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
 
         let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
 

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,29 @@ 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,
+    )
+}

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

@@ -116,7 +116,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
         };
@@ -129,14 +129,23 @@ impl<'a> NeovimBackedTestContext<'a> {
 
     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 +156,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 +223,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,7 @@ pub enum NeovimData {
     Put { state: String },
     Key(String),
     Get { state: String, mode: Option<Mode> },
+    ReadRegister { name: char, value: String },
 }
 
 pub struct NeovimConnection {
@@ -135,7 +139,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 +171,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 +222,36 @@ impl NeovimConnection {
         );
     }
 
+    #[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 +263,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 +300,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 +364,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 +494,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 {
@@ -116,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,183 @@ 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) {
+                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)) =
+                        let Some((new_head, goal)) =
                         motion.move_point(map, current_head, selection.goal, times) else { return };
 
-                    selection.set_head(new_head, goal);
-
-                    // 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 mut goal = s.newest_anchor().goal;
+
+        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 (start, end) = match goal {
+            SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
+            SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
+            _ => (tail.column(), head.column()),
+        };
+        goal = SelectionGoal::ColumnRange { start, end };
+
+        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 +216,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 +245,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 +273,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 +320,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 +391,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 +400,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 +430,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 +438,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 +510,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 +518,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 +563,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 +611,270 @@ 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;
+    }
+
+    #[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_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,32 @@
+{"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"}}

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/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/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 🔗

@@ -1976,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,
@@ -1990,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/workspace.rs 🔗

@@ -12,9 +12,10 @@ mod workspace_settings;
 
 use anyhow::{anyhow, Context, Result};
 use call::ActiveCall;
+use channel::ChannelStore;
 use client::{
     proto::{self, PeerId},
-    ChannelStore, Client, TypedEnvelope, UserStore,
+    Client, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
 use drag_and_drop::DragAndDrop;
@@ -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<_>) })
                     })
@@ -2847,7 +2848,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
@@ -2873,12 +2880,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);
@@ -2906,7 +2912,7 @@ impl Workspace {
                 }
 
                 Some(())
-            })?;
+            });
         }
         Ok(())
     }

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.101.0"
+version = "0.102.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" }
@@ -93,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/javascript/config.toml 🔗

@@ -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 = ["$", "#"]
 
 [overrides.element]
 line_comment = { remove = true }

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

@@ -10,3 +10,4 @@ brackets = [
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
 collapsed_placeholder = "/* ... */"
+word_characters = ["$"]

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 = ["#", "$"]
 
 [overrides.element]
 line_comment = { remove = true }

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

@@ -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,13 +3,12 @@
 
 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,
 };
-use client::{
-    self, ChannelStore, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
-};
+use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
 use db::kvp::KEY_VALUE_STORE;
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use futures::{
@@ -159,6 +158,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);
@@ -166,6 +166,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()))

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

styles/src/component/icon_button.ts 🔗

@@ -44,10 +44,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO
     }
 
     const padding = {
-        top: size === Button.size.Small ? 0 : 2,
-        bottom: size === Button.size.Small ? 0 : 2,
-        left: size === Button.size.Small ? 0 : 4,
-        right: size === Button.size.Small ? 0 : 4,
+        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({
@@ -55,10 +55,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO
             corner_radius: 6,
             padding: padding,
             margin: m,
-            icon_width: 14,
+            icon_width: 12,
             icon_height: 14,
-            button_width: 20,
-            button_height: 16,
+            button_width: size === Button.size.Small ? 16 : 20,
+            button_height: 14,
         },
         state: {
             default: {

styles/src/style_tree/app.ts 🔗

@@ -12,7 +12,6 @@ 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_finder from "./contact_finder"
 import collab_panel from "./collab_panel"
 import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
 import incoming_call_notification from "./incoming_call_notification"
@@ -22,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 {
@@ -54,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/collab_panel.ts 🔗

@@ -14,6 +14,7 @@ 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
@@ -37,7 +38,7 @@ export default function contacts_panel(): any {
             width: 14,
         },
         name: {
-            ...text(layer, "ui_sans", { size: "sm" }),
+            ...text(layer, "sans", { size: "sm" }),
             margin: {
                 left: NAME_MARGIN,
                 right: 4,
@@ -69,7 +70,7 @@ export default function contacts_panel(): any {
     const subheader_row = toggleable({
         base: interactive({
             base: {
-                ...text(layer, "ui_sans", { size: "sm" }),
+                ...text(layer, "sans", { size: "sm" }),
                 padding: {
                     left: SPACING,
                     right: SPACING,
@@ -87,7 +88,7 @@ export default function contacts_panel(): any {
         state: {
             active: {
                 default: {
-                    ...text(theme.lowest, "ui_sans", { size: "sm" }),
+                    ...text(theme.lowest, "sans", { size: "sm" }),
                     background: background(theme.lowest),
                 },
                 clicked: {
@@ -100,8 +101,8 @@ export default function contacts_panel(): any {
     const filter_input = {
         background: background(layer, "on"),
         corner_radius: 6,
-        text: text(layer, "ui_sans", "base"),
-        placeholder_text: text(layer, "ui_sans", "base", "disabled", {
+        text: text(layer, "sans", "base"),
+        placeholder_text: text(layer, "sans", "base", "disabled", {
             size: "xs",
         }),
         selection: theme.players[0],
@@ -140,7 +141,7 @@ export default function contacts_panel(): any {
             },
             active: {
                 default: {
-                    ...text(theme.lowest, "ui_sans", { size: "sm" }),
+                    ...text(theme.lowest, "sans", { size: "sm" }),
                     background: background(theme.lowest),
                 },
                 clicked: {
@@ -152,6 +153,10 @@ export default function contacts_panel(): any {
 
     return {
         ...collab_modals(),
+        disclosure: {
+            button: icon_button({ variant: "ghost", size: "sm" }),
+            spacing: CHANNEL_SPACING,
+        },
         log_in_button: interactive({
             base: {
                 background: background(theme.middle),
@@ -194,10 +199,10 @@ export default function contacts_panel(): any {
         add_channel_button: header_icon_button,
         leave_call_button: header_icon_button,
         row_height: ITEM_HEIGHT,
-        channel_indent: INDENT_SIZE,
+        channel_indent: INDENT_SIZE * 2 + 2,
         section_icon_size: 14,
         header_row: {
-            ...text(layer, "ui_sans", { size: "sm", weight: "bold" }),
+            ...text(layer, "sans", { size: "sm", weight: "bold" }),
             margin: { top: SPACING },
             padding: {
                 left: SPACING,
@@ -251,7 +256,7 @@ export default function contacts_panel(): any {
                 },
                 active: {
                     default: {
-                        ...text(theme.lowest, "ui_sans", { size: "sm" }),
+                        ...text(theme.lowest, "sans", { size: "sm" }),
                         background: background(theme.lowest),
                     },
                     clicked: {
@@ -262,9 +267,9 @@ export default function contacts_panel(): any {
         }),
         channel_row: item_row,
         channel_name: {
-            ...text(layer, "ui_sans", { size: "sm" }),
+            ...text(layer, "sans", { size: "sm" }),
             margin: {
-                left: NAME_MARGIN,
+                left: CHANNEL_SPACING,
             },
         },
         list_empty_label_container: {
@@ -279,7 +284,7 @@ export default function contacts_panel(): any {
         list_empty_state: toggleable({
             base: interactive({
                 base: {
-                    ...text(layer, "ui_sans", "variant", { size: "sm" }),
+                    ...text(layer, "sans", "variant", { size: "sm" }),
                     padding: {
                         top: SPACING / 2,
                         bottom: SPACING / 2,
@@ -301,7 +306,7 @@ export default function contacts_panel(): any {
                 },
                 active: {
                     default: {
-                        ...text(theme.lowest, "ui_sans", { size: "sm" }),
+                        ...text(theme.lowest, "sans", { size: "sm" }),
                         background: background(theme.lowest),
                     },
                     clicked: {
@@ -325,12 +330,12 @@ export default function contacts_panel(): any {
                 right: 4,
             },
             background: background(layer, "hovered"),
-            ...text(layer, "ui_sans", "hovered", { size: "xs" })
+            ...text(layer, "sans", "hovered", { size: "xs" })
         },
         contact_status_free: indicator({ layer, color: "positive" }),
         contact_status_busy: indicator({ layer, color: "negative" }),
         contact_username: {
-            ...text(layer, "ui_sans", { size: "sm" }),
+            ...text(layer, "sans", { size: "sm" }),
             margin: {
                 left: NAME_MARGIN,
             },
@@ -347,7 +352,7 @@ export default function contacts_panel(): any {
             color: foreground(layer, "on"),
         },
         calling_indicator: {
-            ...text(layer, "mono", "variant", { size: "xs" }),
+            ...text(layer, "sans", "variant", { size: "xs" }),
         },
         tree_branch: toggleable({
             base: interactive({
@@ -380,7 +385,7 @@ export default function contacts_panel(): any {
                     },
                     name: {
                         ...project_row.name,
-                        ...text(layer, "mono", { size: "sm" }),
+                        ...text(layer, "sans", { size: "sm" }),
                     },
                 },
                 state: {

styles/src/style_tree/component_test.ts 🔗

@@ -0,0 +1,27 @@
+
+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/context_menu.ts 🔗

@@ -19,7 +19,7 @@ export default function context_menu(): any {
                     icon_width: 14,
                     padding: { left: 6, right: 6, top: 2, bottom: 2 },
                     corner_radius: 6,
-                    label: text(theme.middle, "ui_sans", { size: "sm" }),
+                    label: text(theme.middle, "sans", { size: "sm" }),
                     keystroke: {
                         ...text(theme.middle, "sans", "variant", {
                             size: "sm",

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,

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"));
+                }),
+        )
+}