Merge branch 'zed2' into zed2-workspace

Antonio Scandurra created

Change summary

.github/workflows/release_actions.yml                                               |    4 
Cargo.lock                                                                          |  153 
Cargo.toml                                                                          |    8 
Procfile                                                                            |    2 
assets/icons/bell.svg                                                               |    4 
assets/icons/link.svg                                                               |    1 
assets/icons/public.svg                                                             |    3 
assets/icons/update.svg                                                             |    4 
assets/keymaps/default.json                                                         |  198 
assets/keymaps/vim.json                                                             |  224 
assets/settings/default.json                                                        |   11 
crates/ai/Cargo.toml                                                                |    1 
crates/ai/src/ai.rs                                                                 |    2 
crates/ai/src/completion.rs                                                         |    2 
crates/ai/src/embedding.rs                                                          |   74 
crates/ai/src/models.rs                                                             |   66 
crates/ai/src/templates/base.rs                                                     |  350 
crates/ai/src/templates/file_context.rs                                             |  160 
crates/ai/src/templates/generate.rs                                                 |   95 
crates/ai/src/templates/mod.rs                                                      |    5 
crates/ai/src/templates/preamble.rs                                                 |   52 
crates/ai/src/templates/repository_context.rs                                       |   94 
crates/assistant/Cargo.toml                                                         |    8 
crates/assistant/src/assistant_panel.rs                                             |  592 
crates/assistant/src/codegen.rs                                                     |    3 
crates/assistant/src/prompts.rs                                                     |  131 
crates/call/src/call.rs                                                             |  107 
crates/call/src/room.rs                                                             |   76 
crates/call2/src/participant.rs                                                     |   10 
crates/call2/src/room.rs                                                            |  925 
crates/channel/src/channel.rs                                                       |    7 
crates/channel/src/channel_buffer.rs                                                |   28 
crates/channel/src/channel_chat.rs                                                  |  252 
crates/channel/src/channel_store.rs                                                 |  316 
crates/channel/src/channel_store/channel_index.rs                                   |  138 
crates/channel/src/channel_store_tests.rs                                           |  105 
crates/client/src/telemetry.rs                                                      |   19 
crates/client/src/user.rs                                                           |   41 
crates/client2/src/client2.rs                                                       |   14 
crates/collab/Cargo.toml                                                            |    3 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql                      |   44 
crates/collab/migrations/20231004130100_create_notifications.sql                    |   22 
crates/collab/migrations/20231011214412_add_guest_role.sql                          |    4 
crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql |    8 
crates/collab/migrations/20231018102700_create_mentions.sql                         |   11 
crates/collab/migrations/20231024085546_move_channel_paths_to_channels_table.sql    |   12 
crates/collab/src/bin/seed.rs                                                       |    1 
crates/collab/src/db.rs                                                             |  142 
crates/collab/src/db/ids.rs                                                         |  117 
crates/collab/src/db/queries.rs                                                     |    1 
crates/collab/src/db/queries/access_tokens.rs                                       |    1 
crates/collab/src/db/queries/buffers.rs                                             |   18 
crates/collab/src/db/queries/channels.rs                                            | 1666 
crates/collab/src/db/queries/contacts.rs                                            |   87 
crates/collab/src/db/queries/messages.rs                                            |  233 
crates/collab/src/db/queries/notifications.rs                                       |  262 
crates/collab/src/db/queries/rooms.rs                                               |  216 
crates/collab/src/db/tables.rs                                                      |    4 
crates/collab/src/db/tables/channel.rs                                              |   25 
crates/collab/src/db/tables/channel_member.rs                                       |    6 
crates/collab/src/db/tables/channel_message_mention.rs                              |   43 
crates/collab/src/db/tables/notification.rs                                         |   29 
crates/collab/src/db/tables/notification_kind.rs                                    |   14 
crates/collab/src/db/tests.rs                                                       |   59 
crates/collab/src/db/tests/buffer_tests.rs                                          |    9 
crates/collab/src/db/tests/channel_tests.rs                                         |  961 
crates/collab/src/db/tests/db_tests.rs                                              |   31 
crates/collab/src/db/tests/feature_flag_tests.rs                                    |    2 
crates/collab/src/db/tests/message_tests.rs                                         |  357 
crates/collab/src/lib.rs                                                            |    4 
crates/collab/src/rpc.rs                                                            |  623 
crates/collab/src/tests.rs                                                          |    5 
crates/collab/src/tests/channel_buffer_tests.rs                                     |   33 
crates/collab/src/tests/channel_message_tests.rs                                    |   80 
crates/collab/src/tests/channel_tests.rs                                            |  665 
crates/collab/src/tests/following_tests.rs                                          |    2 
crates/collab/src/tests/integration_tests.rs                                        |  131 
crates/collab/src/tests/notification_tests.rs                                       |  159 
crates/collab/src/tests/random_channel_buffer_tests.rs                              |   25 
crates/collab/src/tests/randomized_test_helpers.rs                                  |    3 
crates/collab/src/tests/test_server.rs                                              |   46 
crates/collab_ui/Cargo.toml                                                         |   11 
crates/collab_ui/src/channel_view.rs                                                |   46 
crates/collab_ui/src/chat_panel.rs                                                  |  356 
crates/collab_ui/src/chat_panel/message_editor.rs                                   |  313 
crates/collab_ui/src/collab_panel.rs                                                |  612 
crates/collab_ui/src/collab_panel/channel_modal.rs                                  |  150 
crates/collab_ui/src/collab_titlebar_item.rs                                        |   49 
crates/collab_ui/src/collab_ui.rs                                                   |   53 
crates/collab_ui/src/contact_notification.rs                                        |  121 
crates/collab_ui/src/notification_panel.rs                                          |  884 
crates/collab_ui/src/notifications.rs                                               |  113 
crates/collab_ui/src/notifications/incoming_call_notification.rs                    |    0 
crates/collab_ui/src/notifications/project_shared_notification.rs                   |    0 
crates/collab_ui/src/panel_settings.rs                                              |   21 
crates/collab_ui/src/sharing_status_indicator.rs                                    |   62 
crates/command_palette/Cargo.toml                                                   |    1 
crates/command_palette/src/command_palette.rs                                       |   17 
crates/copilot/Cargo.toml                                                           |    1 
crates/copilot/src/copilot.rs                                                       |   12 
crates/db/src/db.rs                                                                 |    4 
crates/db2/src/db2.rs                                                               |    4 
crates/diagnostics/src/items.rs                                                     |    4 
crates/editor/Cargo.toml                                                            |    3 
crates/editor/src/display_map.rs                                                    |  346 
crates/editor/src/display_map/block_map.rs                                          |    2 
crates/editor/src/display_map/fold_map.rs                                           |    2 
crates/editor/src/display_map/inlay_map.rs                                          |    6 
crates/editor/src/editor.rs                                                         |  647 
crates/editor/src/editor_settings.rs                                                |    2 
crates/editor/src/editor_tests.rs                                                   |  200 
crates/editor/src/element.rs                                                        |   76 
crates/editor/src/git.rs                                                            |  199 
crates/editor/src/hover_popover.rs                                                  |  176 
crates/editor/src/inlay_hint_cache.rs                                               |   10 
crates/editor/src/movement.rs                                                       |  372 
crates/editor/src/selections_collection.rs                                          |   25 
crates/editor/src/test.rs                                                           |   12 
crates/editor/src/test/editor_lsp_test_context.rs                                   |    4 
crates/editor/src/test/editor_test_context.rs                                       |   19 
crates/gpui/src/elements/flex.rs                                                    |   75 
crates/gpui/src/elements/list.rs                                                    |   15 
crates/gpui/src/text_layout.rs                                                      |   24 
crates/gpui2/src/action.rs                                                          |    4 
crates/gpui2/src/app.rs                                                             |  135 
crates/gpui2/src/app/async_context.rs                                               |   20 
crates/gpui2/src/app/entity_map.rs                                                  |   18 
crates/gpui2/src/app/model_context.rs                                               |   35 
crates/gpui2/src/app/test_context.rs                                                |   10 
crates/gpui2/src/element.rs                                                         |   30 
crates/gpui2/src/executor.rs                                                        |   24 
crates/gpui2/src/focusable.rs                                                       |   23 
crates/gpui2/src/gpui2.rs                                                           |   92 
crates/gpui2/src/interactive.rs                                                     |  117 
crates/gpui2/src/keymap/matcher.rs                                                  |   14 
crates/gpui2/src/platform/mac/platform.rs                                           |   11 
crates/gpui2/src/scene.rs                                                           |  104 
crates/gpui2/src/style.rs                                                           |   11 
crates/gpui2/src/subscription.rs                                                    |    6 
crates/gpui2/src/taffy.rs                                                           |    2 
crates/gpui2/src/test.rs                                                            |    4 
crates/gpui2/src/view.rs                                                            |  182 
crates/gpui2/src/window.rs                                                          |  411 
crates/gpui2_macros/src/test.rs                                                     |    8 
crates/journal2/Cargo.toml                                                          |   27 
crates/journal2/src/journal2.rs                                                     |  176 
crates/language/Cargo.toml                                                          |    1 
crates/language/src/buffer.rs                                                       |  199 
crates/language/src/language.rs                                                     |   74 
crates/language/src/markdown.rs                                                     |  301 
crates/language/src/proto.rs                                                        |    1 
crates/language2/Cargo.toml                                                         |    4 
crates/language2/src/buffer.rs                                                      |    6 
crates/language2/src/buffer_tests.rs                                                | 4838 
crates/language2/src/highlight_map.rs                                               |   60 
crates/language2/src/language2.rs                                                   |    4 
crates/language2/src/syntax_map/syntax_map_tests.rs                                 | 2646 
crates/language_tools/src/lsp_log.rs                                                |  251 
crates/live_kit_client/examples/test_app.rs                                         |    8 
crates/live_kit_client/src/live_kit_client.rs                                       |    1 
crates/live_kit_client/src/prod.rs                                                  |   15 
crates/live_kit_client/src/test.rs                                                  |   14 
crates/live_kit_server/src/api.rs                                                   |   10 
crates/live_kit_server/src/token.rs                                                 |    9 
crates/lsp/src/lsp.rs                                                               |   50 
crates/multi_buffer/Cargo.toml                                                      |   80 
crates/multi_buffer/src/anchor.rs                                                   |   10 
crates/multi_buffer/src/multi_buffer.rs                                             |  338 
crates/node_runtime/src/node_runtime.rs                                             |  105 
crates/notifications/Cargo.toml                                                     |   42 
crates/notifications/src/notification_store.rs                                      |  459 
crates/prettier/Cargo.toml                                                          |    1 
crates/prettier/src/prettier.rs                                                     |  151 
crates/prettier2/src/prettier2.rs                                                   |   92 
crates/project/src/lsp_command.rs                                                   |   38 
crates/project/src/project.rs                                                       |  224 
crates/project/src/worktree.rs                                                      |   39 
crates/project2/src/project2.rs                                                     |   58 
crates/project_panel/src/file_associations.rs                                       |   10 
crates/rich_text/src/rich_text.rs                                                   |  132 
crates/rpc/Cargo.toml                                                               |    3 
crates/rpc/proto/zed.proto                                                          |  275 
crates/rpc/src/notification.rs                                                      |  105 
crates/rpc/src/proto.rs                                                             |  186 
crates/rpc/src/rpc.rs                                                               |    5 
crates/search/src/buffer_search.rs                                                  |    1 
crates/search/src/project_search.rs                                                 |   25 
crates/semantic_index/Cargo.toml                                                    |    4 
crates/semantic_index/src/embedding_queue.rs                                        |   15 
crates/semantic_index/src/semantic_index.rs                                         |   53 
crates/semantic_index/src/semantic_index_tests.rs                                   |   14 
crates/storybook2/src/stories/focus.rs                                              |  151 
crates/storybook2/src/stories/kitchen_sink.rs                                       |   10 
crates/storybook2/src/stories/scroll.rs                                             |    6 
crates/storybook2/src/stories/text.rs                                               |    4 
crates/storybook2/src/stories/z_index.rs                                            |    7 
crates/storybook2/src/story_selector.rs                                             |  174 
crates/storybook2/src/storybook2.rs                                                 |    6 
crates/terminal_view/src/terminal_view.rs                                           |    7 
crates/text/src/selection.rs                                                        |    9 
crates/theme/src/theme.rs                                                           |   59 
crates/theme2/src/theme2.rs                                                         |   17 
crates/theme2/src/themes/one_dark.rs                                                |   46 
crates/theme2/src/themes/rose_pine.rs                                               |  141 
crates/theme2/src/themes/sandcastle.rs                                              |   45 
crates/theme_converter/src/main.rs                                                  |  189 
crates/ui2/Cargo.toml                                                               |    2 
crates/ui2/src/components/assistant_panel.rs                                        |    6 
crates/ui2/src/components/breadcrumb.rs                                             |   25 
crates/ui2/src/components/buffer.rs                                                 |   14 
crates/ui2/src/components/buffer_search.rs                                          |   10 
crates/ui2/src/components/chat_panel.rs                                             |   12 
crates/ui2/src/components/collab_panel.rs                                           |    8 
crates/ui2/src/components/command_palette.rs                                        |    8 
crates/ui2/src/components/context_menu.rs                                           |   11 
crates/ui2/src/components/copilot.rs                                                |    8 
crates/ui2/src/components/editor_pane.rs                                            |   15 
crates/ui2/src/components/facepile.rs                                               |    8 
crates/ui2/src/components/icon_button.rs                                            |   19 
crates/ui2/src/components/keybinding.rs                                             |    8 
crates/ui2/src/components/language_selector.rs                                      |    8 
crates/ui2/src/components/list.rs                                                   |   73 
crates/ui2/src/components/modal.rs                                                  |   26 
crates/ui2/src/components/multi_buffer.rs                                           |    4 
crates/ui2/src/components/notification_toast.rs                                     |    7 
crates/ui2/src/components/notifications_panel.rs                                    |   11 
crates/ui2/src/components/palette.rs                                                |   12 
crates/ui2/src/components/panel.rs                                                  |   22 
crates/ui2/src/components/panes.rs                                                  |    6 
crates/ui2/src/components/player_stack.rs                                           |    2 
crates/ui2/src/components/project_panel.rs                                          |    8 
crates/ui2/src/components/recent_projects.rs                                        |    8 
crates/ui2/src/components/status_bar.rs                                             |    1 
crates/ui2/src/components/tab.rs                                                    |    4 
crates/ui2/src/components/tab_bar.rs                                                |    8 
crates/ui2/src/components/terminal.rs                                               |    8 
crates/ui2/src/components/theme_selector.rs                                         |    8 
crates/ui2/src/components/title_bar.rs                                              |   28 
crates/ui2/src/components/toast.rs                                                  |   20 
crates/ui2/src/components/toolbar.rs                                                |   34 
crates/ui2/src/components/traffic_lights.rs                                         |    8 
crates/ui2/src/components/workspace.rs                                              |   20 
crates/ui2/src/element_ext.rs                                                       |   25 
crates/ui2/src/elements/avatar.rs                                                   |    4 
crates/ui2/src/elements/button.rs                                                   |   20 
crates/ui2/src/elements/details.rs                                                  |   12 
crates/ui2/src/elements/icon.rs                                                     |    4 
crates/ui2/src/elements/input.rs                                                    |    6 
crates/ui2/src/elements/label.rs                                                    |    4 
crates/ui2/src/elements/player.rs                                                   |    4 
crates/ui2/src/elements/stack.rs                                                    |    6 
crates/ui2/src/lib.rs                                                               |    2 
crates/ui2/src/prelude.rs                                                           |    4 
crates/ui2/src/static_data.rs                                                       |   56 
crates/ui2/src/story.rs                                                             |   14 
crates/ui2/src/theme.rs                                                             |   14 
crates/util/src/github.rs                                                           |   11 
crates/util/src/util.rs                                                             |   12 
crates/vcs_menu/Cargo.toml                                                          |    1 
crates/vcs_menu/src/lib.rs                                                          |  149 
crates/vim/src/motion.rs                                                            |  163 
crates/vim/src/normal.rs                                                            |   56 
crates/vim/src/normal/change.rs                                                     |   31 
crates/vim/src/normal/delete.rs                                                     |    9 
crates/vim/src/normal/increment.rs                                                  |   12 
crates/vim/src/normal/paste.rs                                                      |   11 
crates/vim/src/normal/substitute.rs                                                 |   18 
crates/vim/src/normal/yank.rs                                                       |    3 
crates/vim/src/object.rs                                                            |  323 
crates/vim/src/test.rs                                                              |   80 
crates/vim/src/test/neovim_backed_test_context.rs                                   |   48 
crates/vim/src/test/neovim_connection.rs                                            |   33 
crates/vim/src/vim.rs                                                               |   13 
crates/vim/src/visual.rs                                                            |  117 
crates/vim/test_data/test_G.json                                                    |    1 
crates/vim/test_data/test_change_surrounding_character_objects.json                 | 1020 
crates/vim/test_data/test_delete_next_word_end.json                                 |    8 
crates/vim/test_data/test_delete_surrounding_character_objects.json                 | 1020 
crates/vim/test_data/test_e.json                                                    |   32 
crates/vim/test_data/test_increment_steps.json                                      |    1 
crates/vim/test_data/test_j.json                                                    |    3 
crates/vim/test_data/test_multiline_surrounding_character_objects.json              |    5 
crates/vim/test_data/test_singleline_surrounding_character_objects.json             |   27 
crates/vim/test_data/test_visual_block_issue_2123.json                              |    5 
crates/vim/test_data/test_visual_paste.json                                         |   26 
crates/vim/test_data/test_wrapped_motions.json                                      |   15 
crates/workspace/src/workspace.rs                                                   |   67 
crates/zed-actions/Cargo.toml                                                       |    1 
crates/zed-actions/src/lib.rs                                                       |   41 
crates/zed/Cargo.toml                                                               |    8 
crates/zed/contents/dev/embedded.provisionprofile                                   |    0 
crates/zed/contents/preview/embedded.provisionprofile                               |    0 
crates/zed/contents/stable/embedded.provisionprofile                                |    0 
crates/zed/examples/semantic_index_eval.rs                                          |    3 
crates/zed/src/languages.rs                                                         |   33 
crates/zed/src/languages/bash/highlights.scm                                        |    1 
crates/zed/src/languages/css.rs                                                     |    6 
crates/zed/src/languages/css/config.toml                                            |    1 
crates/zed/src/languages/elixir.rs                                                  |    4 
crates/zed/src/languages/elixir/config.toml                                         |    5 
crates/zed/src/languages/erb/config.toml                                            |    1 
crates/zed/src/languages/heex/config.toml                                           |    5 
crates/zed/src/languages/heex/overrides.scm                                         |    4 
crates/zed/src/languages/html.rs                                                    |    6 
crates/zed/src/languages/html/config.toml                                           |    1 
crates/zed/src/languages/javascript/config.toml                                     |    1 
crates/zed/src/languages/json.rs                                                    |    8 
crates/zed/src/languages/json/config.toml                                           |    1 
crates/zed/src/languages/lua.rs                                                     |    4 
crates/zed/src/languages/php/config.toml                                            |    1 
crates/zed/src/languages/svelte.rs                                                  |    9 
crates/zed/src/languages/svelte/config.toml                                         |    8 
crates/zed/src/languages/svelte/overrides.scm                                       |    7 
crates/zed/src/languages/tailwind.rs                                                |   29 
crates/zed/src/languages/tsx/config.toml                                            |    1 
crates/zed/src/languages/typescript.rs                                              |   10 
crates/zed/src/languages/typescript/config.toml                                     |    1 
crates/zed/src/languages/vue.rs                                                     |  220 
crates/zed/src/languages/vue/brackets.scm                                           |    2 
crates/zed/src/languages/vue/config.toml                                            |   14 
crates/zed/src/languages/vue/highlights.scm                                         |   15 
crates/zed/src/languages/vue/injections.scm                                         |    7 
crates/zed/src/languages/yaml.rs                                                    |    7 
crates/zed/src/languages/yaml/config.toml                                           |    1 
crates/zed/src/main.rs                                                              |  246 
crates/zed/src/open_listener.rs                                                     |  211 
crates/zed/src/zed.rs                                                               |   34 
crates/zed2/Cargo.toml                                                              |    2 
crates/zed2/build.rs                                                                |   24 
crates/zed2/contents/dev/embedded.provisionprofile                                  |    0 
crates/zed2/contents/preview/embedded.provisionprofile                              |    0 
crates/zed2/contents/stable/embedded.provisionprofile                               |    0 
crates/zed2/src/main.rs                                                             |    2 
script/bundle                                                                       |   31 
script/evaluate_semantic_index                                                      |    2 
script/zed-local                                                                    |    6 
styles/src/style_tree/app.ts                                                        |    2 
styles/src/style_tree/assistant.ts                                                  |   74 
styles/src/style_tree/chat_panel.ts                                                 |   67 
styles/src/style_tree/collab_modals.ts                                              |   23 
styles/src/style_tree/collab_panel.ts                                               |   10 
styles/src/style_tree/editor.ts                                                     |   10 
styles/src/style_tree/notification_panel.ts                                         |   75 
styles/src/style_tree/search.ts                                                     |    5 
344 files changed, 22,111 insertions(+), 11,944 deletions(-)

Detailed changes

.github/workflows/release_actions.yml 🔗

@@ -20,9 +20,7 @@ jobs:
       id: get-content
       with:
         stringToTruncate: |
-          📣 Zed ${{ github.event.release.tag_name }} was just released!
-
-          Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it.
+          📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released!
 
           ${{ github.event.release.body }}
         maxLength: 2000

Cargo.lock 🔗

@@ -91,6 +91,7 @@ dependencies = [
  "futures 0.3.28",
  "gpui",
  "isahc",
+ "language",
  "lazy_static",
  "log",
  "matrixmultiply",
@@ -103,7 +104,7 @@ dependencies = [
  "rusqlite",
  "serde",
  "serde_json",
- "tiktoken-rs 0.5.4",
+ "tiktoken-rs",
  "util",
 ]
 
@@ -309,6 +310,7 @@ dependencies = [
  "language",
  "log",
  "menu",
+ "multi_buffer",
  "ordered-float 2.10.0",
  "parking_lot 0.11.2",
  "project",
@@ -316,12 +318,13 @@ dependencies = [
  "regex",
  "schemars",
  "search",
+ "semantic_index",
  "serde",
  "serde_json",
  "settings",
  "smol",
  "theme",
- "tiktoken-rs 0.4.5",
+ "tiktoken-rs",
  "util",
  "uuid 1.4.1",
  "workspace",
@@ -1573,7 +1576,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.24.0"
+version = "0.27.0"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1609,6 +1612,7 @@ dependencies = [
  "lsp",
  "nanoid",
  "node_runtime",
+ "notifications",
  "parking_lot 0.11.2",
  "pretty_assertions",
  "project",
@@ -1664,20 +1668,26 @@ dependencies = [
  "fuzzy",
  "gpui",
  "language",
+ "lazy_static",
  "log",
  "menu",
+ "notifications",
  "picker",
  "postage",
+ "pretty_assertions",
  "project",
  "recent_projects",
  "rich_text",
+ "rpc",
  "schemars",
  "serde",
  "serde_derive",
  "settings",
+ "smallvec",
  "theme",
  "theme_selector",
  "time",
+ "tree-sitter-markdown",
  "util",
  "vcs_menu",
  "workspace",
@@ -1731,6 +1741,7 @@ dependencies = [
  "theme",
  "util",
  "workspace",
+ "zed-actions",
 ]
 
 [[package]]
@@ -1810,6 +1821,7 @@ dependencies = [
  "log",
  "lsp",
  "node_runtime",
+ "parking_lot 0.11.2",
  "rpc",
  "serde",
  "serde_derive",
@@ -2556,11 +2568,11 @@ dependencies = [
  "lazy_static",
  "log",
  "lsp",
+ "multi_buffer",
  "ordered-float 2.10.0",
  "parking_lot 0.11.2",
  "postage",
  "project",
- "pulldown-cmark",
  "rand 0.8.5",
  "rich_text",
  "rpc",
@@ -4159,6 +4171,24 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "journal2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "dirs 4.0.0",
+ "editor",
+ "gpui2",
+ "log",
+ "schemars",
+ "serde",
+ "settings2",
+ "shellexpand",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "jpeg-decoder"
 version = "0.1.22"
@@ -4244,6 +4274,7 @@ dependencies = [
  "lsp",
  "parking_lot 0.11.2",
  "postage",
+ "pulldown-cmark",
  "rand 0.8.5",
  "regex",
  "rpc",
@@ -4304,7 +4335,6 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
- "settings",
  "settings2",
  "similar",
  "smallvec",
@@ -4922,6 +4952,55 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389"
 
+[[package]]
+name = "multi_buffer"
+version = "0.1.0"
+dependencies = [
+ "aho-corasick",
+ "anyhow",
+ "client",
+ "clock",
+ "collections",
+ "context_menu",
+ "convert_case 0.6.0",
+ "copilot",
+ "ctor",
+ "env_logger 0.9.3",
+ "futures 0.3.28",
+ "git",
+ "gpui",
+ "indoc",
+ "itertools 0.10.5",
+ "language",
+ "lazy_static",
+ "log",
+ "lsp",
+ "ordered-float 2.10.0",
+ "parking_lot 0.11.2",
+ "postage",
+ "project",
+ "pulldown-cmark",
+ "rand 0.8.5",
+ "rich_text",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "settings",
+ "smallvec",
+ "smol",
+ "snippet",
+ "sum_tree",
+ "text",
+ "theme",
+ "tree-sitter",
+ "tree-sitter-html",
+ "tree-sitter-rust",
+ "tree-sitter-typescript",
+ "unindent",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "multimap"
 version = "0.8.3"
@@ -5071,6 +5150,26 @@ dependencies = [
  "minimal-lexical",
 ]
 
+[[package]]
+name = "notifications"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "channel",
+ "client",
+ "clock",
+ "collections",
+ "db",
+ "feature_flags",
+ "gpui",
+ "rpc",
+ "settings",
+ "sum_tree",
+ "text",
+ "time",
+ "util",
+]
+
 [[package]]
 name = "ntapi"
 version = "0.3.7"
@@ -5887,6 +5986,7 @@ dependencies = [
  "log",
  "lsp",
  "node_runtime",
+ "parking_lot 0.11.2",
  "serde",
  "serde_derive",
  "serde_json",
@@ -6832,8 +6932,10 @@ dependencies = [
  "rsa 0.4.0",
  "serde",
  "serde_derive",
+ "serde_json",
  "smol",
  "smol-timeout",
+ "strum",
  "tempdir",
  "tracing",
  "util",
@@ -7408,7 +7510,7 @@ dependencies = [
  "smol",
  "tempdir",
  "theme",
- "tiktoken-rs 0.5.4",
+ "tiktoken-rs",
  "tree-sitter",
  "tree-sitter-cpp",
  "tree-sitter-elixir",
@@ -7422,7 +7524,6 @@ dependencies = [
  "unindent",
  "util",
  "workspace",
- "zed",
 ]
 
 [[package]]
@@ -8714,21 +8815,6 @@ dependencies = [
  "weezl",
 ]
 
-[[package]]
-name = "tiktoken-rs"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52aacc1cff93ba9d5f198c62c49c77fa0355025c729eed3326beaf7f33bc8614"
-dependencies = [
- "anyhow",
- "base64 0.21.4",
- "bstr",
- "fancy-regex",
- "lazy_static",
- "parking_lot 0.12.1",
- "rustc-hash",
-]
-
 [[package]]
 name = "tiktoken-rs"
 version = "0.5.4"
@@ -9149,8 +9235,8 @@ dependencies = [
 
 [[package]]
 name = "tree-sitter-bash"
-version = "0.19.0"
-source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=1b0321ee85701d5036c334a6f04761cdc672e64c#1b0321ee85701d5036c334a6f04761cdc672e64c"
+version = "0.20.4"
+source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=7331995b19b8f8aba2d5e26deb51d2195c18bc94#7331995b19b8f8aba2d5e26deb51d2195c18bc94"
 dependencies = [
  "cc",
  "tree-sitter",
@@ -9389,6 +9475,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-vue"
+version = "0.0.1"
+source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-yaml"
 version = "0.0.1"
@@ -9470,10 +9565,8 @@ dependencies = [
  "itertools 0.11.0",
  "rand 0.8.5",
  "serde",
- "settings",
  "smallvec",
  "strum",
- "theme",
  "theme2",
 ]
 
@@ -9715,6 +9808,7 @@ name = "vcs_menu"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "fs",
  "fuzzy",
  "gpui",
  "picker",
@@ -10696,9 +10790,10 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.109.0"
+version = "0.111.0"
 dependencies = [
  "activity_indicator",
+ "ai",
  "anyhow",
  "assistant",
  "async-compression",
@@ -10750,6 +10845,7 @@ dependencies = [
  "log",
  "lsp",
  "node_runtime",
+ "notifications",
  "num_cpus",
  "outline",
  "parking_lot 0.11.2",
@@ -10811,6 +10907,7 @@ dependencies = [
  "tree-sitter-svelte",
  "tree-sitter-toml",
  "tree-sitter-typescript",
+ "tree-sitter-vue",
  "tree-sitter-yaml",
  "unindent",
  "url",
@@ -10828,6 +10925,7 @@ name = "zed-actions"
 version = "0.1.0"
 dependencies = [
  "gpui",
+ "serde",
 ]
 
 [[package]]
@@ -10860,6 +10958,7 @@ dependencies = [
  "indexmap 1.9.3",
  "install_cli",
  "isahc",
+ "journal2",
  "language2",
  "language_tools",
  "lazy_static",

Cargo.toml 🔗

@@ -48,6 +48,7 @@ members = [
     "crates/install_cli",
     "crates/install_cli2",
     "crates/journal",
+    "crates/journal2",
     "crates/language",
     "crates/language2",
     "crates/language_selector",
@@ -58,7 +59,9 @@ members = [
     "crates/lsp2",
     "crates/media",
     "crates/menu",
+    "crates/multi_buffer",
     "crates/node_runtime",
+    "crates/notifications",
     "crates/outline",
     "crates/picker",
     "crates/plugin",
@@ -133,6 +136,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 smallvec = { version = "1.6", features = ["union"] }
 smol = { version = "1.2" }
+strum = { version = "0.25.0", features = ["derive"] }
 sysinfo = "0.29.10"
 tempdir = { version = "0.3.7" }
 thiserror = { version = "1.0.29" }
@@ -144,7 +148,7 @@ pretty_assertions = "1.3.0"
 git2 = { version = "0.15", default-features = false}
 uuid = { version = "1.1.2", features = ["v4"] }
 
-tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" }
+tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
 tree-sitter-c = "0.20.1"
 tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
 tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
@@ -170,7 +174,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
 tree-sitter-lua = "0.0.14"
 tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
 tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
-
+tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"}
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
 async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }

Procfile 🔗

@@ -1,4 +1,4 @@
 web: cd ../zed.dev && PORT=3000 npm run dev
-collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
+collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
 livekit: livekit-server --dev
 postgrest: postgrest crates/collab/admin_api.conf

assets/icons/bell.svg 🔗

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

assets/icons/link.svg 🔗

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

assets/icons/public.svg 🔗

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

assets/icons/update.svg 🔗

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

assets/keymaps/default.json 🔗

@@ -370,42 +370,15 @@
   {
     "context": "Pane",
     "bindings": {
-      "ctrl-1": [
-        "pane::ActivateItem",
-        0
-      ],
-      "ctrl-2": [
-        "pane::ActivateItem",
-        1
-      ],
-      "ctrl-3": [
-        "pane::ActivateItem",
-        2
-      ],
-      "ctrl-4": [
-        "pane::ActivateItem",
-        3
-      ],
-      "ctrl-5": [
-        "pane::ActivateItem",
-        4
-      ],
-      "ctrl-6": [
-        "pane::ActivateItem",
-        5
-      ],
-      "ctrl-7": [
-        "pane::ActivateItem",
-        6
-      ],
-      "ctrl-8": [
-        "pane::ActivateItem",
-        7
-      ],
-      "ctrl-9": [
-        "pane::ActivateItem",
-        8
-      ],
+      "ctrl-1": ["pane::ActivateItem", 0],
+      "ctrl-2": ["pane::ActivateItem", 1],
+      "ctrl-3": ["pane::ActivateItem", 2],
+      "ctrl-4": ["pane::ActivateItem", 3],
+      "ctrl-5": ["pane::ActivateItem", 4],
+      "ctrl-6": ["pane::ActivateItem", 5],
+      "ctrl-7": ["pane::ActivateItem", 6],
+      "ctrl-8": ["pane::ActivateItem", 7],
+      "ctrl-9": ["pane::ActivateItem", 8],
       "ctrl-0": "pane::ActivateLastItem",
       "ctrl--": "pane::GoBack",
       "ctrl-_": "pane::GoForward",
@@ -416,42 +389,15 @@
   {
     "context": "Workspace",
     "bindings": {
-      "cmd-1": [
-        "workspace::ActivatePane",
-        0
-      ],
-      "cmd-2": [
-        "workspace::ActivatePane",
-        1
-      ],
-      "cmd-3": [
-        "workspace::ActivatePane",
-        2
-      ],
-      "cmd-4": [
-        "workspace::ActivatePane",
-        3
-      ],
-      "cmd-5": [
-        "workspace::ActivatePane",
-        4
-      ],
-      "cmd-6": [
-        "workspace::ActivatePane",
-        5
-      ],
-      "cmd-7": [
-        "workspace::ActivatePane",
-        6
-      ],
-      "cmd-8": [
-        "workspace::ActivatePane",
-        7
-      ],
-      "cmd-9": [
-        "workspace::ActivatePane",
-        8
-      ],
+      "cmd-1": ["workspace::ActivatePane", 0],
+      "cmd-2": ["workspace::ActivatePane", 1],
+      "cmd-3": ["workspace::ActivatePane", 2],
+      "cmd-4": ["workspace::ActivatePane", 3],
+      "cmd-5": ["workspace::ActivatePane", 4],
+      "cmd-6": ["workspace::ActivatePane", 5],
+      "cmd-7": ["workspace::ActivatePane", 6],
+      "cmd-8": ["workspace::ActivatePane", 7],
+      "cmd-9": ["workspace::ActivatePane", 8],
       "cmd-b": "workspace::ToggleLeftDock",
       "cmd-r": "workspace::ToggleRightDock",
       "cmd-j": "workspace::ToggleBottomDock",
@@ -494,38 +440,14 @@
   },
   {
     "bindings": {
-      "cmd-k cmd-left": [
-        "workspace::ActivatePaneInDirection",
-        "Left"
-      ],
-      "cmd-k cmd-right": [
-        "workspace::ActivatePaneInDirection",
-        "Right"
-      ],
-      "cmd-k cmd-up": [
-        "workspace::ActivatePaneInDirection",
-        "Up"
-      ],
-      "cmd-k cmd-down": [
-        "workspace::ActivatePaneInDirection",
-        "Down"
-      ],
-      "cmd-k shift-left": [
-        "workspace::SwapPaneInDirection",
-        "Left"
-      ],
-      "cmd-k shift-right": [
-        "workspace::SwapPaneInDirection",
-        "Right"
-      ],
-      "cmd-k shift-up": [
-        "workspace::SwapPaneInDirection",
-        "Up"
-      ],
-      "cmd-k shift-down": [
-        "workspace::SwapPaneInDirection",
-        "Down"
-      ]
+      "cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"],
+      "cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"],
+      "cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"],
+      "cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"],
+      "cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
+      "cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
+      "cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
+      "cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"]
     }
   },
   // Bindings from Atom
@@ -627,14 +549,6 @@
       "space": "collab_panel::InsertSpace"
     }
   },
-  {
-    "context": "(CollabPanel && not_editing) > Editor",
-    "bindings": {
-      "cmd-c": "collab_panel::StartLinkChannel",
-      "cmd-x": "collab_panel::StartMoveChannel",
-      "cmd-v": "collab_panel::MoveOrLinkToSelected"
-    }
-  },
   {
     "context": "ChannelModal",
     "bindings": {
@@ -655,57 +569,21 @@
       "cmd-v": "terminal::Paste",
       "cmd-k": "terminal::Clear",
       // Some nice conveniences
-      "cmd-backspace": [
-        "terminal::SendText",
-        "\u0015"
-      ],
-      "cmd-right": [
-        "terminal::SendText",
-        "\u0005"
-      ],
-      "cmd-left": [
-        "terminal::SendText",
-        "\u0001"
-      ],
+      "cmd-backspace": ["terminal::SendText", "\u0015"],
+      "cmd-right": ["terminal::SendText", "\u0005"],
+      "cmd-left": ["terminal::SendText", "\u0001"],
       // Terminal.app compatibility
-      "alt-left": [
-        "terminal::SendText",
-        "\u001bb"
-      ],
-      "alt-right": [
-        "terminal::SendText",
-        "\u001bf"
-      ],
+      "alt-left": ["terminal::SendText", "\u001bb"],
+      "alt-right": ["terminal::SendText", "\u001bf"],
       // There are conflicting bindings for these keys in the global context.
       // these bindings override them, remove at your own risk:
-      "up": [
-        "terminal::SendKeystroke",
-        "up"
-      ],
-      "pageup": [
-        "terminal::SendKeystroke",
-        "pageup"
-      ],
-      "down": [
-        "terminal::SendKeystroke",
-        "down"
-      ],
-      "pagedown": [
-        "terminal::SendKeystroke",
-        "pagedown"
-      ],
-      "escape": [
-        "terminal::SendKeystroke",
-        "escape"
-      ],
-      "enter": [
-        "terminal::SendKeystroke",
-        "enter"
-      ],
-      "ctrl-c": [
-        "terminal::SendKeystroke",
-        "ctrl-c"
-      ]
+      "up": ["terminal::SendKeystroke", "up"],
+      "pageup": ["terminal::SendKeystroke", "pageup"],
+      "down": ["terminal::SendKeystroke", "down"],
+      "pagedown": ["terminal::SendKeystroke", "pagedown"],
+      "escape": ["terminal::SendKeystroke", "escape"],
+      "enter": ["terminal::SendKeystroke", "enter"],
+      "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
     }
   }
 ]

assets/keymaps/vim.json 🔗

@@ -39,6 +39,7 @@
       "w": "vim::NextWordStart",
       "{": "vim::StartOfParagraph",
       "}": "vim::EndOfParagraph",
+      "|": "vim::GoToColumn",
       "shift-w": [
         "vim::NextWordStart",
         {
@@ -97,14 +98,8 @@
       "ctrl-o": "pane::GoBack",
       "ctrl-i": "pane::GoForward",
       "ctrl-]": "editor::GoToDefinition",
-      "escape": [
-        "vim::SwitchMode",
-        "Normal"
-      ],
-      "ctrl+[": [
-        "vim::SwitchMode",
-        "Normal"
-      ],
+      "escape": ["vim::SwitchMode", "Normal"],
+      "ctrl+[": ["vim::SwitchMode", "Normal"],
       "v": "vim::ToggleVisual",
       "shift-v": "vim::ToggleVisualLine",
       "ctrl-v": "vim::ToggleVisualBlock",
@@ -233,123 +228,36 @@
         }
       ],
       // Count support
-      "1": [
-        "vim::Number",
-        1
-      ],
-      "2": [
-        "vim::Number",
-        2
-      ],
-      "3": [
-        "vim::Number",
-        3
-      ],
-      "4": [
-        "vim::Number",
-        4
-      ],
-      "5": [
-        "vim::Number",
-        5
-      ],
-      "6": [
-        "vim::Number",
-        6
-      ],
-      "7": [
-        "vim::Number",
-        7
-      ],
-      "8": [
-        "vim::Number",
-        8
-      ],
-      "9": [
-        "vim::Number",
-        9
-      ],
+      "1": ["vim::Number", 1],
+      "2": ["vim::Number", 2],
+      "3": ["vim::Number", 3],
+      "4": ["vim::Number", 4],
+      "5": ["vim::Number", 5],
+      "6": ["vim::Number", 6],
+      "7": ["vim::Number", 7],
+      "8": ["vim::Number", 8],
+      "9": ["vim::Number", 9],
       // window related commands (ctrl-w X)
-      "ctrl-w left": [
-        "workspace::ActivatePaneInDirection",
-        "Left"
-      ],
-      "ctrl-w right": [
-        "workspace::ActivatePaneInDirection",
-        "Right"
-      ],
-      "ctrl-w up": [
-        "workspace::ActivatePaneInDirection",
-        "Up"
-      ],
-      "ctrl-w down": [
-        "workspace::ActivatePaneInDirection",
-        "Down"
-      ],
-      "ctrl-w h": [
-        "workspace::ActivatePaneInDirection",
-        "Left"
-      ],
-      "ctrl-w l": [
-        "workspace::ActivatePaneInDirection",
-        "Right"
-      ],
-      "ctrl-w k": [
-        "workspace::ActivatePaneInDirection",
-        "Up"
-      ],
-      "ctrl-w j": [
-        "workspace::ActivatePaneInDirection",
-        "Down"
-      ],
-      "ctrl-w ctrl-h": [
-        "workspace::ActivatePaneInDirection",
-        "Left"
-      ],
-      "ctrl-w ctrl-l": [
-        "workspace::ActivatePaneInDirection",
-        "Right"
-      ],
-      "ctrl-w ctrl-k": [
-        "workspace::ActivatePaneInDirection",
-        "Up"
-      ],
-      "ctrl-w ctrl-j": [
-        "workspace::ActivatePaneInDirection",
-        "Down"
-      ],
-      "ctrl-w shift-left": [
-        "workspace::SwapPaneInDirection",
-        "Left"
-      ],
-      "ctrl-w shift-right": [
-        "workspace::SwapPaneInDirection",
-        "Right"
-      ],
-      "ctrl-w shift-up": [
-        "workspace::SwapPaneInDirection",
-        "Up"
-      ],
-      "ctrl-w shift-down": [
-        "workspace::SwapPaneInDirection",
-        "Down"
-      ],
-      "ctrl-w shift-h": [
-        "workspace::SwapPaneInDirection",
-        "Left"
-      ],
-      "ctrl-w shift-l": [
-        "workspace::SwapPaneInDirection",
-        "Right"
-      ],
-      "ctrl-w shift-k": [
-        "workspace::SwapPaneInDirection",
-        "Up"
-      ],
-      "ctrl-w shift-j": [
-        "workspace::SwapPaneInDirection",
-        "Down"
-      ],
+      "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
+      "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
+      "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
+      "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
+      "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
+      "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
+      "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
+      "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
+      "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
+      "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
+      "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
+      "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
+      "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
+      "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
+      "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
+      "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
+      "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
+      "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
+      "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
+      "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
       "ctrl-w g t": "pane::ActivateNextItem",
       "ctrl-w ctrl-g t": "pane::ActivateNextItem",
       "ctrl-w g shift-t": "pane::ActivatePrevItem",
@@ -371,14 +279,8 @@
       "ctrl-w ctrl-q": "pane::CloseAllItems",
       "ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
       "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
-      "ctrl-w n": [
-        "workspace::NewFileInDirection",
-        "Up"
-      ],
-      "ctrl-w ctrl-n": [
-        "workspace::NewFileInDirection",
-        "Up"
-      ]
+      "ctrl-w n": ["workspace::NewFileInDirection", "Up"],
+      "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"]
     }
   },
   {
@@ -393,21 +295,12 @@
     "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
     "bindings": {
       ".": "vim::Repeat",
-      "c": [
-        "vim::PushOperator",
-        "Change"
-      ],
+      "c": ["vim::PushOperator", "Change"],
       "shift-c": "vim::ChangeToEndOfLine",
-      "d": [
-        "vim::PushOperator",
-        "Delete"
-      ],
+      "d": ["vim::PushOperator", "Delete"],
       "shift-d": "vim::DeleteToEndOfLine",
       "shift-j": "vim::JoinLines",
-      "y": [
-        "vim::PushOperator",
-        "Yank"
-      ],
+      "y": ["vim::PushOperator", "Yank"],
       "shift-y": "vim::YankLine",
       "i": "vim::InsertBefore",
       "shift-i": "vim::InsertFirstNonWhitespace",
@@ -443,10 +336,7 @@
           "backwards": true
         }
       ],
-      "r": [
-        "vim::PushOperator",
-        "Replace"
-      ],
+      "r": ["vim::PushOperator", "Replace"],
       "s": "vim::Substitute",
       "shift-s": "vim::SubstituteLine",
       "> >": "editor::Indent",
@@ -458,10 +348,7 @@
   {
     "context": "Editor && VimCount",
     "bindings": {
-      "0": [
-        "vim::Number",
-        0
-      ]
+      "0": ["vim::Number", 0]
     }
   },
   {
@@ -497,12 +384,15 @@
       "'": "vim::Quotes",
       "`": "vim::BackQuotes",
       "\"": "vim::DoubleQuotes",
+      "|": "vim::VerticalBars",
       "(": "vim::Parentheses",
       ")": "vim::Parentheses",
+      "b": "vim::Parentheses",
       "[": "vim::SquareBrackets",
       "]": "vim::SquareBrackets",
       "{": "vim::CurlyBrackets",
       "}": "vim::CurlyBrackets",
+      "shift-b": "vim::CurlyBrackets",
       "<": "vim::AngleBrackets",
       ">": "vim::AngleBrackets"
     }
@@ -548,22 +438,10 @@
       "shift-i": "vim::InsertBefore",
       "shift-a": "vim::InsertAfter",
       "shift-j": "vim::JoinLines",
-      "r": [
-        "vim::PushOperator",
-        "Replace"
-      ],
-      "ctrl-c": [
-        "vim::SwitchMode",
-        "Normal"
-      ],
-      "escape": [
-        "vim::SwitchMode",
-        "Normal"
-      ],
-      "ctrl+[": [
-        "vim::SwitchMode",
-        "Normal"
-      ],
+      "r": ["vim::PushOperator", "Replace"],
+      "ctrl-c": ["vim::SwitchMode", "Normal"],
+      "escape": ["vim::SwitchMode", "Normal"],
+      "ctrl+[": ["vim::SwitchMode", "Normal"],
       ">": "editor::Indent",
       "<": "editor::Outdent",
       "i": [
@@ -602,14 +480,8 @@
     "bindings": {
       "tab": "vim::Tab",
       "enter": "vim::Enter",
-      "escape": [
-        "vim::SwitchMode",
-        "Normal"
-      ],
-      "ctrl+[": [
-        "vim::SwitchMode",
-        "Normal"
-      ]
+      "escape": ["vim::SwitchMode", "Normal"],
+      "ctrl+[": ["vim::SwitchMode", "Normal"]
     }
   },
   {

assets/settings/default.json 🔗

@@ -50,6 +50,9 @@
   // Whether to pop the completions menu while typing in an editor without
   // explicitly requesting it.
   "show_completions_on_input": true,
+  // Whether to display inline and alongside documentation for items in the
+  // completions menu
+  "show_completion_documentation": true,
   // Whether to show wrap guides in the editor. Setting this to true will
   // show a guide at the 'preferred_line_length' value if softwrap is set to
   // 'preferred_line_length', and will show any additional guides as specified
@@ -139,6 +142,14 @@
     // Default width of the channels panel.
     "default_width": 240
   },
+  "notification_panel": {
+    // Whether to show the collaboration panel button in the status bar.
+    "button": true,
+    // Where to dock channels panel. Can be 'left' or 'right'.
+    "dock": "right",
+    // Default width of the channels panel.
+    "default_width": 380
+  },
   "assistant": {
     // Whether to show the assistant panel button in the status bar.
     "button": true,

crates/ai/Cargo.toml 🔗

@@ -11,6 +11,7 @@ doctest = false
 [dependencies]
 gpui = { path = "../gpui" }
 util = { path = "../util" }
+language = { path = "../language" }
 async-trait.workspace = true
 anyhow.workspace = true
 futures.workspace = true

crates/ai/src/ai.rs 🔗

@@ -1,2 +1,4 @@
 pub mod completion;
 pub mod embedding;
+pub mod models;
+pub mod templates;

crates/ai/src/completion.rs 🔗

@@ -53,6 +53,8 @@ pub struct OpenAIRequest {
     pub model: String,
     pub messages: Vec<RequestMessage>,
     pub stream: bool,
+    pub stop: Vec<String>,
+    pub temperature: f32,
 }
 
 #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]

crates/ai/src/embedding.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::AsyncReadExt;
 use gpui::executor::Background;
-use gpui::serde_json;
+use gpui::{serde_json, AppContext};
 use isahc::http::StatusCode;
 use isahc::prelude::Configurable;
 use isahc::{AsyncBody, Response};
@@ -20,9 +20,11 @@ use std::sync::Arc;
 use std::time::{Duration, Instant};
 use tiktoken_rs::{cl100k_base, CoreBPE};
 use util::http::{HttpClient, Request};
+use util::ResultExt;
+
+use crate::completion::OPENAI_API_URL;
 
 lazy_static! {
-    static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
     static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap();
 }
 
@@ -85,25 +87,6 @@ impl Embedding {
     }
 }
 
-// impl FromSql for Embedding {
-//     fn column_result(value: ValueRef) -> FromSqlResult<Self> {
-//         let bytes = value.as_blob()?;
-//         let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
-//         if embedding.is_err() {
-//             return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
-//         }
-//         Ok(Embedding(embedding.unwrap()))
-//     }
-// }
-
-// impl ToSql for Embedding {
-//     fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
-//         let bytes = bincode::serialize(&self.0)
-//             .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
-//         Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
-//     }
-// }
-
 #[derive(Clone)]
 pub struct OpenAIEmbeddings {
     pub client: Arc<dyn HttpClient>,
@@ -139,8 +122,12 @@ struct OpenAIEmbeddingUsage {
 
 #[async_trait]
 pub trait EmbeddingProvider: Sync + Send {
-    fn is_authenticated(&self) -> bool;
-    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
+    fn retrieve_credentials(&self, cx: &AppContext) -> Option<String>;
+    async fn embed_batch(
+        &self,
+        spans: Vec<String>,
+        api_key: Option<String>,
+    ) -> Result<Vec<Embedding>>;
     fn max_tokens_per_batch(&self) -> usize;
     fn truncate(&self, span: &str) -> (String, usize);
     fn rate_limit_expiration(&self) -> Option<Instant>;
@@ -150,13 +137,17 @@ pub struct DummyEmbeddings {}
 
 #[async_trait]
 impl EmbeddingProvider for DummyEmbeddings {
-    fn is_authenticated(&self) -> bool {
-        true
+    fn retrieve_credentials(&self, _cx: &AppContext) -> Option<String> {
+        Some("Dummy API KEY".to_string())
     }
     fn rate_limit_expiration(&self) -> Option<Instant> {
         None
     }
-    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
+    async fn embed_batch(
+        &self,
+        spans: Vec<String>,
+        _api_key: Option<String>,
+    ) -> Result<Vec<Embedding>> {
         // 1024 is the OpenAI Embeddings size for ada models.
         // the model we will likely be starting with.
         let dummy_vec = Embedding::from(vec![0.32 as f32; 1536]);
@@ -255,9 +246,21 @@ impl OpenAIEmbeddings {
 
 #[async_trait]
 impl EmbeddingProvider for OpenAIEmbeddings {
-    fn is_authenticated(&self) -> bool {
-        OPENAI_API_KEY.as_ref().is_some()
+    fn retrieve_credentials(&self, cx: &AppContext) -> Option<String> {
+        if let Ok(api_key) = env::var("OPENAI_API_KEY") {
+            Some(api_key)
+        } else if let Some((_, api_key)) = cx
+            .platform()
+            .read_credentials(OPENAI_API_URL)
+            .log_err()
+            .flatten()
+        {
+            String::from_utf8(api_key).log_err()
+        } else {
+            None
+        }
     }
+
     fn max_tokens_per_batch(&self) -> usize {
         50000
     }
@@ -280,13 +283,17 @@ impl EmbeddingProvider for OpenAIEmbeddings {
         (output, tokens.len())
     }
 
-    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
+    async fn embed_batch(
+        &self,
+        spans: Vec<String>,
+        api_key: Option<String>,
+    ) -> Result<Vec<Embedding>> {
         const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
         const MAX_RETRIES: usize = 4;
 
-        let api_key = OPENAI_API_KEY
-            .as_ref()
-            .ok_or_else(|| anyhow!("no api key"))?;
+        let Some(api_key) = api_key else {
+            return Err(anyhow!("no open ai key provided"));
+        };
 
         let mut request_number = 0;
         let mut rate_limiting = false;
@@ -295,11 +302,12 @@ impl EmbeddingProvider for OpenAIEmbeddings {
         while request_number < MAX_RETRIES {
             response = self
                 .send_request(
-                    api_key,
+                    &api_key,
                     spans.iter().map(|x| &**x).collect(),
                     request_timeout,
                 )
                 .await?;
+
             request_number += 1;
 
             match response.status() {

crates/ai/src/models.rs 🔗

@@ -0,0 +1,66 @@
+use anyhow::anyhow;
+use tiktoken_rs::CoreBPE;
+use util::ResultExt;
+
+pub trait LanguageModel {
+    fn name(&self) -> String;
+    fn count_tokens(&self, content: &str) -> anyhow::Result<usize>;
+    fn truncate(&self, content: &str, length: usize) -> anyhow::Result<String>;
+    fn truncate_start(&self, content: &str, length: usize) -> anyhow::Result<String>;
+    fn capacity(&self) -> anyhow::Result<usize>;
+}
+
+pub struct OpenAILanguageModel {
+    name: String,
+    bpe: Option<CoreBPE>,
+}
+
+impl OpenAILanguageModel {
+    pub fn load(model_name: &str) -> Self {
+        let bpe = tiktoken_rs::get_bpe_from_model(model_name).log_err();
+        OpenAILanguageModel {
+            name: model_name.to_string(),
+            bpe,
+        }
+    }
+}
+
+impl LanguageModel for OpenAILanguageModel {
+    fn name(&self) -> String {
+        self.name.clone()
+    }
+    fn count_tokens(&self, content: &str) -> anyhow::Result<usize> {
+        if let Some(bpe) = &self.bpe {
+            anyhow::Ok(bpe.encode_with_special_tokens(content).len())
+        } else {
+            Err(anyhow!("bpe for open ai model was not retrieved"))
+        }
+    }
+    fn truncate(&self, content: &str, length: usize) -> anyhow::Result<String> {
+        if let Some(bpe) = &self.bpe {
+            let tokens = bpe.encode_with_special_tokens(content);
+            if tokens.len() > length {
+                bpe.decode(tokens[..length].to_vec())
+            } else {
+                bpe.decode(tokens)
+            }
+        } else {
+            Err(anyhow!("bpe for open ai model was not retrieved"))
+        }
+    }
+    fn truncate_start(&self, content: &str, length: usize) -> anyhow::Result<String> {
+        if let Some(bpe) = &self.bpe {
+            let tokens = bpe.encode_with_special_tokens(content);
+            if tokens.len() > length {
+                bpe.decode(tokens[length..].to_vec())
+            } else {
+                bpe.decode(tokens)
+            }
+        } else {
+            Err(anyhow!("bpe for open ai model was not retrieved"))
+        }
+    }
+    fn capacity(&self) -> anyhow::Result<usize> {
+        anyhow::Ok(tiktoken_rs::model::get_context_size(&self.name))
+    }
+}

crates/ai/src/templates/base.rs 🔗

@@ -0,0 +1,350 @@
+use std::cmp::Reverse;
+use std::ops::Range;
+use std::sync::Arc;
+
+use language::BufferSnapshot;
+use util::ResultExt;
+
+use crate::models::LanguageModel;
+use crate::templates::repository_context::PromptCodeSnippet;
+
+pub(crate) enum PromptFileType {
+    Text,
+    Code,
+}
+
+// TODO: Set this up to manage for defaults well
+pub struct PromptArguments {
+    pub model: Arc<dyn LanguageModel>,
+    pub user_prompt: Option<String>,
+    pub language_name: Option<String>,
+    pub project_name: Option<String>,
+    pub snippets: Vec<PromptCodeSnippet>,
+    pub reserved_tokens: usize,
+    pub buffer: Option<BufferSnapshot>,
+    pub selected_range: Option<Range<usize>>,
+}
+
+impl PromptArguments {
+    pub(crate) fn get_file_type(&self) -> PromptFileType {
+        if self
+            .language_name
+            .as_ref()
+            .and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str())))
+            .unwrap_or(true)
+        {
+            PromptFileType::Code
+        } else {
+            PromptFileType::Text
+        }
+    }
+}
+
+pub trait PromptTemplate {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)>;
+}
+
+#[repr(i8)]
+#[derive(PartialEq, Eq, Ord)]
+pub enum PromptPriority {
+    Mandatory,                // Ignores truncation
+    Ordered { order: usize }, // Truncates based on priority
+}
+
+impl PartialOrd for PromptPriority {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        match (self, other) {
+            (Self::Mandatory, Self::Mandatory) => Some(std::cmp::Ordering::Equal),
+            (Self::Mandatory, Self::Ordered { .. }) => Some(std::cmp::Ordering::Greater),
+            (Self::Ordered { .. }, Self::Mandatory) => Some(std::cmp::Ordering::Less),
+            (Self::Ordered { order: a }, Self::Ordered { order: b }) => b.partial_cmp(a),
+        }
+    }
+}
+
+pub struct PromptChain {
+    args: PromptArguments,
+    templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)>,
+}
+
+impl PromptChain {
+    pub fn new(
+        args: PromptArguments,
+        templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)>,
+    ) -> Self {
+        PromptChain { args, templates }
+    }
+
+    pub fn generate(&self, truncate: bool) -> anyhow::Result<(String, usize)> {
+        // Argsort based on Prompt Priority
+        let seperator = "\n";
+        let seperator_tokens = self.args.model.count_tokens(seperator)?;
+        let mut sorted_indices = (0..self.templates.len()).collect::<Vec<_>>();
+        sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0));
+
+        // If Truncate
+        let mut tokens_outstanding = if truncate {
+            Some(self.args.model.capacity()? - self.args.reserved_tokens)
+        } else {
+            None
+        };
+
+        let mut prompts = vec!["".to_string(); sorted_indices.len()];
+        for idx in sorted_indices {
+            let (_, template) = &self.templates[idx];
+
+            if let Some((template_prompt, prompt_token_count)) =
+                template.generate(&self.args, tokens_outstanding).log_err()
+            {
+                if template_prompt != "" {
+                    prompts[idx] = template_prompt;
+
+                    if let Some(remaining_tokens) = tokens_outstanding {
+                        let new_tokens = prompt_token_count + seperator_tokens;
+                        tokens_outstanding = if remaining_tokens > new_tokens {
+                            Some(remaining_tokens - new_tokens)
+                        } else {
+                            Some(0)
+                        };
+                    }
+                }
+            }
+        }
+
+        prompts.retain(|x| x != "");
+
+        let full_prompt = prompts.join(seperator);
+        let total_token_count = self.args.model.count_tokens(&full_prompt)?;
+        anyhow::Ok((prompts.join(seperator), total_token_count))
+    }
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+
+    #[test]
+    pub fn test_prompt_chain() {
+        struct TestPromptTemplate {}
+        impl PromptTemplate for TestPromptTemplate {
+            fn generate(
+                &self,
+                args: &PromptArguments,
+                max_token_length: Option<usize>,
+            ) -> anyhow::Result<(String, usize)> {
+                let mut content = "This is a test prompt template".to_string();
+
+                let mut token_count = args.model.count_tokens(&content)?;
+                if let Some(max_token_length) = max_token_length {
+                    if token_count > max_token_length {
+                        content = args.model.truncate(&content, max_token_length)?;
+                        token_count = max_token_length;
+                    }
+                }
+
+                anyhow::Ok((content, token_count))
+            }
+        }
+
+        struct TestLowPriorityTemplate {}
+        impl PromptTemplate for TestLowPriorityTemplate {
+            fn generate(
+                &self,
+                args: &PromptArguments,
+                max_token_length: Option<usize>,
+            ) -> anyhow::Result<(String, usize)> {
+                let mut content = "This is a low priority test prompt template".to_string();
+
+                let mut token_count = args.model.count_tokens(&content)?;
+                if let Some(max_token_length) = max_token_length {
+                    if token_count > max_token_length {
+                        content = args.model.truncate(&content, max_token_length)?;
+                        token_count = max_token_length;
+                    }
+                }
+
+                anyhow::Ok((content, token_count))
+            }
+        }
+
+        #[derive(Clone)]
+        struct DummyLanguageModel {
+            capacity: usize,
+        }
+
+        impl LanguageModel for DummyLanguageModel {
+            fn name(&self) -> String {
+                "dummy".to_string()
+            }
+            fn count_tokens(&self, content: &str) -> anyhow::Result<usize> {
+                anyhow::Ok(content.chars().collect::<Vec<char>>().len())
+            }
+            fn truncate(&self, content: &str, length: usize) -> anyhow::Result<String> {
+                anyhow::Ok(
+                    content.chars().collect::<Vec<char>>()[..length]
+                        .into_iter()
+                        .collect::<String>(),
+                )
+            }
+            fn truncate_start(&self, content: &str, length: usize) -> anyhow::Result<String> {
+                anyhow::Ok(
+                    content.chars().collect::<Vec<char>>()[length..]
+                        .into_iter()
+                        .collect::<String>(),
+                )
+            }
+            fn capacity(&self) -> anyhow::Result<usize> {
+                anyhow::Ok(self.capacity)
+            }
+        }
+
+        let model: Arc<dyn LanguageModel> = Arc::new(DummyLanguageModel { capacity: 100 });
+        let args = PromptArguments {
+            model: model.clone(),
+            language_name: None,
+            project_name: None,
+            snippets: Vec::new(),
+            reserved_tokens: 0,
+            buffer: None,
+            selected_range: None,
+            user_prompt: None,
+        };
+
+        let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+            (
+                PromptPriority::Ordered { order: 0 },
+                Box::new(TestPromptTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 1 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+        ];
+        let chain = PromptChain::new(args, templates);
+
+        let (prompt, token_count) = chain.generate(false).unwrap();
+
+        assert_eq!(
+            prompt,
+            "This is a test prompt template\nThis is a low priority test prompt template"
+                .to_string()
+        );
+
+        assert_eq!(model.count_tokens(&prompt).unwrap(), token_count);
+
+        // Testing with Truncation Off
+        // Should ignore capacity and return all prompts
+        let model: Arc<dyn LanguageModel> = Arc::new(DummyLanguageModel { capacity: 20 });
+        let args = PromptArguments {
+            model: model.clone(),
+            language_name: None,
+            project_name: None,
+            snippets: Vec::new(),
+            reserved_tokens: 0,
+            buffer: None,
+            selected_range: None,
+            user_prompt: None,
+        };
+
+        let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+            (
+                PromptPriority::Ordered { order: 0 },
+                Box::new(TestPromptTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 1 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+        ];
+        let chain = PromptChain::new(args, templates);
+
+        let (prompt, token_count) = chain.generate(false).unwrap();
+
+        assert_eq!(
+            prompt,
+            "This is a test prompt template\nThis is a low priority test prompt template"
+                .to_string()
+        );
+
+        assert_eq!(model.count_tokens(&prompt).unwrap(), token_count);
+
+        // Testing with Truncation Off
+        // Should ignore capacity and return all prompts
+        let capacity = 20;
+        let model: Arc<dyn LanguageModel> = Arc::new(DummyLanguageModel { capacity });
+        let args = PromptArguments {
+            model: model.clone(),
+            language_name: None,
+            project_name: None,
+            snippets: Vec::new(),
+            reserved_tokens: 0,
+            buffer: None,
+            selected_range: None,
+            user_prompt: None,
+        };
+
+        let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+            (
+                PromptPriority::Ordered { order: 0 },
+                Box::new(TestPromptTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 1 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 2 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+        ];
+        let chain = PromptChain::new(args, templates);
+
+        let (prompt, token_count) = chain.generate(true).unwrap();
+
+        assert_eq!(prompt, "This is a test promp".to_string());
+        assert_eq!(token_count, capacity);
+
+        // Change Ordering of Prompts Based on Priority
+        let capacity = 120;
+        let reserved_tokens = 10;
+        let model: Arc<dyn LanguageModel> = Arc::new(DummyLanguageModel { capacity });
+        let args = PromptArguments {
+            model: model.clone(),
+            language_name: None,
+            project_name: None,
+            snippets: Vec::new(),
+            reserved_tokens,
+            buffer: None,
+            selected_range: None,
+            user_prompt: None,
+        };
+        let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+            (
+                PromptPriority::Mandatory,
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 0 },
+                Box::new(TestPromptTemplate {}),
+            ),
+            (
+                PromptPriority::Ordered { order: 1 },
+                Box::new(TestLowPriorityTemplate {}),
+            ),
+        ];
+        let chain = PromptChain::new(args, templates);
+
+        let (prompt, token_count) = chain.generate(true).unwrap();
+
+        assert_eq!(
+            prompt,
+            "This is a low priority test prompt template\nThis is a test prompt template\nThis is a low priority test prompt "
+                .to_string()
+        );
+        assert_eq!(token_count, capacity - reserved_tokens);
+    }
+}

crates/ai/src/templates/file_context.rs 🔗

@@ -0,0 +1,160 @@
+use anyhow::anyhow;
+use language::BufferSnapshot;
+use language::ToOffset;
+
+use crate::models::LanguageModel;
+use crate::templates::base::PromptArguments;
+use crate::templates::base::PromptTemplate;
+use std::fmt::Write;
+use std::ops::Range;
+use std::sync::Arc;
+
+fn retrieve_context(
+    buffer: &BufferSnapshot,
+    selected_range: &Option<Range<usize>>,
+    model: Arc<dyn LanguageModel>,
+    max_token_count: Option<usize>,
+) -> anyhow::Result<(String, usize, bool)> {
+    let mut prompt = String::new();
+    let mut truncated = false;
+    if let Some(selected_range) = selected_range {
+        let start = selected_range.start.to_offset(buffer);
+        let end = selected_range.end.to_offset(buffer);
+
+        let start_window = buffer.text_for_range(0..start).collect::<String>();
+
+        let mut selected_window = String::new();
+        if start == end {
+            write!(selected_window, "<|START|>").unwrap();
+        } else {
+            write!(selected_window, "<|START|").unwrap();
+        }
+
+        write!(
+            selected_window,
+            "{}",
+            buffer.text_for_range(start..end).collect::<String>()
+        )
+        .unwrap();
+
+        if start != end {
+            write!(selected_window, "|END|>").unwrap();
+        }
+
+        let end_window = buffer.text_for_range(end..buffer.len()).collect::<String>();
+
+        if let Some(max_token_count) = max_token_count {
+            let selected_tokens = model.count_tokens(&selected_window)?;
+            if selected_tokens > max_token_count {
+                return Err(anyhow!(
+                    "selected range is greater than model context window, truncation not possible"
+                ));
+            };
+
+            let mut remaining_tokens = max_token_count - selected_tokens;
+            let start_window_tokens = model.count_tokens(&start_window)?;
+            let end_window_tokens = model.count_tokens(&end_window)?;
+            let outside_tokens = start_window_tokens + end_window_tokens;
+            if outside_tokens > remaining_tokens {
+                let (start_goal_tokens, end_goal_tokens) =
+                    if start_window_tokens < end_window_tokens {
+                        let start_goal_tokens = (remaining_tokens / 2).min(start_window_tokens);
+                        remaining_tokens -= start_goal_tokens;
+                        let end_goal_tokens = remaining_tokens.min(end_window_tokens);
+                        (start_goal_tokens, end_goal_tokens)
+                    } else {
+                        let end_goal_tokens = (remaining_tokens / 2).min(end_window_tokens);
+                        remaining_tokens -= end_goal_tokens;
+                        let start_goal_tokens = remaining_tokens.min(start_window_tokens);
+                        (start_goal_tokens, end_goal_tokens)
+                    };
+
+                let truncated_start_window =
+                    model.truncate_start(&start_window, start_goal_tokens)?;
+                let truncated_end_window = model.truncate(&end_window, end_goal_tokens)?;
+                writeln!(
+                    prompt,
+                    "{truncated_start_window}{selected_window}{truncated_end_window}"
+                )
+                .unwrap();
+                truncated = true;
+            } else {
+                writeln!(prompt, "{start_window}{selected_window}{end_window}").unwrap();
+            }
+        } else {
+            // If we dont have a selected range, include entire file.
+            writeln!(prompt, "{}", &buffer.text()).unwrap();
+
+            // Dumb truncation strategy
+            if let Some(max_token_count) = max_token_count {
+                if model.count_tokens(&prompt)? > max_token_count {
+                    truncated = true;
+                    prompt = model.truncate(&prompt, max_token_count)?;
+                }
+            }
+        }
+    }
+
+    let token_count = model.count_tokens(&prompt)?;
+    anyhow::Ok((prompt, token_count, truncated))
+}
+
+pub struct FileContext {}
+
+impl PromptTemplate for FileContext {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)> {
+        if let Some(buffer) = &args.buffer {
+            let mut prompt = String::new();
+            // Add Initial Preamble
+            // TODO: Do we want to add the path in here?
+            writeln!(
+                prompt,
+                "The file you are currently working on has the following content:"
+            )
+            .unwrap();
+
+            let language_name = args
+                .language_name
+                .clone()
+                .unwrap_or("".to_string())
+                .to_lowercase();
+
+            let (context, _, truncated) = retrieve_context(
+                buffer,
+                &args.selected_range,
+                args.model.clone(),
+                max_token_length,
+            )?;
+            writeln!(prompt, "```{language_name}\n{context}\n```").unwrap();
+
+            if truncated {
+                writeln!(prompt, "Note the content has been truncated and only represents a portion of the file.").unwrap();
+            }
+
+            if let Some(selected_range) = &args.selected_range {
+                let start = selected_range.start.to_offset(buffer);
+                let end = selected_range.end.to_offset(buffer);
+
+                if start == end {
+                    writeln!(prompt, "In particular, the user's cursor is currently on the '<|START|>' span in the above content, with no text selected.").unwrap();
+                } else {
+                    writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
+                }
+            }
+
+            // Really dumb truncation strategy
+            if let Some(max_tokens) = max_token_length {
+                prompt = args.model.truncate(&prompt, max_tokens)?;
+            }
+
+            let token_count = args.model.count_tokens(&prompt)?;
+            anyhow::Ok((prompt, token_count))
+        } else {
+            Err(anyhow!("no buffer provided to retrieve file context from"))
+        }
+    }
+}

crates/ai/src/templates/generate.rs 🔗

@@ -0,0 +1,95 @@
+use crate::templates::base::{PromptArguments, PromptFileType, PromptTemplate};
+use anyhow::anyhow;
+use std::fmt::Write;
+
+pub fn capitalize(s: &str) -> String {
+    let mut c = s.chars();
+    match c.next() {
+        None => String::new(),
+        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
+    }
+}
+
+pub struct GenerateInlineContent {}
+
+impl PromptTemplate for GenerateInlineContent {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)> {
+        let Some(user_prompt) = &args.user_prompt else {
+            return Err(anyhow!("user prompt not provided"));
+        };
+
+        let file_type = args.get_file_type();
+        let content_type = match &file_type {
+            PromptFileType::Code => "code",
+            PromptFileType::Text => "text",
+        };
+
+        let mut prompt = String::new();
+
+        if let Some(selected_range) = &args.selected_range {
+            if selected_range.start == selected_range.end {
+                writeln!(
+                    prompt,
+                    "Assume the cursor is located where the `<|START|>` span is."
+                )
+                .unwrap();
+                writeln!(
+                    prompt,
+                    "{} can't be replaced, so assume your answer will be inserted at the cursor.",
+                    capitalize(content_type)
+                )
+                .unwrap();
+                writeln!(
+                    prompt,
+                    "Generate {content_type} based on the users prompt: {user_prompt}",
+                )
+                .unwrap();
+            } else {
+                writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
+                writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
+                writeln!(prompt, "Double check that you only return code and not the '<|START|' and '|END|'> spans").unwrap();
+            }
+        } else {
+            writeln!(
+                prompt,
+                "Generate {content_type} based on the users prompt: {user_prompt}"
+            )
+            .unwrap();
+        }
+
+        if let Some(language_name) = &args.language_name {
+            writeln!(
+                prompt,
+                "Your answer MUST always and only be valid {}.",
+                language_name
+            )
+            .unwrap();
+        }
+        writeln!(prompt, "Never make remarks about the output.").unwrap();
+        writeln!(
+            prompt,
+            "Do not return anything else, except the generated {content_type}."
+        )
+        .unwrap();
+
+        match file_type {
+            PromptFileType::Code => {
+                // writeln!(prompt, "Always wrap your code in a Markdown block.").unwrap();
+            }
+            _ => {}
+        }
+
+        // Really dumb truncation strategy
+        if let Some(max_tokens) = max_token_length {
+            prompt = args.model.truncate(&prompt, max_tokens)?;
+        }
+
+        let token_count = args.model.count_tokens(&prompt)?;
+
+        anyhow::Ok((prompt, token_count))
+    }
+}

crates/ai/src/templates/preamble.rs 🔗

@@ -0,0 +1,52 @@
+use crate::templates::base::{PromptArguments, PromptFileType, PromptTemplate};
+use std::fmt::Write;
+
+pub struct EngineerPreamble {}
+
+impl PromptTemplate for EngineerPreamble {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)> {
+        let mut prompts = Vec::new();
+
+        match args.get_file_type() {
+            PromptFileType::Code => {
+                prompts.push(format!(
+                    "You are an expert {}engineer.",
+                    args.language_name.clone().unwrap_or("".to_string()) + " "
+                ));
+            }
+            PromptFileType::Text => {
+                prompts.push("You are an expert engineer.".to_string());
+            }
+        }
+
+        if let Some(project_name) = args.project_name.clone() {
+            prompts.push(format!(
+                "You are currently working inside the '{project_name}' project in code editor Zed."
+            ));
+        }
+
+        if let Some(mut remaining_tokens) = max_token_length {
+            let mut prompt = String::new();
+            let mut total_count = 0;
+            for prompt_piece in prompts {
+                let prompt_token_count =
+                    args.model.count_tokens(&prompt_piece)? + args.model.count_tokens("\n")?;
+                if remaining_tokens > prompt_token_count {
+                    writeln!(prompt, "{prompt_piece}").unwrap();
+                    remaining_tokens -= prompt_token_count;
+                    total_count += prompt_token_count;
+                }
+            }
+
+            anyhow::Ok((prompt, total_count))
+        } else {
+            let prompt = prompts.join("\n");
+            let token_count = args.model.count_tokens(&prompt)?;
+            anyhow::Ok((prompt, token_count))
+        }
+    }
+}

crates/ai/src/templates/repository_context.rs 🔗

@@ -0,0 +1,94 @@
+use crate::templates::base::{PromptArguments, PromptTemplate};
+use std::fmt::Write;
+use std::{ops::Range, path::PathBuf};
+
+use gpui::{AsyncAppContext, ModelHandle};
+use language::{Anchor, Buffer};
+
+#[derive(Clone)]
+pub struct PromptCodeSnippet {
+    path: Option<PathBuf>,
+    language_name: Option<String>,
+    content: String,
+}
+
+impl PromptCodeSnippet {
+    pub fn new(buffer: ModelHandle<Buffer>, range: Range<Anchor>, cx: &AsyncAppContext) -> Self {
+        let (content, language_name, file_path) = buffer.read_with(cx, |buffer, _| {
+            let snapshot = buffer.snapshot();
+            let content = snapshot.text_for_range(range.clone()).collect::<String>();
+
+            let language_name = buffer
+                .language()
+                .and_then(|language| Some(language.name().to_string().to_lowercase()));
+
+            let file_path = buffer
+                .file()
+                .and_then(|file| Some(file.path().to_path_buf()));
+
+            (content, language_name, file_path)
+        });
+
+        PromptCodeSnippet {
+            path: file_path,
+            language_name,
+            content,
+        }
+    }
+}
+
+impl ToString for PromptCodeSnippet {
+    fn to_string(&self) -> String {
+        let path = self
+            .path
+            .as_ref()
+            .and_then(|path| Some(path.to_string_lossy().to_string()))
+            .unwrap_or("".to_string());
+        let language_name = self.language_name.clone().unwrap_or("".to_string());
+        let content = self.content.clone();
+
+        format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```")
+    }
+}
+
+pub struct RepositoryContext {}
+
+impl PromptTemplate for RepositoryContext {
+    fn generate(
+        &self,
+        args: &PromptArguments,
+        max_token_length: Option<usize>,
+    ) -> anyhow::Result<(String, usize)> {
+        const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500;
+        let template = "You are working inside a large repository, here are a few code snippets that may be useful.";
+        let mut prompt = String::new();
+
+        let mut remaining_tokens = max_token_length.clone();
+        let seperator_token_length = args.model.count_tokens("\n")?;
+        for snippet in &args.snippets {
+            let mut snippet_prompt = template.to_string();
+            let content = snippet.to_string();
+            writeln!(snippet_prompt, "{content}").unwrap();
+
+            let token_count = args.model.count_tokens(&snippet_prompt)?;
+            if token_count <= MAXIMUM_SNIPPET_TOKEN_COUNT {
+                if let Some(tokens_left) = remaining_tokens {
+                    if tokens_left >= token_count {
+                        writeln!(prompt, "{snippet_prompt}").unwrap();
+                        remaining_tokens = if tokens_left >= (token_count + seperator_token_length)
+                        {
+                            Some(tokens_left - token_count - seperator_token_length)
+                        } else {
+                            Some(0)
+                        };
+                    }
+                } else {
+                    writeln!(prompt, "{snippet_prompt}").unwrap();
+                }
+            }
+        }
+
+        let total_token_count = args.model.count_tokens(&prompt)?;
+        anyhow::Ok((prompt, total_token_count))
+    }
+}

crates/assistant/Cargo.toml 🔗

@@ -17,13 +17,17 @@ fs = { path = "../fs" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 menu = { path = "../menu" }
+multi_buffer = { path = "../multi_buffer" }
 search = { path = "../search" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
-uuid.workspace = true
+semantic_index = { path = "../semantic_index" }
+project = { path = "../project" }
 
+uuid.workspace = true
+log.workspace = true
 anyhow.workspace = true
 chrono = { version = "0.4", features = ["serde"] }
 futures.workspace = true
@@ -36,7 +40,7 @@ schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 smol.workspace = true
-tiktoken-rs = "0.4"
+tiktoken-rs = "0.5"
 
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }

crates/assistant/src/assistant_panel.rs 🔗

@@ -5,8 +5,11 @@ use crate::{
     MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
     SavedMessage,
 };
-use ai::completion::{
-    stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
+use ai::{
+    completion::{
+        stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
+    },
+    templates::repository_context::PromptCodeSnippet,
 };
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
@@ -29,13 +32,15 @@ use gpui::{
     },
     fonts::HighlightStyle,
     geometry::vector::{vec2f, Vector2F},
-    platform::{CursorStyle, MouseButton},
+    platform::{CursorStyle, MouseButton, PromptLevel},
     Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext,
-    ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
-    WindowContext,
+    ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
+    WeakModelHandle, WeakViewHandle, WindowContext,
 };
 use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
+use project::Project;
 use search::BufferSearchBar;
+use semantic_index::{SemanticIndex, SemanticIndexStatus};
 use settings::SettingsStore;
 use std::{
     cell::{Cell, RefCell},
@@ -46,7 +51,7 @@ use std::{
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
-    time::Duration,
+    time::{Duration, Instant},
 };
 use theme::{
     components::{action_button::Button, ComponentExt},
@@ -72,6 +77,7 @@ actions!(
         ResetKey,
         InlineAssist,
         ToggleIncludeConversation,
+        ToggleRetrieveContext,
     ]
 );
 
@@ -108,6 +114,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(InlineAssistant::confirm);
     cx.add_action(InlineAssistant::cancel);
     cx.add_action(InlineAssistant::toggle_include_conversation);
+    cx.add_action(InlineAssistant::toggle_retrieve_context);
     cx.add_action(InlineAssistant::move_up);
     cx.add_action(InlineAssistant::move_down);
 }
@@ -145,6 +152,8 @@ pub struct AssistantPanel {
     include_conversation_in_next_inline_assist: bool,
     inline_prompt_history: VecDeque<String>,
     _watch_saved_conversations: Task<Result<()>>,
+    semantic_index: Option<ModelHandle<SemanticIndex>>,
+    retrieve_context_in_next_inline_assist: bool,
 }
 
 impl AssistantPanel {
@@ -191,6 +200,9 @@ impl AssistantPanel {
                         toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
                         toolbar
                     });
+
+                    let semantic_index = SemanticIndex::global(cx);
+
                     let mut this = Self {
                         workspace: workspace_handle,
                         active_editor_index: Default::default(),
@@ -215,6 +227,8 @@ impl AssistantPanel {
                         include_conversation_in_next_inline_assist: false,
                         inline_prompt_history: Default::default(),
                         _watch_saved_conversations,
+                        semantic_index,
+                        retrieve_context_in_next_inline_assist: false,
                     };
 
                     let mut old_dock_position = this.position(cx);
@@ -262,12 +276,19 @@ impl AssistantPanel {
             return;
         };
 
+        let project = workspace.project();
+
         this.update(cx, |assistant, cx| {
-            assistant.new_inline_assist(&active_editor, cx)
+            assistant.new_inline_assist(&active_editor, cx, project)
         });
     }
 
-    fn new_inline_assist(&mut self, editor: &ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+    fn new_inline_assist(
+        &mut self,
+        editor: &ViewHandle<Editor>,
+        cx: &mut ViewContext<Self>,
+        project: &ModelHandle<Project>,
+    ) {
         let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
             api_key
         } else {
@@ -275,7 +296,7 @@ impl AssistantPanel {
         };
 
         let selection = editor.read(cx).selections.newest_anchor().clone();
-        if selection.start.excerpt_id() != selection.end.excerpt_id() {
+        if selection.start.excerpt_id != selection.end.excerpt_id {
             return;
         }
         let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
@@ -312,6 +333,27 @@ impl AssistantPanel {
             Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
         });
 
+        if let Some(semantic_index) = self.semantic_index.clone() {
+            let project = project.clone();
+            cx.spawn(|_, mut cx| async move {
+                let previously_indexed = semantic_index
+                    .update(&mut cx, |index, cx| {
+                        index.project_previously_indexed(&project, cx)
+                    })
+                    .await
+                    .unwrap_or(false);
+                if previously_indexed {
+                    let _ = semantic_index
+                        .update(&mut cx, |index, cx| {
+                            index.index_project(project.clone(), cx)
+                        })
+                        .await;
+                }
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+
         let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
         let inline_assistant = cx.add_view(|cx| {
             let assistant = InlineAssistant::new(
@@ -322,6 +364,9 @@ impl AssistantPanel {
                 codegen.clone(),
                 self.workspace.clone(),
                 cx,
+                self.retrieve_context_in_next_inline_assist,
+                self.semantic_index.clone(),
+                project.clone(),
             );
             cx.focus_self();
             assistant
@@ -362,6 +407,7 @@ impl AssistantPanel {
                 editor: editor.downgrade(),
                 inline_assistant: Some((block_id, inline_assistant.clone())),
                 codegen: codegen.clone(),
+                project: project.downgrade(),
                 _subscriptions: vec![
                     cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
                     cx.subscribe(editor, {
@@ -440,8 +486,15 @@ impl AssistantPanel {
             InlineAssistantEvent::Confirmed {
                 prompt,
                 include_conversation,
+                retrieve_context,
             } => {
-                self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
+                self.confirm_inline_assist(
+                    assist_id,
+                    prompt,
+                    *include_conversation,
+                    cx,
+                    *retrieve_context,
+                );
             }
             InlineAssistantEvent::Canceled => {
                 self.finish_inline_assist(assist_id, true, cx);
@@ -454,6 +507,9 @@ impl AssistantPanel {
             } => {
                 self.include_conversation_in_next_inline_assist = *include_conversation;
             }
+            InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => {
+                self.retrieve_context_in_next_inline_assist = *retrieve_context
+            }
         }
     }
 
@@ -532,6 +588,7 @@ impl AssistantPanel {
         user_prompt: &str,
         include_conversation: bool,
         cx: &mut ViewContext<Self>,
+        retrieve_context: bool,
     ) {
         let conversation = if include_conversation {
             self.active_editor()
@@ -553,6 +610,20 @@ impl AssistantPanel {
             return;
         };
 
+        let project = pending_assist.project.clone();
+
+        let project_name = if let Some(project) = project.upgrade(cx) {
+            Some(
+                project
+                    .read(cx)
+                    .worktree_root_names(cx)
+                    .collect::<Vec<&str>>()
+                    .join("/"),
+            )
+        } else {
+            None
+        };
+
         self.inline_prompt_history
             .retain(|prompt| prompt != user_prompt);
         self.inline_prompt_history.push_back(user_prompt.into());
@@ -590,13 +661,70 @@ impl AssistantPanel {
             None
         };
 
-        let codegen_kind = codegen.read(cx).kind().clone();
+        // Higher Temperature increases the randomness of model outputs.
+        // If Markdown or No Language is Known, increase the randomness for more creative output
+        // If Code, decrease temperature to get more deterministic outputs
+        let temperature = if let Some(language) = language_name.clone() {
+            if language.to_string() != "Markdown".to_string() {
+                0.5
+            } else {
+                1.0
+            }
+        } else {
+            1.0
+        };
+
         let user_prompt = user_prompt.to_string();
 
-        let mut messages = Vec::new();
+        let snippets = if retrieve_context {
+            let Some(project) = project.upgrade(cx) else {
+                return;
+            };
+
+            let search_results = if let Some(semantic_index) = self.semantic_index.clone() {
+                let search_results = semantic_index.update(cx, |this, cx| {
+                    this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx)
+                });
+
+                cx.background()
+                    .spawn(async move { search_results.await.unwrap_or_default() })
+            } else {
+                Task::ready(Vec::new())
+            };
+
+            let snippets = cx.spawn(|_, cx| async move {
+                let mut snippets = Vec::new();
+                for result in search_results.await {
+                    snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &cx));
+                }
+                snippets
+            });
+            snippets
+        } else {
+            Task::ready(Vec::new())
+        };
+
         let mut model = settings::get::<AssistantSettings>(cx)
             .default_open_ai_model
             .clone();
+        let model_name = model.full_name();
+
+        let prompt = cx.background().spawn(async move {
+            let snippets = snippets.await;
+
+            let language_name = language_name.as_deref();
+            generate_content_prompt(
+                user_prompt,
+                language_name,
+                buffer,
+                range,
+                snippets,
+                model_name,
+                project_name,
+            )
+        });
+
+        let mut messages = Vec::new();
         if let Some(conversation) = conversation {
             let conversation = conversation.read(cx);
             let buffer = conversation.buffer.read(cx);
@@ -608,24 +736,24 @@ impl AssistantPanel {
             model = conversation.model.clone();
         }
 
-        let prompt = cx.background().spawn(async move {
-            let language_name = language_name.as_deref();
-            generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
-        });
-
         cx.spawn(|_, mut cx| async move {
-            let prompt = prompt.await;
+            // I Don't know if we want to return a ? here.
+            let prompt = prompt.await?;
 
             messages.push(RequestMessage {
                 role: Role::User,
                 content: prompt,
             });
+
             let request = OpenAIRequest {
                 model: model.full_name().into(),
                 messages,
                 stream: true,
+                stop: vec!["|END|>".to_string()],
+                temperature,
             };
             codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx));
+            anyhow::Ok(())
         })
         .detach();
     }
@@ -1514,12 +1642,14 @@ impl Conversation {
                         Role::Assistant => "assistant".into(),
                         Role::System => "system".into(),
                     },
-                    content: self
-                        .buffer
-                        .read(cx)
-                        .text_for_range(message.offset_range)
-                        .collect(),
+                    content: Some(
+                        self.buffer
+                            .read(cx)
+                            .text_for_range(message.offset_range)
+                            .collect(),
+                    ),
                     name: None,
+                    function_call: None,
                 })
             })
             .collect::<Vec<_>>();
@@ -1613,6 +1743,8 @@ impl Conversation {
                     .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
                     .collect(),
                 stream: true,
+                stop: vec![],
+                temperature: 1.0,
             };
 
             let stream = stream_completion(api_key, cx.background().clone(), request);
@@ -1897,6 +2029,8 @@ impl Conversation {
                     model: self.model.full_name().to_string(),
                     messages: messages.collect(),
                     stream: true,
+                    stop: vec![],
+                    temperature: 1.0,
                 };
 
                 let stream = stream_completion(api_key, cx.background().clone(), request);
@@ -2638,12 +2772,16 @@ enum InlineAssistantEvent {
     Confirmed {
         prompt: String,
         include_conversation: bool,
+        retrieve_context: bool,
     },
     Canceled,
     Dismissed,
     IncludeConversationToggled {
         include_conversation: bool,
     },
+    RetrieveContextToggled {
+        retrieve_context: bool,
+    },
 }
 
 struct InlineAssistant {
@@ -2659,6 +2797,11 @@ struct InlineAssistant {
     pending_prompt: String,
     codegen: ModelHandle<Codegen>,
     _subscriptions: Vec<Subscription>,
+    retrieve_context: bool,
+    semantic_index: Option<ModelHandle<SemanticIndex>>,
+    semantic_permissioned: Option<bool>,
+    project: WeakModelHandle<Project>,
+    maintain_rate_limit: Option<Task<()>>,
 }
 
 impl Entity for InlineAssistant {
@@ -2675,51 +2818,65 @@ impl View for InlineAssistant {
         let theme = theme::current(cx);
 
         Flex::row()
-            .with_child(
-                Flex::row()
-                    .with_child(
-                        Button::action(ToggleIncludeConversation)
-                            .with_tooltip("Include Conversation", theme.tooltip.clone())
+            .with_children([Flex::row()
+                .with_child(
+                    Button::action(ToggleIncludeConversation)
+                        .with_tooltip("Include Conversation", theme.tooltip.clone())
+                        .with_id(self.id)
+                        .with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
+                        .toggleable(self.include_conversation)
+                        .with_style(theme.assistant.inline.include_conversation.clone())
+                        .element()
+                        .aligned(),
+                )
+                .with_children(if SemanticIndex::enabled(cx) {
+                    Some(
+                        Button::action(ToggleRetrieveContext)
+                            .with_tooltip("Retrieve Context", theme.tooltip.clone())
                             .with_id(self.id)
-                            .with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
-                            .toggleable(self.include_conversation)
-                            .with_style(theme.assistant.inline.include_conversation.clone())
+                            .with_contents(theme::components::svg::Svg::new(
+                                "icons/magnifying_glass.svg",
+                            ))
+                            .toggleable(self.retrieve_context)
+                            .with_style(theme.assistant.inline.retrieve_context.clone())
                             .element()
                             .aligned(),
                     )
-                    .with_children(if let Some(error) = self.codegen.read(cx).error() {
-                        Some(
-                            Svg::new("icons/error.svg")
-                                .with_color(theme.assistant.error_icon.color)
-                                .constrained()
-                                .with_width(theme.assistant.error_icon.width)
-                                .contained()
-                                .with_style(theme.assistant.error_icon.container)
-                                .with_tooltip::<ErrorIcon>(
-                                    self.id,
-                                    error.to_string(),
-                                    None,
-                                    theme.tooltip.clone(),
-                                    cx,
-                                )
-                                .aligned(),
-                        )
-                    } else {
-                        None
-                    })
-                    .aligned()
-                    .constrained()
-                    .dynamically({
-                        let measurements = self.measurements.clone();
-                        move |constraint, _, _| {
-                            let measurements = measurements.get();
-                            SizeConstraint {
-                                min: vec2f(measurements.gutter_width, constraint.min.y()),
-                                max: vec2f(measurements.gutter_width, constraint.max.y()),
-                            }
+                } else {
+                    None
+                })
+                .with_children(if let Some(error) = self.codegen.read(cx).error() {
+                    Some(
+                        Svg::new("icons/error.svg")
+                            .with_color(theme.assistant.error_icon.color)
+                            .constrained()
+                            .with_width(theme.assistant.error_icon.width)
+                            .contained()
+                            .with_style(theme.assistant.error_icon.container)
+                            .with_tooltip::<ErrorIcon>(
+                                self.id,
+                                error.to_string(),
+                                None,
+                                theme.tooltip.clone(),
+                                cx,
+                            )
+                            .aligned(),
+                    )
+                } else {
+                    None
+                })
+                .aligned()
+                .constrained()
+                .dynamically({
+                    let measurements = self.measurements.clone();
+                    move |constraint, _, _| {
+                        let measurements = measurements.get();
+                        SizeConstraint {
+                            min: vec2f(measurements.gutter_width, constraint.min.y()),
+                            max: vec2f(measurements.gutter_width, constraint.max.y()),
                         }
-                    }),
-            )
+                    }
+                })])
             .with_child(Empty::new().constrained().dynamically({
                 let measurements = self.measurements.clone();
                 move |constraint, _, _| {
@@ -2742,6 +2899,16 @@ impl View for InlineAssistant {
                     .left()
                     .flex(1., true),
             )
+            .with_children(if self.retrieve_context {
+                Some(
+                    Flex::row()
+                        .with_children(self.retrieve_context_status(cx))
+                        .flex(1., true)
+                        .aligned(),
+                )
+            } else {
+                None
+            })
             .contained()
             .with_style(theme.assistant.inline.container)
             .into_any()
@@ -2767,6 +2934,9 @@ impl InlineAssistant {
         codegen: ModelHandle<Codegen>,
         workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
+        retrieve_context: bool,
+        semantic_index: Option<ModelHandle<SemanticIndex>>,
+        project: ModelHandle<Project>,
     ) -> Self {
         let prompt_editor = cx.add_view(|cx| {
             let mut editor = Editor::single_line(
@@ -2780,11 +2950,16 @@ impl InlineAssistant {
             editor.set_placeholder_text(placeholder, cx);
             editor
         });
-        let subscriptions = vec![
+        let mut subscriptions = vec![
             cx.observe(&codegen, Self::handle_codegen_changed),
             cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
         ];
-        Self {
+
+        if let Some(semantic_index) = semantic_index.clone() {
+            subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed));
+        }
+
+        let assistant = Self {
             id,
             prompt_editor,
             workspace,
@@ -2797,7 +2972,33 @@ impl InlineAssistant {
             pending_prompt: String::new(),
             codegen,
             _subscriptions: subscriptions,
+            retrieve_context,
+            semantic_permissioned: None,
+            semantic_index,
+            project: project.downgrade(),
+            maintain_rate_limit: None,
+        };
+
+        assistant.index_project(cx).log_err();
+
+        assistant
+    }
+
+    fn semantic_permissioned(&self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
+        if let Some(value) = self.semantic_permissioned {
+            return Task::ready(Ok(value));
         }
+
+        let Some(project) = self.project.upgrade(cx) else {
+            return Task::ready(Err(anyhow!("project was dropped")));
+        };
+
+        self.semantic_index
+            .as_ref()
+            .map(|semantic| {
+                semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
+            })
+            .unwrap_or(Task::ready(Ok(false)))
     }
 
     fn handle_prompt_editor_events(
@@ -2812,6 +3013,37 @@ impl InlineAssistant {
         }
     }
 
+    fn semantic_index_changed(
+        &mut self,
+        semantic_index: ModelHandle<SemanticIndex>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let Some(project) = self.project.upgrade(cx) else {
+            return;
+        };
+
+        let status = semantic_index.read(cx).status(&project);
+        match status {
+            SemanticIndexStatus::Indexing {
+                rate_limit_expiry: Some(_),
+                ..
+            } => {
+                if self.maintain_rate_limit.is_none() {
+                    self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move {
+                        loop {
+                            cx.background().timer(Duration::from_secs(1)).await;
+                            this.update(&mut cx, |_, cx| cx.notify()).log_err();
+                        }
+                    }));
+                }
+                return;
+            }
+            _ => {
+                self.maintain_rate_limit = None;
+            }
+        }
+    }
+
     fn handle_codegen_changed(&mut self, _: ModelHandle<Codegen>, cx: &mut ViewContext<Self>) {
         let is_read_only = !self.codegen.read(cx).idle();
         self.prompt_editor.update(cx, |editor, cx| {
@@ -2861,12 +3093,241 @@ impl InlineAssistant {
             cx.emit(InlineAssistantEvent::Confirmed {
                 prompt,
                 include_conversation: self.include_conversation,
+                retrieve_context: self.retrieve_context,
             });
             self.confirmed = true;
             cx.notify();
         }
     }
 
+    fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext<Self>) {
+        let semantic_permissioned = self.semantic_permissioned(cx);
+
+        let Some(project) = self.project.upgrade(cx) else {
+            return;
+        };
+
+        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 context retrieval? This requires sending code to the OpenAI API", project_name,
+            if is_plural {
+                "s"
+            } else {""});
+
+        cx.spawn(|this, mut cx| async move {
+            // If Necessary prompt user
+            if !semantic_permissioned.await.unwrap_or(false) {
+                let mut answer = this.update(&mut cx, |_, cx| {
+                    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 {
+                    return anyhow::Ok(());
+                }
+            }
+
+            // If permissioned, update context appropriately
+            this.update(&mut cx, |this, cx| {
+                this.retrieve_context = !this.retrieve_context;
+
+                cx.emit(InlineAssistantEvent::RetrieveContextToggled {
+                    retrieve_context: this.retrieve_context,
+                });
+
+                if this.retrieve_context {
+                    this.index_project(cx).log_err();
+                }
+
+                cx.notify();
+            })?;
+
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    fn index_project(&self, cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
+        let Some(project) = self.project.upgrade(cx) else {
+            return Err(anyhow!("project was dropped!"));
+        };
+
+        let semantic_permissioned = self.semantic_permissioned(cx);
+        if let Some(semantic_index) = SemanticIndex::global(cx) {
+            cx.spawn(|_, mut cx| async move {
+                // This has to be updated to accomodate for semantic_permissions
+                if semantic_permissioned.await.unwrap_or(false) {
+                    semantic_index
+                        .update(&mut cx, |index, cx| index.index_project(project, cx))
+                        .await
+                } else {
+                    Err(anyhow!("project is not permissioned for semantic indexing"))
+                }
+            })
+            .detach_and_log_err(cx);
+        }
+
+        anyhow::Ok(())
+    }
+
+    fn retrieve_context_status(
+        &self,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<AnyElement<InlineAssistant>> {
+        enum ContextStatusIcon {}
+
+        let Some(project) = self.project.upgrade(cx) else {
+            return None;
+        };
+
+        if let Some(semantic_index) = SemanticIndex::global(cx) {
+            let status = semantic_index.update(cx, |index, _| index.status(&project));
+            let theme = theme::current(cx);
+            match status {
+                SemanticIndexStatus::NotAuthenticated {} => Some(
+                    Svg::new("icons/error.svg")
+                        .with_color(theme.assistant.error_icon.color)
+                        .constrained()
+                        .with_width(theme.assistant.error_icon.width)
+                        .contained()
+                        .with_style(theme.assistant.error_icon.container)
+                        .with_tooltip::<ContextStatusIcon>(
+                            self.id,
+                            "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.",
+                            None,
+                            theme.tooltip.clone(),
+                            cx,
+                        )
+                        .aligned()
+                        .into_any(),
+                ),
+                SemanticIndexStatus::NotIndexed {} => Some(
+                    Svg::new("icons/error.svg")
+                        .with_color(theme.assistant.inline.context_status.error_icon.color)
+                        .constrained()
+                        .with_width(theme.assistant.inline.context_status.error_icon.width)
+                        .contained()
+                        .with_style(theme.assistant.inline.context_status.error_icon.container)
+                        .with_tooltip::<ContextStatusIcon>(
+                            self.id,
+                            "Not Indexed",
+                            None,
+                            theme.tooltip.clone(),
+                            cx,
+                        )
+                        .aligned()
+                        .into_any(),
+                ),
+                SemanticIndexStatus::Indexing {
+                    remaining_files,
+                    rate_limit_expiry,
+                } => {
+
+                    let mut status_text = if remaining_files == 0 {
+                        "Indexing...".to_string()
+                    } else {
+                        format!("Remaining files to index: {remaining_files}")
+                    };
+
+                    if let Some(rate_limit_expiry) = rate_limit_expiry {
+                        let remaining_seconds = rate_limit_expiry.duration_since(Instant::now());
+                        if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 {
+                            write!(
+                                status_text,
+                                " (rate limit expires in {}s)",
+                                remaining_seconds.as_secs()
+                            )
+                            .unwrap();
+                        }
+                    }
+                    Some(
+                        Svg::new("icons/update.svg")
+                            .with_color(theme.assistant.inline.context_status.in_progress_icon.color)
+                            .constrained()
+                            .with_width(theme.assistant.inline.context_status.in_progress_icon.width)
+                            .contained()
+                            .with_style(theme.assistant.inline.context_status.in_progress_icon.container)
+                            .with_tooltip::<ContextStatusIcon>(
+                                self.id,
+                                status_text,
+                                None,
+                                theme.tooltip.clone(),
+                                cx,
+                            )
+                            .aligned()
+                            .into_any(),
+                    )
+                }
+                SemanticIndexStatus::Indexed {} => Some(
+                    Svg::new("icons/check.svg")
+                        .with_color(theme.assistant.inline.context_status.complete_icon.color)
+                        .constrained()
+                        .with_width(theme.assistant.inline.context_status.complete_icon.width)
+                        .contained()
+                        .with_style(theme.assistant.inline.context_status.complete_icon.container)
+                        .with_tooltip::<ContextStatusIcon>(
+                            self.id,
+                            "Index up to date",
+                            None,
+                            theme.tooltip.clone(),
+                            cx,
+                        )
+                        .aligned()
+                        .into_any(),
+                ),
+            }
+        } else {
+            None
+        }
+    }
+
+    // fn retrieve_context_status(&self, cx: &mut ViewContext<Self>) -> String {
+    //     let project = self.project.clone();
+    //     if let Some(semantic_index) = self.semantic_index.clone() {
+    //         let status = semantic_index.update(cx, |index, cx| index.status(&project));
+    //         return match status {
+    //             // This theoretically shouldnt be a valid code path
+    //             // As the inline assistant cant be launched without an API key
+    //             // We keep it here for safety
+    //             semantic_index::SemanticIndexStatus::NotAuthenticated => {
+    //                 "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string()
+    //             }
+    //             semantic_index::SemanticIndexStatus::Indexed => {
+    //                 "Indexing Complete!".to_string()
+    //             }
+    //             semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => {
+
+    //                 let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}");
+
+    //                 if let Some(rate_limit_expiry) = rate_limit_expiry {
+    //                     let remaining_seconds =
+    //                             rate_limit_expiry.duration_since(Instant::now());
+    //                     if remaining_seconds > Duration::from_secs(0) {
+    //                         write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap();
+    //                     }
+    //                 }
+    //                 status
+    //             }
+    //             semantic_index::SemanticIndexStatus::NotIndexed => {
+    //                 "Not Indexed for Context Retrieval".to_string()
+    //             }
+    //         };
+    //     }
+
+    //     "".to_string()
+    // }
+
     fn toggle_include_conversation(
         &mut self,
         _: &ToggleIncludeConversation,
@@ -2929,6 +3390,7 @@ struct PendingInlineAssist {
     inline_assistant: Option<(BlockId, ViewHandle<InlineAssistant>)>,
     codegen: ModelHandle<Codegen>,
     _subscriptions: Vec<Subscription>,
+    project: WeakModelHandle<Project>,
 }
 
 fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {

crates/assistant/src/codegen.rs 🔗

@@ -1,10 +1,11 @@
 use crate::streaming_diff::{Hunk, StreamingDiff};
 use ai::completion::{CompletionProvider, OpenAIRequest};
 use anyhow::Result;
-use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
+use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
 use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
 use gpui::{Entity, ModelContext, ModelHandle, Task};
 use language::{Rope, TransactionId};
+use multi_buffer;
 use std::{cmp, future, ops::Range, sync::Arc};
 
 pub enum Event {

crates/assistant/src/prompts.rs 🔗

@@ -1,8 +1,13 @@
-use crate::codegen::CodegenKind;
+use ai::models::{LanguageModel, OpenAILanguageModel};
+use ai::templates::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
+use ai::templates::file_context::FileContext;
+use ai::templates::generate::GenerateInlineContent;
+use ai::templates::preamble::EngineerPreamble;
+use ai::templates::repository_context::{PromptCodeSnippet, RepositoryContext};
 use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
 use std::cmp::{self, Reverse};
-use std::fmt::Write;
 use std::ops::Range;
+use std::sync::Arc;
 
 #[allow(dead_code)]
 fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
@@ -118,86 +123,50 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> S
 pub fn generate_content_prompt(
     user_prompt: String,
     language_name: Option<&str>,
-    buffer: &BufferSnapshot,
-    range: Range<impl ToOffset>,
-    kind: CodegenKind,
-) -> String {
-    let range = range.to_offset(buffer);
-    let mut prompt = String::new();
-
-    // General Preamble
-    if let Some(language_name) = language_name {
-        writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap();
+    buffer: BufferSnapshot,
+    range: Range<usize>,
+    search_results: Vec<PromptCodeSnippet>,
+    model: &str,
+    project_name: Option<String>,
+) -> anyhow::Result<String> {
+    // Using new Prompt Templates
+    let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAILanguageModel::load(model));
+    let lang_name = if let Some(language_name) = language_name {
+        Some(language_name.to_string())
     } else {
-        writeln!(prompt, "You're an expert engineer.\n").unwrap();
-    }
-
-    let mut content = String::new();
-    content.extend(buffer.text_for_range(0..range.start));
-    if range.start == range.end {
-        content.push_str("<|START|>");
-    } else {
-        content.push_str("<|START|");
-    }
-    content.extend(buffer.text_for_range(range.clone()));
-    if range.start != range.end {
-        content.push_str("|END|>");
-    }
-    content.extend(buffer.text_for_range(range.end..buffer.len()));
-
-    writeln!(
-        prompt,
-        "The file you are currently working on has the following content:"
-    )
-    .unwrap();
-    if let Some(language_name) = language_name {
-        let language_name = language_name.to_lowercase();
-        writeln!(prompt, "```{language_name}\n{content}\n```").unwrap();
-    } else {
-        writeln!(prompt, "```\n{content}\n```").unwrap();
-    }
-
-    match kind {
-        CodegenKind::Generate { position: _ } => {
-            writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap();
-            writeln!(
-                prompt,
-                "Assume the cursor is located where the `<|START|` marker is."
-            )
-            .unwrap();
-            writeln!(
-                prompt,
-                "Text can't be replaced, so assume your answer will be inserted at the cursor."
-            )
-            .unwrap();
-            writeln!(
-                prompt,
-                "Generate text based on the users prompt: {user_prompt}"
-            )
-            .unwrap();
-        }
-        CodegenKind::Transform { range: _ } => {
-            writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
-            writeln!(
-                prompt,
-                "Modify the users code selected text based upon the users prompt: {user_prompt}"
-            )
-            .unwrap();
-            writeln!(
-                prompt,
-                "You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file."
-            )
-            .unwrap();
-        }
-    }
-
-    if let Some(language_name) = language_name {
-        writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap();
-    }
-    writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap();
-    writeln!(prompt, "Never make remarks about the output.").unwrap();
-
-    prompt
+        None
+    };
+
+    let args = PromptArguments {
+        model: openai_model,
+        language_name: lang_name.clone(),
+        project_name,
+        snippets: search_results.clone(),
+        reserved_tokens: 1000,
+        buffer: Some(buffer),
+        selected_range: Some(range),
+        user_prompt: Some(user_prompt.clone()),
+    };
+
+    let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
+        (PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
+        (
+            PromptPriority::Ordered { order: 1 },
+            Box::new(RepositoryContext {}),
+        ),
+        (
+            PromptPriority::Ordered { order: 0 },
+            Box::new(FileContext {}),
+        ),
+        (
+            PromptPriority::Mandatory,
+            Box::new(GenerateInlineContent {}),
+        ),
+    ];
+    let chain = PromptChain::new(args, templates);
+    let (prompt, _) = chain.generate(true)?;
+
+    anyhow::Ok(prompt)
 }
 
 #[cfg(test)]

crates/call/src/call.rs 🔗

@@ -10,7 +10,7 @@ use client::{
     ZED_ALWAYS_ACTIVE,
 };
 use collections::HashSet;
-use futures::{future::Shared, FutureExt};
+use futures::{channel::oneshot, future::Shared, Future, FutureExt};
 use gpui::{
     AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
     WeakModelHandle,
@@ -37,10 +37,42 @@ pub struct IncomingCall {
     pub initial_project: Option<proto::ParticipantProject>,
 }
 
+pub struct OneAtATime {
+    cancel: Option<oneshot::Sender<()>>,
+}
+
+impl OneAtATime {
+    /// spawn a task in the given context.
+    /// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
+    /// otherwise you'll see the result of the task.
+    fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
+    where
+        F: 'static + FnOnce(AsyncAppContext) -> Fut,
+        Fut: Future<Output = Result<R>>,
+        R: 'static,
+    {
+        let (tx, rx) = oneshot::channel();
+        self.cancel.replace(tx);
+        cx.spawn(|cx| async move {
+            futures::select_biased! {
+                _ = rx.fuse() => Ok(None),
+                result = f(cx).fuse() => result.map(Some),
+            }
+        })
+    }
+
+    fn running(&self) -> bool {
+        self.cancel
+            .as_ref()
+            .is_some_and(|cancel| !cancel.is_canceled())
+    }
+}
+
 /// Singleton global maintaining the user's participation in a room across workspaces.
 pub struct ActiveCall {
     room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
     pending_room_creation: Option<Shared<Task<Result<ModelHandle<Room>, Arc<anyhow::Error>>>>>,
+    _join_debouncer: OneAtATime,
     location: Option<WeakModelHandle<Project>>,
     pending_invites: HashSet<u64>,
     incoming_call: (
@@ -69,6 +101,7 @@ impl ActiveCall {
             pending_invites: Default::default(),
             incoming_call: watch::channel(),
 
+            _join_debouncer: OneAtATime { cancel: None },
             _subscriptions: vec![
                 client.add_request_handler(cx.handle(), Self::handle_incoming_call),
                 client.add_message_handler(cx.handle(), Self::handle_call_canceled),
@@ -143,6 +176,10 @@ impl ActiveCall {
         }
         cx.notify();
 
+        if self._join_debouncer.running() {
+            return Task::ready(Ok(()));
+        }
+
         let room = if let Some(room) = self.room().cloned() {
             Some(Task::ready(Ok(room)).shared())
         } else {
@@ -259,11 +296,20 @@ impl ActiveCall {
             return Task::ready(Err(anyhow!("no incoming call")));
         };
 
-        let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
+        if self.pending_room_creation.is_some() {
+            return Task::ready(Ok(()));
+        }
+
+        let room_id = call.room_id.clone();
+        let client = self.client.clone();
+        let user_store = self.user_store.clone();
+        let join = self
+            ._join_debouncer
+            .spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
 
         cx.spawn(|this, mut cx| async move {
             let room = join.await?;
-            this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
+            this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))
                 .await?;
             this.update(&mut cx, |this, cx| {
                 this.report_call_event("accept incoming", cx)
@@ -290,20 +336,28 @@ impl ActiveCall {
         &mut self,
         channel_id: u64,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<ModelHandle<Room>>> {
+    ) -> Task<Result<Option<ModelHandle<Room>>>> {
         if let Some(room) = self.room().cloned() {
             if room.read(cx).channel_id() == Some(channel_id) {
-                return Task::ready(Ok(room));
+                return Task::ready(Ok(Some(room)));
             } else {
                 room.update(cx, |room, cx| room.clear_state(cx));
             }
         }
 
-        let join = Room::join_channel(channel_id, self.client.clone(), self.user_store.clone(), cx);
+        if self.pending_room_creation.is_some() {
+            return Task::ready(Ok(None));
+        }
 
-        cx.spawn(|this, mut cx| async move {
+        let client = self.client.clone();
+        let user_store = self.user_store.clone();
+        let join = self._join_debouncer.spawn(cx, move |cx| async move {
+            Room::join_channel(channel_id, client, user_store, cx).await
+        });
+
+        cx.spawn(move |this, mut cx| async move {
             let room = join.await?;
-            this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
+            this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))
                 .await?;
             this.update(&mut cx, |this, cx| {
                 this.report_call_event("join channel", cx)
@@ -457,3 +511,40 @@ pub fn report_call_event_for_channel(
     };
     telemetry.report_clickhouse_event(event, telemetry_settings);
 }
+
+#[cfg(test)]
+mod test {
+    use gpui::TestAppContext;
+
+    use crate::OneAtATime;
+
+    #[gpui::test]
+    async fn test_one_at_a_time(cx: &mut TestAppContext) {
+        let mut one_at_a_time = OneAtATime { cancel: None };
+
+        assert_eq!(
+            cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
+                .await
+                .unwrap(),
+            Some(1)
+        );
+
+        let (a, b) = cx.update(|cx| {
+            (
+                one_at_a_time.spawn(cx, |_| async {
+                    assert!(false);
+                    Ok(2)
+                }),
+                one_at_a_time.spawn(cx, |_| async { Ok(3) }),
+            )
+        });
+
+        assert_eq!(a.await.unwrap(), None);
+        assert_eq!(b.await.unwrap(), Some(3));
+
+        let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
+        drop(one_at_a_time);
+
+        assert_eq!(promise.await.unwrap(), None);
+    }
+}

crates/call/src/room.rs 🔗

@@ -1,7 +1,6 @@
 use crate::{
     call_settings::CallSettings,
     participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
-    IncomingCall,
 };
 use anyhow::{anyhow, Result};
 use audio::{Audio, Sound};
@@ -55,7 +54,7 @@ pub enum Event {
 
 pub struct Room {
     id: u64,
-    channel_id: Option<u64>,
+    pub channel_id: Option<u64>,
     live_kit: Option<LiveKitRoom>,
     status: RoomStatus,
     shared_projects: HashSet<WeakModelHandle<Project>>,
@@ -122,6 +121,10 @@ impl Room {
         }
     }
 
+    pub fn can_publish(&self) -> bool {
+        self.live_kit.as_ref().is_some_and(|room| room.can_publish)
+    }
+
     fn new(
         id: u64,
         channel_id: Option<u64>,
@@ -181,20 +184,23 @@ impl Room {
             });
 
             let connect = room.connect(&connection_info.server_url, &connection_info.token);
-            cx.spawn(|this, mut cx| async move {
-                connect.await?;
+            if connection_info.can_publish {
+                cx.spawn(|this, mut cx| async move {
+                    connect.await?;
 
-                if !cx.read(Self::mute_on_join) {
-                    this.update(&mut cx, |this, cx| this.share_microphone(cx))
-                        .await?;
-                }
+                    if !cx.read(Self::mute_on_join) {
+                        this.update(&mut cx, |this, cx| this.share_microphone(cx))
+                            .await?;
+                    }
 
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
+                    anyhow::Ok(())
+                })
+                .detach_and_log_err(cx);
+            }
 
             Some(LiveKitRoom {
                 room,
+                can_publish: connection_info.can_publish,
                 screen_track: LocalTrack::None,
                 microphone_track: LocalTrack::None,
                 next_publish_id: 0,
@@ -284,37 +290,32 @@ impl Room {
         })
     }
 
-    pub(crate) fn join_channel(
+    pub(crate) async fn join_channel(
         channel_id: u64,
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
-        cx: &mut AppContext,
-    ) -> Task<Result<ModelHandle<Self>>> {
-        cx.spawn(|cx| async move {
-            Self::from_join_response(
-                client.request(proto::JoinChannel { channel_id }).await?,
-                client,
-                user_store,
-                cx,
-            )
-        })
+        cx: AsyncAppContext,
+    ) -> Result<ModelHandle<Self>> {
+        Self::from_join_response(
+            client.request(proto::JoinChannel { channel_id }).await?,
+            client,
+            user_store,
+            cx,
+        )
     }
 
-    pub(crate) fn join(
-        call: &IncomingCall,
+    pub(crate) async fn join(
+        room_id: u64,
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
-        cx: &mut AppContext,
-    ) -> Task<Result<ModelHandle<Self>>> {
-        let id = call.room_id;
-        cx.spawn(|cx| async move {
-            Self::from_join_response(
-                client.request(proto::JoinRoom { id }).await?,
-                client,
-                user_store,
-                cx,
-            )
-        })
+        cx: AsyncAppContext,
+    ) -> Result<ModelHandle<Self>> {
+        Self::from_join_response(
+            client.request(proto::JoinRoom { id: room_id }).await?,
+            client,
+            user_store,
+            cx,
+        )
     }
 
     pub fn mute_on_join(cx: &AppContext) -> bool {
@@ -1251,7 +1252,7 @@ impl Room {
                     .read_with(&cx, |this, _| {
                         this.live_kit
                             .as_ref()
-                            .map(|live_kit| live_kit.room.publish_audio_track(&track))
+                            .map(|live_kit| live_kit.room.publish_audio_track(track))
                     })
                     .ok_or_else(|| anyhow!("live-kit was not initialized"))?
                     .await
@@ -1337,7 +1338,7 @@ impl Room {
                     .read_with(&cx, |this, _| {
                         this.live_kit
                             .as_ref()
-                            .map(|live_kit| live_kit.room.publish_video_track(&track))
+                            .map(|live_kit| live_kit.room.publish_video_track(track))
                     })
                     .ok_or_else(|| anyhow!("live-kit was not initialized"))?
                     .await
@@ -1498,6 +1499,7 @@ struct LiveKitRoom {
     deafened: bool,
     speaking: bool,
     next_publish_id: usize,
+    can_publish: bool,
     _maintain_room: Task<()>,
     _maintain_tracks: [Task<()>; 2],
 }

crates/call2/src/participant.rs 🔗

@@ -1,10 +1,8 @@
 use anyhow::{anyhow, Result};
 use client2::ParticipantIndex;
 use client2::{proto, User};
-use collections::HashMap;
 use gpui2::WeakHandle;
 pub use live_kit_client::Frame;
-use live_kit_client::RemoteAudioTrack;
 use project2::Project;
 use std::{fmt, sync::Arc};
 
@@ -47,8 +45,8 @@ pub struct RemoteParticipant {
     pub participant_index: ParticipantIndex,
     pub muted: bool,
     pub speaking: bool,
-    pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
-    pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
+    // pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
+    // pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
 }
 
 #[derive(Clone)]
@@ -56,6 +54,10 @@ pub struct RemoteVideoTrack {
     pub(crate) live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
 }
 
+unsafe impl Send for RemoteVideoTrack {}
+// todo!("remove this sync because it's not legit")
+unsafe impl Sync for RemoteVideoTrack {}
+
 impl fmt::Debug for RemoteVideoTrack {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("RemoteVideoTrack").finish()

crates/call2/src/room.rs 🔗

@@ -1,3 +1,6 @@
+#![allow(dead_code, unused)]
+// todo!()
+
 use crate::{
     call_settings::CallSettings,
     participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
@@ -16,15 +19,12 @@ use gpui2::{
     AppContext, AsyncAppContext, Context, EventEmitter, Handle, ModelContext, Task, WeakHandle,
 };
 use language2::LanguageRegistry;
-use live_kit_client::{
-    LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
-    RemoteVideoTrackUpdate,
-};
+use live_kit_client::{LocalTrackPublication, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate};
 use postage::{sink::Sink, stream::Stream, watch};
 use project2::Project;
 use settings2::Settings;
-use std::{future::Future, mem, sync::Arc, time::Duration};
-use util::{post_inc, ResultExt, TryFutureExt};
+use std::{future::Future, sync::Arc, time::Duration};
+use util::{ResultExt, TryFutureExt};
 
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 
@@ -59,7 +59,7 @@ pub enum Event {
 pub struct Room {
     id: u64,
     channel_id: Option<u64>,
-    live_kit: Option<LiveKitRoom>,
+    // live_kit: Option<LiveKitRoom>,
     status: RoomStatus,
     shared_projects: HashSet<WeakHandle<Project>>,
     joined_projects: HashSet<WeakHandle<Project>>,
@@ -95,14 +95,15 @@ impl Room {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn is_connected(&self) -> bool {
-        if let Some(live_kit) = self.live_kit.as_ref() {
-            matches!(
-                *live_kit.room.status().borrow(),
-                live_kit_client::ConnectionState::Connected { .. }
-            )
-        } else {
-            false
-        }
+        false
+        // if let Some(live_kit) = self.live_kit.as_ref() {
+        //     matches!(
+        //         *live_kit.room.status().borrow(),
+        //         live_kit_client::ConnectionState::Connected { .. }
+        //     )
+        // } else {
+        //     false
+        // }
     }
 
     fn new(
@@ -113,124 +114,125 @@ impl Room {
         user_store: Handle<UserStore>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
-            let room = live_kit_client::Room::new();
-            let mut status = room.status();
-            // Consume the initial status of the room.
-            let _ = status.try_recv();
-            let _maintain_room = cx.spawn(|this, mut cx| async move {
-                while let Some(status) = status.next().await {
-                    let this = if let Some(this) = this.upgrade() {
-                        this
-                    } else {
-                        break;
-                    };
-
-                    if status == live_kit_client::ConnectionState::Disconnected {
-                        this.update(&mut cx, |this, cx| this.leave(cx).log_err())
-                            .ok();
-                        break;
-                    }
-                }
-            });
-
-            let mut track_video_changes = room.remote_video_track_updates();
-            let _maintain_video_tracks = cx.spawn(|this, mut cx| async move {
-                while let Some(track_change) = track_video_changes.next().await {
-                    let this = if let Some(this) = this.upgrade() {
-                        this
-                    } else {
-                        break;
-                    };
-
-                    this.update(&mut cx, |this, cx| {
-                        this.remote_video_track_updated(track_change, cx).log_err()
-                    })
-                    .ok();
-                }
-            });
-
-            let mut track_audio_changes = room.remote_audio_track_updates();
-            let _maintain_audio_tracks = cx.spawn(|this, mut cx| async move {
-                while let Some(track_change) = track_audio_changes.next().await {
-                    let this = if let Some(this) = this.upgrade() {
-                        this
-                    } else {
-                        break;
-                    };
-
-                    this.update(&mut cx, |this, cx| {
-                        this.remote_audio_track_updated(track_change, cx).log_err()
-                    })
-                    .ok();
-                }
-            });
-
-            let connect = room.connect(&connection_info.server_url, &connection_info.token);
-            cx.spawn(|this, mut cx| async move {
-                connect.await?;
-
-                if !cx.update(|cx| Self::mute_on_join(cx))? {
-                    this.update(&mut cx, |this, cx| this.share_microphone(cx))?
-                        .await?;
-                }
-
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
-
-            Some(LiveKitRoom {
-                room,
-                screen_track: LocalTrack::None,
-                microphone_track: LocalTrack::None,
-                next_publish_id: 0,
-                muted_by_user: false,
-                deafened: false,
-                speaking: false,
-                _maintain_room,
-                _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks],
-            })
-        } else {
-            None
-        };
-
-        let maintain_connection = cx.spawn({
-            let client = client.clone();
-            move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
-        });
-
-        Audio::play_sound(Sound::Joined, cx);
-
-        let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
-
-        Self {
-            id,
-            channel_id,
-            live_kit: live_kit_room,
-            status: RoomStatus::Online,
-            shared_projects: Default::default(),
-            joined_projects: Default::default(),
-            participant_user_ids: Default::default(),
-            local_participant: Default::default(),
-            remote_participants: Default::default(),
-            pending_participants: Default::default(),
-            pending_call_count: 0,
-            client_subscriptions: vec![
-                client.add_message_handler(cx.weak_handle(), Self::handle_room_updated)
-            ],
-            _subscriptions: vec![
-                cx.on_release(Self::released),
-                cx.on_app_quit(Self::app_will_quit),
-            ],
-            leave_when_empty: false,
-            pending_room_update: None,
-            client,
-            user_store,
-            follows_by_leader_id_project_id: Default::default(),
-            maintain_connection: Some(maintain_connection),
-            room_update_completed_tx,
-            room_update_completed_rx,
-        }
+        todo!()
+        // let _live_kit_room = if let Some(connection_info) = live_kit_connection_info {
+        //     let room = live_kit_client::Room::new();
+        //     let mut status = room.status();
+        //     // Consume the initial status of the room.
+        //     let _ = status.try_recv();
+        //     let _maintain_room = cx.spawn(|this, mut cx| async move {
+        //         while let Some(status) = status.next().await {
+        //             let this = if let Some(this) = this.upgrade() {
+        //                 this
+        //             } else {
+        //                 break;
+        //             };
+
+        //             if status == live_kit_client::ConnectionState::Disconnected {
+        //                 this.update(&mut cx, |this, cx| this.leave(cx).log_err())
+        //                     .ok();
+        //                 break;
+        //             }
+        //         }
+        //     });
+
+        //     let mut track_video_changes = room.remote_video_track_updates();
+        //     let _maintain_video_tracks = cx.spawn(|this, mut cx| async move {
+        //         while let Some(track_change) = track_video_changes.next().await {
+        //             let this = if let Some(this) = this.upgrade() {
+        //                 this
+        //             } else {
+        //                 break;
+        //             };
+
+        //             this.update(&mut cx, |this, cx| {
+        //                 this.remote_video_track_updated(track_change, cx).log_err()
+        //             })
+        //             .ok();
+        //         }
+        //     });
+
+        //     let mut track_audio_changes = room.remote_audio_track_updates();
+        //     let _maintain_audio_tracks = cx.spawn(|this, mut cx| async move {
+        //         while let Some(track_change) = track_audio_changes.next().await {
+        //             let this = if let Some(this) = this.upgrade() {
+        //                 this
+        //             } else {
+        //                 break;
+        //             };
+
+        //             this.update(&mut cx, |this, cx| {
+        //                 this.remote_audio_track_updated(track_change, cx).log_err()
+        //             })
+        //             .ok();
+        //         }
+        //     });
+
+        //     let connect = room.connect(&connection_info.server_url, &connection_info.token);
+        //     cx.spawn(|this, mut cx| async move {
+        //         connect.await?;
+
+        //         if !cx.update(|cx| Self::mute_on_join(cx))? {
+        //             this.update(&mut cx, |this, cx| this.share_microphone(cx))?
+        //                 .await?;
+        //         }
+
+        //         anyhow::Ok(())
+        //     })
+        //     .detach_and_log_err(cx);
+
+        //     Some(LiveKitRoom {
+        //         room,
+        //         screen_track: LocalTrack::None,
+        //         microphone_track: LocalTrack::None,
+        //         next_publish_id: 0,
+        //         muted_by_user: false,
+        //         deafened: false,
+        //         speaking: false,
+        //         _maintain_room,
+        //         _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks],
+        //     })
+        // } else {
+        //     None
+        // };
+
+        // let maintain_connection = cx.spawn({
+        //     let client = client.clone();
+        //     move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
+        // });
+
+        // Audio::play_sound(Sound::Joined, cx);
+
+        // let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
+
+        // Self {
+        //     id,
+        //     channel_id,
+        //     // live_kit: live_kit_room,
+        //     status: RoomStatus::Online,
+        //     shared_projects: Default::default(),
+        //     joined_projects: Default::default(),
+        //     participant_user_ids: Default::default(),
+        //     local_participant: Default::default(),
+        //     remote_participants: Default::default(),
+        //     pending_participants: Default::default(),
+        //     pending_call_count: 0,
+        //     client_subscriptions: vec![
+        //         client.add_message_handler(cx.weak_handle(), Self::handle_room_updated)
+        //     ],
+        //     _subscriptions: vec![
+        //         cx.on_release(Self::released),
+        //         cx.on_app_quit(Self::app_will_quit),
+        //     ],
+        //     leave_when_empty: false,
+        //     pending_room_update: None,
+        //     client,
+        //     user_store,
+        //     follows_by_leader_id_project_id: Default::default(),
+        //     maintain_connection: Some(maintain_connection),
+        //     room_update_completed_tx,
+        //     room_update_completed_rx,
+        // }
     }
 
     pub(crate) fn create(
@@ -416,7 +418,7 @@ impl Room {
         self.pending_participants.clear();
         self.participant_user_ids.clear();
         self.client_subscriptions.clear();
-        self.live_kit.take();
+        // self.live_kit.take();
         self.pending_room_update.take();
         self.maintain_connection.take();
     }
@@ -792,43 +794,43 @@ impl Room {
                                     location,
                                     muted: true,
                                     speaking: false,
-                                    video_tracks: Default::default(),
-                                    audio_tracks: Default::default(),
+                                    // video_tracks: Default::default(),
+                                    // audio_tracks: Default::default(),
                                 },
                             );
 
                             Audio::play_sound(Sound::Joined, cx);
 
-                            if let Some(live_kit) = this.live_kit.as_ref() {
-                                let video_tracks =
-                                    live_kit.room.remote_video_tracks(&user.id.to_string());
-                                let audio_tracks =
-                                    live_kit.room.remote_audio_tracks(&user.id.to_string());
-                                let publications = live_kit
-                                    .room
-                                    .remote_audio_track_publications(&user.id.to_string());
-
-                                for track in video_tracks {
-                                    this.remote_video_track_updated(
-                                        RemoteVideoTrackUpdate::Subscribed(track),
-                                        cx,
-                                    )
-                                    .log_err();
-                                }
-
-                                for (track, publication) in
-                                    audio_tracks.iter().zip(publications.iter())
-                                {
-                                    this.remote_audio_track_updated(
-                                        RemoteAudioTrackUpdate::Subscribed(
-                                            track.clone(),
-                                            publication.clone(),
-                                        ),
-                                        cx,
-                                    )
-                                    .log_err();
-                                }
-                            }
+                            // if let Some(live_kit) = this.live_kit.as_ref() {
+                            //     let video_tracks =
+                            //         live_kit.room.remote_video_tracks(&user.id.to_string());
+                            //     let audio_tracks =
+                            //         live_kit.room.remote_audio_tracks(&user.id.to_string());
+                            //     let publications = live_kit
+                            //         .room
+                            //         .remote_audio_track_publications(&user.id.to_string());
+
+                            //     for track in video_tracks {
+                            //         this.remote_video_track_updated(
+                            //             RemoteVideoTrackUpdate::Subscribed(track),
+                            //             cx,
+                            //         )
+                            //         .log_err();
+                            //     }
+
+                            //     for (track, publication) in
+                            //         audio_tracks.iter().zip(publications.iter())
+                            //     {
+                            //         this.remote_audio_track_updated(
+                            //             RemoteAudioTrackUpdate::Subscribed(
+                            //                 track.clone(),
+                            //                 publication.clone(),
+                            //             ),
+                            //             cx,
+                            //         )
+                            //         .log_err();
+                            //     }
+                            // }
                         }
                     }
 
@@ -916,6 +918,7 @@ impl Room {
         change: RemoteVideoTrackUpdate,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
+        todo!();
         match change {
             RemoteVideoTrackUpdate::Subscribed(track) => {
                 let user_id = track.publisher_id().parse()?;
@@ -924,12 +927,12 @@ impl Room {
                     .remote_participants
                     .get_mut(&user_id)
                     .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
-                participant.video_tracks.insert(
-                    track_id.clone(),
-                    Arc::new(RemoteVideoTrack {
-                        live_kit_track: track,
-                    }),
-                );
+                // participant.video_tracks.insert(
+                //     track_id.clone(),
+                //     Arc::new(RemoteVideoTrack {
+                //         live_kit_track: track,
+                //     }),
+                // );
                 cx.emit(Event::RemoteVideoTracksChanged {
                     participant_id: participant.peer_id,
                 });
@@ -943,7 +946,7 @@ impl Room {
                     .remote_participants
                     .get_mut(&user_id)
                     .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
-                participant.video_tracks.remove(&track_id);
+                // participant.video_tracks.remove(&track_id);
                 cx.emit(Event::RemoteVideoTracksChanged {
                     participant_id: participant.peer_id,
                 });
@@ -973,62 +976,65 @@ impl Room {
                         participant.speaking = false;
                     }
                 }
-                if let Some(id) = self.client.user_id() {
-                    if let Some(room) = &mut self.live_kit {
-                        if let Ok(_) = speaker_ids.binary_search(&id) {
-                            room.speaking = true;
-                        } else {
-                            room.speaking = false;
-                        }
-                    }
-                }
+                // todo!()
+                // if let Some(id) = self.client.user_id() {
+                // if let Some(room) = &mut self.live_kit {
+                //     if let Ok(_) = speaker_ids.binary_search(&id) {
+                //         room.speaking = true;
+                //     } else {
+                //         room.speaking = false;
+                //     }
+                // }
+                // }
                 cx.notify();
             }
             RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => {
-                let mut found = false;
-                for participant in &mut self.remote_participants.values_mut() {
-                    for track in participant.audio_tracks.values() {
-                        if track.sid() == track_id {
-                            found = true;
-                            break;
-                        }
-                    }
-                    if found {
-                        participant.muted = muted;
-                        break;
-                    }
-                }
+                // todo!()
+                // let mut found = false;
+                // for participant in &mut self.remote_participants.values_mut() {
+                //     for track in participant.audio_tracks.values() {
+                //         if track.sid() == track_id {
+                //             found = true;
+                //             break;
+                //         }
+                //     }
+                //     if found {
+                //         participant.muted = muted;
+                //         break;
+                //     }
+                // }
 
                 cx.notify();
             }
             RemoteAudioTrackUpdate::Subscribed(track, publication) => {
-                let user_id = track.publisher_id().parse()?;
-                let track_id = track.sid().to_string();
-                let participant = self
-                    .remote_participants
-                    .get_mut(&user_id)
-                    .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
-
-                participant.audio_tracks.insert(track_id.clone(), track);
-                participant.muted = publication.is_muted();
-
-                cx.emit(Event::RemoteAudioTracksChanged {
-                    participant_id: participant.peer_id,
-                });
+                // todo!()
+                // let user_id = track.publisher_id().parse()?;
+                // let track_id = track.sid().to_string();
+                // let participant = self
+                //     .remote_participants
+                //     .get_mut(&user_id)
+                //     .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
+                // // participant.audio_tracks.insert(track_id.clone(), track);
+                // participant.muted = publication.is_muted();
+
+                // cx.emit(Event::RemoteAudioTracksChanged {
+                //     participant_id: participant.peer_id,
+                // });
             }
             RemoteAudioTrackUpdate::Unsubscribed {
                 publisher_id,
                 track_id,
             } => {
-                let user_id = publisher_id.parse()?;
-                let participant = self
-                    .remote_participants
-                    .get_mut(&user_id)
-                    .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
-                participant.audio_tracks.remove(&track_id);
-                cx.emit(Event::RemoteAudioTracksChanged {
-                    participant_id: participant.peer_id,
-                });
+                // todo!()
+                // let user_id = publisher_id.parse()?;
+                // let participant = self
+                //     .remote_participants
+                //     .get_mut(&user_id)
+                //     .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
+                // participant.audio_tracks.remove(&track_id);
+                // cx.emit(Event::RemoteAudioTracksChanged {
+                //     participant_id: participant.peer_id,
+                // });
             }
         }
 
@@ -1209,269 +1215,278 @@ impl Room {
     }
 
     pub fn is_screen_sharing(&self) -> bool {
-        self.live_kit.as_ref().map_or(false, |live_kit| {
-            !matches!(live_kit.screen_track, LocalTrack::None)
-        })
+        todo!()
+        // self.live_kit.as_ref().map_or(false, |live_kit| {
+        //     !matches!(live_kit.screen_track, LocalTrack::None)
+        // })
     }
 
     pub fn is_sharing_mic(&self) -> bool {
-        self.live_kit.as_ref().map_or(false, |live_kit| {
-            !matches!(live_kit.microphone_track, LocalTrack::None)
-        })
+        todo!()
+        // self.live_kit.as_ref().map_or(false, |live_kit| {
+        //     !matches!(live_kit.microphone_track, LocalTrack::None)
+        // })
     }
 
     pub fn is_muted(&self, cx: &AppContext) -> bool {
-        self.live_kit
-            .as_ref()
-            .and_then(|live_kit| match &live_kit.microphone_track {
-                LocalTrack::None => Some(Self::mute_on_join(cx)),
-                LocalTrack::Pending { muted, .. } => Some(*muted),
-                LocalTrack::Published { muted, .. } => Some(*muted),
-            })
-            .unwrap_or(false)
+        todo!()
+        // self.live_kit
+        //     .as_ref()
+        //     .and_then(|live_kit| match &live_kit.microphone_track {
+        //         LocalTrack::None => Some(Self::mute_on_join(cx)),
+        //         LocalTrack::Pending { muted, .. } => Some(*muted),
+        //         LocalTrack::Published { muted, .. } => Some(*muted),
+        //     })
+        //     .unwrap_or(false)
     }
 
     pub fn is_speaking(&self) -> bool {
-        self.live_kit
-            .as_ref()
-            .map_or(false, |live_kit| live_kit.speaking)
+        todo!()
+        // self.live_kit
+        //     .as_ref()
+        //     .map_or(false, |live_kit| live_kit.speaking)
     }
 
     pub fn is_deafened(&self) -> Option<bool> {
-        self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
+        // self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
+        todo!()
     }
 
     #[track_caller]
     pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        if self.status.is_offline() {
-            return Task::ready(Err(anyhow!("room is offline")));
-        } else if self.is_sharing_mic() {
-            return Task::ready(Err(anyhow!("microphone was already shared")));
-        }
-
-        let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
-            let publish_id = post_inc(&mut live_kit.next_publish_id);
-            live_kit.microphone_track = LocalTrack::Pending {
-                publish_id,
-                muted: false,
-            };
-            cx.notify();
-            publish_id
-        } else {
-            return Task::ready(Err(anyhow!("live-kit was not initialized")));
-        };
-
-        cx.spawn(move |this, mut cx| async move {
-            let publish_track = async {
-                let track = LocalAudioTrack::create();
-                this.upgrade()
-                    .ok_or_else(|| anyhow!("room was dropped"))?
-                    .update(&mut cx, |this, _| {
-                        this.live_kit
-                            .as_ref()
-                            .map(|live_kit| live_kit.room.publish_audio_track(&track))
-                    })?
-                    .ok_or_else(|| anyhow!("live-kit was not initialized"))?
-                    .await
-            };
-
-            let publication = publish_track.await;
-            this.upgrade()
-                .ok_or_else(|| anyhow!("room was dropped"))?
-                .update(&mut cx, |this, cx| {
-                    let live_kit = this
-                        .live_kit
-                        .as_mut()
-                        .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
-
-                    let (canceled, muted) = if let LocalTrack::Pending {
-                        publish_id: cur_publish_id,
-                        muted,
-                    } = &live_kit.microphone_track
-                    {
-                        (*cur_publish_id != publish_id, *muted)
-                    } else {
-                        (true, false)
-                    };
-
-                    match publication {
-                        Ok(publication) => {
-                            if canceled {
-                                live_kit.room.unpublish_track(publication);
-                            } else {
-                                if muted {
-                                    cx.executor().spawn(publication.set_mute(muted)).detach();
-                                }
-                                live_kit.microphone_track = LocalTrack::Published {
-                                    track_publication: publication,
-                                    muted,
-                                };
-                                cx.notify();
-                            }
-                            Ok(())
-                        }
-                        Err(error) => {
-                            if canceled {
-                                Ok(())
-                            } else {
-                                live_kit.microphone_track = LocalTrack::None;
-                                cx.notify();
-                                Err(error)
-                            }
-                        }
-                    }
-                })?
-        })
+        todo!()
+        // if self.status.is_offline() {
+        //     return Task::ready(Err(anyhow!("room is offline")));
+        // } else if self.is_sharing_mic() {
+        //     return Task::ready(Err(anyhow!("microphone was already shared")));
+        // }
+
+        // let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
+        //     let publish_id = post_inc(&mut live_kit.next_publish_id);
+        //     live_kit.microphone_track = LocalTrack::Pending {
+        //         publish_id,
+        //         muted: false,
+        //     };
+        //     cx.notify();
+        //     publish_id
+        // } else {
+        // return Task::ready(Err(anyhow!("live-kit was not initialized")));
+        // };
+
+        // cx.spawn(move |this, mut cx| async move {
+        //     let publish_track = async {
+        //         let track = LocalAudioTrack::create();
+        //         this.upgrade()
+        //             .ok_or_else(|| anyhow!("room was dropped"))?
+        //             .update(&mut cx, |this, _| {
+        //                 this.live_kit
+        //                     .as_ref()
+        //                     .map(|live_kit| live_kit.room.publish_audio_track(track))
+        //             })?
+        //             .ok_or_else(|| anyhow!("live-kit was not initialized"))?
+        //             .await
+        //     };
+
+        //     let publication = publish_track.await;
+        //     this.upgrade()
+        //         .ok_or_else(|| anyhow!("room was dropped"))?
+        //         .update(&mut cx, |this, cx| {
+        //             let live_kit = this
+        //                 .live_kit
+        //                 .as_mut()
+        //                 .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
+
+        //             let (canceled, muted) = if let LocalTrack::Pending {
+        //                 publish_id: cur_publish_id,
+        //                 muted,
+        //             } = &live_kit.microphone_track
+        //             {
+        //                 (*cur_publish_id != publish_id, *muted)
+        //             } else {
+        //                 (true, false)
+        //             };
+
+        //             match publication {
+        //                 Ok(publication) => {
+        //                     if canceled {
+        //                         live_kit.room.unpublish_track(publication);
+        //                     } else {
+        //                         if muted {
+        //                             cx.executor().spawn(publication.set_mute(muted)).detach();
+        //                         }
+        //                         live_kit.microphone_track = LocalTrack::Published {
+        //                             track_publication: publication,
+        //                             muted,
+        //                         };
+        //                         cx.notify();
+        //                     }
+        //                     Ok(())
+        //                 }
+        //                 Err(error) => {
+        //                     if canceled {
+        //                         Ok(())
+        //                     } else {
+        //                         live_kit.microphone_track = LocalTrack::None;
+        //                         cx.notify();
+        //                         Err(error)
+        //                     }
+        //                 }
+        //             }
+        //         })?
+        // })
     }
 
     pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        if self.status.is_offline() {
-            return Task::ready(Err(anyhow!("room is offline")));
-        } else if self.is_screen_sharing() {
-            return Task::ready(Err(anyhow!("screen was already shared")));
-        }
-
-        let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
-            let publish_id = post_inc(&mut live_kit.next_publish_id);
-            live_kit.screen_track = LocalTrack::Pending {
-                publish_id,
-                muted: false,
-            };
-            cx.notify();
-            (live_kit.room.display_sources(), publish_id)
-        } else {
-            return Task::ready(Err(anyhow!("live-kit was not initialized")));
-        };
-
-        cx.spawn(move |this, mut cx| async move {
-            let publish_track = async {
-                let displays = displays.await?;
-                let display = displays
-                    .first()
-                    .ok_or_else(|| anyhow!("no display found"))?;
-                let track = LocalVideoTrack::screen_share_for_display(&display);
-                this.upgrade()
-                    .ok_or_else(|| anyhow!("room was dropped"))?
-                    .update(&mut cx, |this, _| {
-                        this.live_kit
-                            .as_ref()
-                            .map(|live_kit| live_kit.room.publish_video_track(&track))
-                    })?
-                    .ok_or_else(|| anyhow!("live-kit was not initialized"))?
-                    .await
-            };
-
-            let publication = publish_track.await;
-            this.upgrade()
-                .ok_or_else(|| anyhow!("room was dropped"))?
-                .update(&mut cx, |this, cx| {
-                    let live_kit = this
-                        .live_kit
-                        .as_mut()
-                        .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
-
-                    let (canceled, muted) = if let LocalTrack::Pending {
-                        publish_id: cur_publish_id,
-                        muted,
-                    } = &live_kit.screen_track
-                    {
-                        (*cur_publish_id != publish_id, *muted)
-                    } else {
-                        (true, false)
-                    };
-
-                    match publication {
-                        Ok(publication) => {
-                            if canceled {
-                                live_kit.room.unpublish_track(publication);
-                            } else {
-                                if muted {
-                                    cx.executor().spawn(publication.set_mute(muted)).detach();
-                                }
-                                live_kit.screen_track = LocalTrack::Published {
-                                    track_publication: publication,
-                                    muted,
-                                };
-                                cx.notify();
-                            }
-
-                            Audio::play_sound(Sound::StartScreenshare, cx);
-
-                            Ok(())
-                        }
-                        Err(error) => {
-                            if canceled {
-                                Ok(())
-                            } else {
-                                live_kit.screen_track = LocalTrack::None;
-                                cx.notify();
-                                Err(error)
-                            }
-                        }
-                    }
-                })?
-        })
+        todo!()
+        // if self.status.is_offline() {
+        //     return Task::ready(Err(anyhow!("room is offline")));
+        // } else if self.is_screen_sharing() {
+        //     return Task::ready(Err(anyhow!("screen was already shared")));
+        // }
+
+        // let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
+        //     let publish_id = post_inc(&mut live_kit.next_publish_id);
+        //     live_kit.screen_track = LocalTrack::Pending {
+        //         publish_id,
+        //         muted: false,
+        //     };
+        //     cx.notify();
+        //     (live_kit.room.display_sources(), publish_id)
+        // } else {
+        //     return Task::ready(Err(anyhow!("live-kit was not initialized")));
+        // };
+
+        // cx.spawn(move |this, mut cx| async move {
+        //     let publish_track = async {
+        //         let displays = displays.await?;
+        //         let display = displays
+        //             .first()
+        //             .ok_or_else(|| anyhow!("no display found"))?;
+        //         let track = LocalVideoTrack::screen_share_for_display(&display);
+        //         this.upgrade()
+        //             .ok_or_else(|| anyhow!("room was dropped"))?
+        //             .update(&mut cx, |this, _| {
+        //                 this.live_kit
+        //                     .as_ref()
+        //                     .map(|live_kit| live_kit.room.publish_video_track(track))
+        //             })?
+        //             .ok_or_else(|| anyhow!("live-kit was not initialized"))?
+        //             .await
+        //     };
+
+        //     let publication = publish_track.await;
+        //     this.upgrade()
+        //         .ok_or_else(|| anyhow!("room was dropped"))?
+        //         .update(&mut cx, |this, cx| {
+        //             let live_kit = this
+        //                 .live_kit
+        //                 .as_mut()
+        //                 .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
+
+        //             let (canceled, muted) = if let LocalTrack::Pending {
+        //                 publish_id: cur_publish_id,
+        //                 muted,
+        //             } = &live_kit.screen_track
+        //             {
+        //                 (*cur_publish_id != publish_id, *muted)
+        //             } else {
+        //                 (true, false)
+        //             };
+
+        //             match publication {
+        //                 Ok(publication) => {
+        //                     if canceled {
+        //                         live_kit.room.unpublish_track(publication);
+        //                     } else {
+        //                         if muted {
+        //                             cx.executor().spawn(publication.set_mute(muted)).detach();
+        //                         }
+        //                         live_kit.screen_track = LocalTrack::Published {
+        //                             track_publication: publication,
+        //                             muted,
+        //                         };
+        //                         cx.notify();
+        //                     }
+
+        //                     Audio::play_sound(Sound::StartScreenshare, cx);
+
+        //                     Ok(())
+        //                 }
+        //                 Err(error) => {
+        //                     if canceled {
+        //                         Ok(())
+        //                     } else {
+        //                         live_kit.screen_track = LocalTrack::None;
+        //                         cx.notify();
+        //                         Err(error)
+        //                     }
+        //                 }
+        //             }
+        //         })?
+        // })
     }
 
     pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
-        let should_mute = !self.is_muted(cx);
-        if let Some(live_kit) = self.live_kit.as_mut() {
-            if matches!(live_kit.microphone_track, LocalTrack::None) {
-                return Ok(self.share_microphone(cx));
-            }
-
-            let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
-            live_kit.muted_by_user = should_mute;
-
-            if old_muted == true && live_kit.deafened == true {
-                if let Some(task) = self.toggle_deafen(cx).ok() {
-                    task.detach();
-                }
-            }
-
-            Ok(ret_task)
-        } else {
-            Err(anyhow!("LiveKit not started"))
-        }
+        todo!()
+        // let should_mute = !self.is_muted(cx);
+        // if let Some(live_kit) = self.live_kit.as_mut() {
+        //     if matches!(live_kit.microphone_track, LocalTrack::None) {
+        //         return Ok(self.share_microphone(cx));
+        //     }
+
+        //     let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
+        //     live_kit.muted_by_user = should_mute;
+
+        //     if old_muted == true && live_kit.deafened == true {
+        //         if let Some(task) = self.toggle_deafen(cx).ok() {
+        //             task.detach();
+        //         }
+        //     }
+
+        //     Ok(ret_task)
+        // } else {
+        //     Err(anyhow!("LiveKit not started"))
+        // }
     }
 
     pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
-        if let Some(live_kit) = self.live_kit.as_mut() {
-            (*live_kit).deafened = !live_kit.deafened;
-
-            let mut tasks = Vec::with_capacity(self.remote_participants.len());
-            // Context notification is sent within set_mute itself.
-            let mut mute_task = None;
-            // When deafening, mute user's mic as well.
-            // When undeafening, unmute user's mic unless it was manually muted prior to deafening.
-            if live_kit.deafened || !live_kit.muted_by_user {
-                mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0);
-            };
-            for participant in self.remote_participants.values() {
-                for track in live_kit
-                    .room
-                    .remote_audio_track_publications(&participant.user.id.to_string())
-                {
-                    let deafened = live_kit.deafened;
-                    tasks.push(
-                        cx.executor()
-                            .spawn_on_main(move || track.set_enabled(!deafened)),
-                    );
-                }
-            }
-
-            Ok(cx.executor().spawn_on_main(|| async {
-                if let Some(mute_task) = mute_task {
-                    mute_task.await?;
-                }
-                for task in tasks {
-                    task.await?;
-                }
-                Ok(())
-            }))
-        } else {
-            Err(anyhow!("LiveKit not started"))
-        }
+        todo!()
+        // if let Some(live_kit) = self.live_kit.as_mut() {
+        //     (*live_kit).deafened = !live_kit.deafened;
+
+        //     let mut tasks = Vec::with_capacity(self.remote_participants.len());
+        //     // Context notification is sent within set_mute itself.
+        //     let mut mute_task = None;
+        //     // When deafening, mute user's mic as well.
+        //     // When undeafening, unmute user's mic unless it was manually muted prior to deafening.
+        //     if live_kit.deafened || !live_kit.muted_by_user {
+        //         mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0);
+        //     };
+        //     for participant in self.remote_participants.values() {
+        //         for track in live_kit
+        //             .room
+        //             .remote_audio_track_publications(&participant.user.id.to_string())
+        //         {
+        //             let deafened = live_kit.deafened;
+        //             tasks.push(
+        //                 cx.executor()
+        //                     .spawn_on_main(move || track.set_enabled(!deafened)),
+        //             );
+        //         }
+        //     }
+
+        //     Ok(cx.executor().spawn_on_main(|| async {
+        //         if let Some(mute_task) = mute_task {
+        //             mute_task.await?;
+        //         }
+        //         for task in tasks {
+        //             task.await?;
+        //         }
+        //         Ok(())
+        //     }))
+        // } else {
+        //     Err(anyhow!("LiveKit not started"))
+        // }
     }
 
     pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {

crates/channel/src/channel.rs 🔗

@@ -7,10 +7,11 @@ use gpui::{AppContext, ModelHandle};
 use std::sync::Arc;
 
 pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
-pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
-pub use channel_store::{
-    Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
+pub use channel_chat::{
+    mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
+    MessageParams,
 };
+pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore};
 
 #[cfg(test)]
 mod channel_store_tests;

crates/channel/src/channel_buffer.rs 🔗

@@ -1,4 +1,4 @@
-use crate::Channel;
+use crate::{Channel, ChannelId, ChannelStore};
 use anyhow::Result;
 use client::{Client, Collaborator, UserStore};
 use collections::HashMap;
@@ -19,10 +19,11 @@ pub(crate) fn init(client: &Arc<Client>) {
 }
 
 pub struct ChannelBuffer {
-    pub(crate) channel: Arc<Channel>,
+    pub channel_id: ChannelId,
     connected: bool,
     collaborators: HashMap<PeerId, Collaborator>,
     user_store: ModelHandle<UserStore>,
+    channel_store: ModelHandle<ChannelStore>,
     buffer: ModelHandle<language::Buffer>,
     buffer_epoch: u64,
     client: Arc<Client>,
@@ -34,6 +35,7 @@ pub enum ChannelBufferEvent {
     CollaboratorsChanged,
     Disconnected,
     BufferEdited,
+    ChannelChanged,
 }
 
 impl Entity for ChannelBuffer {
@@ -46,7 +48,7 @@ impl Entity for ChannelBuffer {
             }
             self.client
                 .send(proto::LeaveChannelBuffer {
-                    channel_id: self.channel.id,
+                    channel_id: self.channel_id,
                 })
                 .log_err();
         }
@@ -58,6 +60,7 @@ impl ChannelBuffer {
         channel: Arc<Channel>,
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
+        channel_store: ModelHandle<ChannelStore>,
         mut cx: AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
         let response = client
@@ -90,9 +93,10 @@ impl ChannelBuffer {
                 connected: true,
                 collaborators: Default::default(),
                 acknowledge_task: None,
-                channel,
+                channel_id: channel.id,
                 subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
                 user_store,
+                channel_store,
             };
             this.replace_collaborators(response.collaborators, cx);
             this
@@ -179,7 +183,7 @@ impl ChannelBuffer {
                 let operation = language::proto::serialize_operation(operation);
                 self.client
                     .send(proto::UpdateChannelBuffer {
-                        channel_id: self.channel.id,
+                        channel_id: self.channel_id,
                         operations: vec![operation],
                     })
                     .log_err();
@@ -223,12 +227,15 @@ impl ChannelBuffer {
         &self.collaborators
     }
 
-    pub fn channel(&self) -> Arc<Channel> {
-        self.channel.clone()
+    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+        self.channel_store
+            .read(cx)
+            .channel_for_id(self.channel_id)
+            .cloned()
     }
 
     pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
-        log::info!("channel buffer {} disconnected", self.channel.id);
+        log::info!("channel buffer {} disconnected", self.channel_id);
         if self.connected {
             self.connected = false;
             self.subscription.take();
@@ -237,6 +244,11 @@ impl ChannelBuffer {
         }
     }
 
+    pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext<Self>) {
+        cx.emit(ChannelBufferEvent::ChannelChanged);
+        cx.notify()
+    }
+
     pub fn is_connected(&self) -> bool {
         self.connected
     }

crates/channel/src/channel_chat.rs 🔗

@@ -3,19 +3,25 @@ use anyhow::{anyhow, Result};
 use client::{
     proto,
     user::{User, UserStore},
-    Client, Subscription, TypedEnvelope,
+    Client, Subscription, TypedEnvelope, UserId,
 };
 use futures::lock::Mutex;
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
 use rand::prelude::*;
-use std::{collections::HashSet, mem, ops::Range, sync::Arc};
+use std::{
+    collections::HashSet,
+    mem,
+    ops::{ControlFlow, Range},
+    sync::Arc,
+};
 use sum_tree::{Bias, SumTree};
 use time::OffsetDateTime;
 use util::{post_inc, ResultExt as _, TryFutureExt};
 
 pub struct ChannelChat {
-    channel: Arc<Channel>,
+    pub channel_id: ChannelId,
     messages: SumTree<ChannelMessage>,
+    acknowledged_message_ids: HashSet<u64>,
     channel_store: ModelHandle<ChannelStore>,
     loaded_all_messages: bool,
     last_acknowledged_id: Option<u64>,
@@ -27,6 +33,12 @@ pub struct ChannelChat {
     _subscription: Subscription,
 }
 
+#[derive(Debug, PartialEq, Eq)]
+pub struct MessageParams {
+    pub text: String,
+    pub mentions: Vec<(Range<usize>, UserId)>,
+}
+
 #[derive(Clone, Debug)]
 pub struct ChannelMessage {
     pub id: ChannelMessageId,
@@ -34,6 +46,7 @@ pub struct ChannelMessage {
     pub timestamp: OffsetDateTime,
     pub sender: Arc<User>,
     pub nonce: u128,
+    pub mentions: Vec<(Range<usize>, UserId)>,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -74,7 +87,7 @@ impl Entity for ChannelChat {
     fn release(&mut self, _: &mut AppContext) {
         self.rpc
             .send(proto::LeaveChannelChat {
-                channel_id: self.channel.id,
+                channel_id: self.channel_id,
             })
             .log_err();
     }
@@ -99,12 +112,13 @@ impl ChannelChat {
 
         Ok(cx.add_model(|cx| {
             let mut this = Self {
-                channel,
+                channel_id: channel.id,
                 user_store,
                 channel_store,
                 rpc: client,
                 outgoing_messages_lock: Default::default(),
                 messages: Default::default(),
+                acknowledged_message_ids: Default::default(),
                 loaded_all_messages,
                 next_pending_message_id: 0,
                 last_acknowledged_id: None,
@@ -116,16 +130,23 @@ impl ChannelChat {
         }))
     }
 
-    pub fn channel(&self) -> &Arc<Channel> {
-        &self.channel
+    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+        self.channel_store
+            .read(cx)
+            .channel_for_id(self.channel_id)
+            .cloned()
+    }
+
+    pub fn client(&self) -> &Arc<Client> {
+        &self.rpc
     }
 
     pub fn send_message(
         &mut self,
-        body: String,
+        message: MessageParams,
         cx: &mut ModelContext<Self>,
-    ) -> Result<Task<Result<()>>> {
-        if body.is_empty() {
+    ) -> Result<Task<Result<u64>>> {
+        if message.text.is_empty() {
             Err(anyhow!("message body can't be empty"))?;
         }
 
@@ -135,16 +156,17 @@ impl ChannelChat {
             .current_user()
             .ok_or_else(|| anyhow!("current_user is not present"))?;
 
-        let channel_id = self.channel.id;
+        let channel_id = self.channel_id;
         let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
         let nonce = self.rng.gen();
         self.insert_messages(
             SumTree::from_item(
                 ChannelMessage {
                     id: pending_id,
-                    body: body.clone(),
+                    body: message.text.clone(),
                     sender: current_user,
                     timestamp: OffsetDateTime::now_utc(),
+                    mentions: message.mentions.clone(),
                     nonce,
                 },
                 &(),
@@ -158,27 +180,25 @@ impl ChannelChat {
             let outgoing_message_guard = outgoing_messages_lock.lock().await;
             let request = rpc.request(proto::SendChannelMessage {
                 channel_id,
-                body,
+                body: message.text,
                 nonce: Some(nonce.into()),
+                mentions: mentions_to_proto(&message.mentions),
             });
             let response = request.await?;
             drop(outgoing_message_guard);
-            let message = ChannelMessage::from_proto(
-                response.message.ok_or_else(|| anyhow!("invalid message"))?,
-                &user_store,
-                &mut cx,
-            )
-            .await?;
+            let response = response.message.ok_or_else(|| anyhow!("invalid message"))?;
+            let id = response.id;
+            let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?;
             this.update(&mut cx, |this, cx| {
                 this.insert_messages(SumTree::from_item(message, &()), cx);
-                Ok(())
+                Ok(id)
             })
         }))
     }
 
     pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         let response = self.rpc.request(proto::RemoveChannelMessage {
-            channel_id: self.channel.id,
+            channel_id: self.channel_id,
             message_id: id,
         });
         cx.spawn(|this, mut cx| async move {
@@ -191,41 +211,76 @@ impl ChannelChat {
         })
     }
 
-    pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
-        if !self.loaded_all_messages {
-            let rpc = self.rpc.clone();
-            let user_store = self.user_store.clone();
-            let channel_id = self.channel.id;
-            if let Some(before_message_id) =
-                self.messages.first().and_then(|message| match message.id {
-                    ChannelMessageId::Saved(id) => Some(id),
-                    ChannelMessageId::Pending(_) => None,
-                })
-            {
-                cx.spawn(|this, mut cx| {
-                    async move {
-                        let response = rpc
-                            .request(proto::GetChannelMessages {
-                                channel_id,
-                                before_message_id,
-                            })
-                            .await?;
-                        let loaded_all_messages = response.done;
-                        let messages =
-                            messages_from_proto(response.messages, &user_store, &mut cx).await?;
-                        this.update(&mut cx, |this, cx| {
-                            this.loaded_all_messages = loaded_all_messages;
-                            this.insert_messages(messages, cx);
-                        });
-                        anyhow::Ok(())
+    pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Option<()>>> {
+        if self.loaded_all_messages {
+            return None;
+        }
+
+        let rpc = self.rpc.clone();
+        let user_store = self.user_store.clone();
+        let channel_id = self.channel_id;
+        let before_message_id = self.first_loaded_message_id()?;
+        Some(cx.spawn(|this, mut cx| {
+            async move {
+                let response = rpc
+                    .request(proto::GetChannelMessages {
+                        channel_id,
+                        before_message_id,
+                    })
+                    .await?;
+                let loaded_all_messages = response.done;
+                let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
+                this.update(&mut cx, |this, cx| {
+                    this.loaded_all_messages = loaded_all_messages;
+                    this.insert_messages(messages, cx);
+                });
+                anyhow::Ok(())
+            }
+            .log_err()
+        }))
+    }
+
+    pub fn first_loaded_message_id(&mut self) -> Option<u64> {
+        self.messages.first().and_then(|message| match message.id {
+            ChannelMessageId::Saved(id) => Some(id),
+            ChannelMessageId::Pending(_) => None,
+        })
+    }
+
+    /// Load all of the chat messages since a certain message id.
+    ///
+    /// For now, we always maintain a suffix of the channel's messages.
+    pub async fn load_history_since_message(
+        chat: ModelHandle<Self>,
+        message_id: u64,
+        mut cx: AsyncAppContext,
+    ) -> Option<usize> {
+        loop {
+            let step = chat.update(&mut cx, |chat, cx| {
+                if let Some(first_id) = chat.first_loaded_message_id() {
+                    if first_id <= message_id {
+                        let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>();
+                        let message_id = ChannelMessageId::Saved(message_id);
+                        cursor.seek(&message_id, Bias::Left, &());
+                        return ControlFlow::Break(
+                            if cursor
+                                .item()
+                                .map_or(false, |message| message.id == message_id)
+                            {
+                                Some(cursor.start().1 .0)
+                            } else {
+                                None
+                            },
+                        );
                     }
-                    .log_err()
-                })
-                .detach();
-                return true;
+                }
+                ControlFlow::Continue(chat.load_more_messages(cx))
+            });
+            match step {
+                ControlFlow::Break(ix) => return ix,
+                ControlFlow::Continue(task) => task?.await?,
             }
         }
-        false
     }
 
     pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
@@ -236,13 +291,13 @@ impl ChannelChat {
             {
                 self.rpc
                     .send(proto::AckChannelMessage {
-                        channel_id: self.channel.id,
+                        channel_id: self.channel_id,
                         message_id: latest_message_id,
                     })
                     .ok();
                 self.last_acknowledged_id = Some(latest_message_id);
                 self.channel_store.update(cx, |store, cx| {
-                    store.acknowledge_message_id(self.channel.id, latest_message_id, cx);
+                    store.acknowledge_message_id(self.channel_id, latest_message_id, cx);
                 });
             }
         }
@@ -251,7 +306,7 @@ impl ChannelChat {
     pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
         let user_store = self.user_store.clone();
         let rpc = self.rpc.clone();
-        let channel_id = self.channel.id;
+        let channel_id = self.channel_id;
         cx.spawn(|this, mut cx| {
             async move {
                 let response = rpc.request(proto::JoinChannelChat { channel_id }).await?;
@@ -284,6 +339,7 @@ impl ChannelChat {
                     let request = rpc.request(proto::SendChannelMessage {
                         channel_id,
                         body: pending_message.body,
+                        mentions: mentions_to_proto(&pending_message.mentions),
                         nonce: Some(pending_message.nonce.into()),
                     });
                     let response = request.await?;
@@ -319,6 +375,17 @@ impl ChannelChat {
         cursor.item().unwrap()
     }
 
+    pub fn acknowledge_message(&mut self, id: u64) {
+        if self.acknowledged_message_ids.insert(id) {
+            self.rpc
+                .send(proto::AckChannelMessage {
+                    channel_id: self.channel_id,
+                    message_id: id,
+                })
+                .ok();
+        }
+    }
+
     pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
         let mut cursor = self.messages.cursor::<Count>();
         cursor.seek(&Count(range.start), Bias::Right, &());
@@ -348,7 +415,7 @@ impl ChannelChat {
         this.update(&mut cx, |this, cx| {
             this.insert_messages(SumTree::from_item(message, &()), cx);
             cx.emit(ChannelChatEvent::NewMessage {
-                channel_id: this.channel.id,
+                channel_id: this.channel_id,
                 message_id,
             })
         });
@@ -451,22 +518,7 @@ async fn messages_from_proto(
     user_store: &ModelHandle<UserStore>,
     cx: &mut AsyncAppContext,
 ) -> Result<SumTree<ChannelMessage>> {
-    let unique_user_ids = proto_messages
-        .iter()
-        .map(|m| m.sender_id)
-        .collect::<HashSet<_>>()
-        .into_iter()
-        .collect();
-    user_store
-        .update(cx, |user_store, cx| {
-            user_store.get_users(unique_user_ids, cx)
-        })
-        .await?;
-
-    let mut messages = Vec::with_capacity(proto_messages.len());
-    for message in proto_messages {
-        messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
-    }
+    let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?;
     let mut result = SumTree::new();
     result.extend(messages, &());
     Ok(result)
@@ -486,6 +538,14 @@ impl ChannelMessage {
         Ok(ChannelMessage {
             id: ChannelMessageId::Saved(message.id),
             body: message.body,
+            mentions: message
+                .mentions
+                .into_iter()
+                .filter_map(|mention| {
+                    let range = mention.range?;
+                    Some((range.start as usize..range.end as usize, mention.user_id))
+                })
+                .collect(),
             timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
             sender,
             nonce: message
@@ -498,6 +558,43 @@ impl ChannelMessage {
     pub fn is_pending(&self) -> bool {
         matches!(self.id, ChannelMessageId::Pending(_))
     }
+
+    pub async fn from_proto_vec(
+        proto_messages: Vec<proto::ChannelMessage>,
+        user_store: &ModelHandle<UserStore>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Vec<Self>> {
+        let unique_user_ids = proto_messages
+            .iter()
+            .map(|m| m.sender_id)
+            .collect::<HashSet<_>>()
+            .into_iter()
+            .collect();
+        user_store
+            .update(cx, |user_store, cx| {
+                user_store.get_users(unique_user_ids, cx)
+            })
+            .await?;
+
+        let mut messages = Vec::with_capacity(proto_messages.len());
+        for message in proto_messages {
+            messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
+        }
+        Ok(messages)
+    }
+}
+
+pub fn mentions_to_proto(mentions: &[(Range<usize>, UserId)]) -> Vec<proto::ChatMention> {
+    mentions
+        .iter()
+        .map(|(range, user_id)| proto::ChatMention {
+            range: Some(proto::Range {
+                start: range.start as u64,
+                end: range.end as u64,
+            }),
+            user_id: *user_id as u64,
+        })
+        .collect()
 }
 
 impl sum_tree::Item for ChannelMessage {
@@ -538,3 +635,12 @@ impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
         self.0 += summary.count;
     }
 }
+
+impl<'a> From<&'a str> for MessageParams {
+    fn from(value: &'a str) -> Self {
+        Self {
+            text: value.into(),
+            mentions: Vec::new(),
+        }
+    }
+}

crates/channel/src/channel_store.rs 🔗

@@ -1,6 +1,6 @@
 mod channel_index;
 
-use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
+use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
 use anyhow::{anyhow, Result};
 use channel_index::ChannelIndex;
 use client::{Client, Subscription, User, UserId, UserStore};
@@ -9,11 +9,10 @@ use db::RELEASE_CHANNEL;
 use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
 use rpc::{
-    proto::{self, ChannelEdge, ChannelPermission},
+    proto::{self, ChannelVisibility},
     TypedEnvelope,
 };
-use serde_derive::{Deserialize, Serialize};
-use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
+use std::{mem, sync::Arc, time::Duration};
 use util::ResultExt;
 
 pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
@@ -27,10 +26,9 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 pub type ChannelId = u64;
 
 pub struct ChannelStore {
-    channel_index: ChannelIndex,
+    pub channel_index: ChannelIndex,
     channel_invitations: Vec<Arc<Channel>>,
     channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
-    channels_with_admin_privileges: HashSet<ChannelId>,
     outgoing_invites: HashSet<(ChannelId, UserId)>,
     update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
     opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
@@ -43,14 +41,15 @@ pub struct ChannelStore {
     _update_channels: Task<()>,
 }
 
-pub type ChannelData = (Channel, ChannelPath);
-
 #[derive(Clone, Debug, PartialEq)]
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
+    pub visibility: proto::ChannelVisibility,
+    pub role: proto::ChannelRole,
     pub unseen_note_version: Option<(u64, clock::Global)>,
     pub unseen_message_id: Option<u64>,
+    pub parent_path: Vec<u64>,
 }
 
 impl Channel {
@@ -71,15 +70,41 @@ impl Channel {
 
         slug.trim_matches(|c| c == '-').to_string()
     }
-}
 
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
-pub struct ChannelPath(Arc<[ChannelId]>);
+    pub fn can_edit_notes(&self) -> bool {
+        self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin
+    }
+}
 
 pub struct ChannelMembership {
     pub user: Arc<User>,
     pub kind: proto::channel_member::Kind,
-    pub admin: bool,
+    pub role: proto::ChannelRole,
+}
+impl ChannelMembership {
+    pub fn sort_key(&self) -> MembershipSortKey {
+        MembershipSortKey {
+            role_order: match self.role {
+                proto::ChannelRole::Admin => 0,
+                proto::ChannelRole::Member => 1,
+                proto::ChannelRole::Banned => 2,
+                proto::ChannelRole::Guest => 3,
+            },
+            kind_order: match self.kind {
+                proto::channel_member::Kind::Member => 0,
+                proto::channel_member::Kind::AncestorMember => 1,
+                proto::channel_member::Kind::Invitee => 2,
+            },
+            username_order: self.user.github_login.as_str(),
+        }
+    }
+}
+
+#[derive(PartialOrd, Ord, PartialEq, Eq)]
+pub struct MembershipSortKey<'a> {
+    role_order: u8,
+    kind_order: u8,
+    username_order: &'a str,
 }
 
 pub enum ChannelEvent {
@@ -127,9 +152,6 @@ impl ChannelStore {
                         this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx));
                     }
                 }
-                if status.is_connected() {
-                } else {
-                }
             }
             Some(())
         });
@@ -138,7 +160,6 @@ impl ChannelStore {
             channel_invitations: Vec::default(),
             channel_index: ChannelIndex::default(),
             channel_participants: Default::default(),
-            channels_with_admin_privileges: Default::default(),
             outgoing_invites: Default::default(),
             opened_buffers: Default::default(),
             opened_chats: Default::default(),
@@ -167,16 +188,6 @@ impl ChannelStore {
         self.client.clone()
     }
 
-    pub fn has_children(&self, channel_id: ChannelId) -> bool {
-        self.channel_index.iter().any(|path| {
-            if let Some(ix) = path.iter().position(|id| *id == channel_id) {
-                path.len() > ix + 1
-            } else {
-                false
-            }
-        })
-    }
-
     /// Returns the number of unique channels in the store
     pub fn channel_count(&self) -> usize {
         self.channel_index.by_id().len()
@@ -196,26 +207,31 @@ impl ChannelStore {
     }
 
     /// Iterate over all entries in the channel DAG
-    pub fn channel_dag_entries(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
-        self.channel_index.iter().map(move |path| {
-            let id = path.last().unwrap();
-            let channel = self.channel_for_id(*id).unwrap();
-            (path.len() - 1, channel)
-        })
+    pub fn ordered_channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
+        self.channel_index
+            .ordered_channels()
+            .iter()
+            .filter_map(move |id| {
+                let channel = self.channel_index.by_id().get(id)?;
+                Some((channel.parent_path.len(), channel))
+            })
     }
 
-    pub fn channel_dag_entry_at(&self, ix: usize) -> Option<(&Arc<Channel>, &ChannelPath)> {
-        let path = self.channel_index.get(ix)?;
-        let id = path.last().unwrap();
-        let channel = self.channel_for_id(*id).unwrap();
-
-        Some((channel, path))
+    pub fn channel_at_index(&self, ix: usize) -> Option<&Arc<Channel>> {
+        let channel_id = self.channel_index.ordered_channels().get(ix)?;
+        self.channel_index.by_id().get(channel_id)
     }
 
     pub fn channel_at(&self, ix: usize) -> Option<&Arc<Channel>> {
         self.channel_index.by_id().values().nth(ix)
     }
 
+    pub fn has_channel_invitation(&self, channel_id: ChannelId) -> bool {
+        self.channel_invitations
+            .iter()
+            .any(|channel| channel.id == channel_id)
+    }
+
     pub fn channel_invitations(&self) -> &[Arc<Channel>] {
         &self.channel_invitations
     }
@@ -240,14 +256,42 @@ impl ChannelStore {
     ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
         let client = self.client.clone();
         let user_store = self.user_store.clone();
+        let channel_store = cx.handle();
         self.open_channel_resource(
             channel_id,
             |this| &mut this.opened_buffers,
-            |channel, cx| ChannelBuffer::new(channel, client, user_store, cx),
+            |channel, cx| ChannelBuffer::new(channel, client, user_store, channel_store, cx),
             cx,
         )
     }
 
+    pub fn fetch_channel_messages(
+        &self,
+        message_ids: Vec<u64>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<ChannelMessage>>> {
+        let request = if message_ids.is_empty() {
+            None
+        } else {
+            Some(
+                self.client
+                    .request(proto::GetChannelMessagesById { message_ids }),
+            )
+        };
+        cx.spawn_weak(|this, mut cx| async move {
+            if let Some(request) = request {
+                let response = request.await?;
+                let this = this
+                    .upgrade(&cx)
+                    .ok_or_else(|| anyhow!("channel store dropped"))?;
+                let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
+                ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await
+            } else {
+                Ok(Vec::new())
+            }
+        })
+    }
+
     pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> {
         self.channel_index
             .by_id()
@@ -393,16 +437,11 @@ impl ChannelStore {
             .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) })
     }
 
-    pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
-        self.channel_index.iter().any(|path| {
-            if let Some(ix) = path.iter().position(|id| *id == channel_id) {
-                path[..=ix]
-                    .iter()
-                    .any(|id| self.channels_with_admin_privileges.contains(id))
-            } else {
-                false
-            }
-        })
+    pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool {
+        let Some(channel) = self.channel_for_id(channel_id) else {
+            return false;
+        };
+        channel.role == proto::ChannelRole::Admin
     }
 
     pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
@@ -429,24 +468,19 @@ impl ChannelStore {
                 .ok_or_else(|| anyhow!("missing channel in response"))?;
             let channel_id = channel.id;
 
-            let parent_edge = if let Some(parent_id) = parent_id {
-                vec![ChannelEdge {
-                    channel_id: channel.id,
-                    parent_id,
-                }]
-            } else {
-                vec![]
-            };
+            // let parent_edge = if let Some(parent_id) = parent_id {
+            //     vec![ChannelEdge {
+            //         channel_id: channel.id,
+            //         parent_id,
+            //     }]
+            // } else {
+            //     vec![]
+            // };
 
             this.update(&mut cx, |this, cx| {
                 let task = this.update_channels(
                     proto::UpdateChannels {
                         channels: vec![channel],
-                        insert_edge: parent_edge,
-                        channel_permissions: vec![ChannelPermission {
-                            channel_id,
-                            is_admin: true,
-                        }],
                         ..Default::default()
                     },
                     cx,
@@ -464,52 +498,34 @@ impl ChannelStore {
         })
     }
 
-    pub fn link_channel(
-        &mut self,
-        channel_id: ChannelId,
-        to: ChannelId,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<Result<()>> {
-        let client = self.client.clone();
-        cx.spawn(|_, _| async move {
-            let _ = client
-                .request(proto::LinkChannel { channel_id, to })
-                .await?;
-
-            Ok(())
-        })
-    }
-
-    pub fn unlink_channel(
+    pub fn move_channel(
         &mut self,
         channel_id: ChannelId,
-        from: ChannelId,
+        to: Option<ChannelId>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         let client = self.client.clone();
         cx.spawn(|_, _| async move {
             let _ = client
-                .request(proto::UnlinkChannel { channel_id, from })
+                .request(proto::MoveChannel { channel_id, to })
                 .await?;
 
             Ok(())
         })
     }
 
-    pub fn move_channel(
+    pub fn set_channel_visibility(
         &mut self,
         channel_id: ChannelId,
-        from: ChannelId,
-        to: ChannelId,
+        visibility: ChannelVisibility,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         let client = self.client.clone();
         cx.spawn(|_, _| async move {
             let _ = client
-                .request(proto::MoveChannel {
+                .request(proto::SetChannelVisibility {
                     channel_id,
-                    from,
-                    to,
+                    visibility: visibility.into(),
                 })
                 .await?;
 
@@ -521,7 +537,7 @@ impl ChannelStore {
         &mut self,
         channel_id: ChannelId,
         user_id: UserId,
-        admin: bool,
+        role: proto::ChannelRole,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         if !self.outgoing_invites.insert((channel_id, user_id)) {
@@ -535,7 +551,7 @@ impl ChannelStore {
                 .request(proto::InviteChannelMember {
                     channel_id,
                     user_id,
-                    admin,
+                    role: role.into(),
                 })
                 .await;
 
@@ -579,11 +595,11 @@ impl ChannelStore {
         })
     }
 
-    pub fn set_member_admin(
+    pub fn set_member_role(
         &mut self,
         channel_id: ChannelId,
         user_id: UserId,
-        admin: bool,
+        role: proto::ChannelRole,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         if !self.outgoing_invites.insert((channel_id, user_id)) {
@@ -594,10 +610,10 @@ impl ChannelStore {
         let client = self.client.clone();
         cx.spawn(|this, mut cx| async move {
             let result = client
-                .request(proto::SetChannelMemberAdmin {
+                .request(proto::SetChannelMemberRole {
                     channel_id,
                     user_id,
-                    admin,
+                    role: role.into(),
                 })
                 .await;
 
@@ -649,14 +665,15 @@ impl ChannelStore {
         &mut self,
         channel_id: ChannelId,
         accept: bool,
-    ) -> impl Future<Output = Result<()>> {
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
         let client = self.client.clone();
-        async move {
+        cx.background().spawn(async move {
             client
                 .request(proto::RespondToChannelInvite { channel_id, accept })
                 .await?;
             Ok(())
-        }
+        })
     }
 
     pub fn get_channel_member_details(
@@ -685,8 +702,8 @@ impl ChannelStore {
                 .filter_map(|(user, member)| {
                     Some(ChannelMembership {
                         user,
-                        admin: member.admin,
-                        kind: proto::channel_member::Kind::from_i32(member.kind)?,
+                        role: member.role(),
+                        kind: member.kind(),
                     })
                 })
                 .collect())
@@ -724,6 +741,11 @@ impl ChannelStore {
     }
 
     fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        self.channel_index.clear();
+        self.channel_invitations.clear();
+        self.channel_participants.clear();
+        self.channel_index.clear();
+        self.outgoing_invites.clear();
         self.disconnect_channel_buffers_task.take();
 
         for chat in self.opened_chats.values() {
@@ -743,7 +765,7 @@ impl ChannelStore {
                     let channel_buffer = buffer.read(cx);
                     let buffer = channel_buffer.buffer().read(cx);
                     buffer_versions.push(proto::ChannelBufferVersion {
-                        channel_id: channel_buffer.channel().id,
+                        channel_id: channel_buffer.channel_id,
                         epoch: channel_buffer.epoch(),
                         version: language::proto::serialize_version(&buffer.version()),
                     });
@@ -770,13 +792,13 @@ impl ChannelStore {
                         };
 
                         channel_buffer.update(cx, |channel_buffer, cx| {
-                            let channel_id = channel_buffer.channel().id;
+                            let channel_id = channel_buffer.channel_id;
                             if let Some(remote_buffer) = response
                                 .buffers
                                 .iter_mut()
                                 .find(|buffer| buffer.channel_id == channel_id)
                             {
-                                let channel_id = channel_buffer.channel().id;
+                                let channel_id = channel_buffer.channel_id;
                                 let remote_version =
                                     language::proto::deserialize_version(&remote_buffer.version);
 
@@ -833,12 +855,6 @@ impl ChannelStore {
     }
 
     fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
-        self.channel_index.clear();
-        self.channel_invitations.clear();
-        self.channel_participants.clear();
-        self.channels_with_admin_privileges.clear();
-        self.channel_index.clear();
-        self.outgoing_invites.clear();
         cx.notify();
 
         self.disconnect_channel_buffers_task.get_or_insert_with(|| {
@@ -881,9 +897,12 @@ impl ChannelStore {
                     ix,
                     Arc::new(Channel {
                         id: channel.id,
+                        visibility: channel.visibility(),
+                        role: channel.role(),
                         name: channel.name,
                         unseen_note_version: None,
                         unseen_message_id: None,
+                        parent_path: channel.parent_path,
                     }),
                 ),
             }
@@ -891,8 +910,6 @@ impl ChannelStore {
 
         let channels_changed = !payload.channels.is_empty()
             || !payload.delete_channels.is_empty()
-            || !payload.insert_edge.is_empty()
-            || !payload.delete_edge.is_empty()
             || !payload.unseen_channel_messages.is_empty()
             || !payload.unseen_channel_buffer_changes.is_empty();
 
@@ -900,12 +917,17 @@ impl ChannelStore {
             if !payload.delete_channels.is_empty() {
                 self.channel_index.delete_channels(&payload.delete_channels);
                 self.channel_participants
-                    .retain(|channel_id, _| !payload.delete_channels.contains(channel_id));
-                self.channels_with_admin_privileges
-                    .retain(|channel_id| !payload.delete_channels.contains(channel_id));
+                    .retain(|channel_id, _| !&payload.delete_channels.contains(channel_id));
 
                 for channel_id in &payload.delete_channels {
                     let channel_id = *channel_id;
+                    if payload
+                        .channels
+                        .iter()
+                        .any(|channel| channel.id == channel_id)
+                    {
+                        continue;
+                    }
                     if let Some(OpenedModelHandle::Open(buffer)) =
                         self.opened_buffers.remove(&channel_id)
                     {
@@ -918,7 +940,16 @@ impl ChannelStore {
 
             let mut index = self.channel_index.bulk_insert();
             for channel in payload.channels {
-                index.insert(channel)
+                let id = channel.id;
+                let channel_changed = index.insert(channel);
+
+                if channel_changed {
+                    if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.get(&id) {
+                        if let Some(buffer) = buffer.upgrade(cx) {
+                            buffer.update(cx, ChannelBuffer::channel_changed);
+                        }
+                    }
+                }
             }
 
             for unseen_buffer_change in payload.unseen_channel_buffer_changes {
@@ -936,24 +967,6 @@ impl ChannelStore {
                     unseen_channel_message.message_id,
                 );
             }
-
-            for edge in payload.insert_edge {
-                index.insert_edge(edge.channel_id, edge.parent_id);
-            }
-
-            for edge in payload.delete_edge {
-                index.delete_edge(edge.parent_id, edge.channel_id);
-            }
-        }
-
-        for permission in payload.channel_permissions {
-            if permission.is_admin {
-                self.channels_with_admin_privileges
-                    .insert(permission.channel_id);
-            } else {
-                self.channels_with_admin_privileges
-                    .remove(&permission.channel_id);
-            }
         }
 
         cx.notify();
@@ -1002,44 +1015,3 @@ impl ChannelStore {
         }))
     }
 }
-
-impl Deref for ChannelPath {
-    type Target = [ChannelId];
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl ChannelPath {
-    pub fn new(path: Arc<[ChannelId]>) -> Self {
-        debug_assert!(path.len() >= 1);
-        Self(path)
-    }
-
-    pub fn parent_id(&self) -> Option<ChannelId> {
-        self.0.len().checked_sub(2).map(|i| self.0[i])
-    }
-
-    pub fn channel_id(&self) -> ChannelId {
-        self.0[self.0.len() - 1]
-    }
-}
-
-impl From<ChannelPath> for Cow<'static, ChannelPath> {
-    fn from(value: ChannelPath) -> Self {
-        Cow::Owned(value)
-    }
-}
-
-impl<'a> From<&'a ChannelPath> for Cow<'a, ChannelPath> {
-    fn from(value: &'a ChannelPath) -> Self {
-        Cow::Borrowed(value)
-    }
-}
-
-impl Default for ChannelPath {
-    fn default() -> Self {
-        ChannelPath(Arc::from([]))
-    }
-}

crates/channel/src/channel_store/channel_index.rs 🔗

@@ -1,14 +1,11 @@
-use std::{ops::Deref, sync::Arc};
-
 use crate::{Channel, ChannelId};
 use collections::BTreeMap;
 use rpc::proto;
-
-use super::ChannelPath;
+use std::sync::Arc;
 
 #[derive(Default, Debug)]
 pub struct ChannelIndex {
-    paths: Vec<ChannelPath>,
+    channels_ordered: Vec<ChannelId>,
     channels_by_id: BTreeMap<ChannelId, Arc<Channel>>,
 }
 
@@ -17,8 +14,12 @@ impl ChannelIndex {
         &self.channels_by_id
     }
 
+    pub fn ordered_channels(&self) -> &[ChannelId] {
+        &self.channels_ordered
+    }
+
     pub fn clear(&mut self) {
-        self.paths.clear();
+        self.channels_ordered.clear();
         self.channels_by_id.clear();
     }
 
@@ -26,15 +27,13 @@ impl ChannelIndex {
     pub fn delete_channels(&mut self, channels: &[ChannelId]) {
         self.channels_by_id
             .retain(|channel_id, _| !channels.contains(channel_id));
-        self.paths.retain(|path| {
-            path.iter()
-                .all(|channel_id| self.channels_by_id.contains_key(channel_id))
-        });
+        self.channels_ordered
+            .retain(|channel_id| !channels.contains(channel_id));
     }
 
     pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {
         ChannelPathsInsertGuard {
-            paths: &mut self.paths,
+            channels_ordered: &mut self.channels_ordered,
             channels_by_id: &mut self.channels_by_id,
         }
     }
@@ -77,42 +76,15 @@ impl ChannelIndex {
     }
 }
 
-impl Deref for ChannelIndex {
-    type Target = [ChannelPath];
-
-    fn deref(&self) -> &Self::Target {
-        &self.paths
-    }
-}
-
 /// A guard for ensuring that the paths index maintains its sort and uniqueness
 /// invariants after a series of insertions
 #[derive(Debug)]
 pub struct ChannelPathsInsertGuard<'a> {
-    paths: &'a mut Vec<ChannelPath>,
+    channels_ordered: &'a mut Vec<ChannelId>,
     channels_by_id: &'a mut BTreeMap<ChannelId, Arc<Channel>>,
 }
 
 impl<'a> ChannelPathsInsertGuard<'a> {
-    /// Remove the given edge from this index. This will not remove the channel.
-    /// If this operation would result in a dangling edge, re-insert it.
-    pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) {
-        self.paths.retain(|path| {
-            !path
-                .windows(2)
-                .any(|window| window == [parent_id, channel_id])
-        });
-
-        // Ensure that there is at least one channel path in the index
-        if !self
-            .paths
-            .iter()
-            .any(|path| path.iter().any(|id| id == &channel_id))
-        {
-            self.insert_root(channel_id);
-        }
-    }
-
     pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
         insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version);
     }
@@ -121,91 +93,65 @@ impl<'a> ChannelPathsInsertGuard<'a> {
         insert_new_message(&mut self.channels_by_id, channel_id, message_id)
     }
 
-    pub fn insert(&mut self, channel_proto: proto::Channel) {
+    pub fn insert(&mut self, channel_proto: proto::Channel) -> bool {
+        let mut ret = false;
         if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
-            Arc::make_mut(existing_channel).name = channel_proto.name;
+            let existing_channel = Arc::make_mut(existing_channel);
+
+            ret = existing_channel.visibility != channel_proto.visibility()
+                || existing_channel.role != channel_proto.role()
+                || existing_channel.name != channel_proto.name;
+
+            existing_channel.visibility = channel_proto.visibility();
+            existing_channel.role = channel_proto.role();
+            existing_channel.name = channel_proto.name;
         } else {
             self.channels_by_id.insert(
                 channel_proto.id,
                 Arc::new(Channel {
                     id: channel_proto.id,
+                    visibility: channel_proto.visibility(),
+                    role: channel_proto.role(),
                     name: channel_proto.name,
                     unseen_note_version: None,
                     unseen_message_id: None,
+                    parent_path: channel_proto.parent_path,
                 }),
             );
             self.insert_root(channel_proto.id);
         }
-    }
-
-    pub fn insert_edge(&mut self, channel_id: ChannelId, parent_id: ChannelId) {
-        let mut parents = Vec::new();
-        let mut descendants = Vec::new();
-        let mut ixs_to_remove = Vec::new();
-
-        for (ix, path) in self.paths.iter().enumerate() {
-            if path
-                .windows(2)
-                .any(|window| window[0] == parent_id && window[1] == channel_id)
-            {
-                // We already have this edge in the index
-                return;
-            }
-            if path.ends_with(&[parent_id]) {
-                parents.push(path);
-            } else if let Some(position) = path.iter().position(|id| id == &channel_id) {
-                if position == 0 {
-                    ixs_to_remove.push(ix);
-                }
-                descendants.push(path.split_at(position).1);
-            }
-        }
-
-        let mut new_paths = Vec::new();
-        for parent in parents.iter() {
-            if descendants.is_empty() {
-                let mut new_path = Vec::with_capacity(parent.len() + 1);
-                new_path.extend_from_slice(parent);
-                new_path.push(channel_id);
-                new_paths.push(ChannelPath::new(new_path.into()));
-            } else {
-                for descendant in descendants.iter() {
-                    let mut new_path = Vec::with_capacity(parent.len() + descendant.len());
-                    new_path.extend_from_slice(parent);
-                    new_path.extend_from_slice(descendant);
-                    new_paths.push(ChannelPath::new(new_path.into()));
-                }
-            }
-        }
-
-        for ix in ixs_to_remove.into_iter().rev() {
-            self.paths.swap_remove(ix);
-        }
-        self.paths.extend(new_paths)
+        ret
     }
 
     fn insert_root(&mut self, channel_id: ChannelId) {
-        self.paths.push(ChannelPath::new(Arc::from([channel_id])));
+        self.channels_ordered.push(channel_id);
     }
 }
 
 impl<'a> Drop for ChannelPathsInsertGuard<'a> {
     fn drop(&mut self) {
-        self.paths.sort_by(|a, b| {
-            let a = channel_path_sorting_key(a, &self.channels_by_id);
-            let b = channel_path_sorting_key(b, &self.channels_by_id);
+        self.channels_ordered.sort_by(|a, b| {
+            let a = channel_path_sorting_key(*a, &self.channels_by_id);
+            let b = channel_path_sorting_key(*b, &self.channels_by_id);
             a.cmp(b)
         });
-        self.paths.dedup();
+        self.channels_ordered.dedup();
     }
 }
 
 fn channel_path_sorting_key<'a>(
-    path: &'a [ChannelId],
+    id: ChannelId,
     channels_by_id: &'a BTreeMap<ChannelId, Arc<Channel>>,
-) -> impl 'a + Iterator<Item = Option<&'a str>> {
-    path.iter()
-        .map(|id| Some(channels_by_id.get(id)?.name.as_str()))
+) -> impl Iterator<Item = &str> {
+    let (parent_path, name) = channels_by_id
+        .get(&id)
+        .map_or((&[] as &[_], None), |channel| {
+            (channel.parent_path.as_slice(), Some(channel.name.as_str()))
+        });
+    parent_path
+        .iter()
+        .filter_map(|id| Some(channels_by_id.get(id)?.name.as_str()))
+        .chain(name)
 }
 
 fn insert_note_changed(

crates/channel/src/channel_store_tests.rs 🔗

@@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
 use super::*;
 use client::{test::FakeServer, Client, UserStore};
 use gpui::{AppContext, ModelHandle, TestAppContext};
-use rpc::proto;
+use rpc::proto::{self};
 use settings::SettingsStore;
 use util::http::FakeHttpClient;
 
@@ -18,16 +18,18 @@ fn test_update_channels(cx: &mut AppContext) {
                 proto::Channel {
                     id: 1,
                     name: "b".to_string(),
+                    visibility: proto::ChannelVisibility::Members as i32,
+                    role: proto::ChannelRole::Admin.into(),
+                    parent_path: Vec::new(),
                 },
                 proto::Channel {
                     id: 2,
                     name: "a".to_string(),
+                    visibility: proto::ChannelVisibility::Members as i32,
+                    role: proto::ChannelRole::Member.into(),
+                    parent_path: Vec::new(),
                 },
             ],
-            channel_permissions: vec![proto::ChannelPermission {
-                channel_id: 1,
-                is_admin: true,
-            }],
             ..Default::default()
         },
         cx,
@@ -36,8 +38,8 @@ fn test_update_channels(cx: &mut AppContext) {
         &channel_store,
         &[
             //
-            (0, "a".to_string(), false),
-            (0, "b".to_string(), true),
+            (0, "a".to_string(), proto::ChannelRole::Member),
+            (0, "b".to_string(), proto::ChannelRole::Admin),
         ],
         cx,
     );
@@ -49,20 +51,16 @@ fn test_update_channels(cx: &mut AppContext) {
                 proto::Channel {
                     id: 3,
                     name: "x".to_string(),
+                    visibility: proto::ChannelVisibility::Members as i32,
+                    role: proto::ChannelRole::Admin.into(),
+                    parent_path: vec![1],
                 },
                 proto::Channel {
                     id: 4,
                     name: "y".to_string(),
-                },
-            ],
-            insert_edge: vec![
-                proto::ChannelEdge {
-                    parent_id: 1,
-                    channel_id: 3,
-                },
-                proto::ChannelEdge {
-                    parent_id: 2,
-                    channel_id: 4,
+                    visibility: proto::ChannelVisibility::Members as i32,
+                    role: proto::ChannelRole::Member.into(),
+                    parent_path: vec![2],
                 },
             ],
             ..Default::default()
@@ -72,10 +70,10 @@ fn test_update_channels(cx: &mut AppContext) {
     assert_channels(
         &channel_store,
         &[
-            (0, "a".to_string(), false),
-            (1, "y".to_string(), false),
-            (0, "b".to_string(), true),
-            (1, "x".to_string(), true),
+            (0, "a".to_string(), proto::ChannelRole::Member),
+            (1, "y".to_string(), proto::ChannelRole::Member),
+            (0, "b".to_string(), proto::ChannelRole::Admin),
+            (1, "x".to_string(), proto::ChannelRole::Admin),
         ],
         cx,
     );
@@ -92,30 +90,25 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
                 proto::Channel {
                     id: 0,
                     name: "a".to_string(),
+                    visibility: proto::ChannelVisibility::Members as i32,
+                    role: proto::ChannelRole::Admin.into(),
+                    parent_path: vec![],
                 },
                 proto::Channel {
                     id: 1,
                     name: "b".to_string(),
+                    visibility: proto::ChannelVisibility::Members as i32,
+                    role: proto::ChannelRole::Admin.into(),
+                    parent_path: vec![0],
                 },
                 proto::Channel {
                     id: 2,
                     name: "c".to_string(),
+                    visibility: proto::ChannelVisibility::Members as i32,
+                    role: proto::ChannelRole::Admin.into(),
+                    parent_path: vec![0, 1],
                 },
             ],
-            insert_edge: vec![
-                proto::ChannelEdge {
-                    parent_id: 0,
-                    channel_id: 1,
-                },
-                proto::ChannelEdge {
-                    parent_id: 1,
-                    channel_id: 2,
-                },
-            ],
-            channel_permissions: vec![proto::ChannelPermission {
-                channel_id: 0,
-                is_admin: true,
-            }],
             ..Default::default()
         },
         cx,
@@ -125,9 +118,9 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
         &channel_store,
         &[
             //
-            (0, "a".to_string(), true),
-            (1, "b".to_string(), true),
-            (2, "c".to_string(), true),
+            (0, "a".to_string(), proto::ChannelRole::Admin),
+            (1, "b".to_string(), proto::ChannelRole::Admin),
+            (2, "c".to_string(), proto::ChannelRole::Admin),
         ],
         cx,
     );
@@ -142,7 +135,11 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
     );
 
     // Make sure that the 1/2/3 path is gone
-    assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx);
+    assert_channels(
+        &channel_store,
+        &[(0, "a".to_string(), proto::ChannelRole::Admin)],
+        cx,
+    );
 }
 
 #[gpui::test]
@@ -158,12 +155,19 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
         channels: vec![proto::Channel {
             id: channel_id,
             name: "the-channel".to_string(),
+            visibility: proto::ChannelVisibility::Members as i32,
+            role: proto::ChannelRole::Member.into(),
+            parent_path: vec![],
         }],
         ..Default::default()
     });
     cx.foreground().run_until_parked();
     cx.read(|cx| {
-        assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], cx);
+        assert_channels(
+            &channel_store,
+            &[(0, "the-channel".to_string(), proto::ChannelRole::Member)],
+            cx,
+        );
     });
 
     let get_users = server.receive::<proto::GetUsers>().await.unwrap();
@@ -181,7 +185,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
 
     // Join a channel and populate its existing messages.
     let channel = channel_store.update(cx, |store, cx| {
-        let channel_id = store.channel_dag_entries().next().unwrap().1.id;
+        let channel_id = store.ordered_channels().next().unwrap().1.id;
         store.open_channel_chat(channel_id, cx)
     });
     let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
@@ -194,6 +198,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     body: "a".into(),
                     timestamp: 1000,
                     sender_id: 5,
+                    mentions: vec![],
                     nonce: Some(1.into()),
                 },
                 proto::ChannelMessage {
@@ -201,6 +206,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     body: "b".into(),
                     timestamp: 1001,
                     sender_id: 6,
+                    mentions: vec![],
                     nonce: Some(2.into()),
                 },
             ],
@@ -247,6 +253,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
             body: "c".into(),
             timestamp: 1002,
             sender_id: 7,
+            mentions: vec![],
             nonce: Some(3.into()),
         }),
     });
@@ -284,7 +291,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
 
     // Scroll up to view older messages.
     channel.update(cx, |channel, cx| {
-        assert!(channel.load_more_messages(cx));
+        channel.load_more_messages(cx).unwrap().detach();
     });
     let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
     assert_eq!(get_messages.payload.channel_id, 5);
@@ -300,6 +307,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     timestamp: 998,
                     sender_id: 5,
                     nonce: Some(4.into()),
+                    mentions: vec![],
                 },
                 proto::ChannelMessage {
                     id: 9,
@@ -307,6 +315,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
                     timestamp: 999,
                     sender_id: 6,
                     nonce: Some(5.into()),
+                    mentions: vec![],
                 },
             ],
         },
@@ -358,19 +367,13 @@ fn update_channels(
 #[track_caller]
 fn assert_channels(
     channel_store: &ModelHandle<ChannelStore>,
-    expected_channels: &[(usize, String, bool)],
+    expected_channels: &[(usize, String, proto::ChannelRole)],
     cx: &AppContext,
 ) {
     let actual = channel_store.read_with(cx, |store, _| {
         store
-            .channel_dag_entries()
-            .map(|(depth, channel)| {
-                (
-                    depth,
-                    channel.name.to_string(),
-                    store.is_user_admin(channel.id),
-                )
-            })
+            .ordered_channels()
+            .map(|(depth, channel)| (depth, channel.name.to_string(), channel.role))
             .collect::<Vec<_>>()
     });
     assert_eq!(actual, expected_channels);

crates/client/src/telemetry.rs 🔗

@@ -4,7 +4,9 @@ use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Serialize;
 use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
-use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
+use sysinfo::{
+    CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
+};
 use tempfile::NamedTempFile;
 use util::http::HttpClient;
 use util::{channel::ReleaseChannel, TryFutureExt};
@@ -166,8 +168,16 @@ impl Telemetry {
 
         let this = self.clone();
         cx.spawn(|mut cx| async move {
-            let mut system = System::new_all();
-            system.refresh_all();
+            // Avoiding calling `System::new_all()`, as there have been crashes related to it
+            let refresh_kind = RefreshKind::new()
+                .with_memory() // For memory usage
+                .with_processes(ProcessRefreshKind::everything()) // For process usage
+                .with_cpu(CpuRefreshKind::everything()); // For core count
+
+            let mut system = System::new_with_specifics(refresh_kind);
+
+            // Avoiding calling `refresh_all()`, just update what we need
+            system.refresh_specifics(refresh_kind);
 
             loop {
                 // Waiting some amount of time before the first query is important to get a reasonable value
@@ -175,8 +185,7 @@ impl Telemetry {
                 const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
                 smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
 
-                system.refresh_memory();
-                system.refresh_processes();
+                system.refresh_specifics(refresh_kind);
 
                 let current_process = Pid::from_u32(std::process::id());
                 let Some(process) = system.processes().get(&current_process) else {

crates/client/src/user.rs 🔗

@@ -293,21 +293,19 @@ impl UserStore {
                     // No need to paralellize here
                     let mut updated_contacts = Vec::new();
                     for contact in message.contacts {
-                        let should_notify = contact.should_notify;
-                        updated_contacts.push((
-                            Arc::new(Contact::from_proto(contact, &this, &mut cx).await?),
-                            should_notify,
+                        updated_contacts.push(Arc::new(
+                            Contact::from_proto(contact, &this, &mut cx).await?,
                         ));
                     }
 
                     let mut incoming_requests = Vec::new();
                     for request in message.incoming_requests {
-                        incoming_requests.push({
-                            let user = this
-                                .update(&mut cx, |this, cx| this.get_user(request.requester_id, cx))
-                                .await?;
-                            (user, request.should_notify)
-                        });
+                        incoming_requests.push(
+                            this.update(&mut cx, |this, cx| {
+                                this.get_user(request.requester_id, cx)
+                            })
+                            .await?,
+                        );
                     }
 
                     let mut outgoing_requests = Vec::new();
@@ -330,13 +328,7 @@ impl UserStore {
                         this.contacts
                             .retain(|contact| !removed_contacts.contains(&contact.user.id));
                         // Update existing contacts and insert new ones
-                        for (updated_contact, should_notify) in updated_contacts {
-                            if should_notify {
-                                cx.emit(Event::Contact {
-                                    user: updated_contact.user.clone(),
-                                    kind: ContactEventKind::Accepted,
-                                });
-                            }
+                        for updated_contact in updated_contacts {
                             match this.contacts.binary_search_by_key(
                                 &&updated_contact.user.github_login,
                                 |contact| &contact.user.github_login,
@@ -359,14 +351,7 @@ impl UserStore {
                             }
                         });
                         // Update existing incoming requests and insert new ones
-                        for (user, should_notify) in incoming_requests {
-                            if should_notify {
-                                cx.emit(Event::Contact {
-                                    user: user.clone(),
-                                    kind: ContactEventKind::Requested,
-                                });
-                            }
-
+                        for user in incoming_requests {
                             match this
                                 .incoming_contact_requests
                                 .binary_search_by_key(&&user.github_login, |contact| {
@@ -415,6 +400,12 @@ impl UserStore {
         &self.incoming_contact_requests
     }
 
+    pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
+        self.incoming_contact_requests
+            .iter()
+            .any(|user| user.id == user_id)
+    }
+
     pub fn outgoing_contact_requests(&self) -> &[Arc<User>] {
         &self.outgoing_contact_requests
     }

crates/client2/src/client2.rs 🔗

@@ -312,7 +312,7 @@ pub struct PendingEntitySubscription<T: 'static> {
 
 impl<T> PendingEntitySubscription<T>
 where
-    T: 'static + Send + Sync,
+    T: 'static + Send,
 {
     pub fn set_model(mut self, model: &Handle<T>, cx: &mut AsyncAppContext) -> Subscription {
         self.consumed = true;
@@ -529,7 +529,7 @@ impl Client {
         remote_id: u64,
     ) -> Result<PendingEntitySubscription<T>>
     where
-        T: 'static + Send + Sync,
+        T: 'static + Send,
     {
         let id = (TypeId::of::<T>(), remote_id);
 
@@ -557,7 +557,7 @@ impl Client {
     ) -> Subscription
     where
         M: EnvelopedMessage,
-        E: 'static + Send + Sync,
+        E: 'static + Send,
         H: 'static + Send + Sync + Fn(Handle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<()>> + Send,
     {
@@ -599,7 +599,7 @@ impl Client {
     ) -> Subscription
     where
         M: RequestMessage,
-        E: 'static + Send + Sync,
+        E: 'static + Send,
         H: 'static + Send + Sync + Fn(Handle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<M::Response>> + Send,
     {
@@ -615,7 +615,7 @@ impl Client {
     pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage,
-        E: 'static + Send + Sync,
+        E: 'static + Send,
         H: 'static + Send + Sync + Fn(Handle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<()>> + Send,
     {
@@ -627,7 +627,7 @@ impl Client {
     fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage,
-        E: 'static + Send + Sync,
+        E: 'static + Send,
         H: 'static + Send + Sync + Fn(AnyHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<()>> + Send,
     {
@@ -666,7 +666,7 @@ impl Client {
     pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage + RequestMessage,
-        E: 'static + Send + Sync,
+        E: 'static + Send,
         H: 'static + Send + Sync + Fn(Handle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<M::Response>> + Send,
     {

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.24.0"
+version = "0.27.0"
 publish = false
 
 [[bin]]
@@ -73,6 +73,7 @@ git = { path = "../git", features = ["test-support"] }
 live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
 node_runtime = { path = "../node_runtime" }
+notifications = { path = "../notifications", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }

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

@@ -44,7 +44,7 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
 
 CREATE TABLE "projects" (
     "id" INTEGER PRIMARY KEY AUTOINCREMENT,
-    "room_id" INTEGER REFERENCES rooms (id) NOT NULL,
+    "room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
     "host_user_id" INTEGER REFERENCES users (id) NOT NULL,
     "host_connection_id" INTEGER,
     "host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
@@ -192,9 +192,13 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
 CREATE TABLE "channels" (
     "id" INTEGER PRIMARY KEY AUTOINCREMENT,
     "name" VARCHAR NOT NULL,
-    "created_at" TIMESTAMP NOT NULL DEFAULT now
+    "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "visibility" VARCHAR NOT NULL,
+    "parent_path" TEXT
 );
 
+CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path");
+
 CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
     "id" INTEGER PRIMARY KEY AUTOINCREMENT,
     "user_id" INTEGER NOT NULL REFERENCES users (id),
@@ -213,19 +217,22 @@ CREATE TABLE IF NOT EXISTS "channel_messages" (
     "nonce" BLOB NOT NULL
 );
 CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
-CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce");
+CREATE UNIQUE INDEX "index_channel_messages_on_sender_id_nonce" ON "channel_messages" ("sender_id", "nonce");
 
-CREATE TABLE "channel_paths" (
-    "id_path" TEXT NOT NULL PRIMARY KEY,
-    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE
+CREATE TABLE "channel_message_mentions" (
+    "message_id" INTEGER NOT NULL REFERENCES channel_messages (id) ON DELETE CASCADE,
+    "start_offset" INTEGER NOT NULL,
+    "end_offset" INTEGER NOT NULL,
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    PRIMARY KEY(message_id, start_offset)
 );
-CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id");
 
 CREATE TABLE "channel_members" (
     "id" INTEGER PRIMARY KEY AUTOINCREMENT,
     "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
     "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
     "admin" BOOLEAN NOT NULL DEFAULT false,
+    "role" VARCHAR,
     "accepted" BOOLEAN NOT NULL DEFAULT false,
     "updated_at" TIMESTAMP NOT NULL DEFAULT now
 );
@@ -312,3 +319,26 @@ CREATE TABLE IF NOT EXISTS "observed_channel_messages" (
 );
 
 CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id");
+
+CREATE TABLE "notification_kinds" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "name" VARCHAR NOT NULL
+);
+
+CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ("name");
+
+CREATE TABLE "notifications" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "created_at" TIMESTAMP NOT NULL default CURRENT_TIMESTAMP,
+    "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "kind" INTEGER NOT NULL REFERENCES notification_kinds (id),
+    "entity_id" INTEGER,
+    "content" TEXT,
+    "is_read" BOOLEAN NOT NULL DEFAULT FALSE,
+    "response" BOOLEAN
+);
+
+CREATE INDEX
+    "index_notifications_on_recipient_id_is_read_kind_entity_id"
+    ON "notifications"
+    ("recipient_id", "is_read", "kind", "entity_id");

crates/collab/migrations/20231004130100_create_notifications.sql 🔗

@@ -0,0 +1,22 @@
+CREATE TABLE "notification_kinds" (
+    "id" SERIAL PRIMARY KEY,
+    "name" VARCHAR NOT NULL
+);
+
+CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ("name");
+
+CREATE TABLE notifications (
+    "id" SERIAL PRIMARY KEY,
+    "created_at" TIMESTAMP NOT NULL DEFAULT now(),
+    "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "kind" INTEGER NOT NULL REFERENCES notification_kinds (id),
+    "entity_id" INTEGER,
+    "content" TEXT,
+    "is_read" BOOLEAN NOT NULL DEFAULT FALSE,
+    "response" BOOLEAN
+);
+
+CREATE INDEX
+    "index_notifications_on_recipient_id_is_read_kind_entity_id"
+    ON "notifications"
+    ("recipient_id", "is_read", "kind", "entity_id");

crates/collab/migrations/20231018102700_create_mentions.sql 🔗

@@ -0,0 +1,11 @@
+CREATE TABLE "channel_message_mentions" (
+    "message_id" INTEGER NOT NULL REFERENCES channel_messages (id) ON DELETE CASCADE,
+    "start_offset" INTEGER NOT NULL,
+    "end_offset" INTEGER NOT NULL,
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    PRIMARY KEY(message_id, start_offset)
+);
+
+-- We use 'on conflict update' with this index, so it should be per-user.
+CREATE UNIQUE INDEX "index_channel_messages_on_sender_id_nonce" ON "channel_messages" ("sender_id", "nonce");
+DROP INDEX "index_channel_messages_on_nonce";

crates/collab/migrations/20231024085546_move_channel_paths_to_channels_table.sql 🔗

@@ -0,0 +1,12 @@
+ALTER TABLE channels ADD COLUMN parent_path TEXT;
+
+UPDATE channels
+SET parent_path = substr(
+    channel_paths.id_path,
+    2,
+    length(channel_paths.id_path) - length('/' || channel_paths.channel_id::text || '/')
+)
+FROM channel_paths
+WHERE channel_paths.channel_id = channels.id;
+
+CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path");

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

@@ -71,7 +71,6 @@ async fn main() {
                     db::NewUserParams {
                         github_login: github_user.login,
                         github_user_id: github_user.id,
-                        invite_count: 5,
                     },
                 )
                 .await

crates/collab/src/db.rs 🔗

@@ -20,7 +20,7 @@ use rpc::{
 };
 use sea_orm::{
     entity::prelude::*,
-    sea_query::{Alias, Expr, OnConflict, Query},
+    sea_query::{Alias, Expr, OnConflict},
     ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr,
     FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement,
     TransactionTrait,
@@ -47,14 +47,14 @@ pub use ids::*;
 pub use sea_orm::ConnectOptions;
 pub use tables::user::Model as User;
 
-use self::queries::channels::ChannelGraph;
-
 pub struct Database {
     options: ConnectOptions,
     pool: DatabaseConnection,
     rooms: DashMap<RoomId, Arc<Mutex<()>>>,
     rng: Mutex<StdRng>,
     executor: Executor,
+    notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
+    notification_kinds_by_name: HashMap<String, NotificationKindId>,
     #[cfg(test)]
     runtime: Option<tokio::runtime::Runtime>,
 }
@@ -69,6 +69,8 @@ impl Database {
             pool: sea_orm::Database::connect(options).await?,
             rooms: DashMap::with_capacity(16384),
             rng: Mutex::new(StdRng::seed_from_u64(0)),
+            notification_kinds_by_id: HashMap::default(),
+            notification_kinds_by_name: HashMap::default(),
             executor,
             #[cfg(test)]
             runtime: None,
@@ -121,6 +123,11 @@ impl Database {
         Ok(new_migrations)
     }
 
+    pub async fn initialize_static_data(&mut self) -> Result<()> {
+        self.initialize_notification_kinds().await?;
+        Ok(())
+    }
+
     pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
     where
         F: Send + Fn(TransactionHandle) -> Fut,
@@ -361,18 +368,9 @@ impl<T> RoomGuard<T> {
 
 #[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,
-    },
+    Accepted { user_id: UserId, busy: bool },
+    Outgoing { user_id: UserId },
+    Incoming { user_id: UserId },
 }
 
 impl Contact {
@@ -385,6 +383,15 @@ impl Contact {
     }
 }
 
+pub type NotificationBatch = Vec<(UserId, proto::Notification)>;
+
+pub struct CreatedChannelMessage {
+    pub message_id: MessageId,
+    pub participant_connection_ids: Vec<ConnectionId>,
+    pub channel_members: Vec<UserId>,
+    pub notifications: NotificationBatch,
+}
+
 #[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
 pub struct Invite {
     pub email_address: String,
@@ -417,7 +424,6 @@ pub struct WaitlistSummary {
 pub struct NewUserParams {
     pub github_login: String,
     pub github_user_id: i32,
-    pub invite_count: i32,
 }
 
 #[derive(Debug)]
@@ -428,17 +434,115 @@ pub struct NewUserResult {
     pub signup_device_id: Option<String>,
 }
 
-#[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)]
+#[derive(Debug)]
+pub struct MoveChannelResult {
+    pub participants_to_update: HashMap<UserId, ChannelsForUser>,
+    pub participants_to_remove: HashSet<UserId>,
+    pub moved_channels: HashSet<ChannelId>,
+}
+
+#[derive(Debug)]
+pub struct RenameChannelResult {
+    pub channel: Channel,
+    pub participants_to_update: HashMap<UserId, Channel>,
+}
+
+#[derive(Debug)]
+pub struct CreateChannelResult {
+    pub channel: Channel,
+    pub participants_to_update: Vec<(UserId, ChannelsForUser)>,
+}
+
+#[derive(Debug)]
+pub struct SetChannelVisibilityResult {
+    pub participants_to_update: HashMap<UserId, ChannelsForUser>,
+    pub participants_to_remove: HashSet<UserId>,
+    pub channels_to_remove: Vec<ChannelId>,
+}
+
+#[derive(Debug)]
+pub struct MembershipUpdated {
+    pub channel_id: ChannelId,
+    pub new_channels: ChannelsForUser,
+    pub removed_channels: Vec<ChannelId>,
+}
+
+#[derive(Debug)]
+pub enum SetMemberRoleResult {
+    InviteUpdated(Channel),
+    MembershipUpdated(MembershipUpdated),
+}
+
+#[derive(Debug)]
+pub struct InviteMemberResult {
+    pub channel: Channel,
+    pub notifications: NotificationBatch,
+}
+
+#[derive(Debug)]
+pub struct RespondToChannelInvite {
+    pub membership_update: Option<MembershipUpdated>,
+    pub notifications: NotificationBatch,
+}
+
+#[derive(Debug)]
+pub struct RemoveChannelMemberResult {
+    pub membership_update: MembershipUpdated,
+    pub notification_id: Option<NotificationId>,
+}
+
+#[derive(Debug, PartialEq, Eq, Hash)]
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
+    pub visibility: ChannelVisibility,
+    pub role: ChannelRole,
+    pub parent_path: Vec<ChannelId>,
+}
+
+impl Channel {
+    fn from_model(value: channel::Model, role: ChannelRole) -> Self {
+        Channel {
+            id: value.id,
+            visibility: value.visibility,
+            name: value.clone().name,
+            role,
+            parent_path: value.ancestors().collect(),
+        }
+    }
+
+    pub fn to_proto(&self) -> proto::Channel {
+        proto::Channel {
+            id: self.id.to_proto(),
+            name: self.name.clone(),
+            visibility: self.visibility.into(),
+            role: self.role.into(),
+            parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Hash)]
+pub struct ChannelMember {
+    pub role: ChannelRole,
+    pub user_id: UserId,
+    pub kind: proto::channel_member::Kind,
+}
+
+impl ChannelMember {
+    pub fn to_proto(&self) -> proto::ChannelMember {
+        proto::ChannelMember {
+            role: self.role.into(),
+            user_id: self.user_id.to_proto(),
+            kind: self.kind.into(),
+        }
+    }
 }
 
 #[derive(Debug, PartialEq)]
 pub struct ChannelsForUser {
-    pub channels: ChannelGraph,
+    pub channels: Vec<Channel>,
     pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
-    pub channels_with_admin_privileges: HashSet<ChannelId>,
     pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>,
     pub channel_messages: Vec<proto::UnseenChannelMessage>,
 }

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

@@ -1,4 +1,5 @@
 use crate::Result;
+use rpc::proto;
 use sea_orm::{entity::prelude::*, DbErr};
 use serde::{Deserialize, Serialize};
 
@@ -80,3 +81,119 @@ id_type!(SignupId);
 id_type!(UserId);
 id_type!(ChannelBufferCollaboratorId);
 id_type!(FlagId);
+id_type!(NotificationId);
+id_type!(NotificationKindId);
+
+#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
+#[sea_orm(rs_type = "String", db_type = "String(None)")]
+pub enum ChannelRole {
+    #[sea_orm(string_value = "admin")]
+    Admin,
+    #[sea_orm(string_value = "member")]
+    #[default]
+    Member,
+    #[sea_orm(string_value = "guest")]
+    Guest,
+    #[sea_orm(string_value = "banned")]
+    Banned,
+}
+
+impl ChannelRole {
+    pub fn should_override(&self, other: Self) -> bool {
+        use ChannelRole::*;
+        match self {
+            Admin => matches!(other, Member | Banned | Guest),
+            Member => matches!(other, Banned | Guest),
+            Banned => matches!(other, Guest),
+            Guest => false,
+        }
+    }
+
+    pub fn max(&self, other: Self) -> Self {
+        if self.should_override(other) {
+            *self
+        } else {
+            other
+        }
+    }
+
+    pub fn can_see_all_descendants(&self) -> bool {
+        use ChannelRole::*;
+        match self {
+            Admin | Member => true,
+            Guest | Banned => false,
+        }
+    }
+
+    pub fn can_only_see_public_descendants(&self) -> bool {
+        use ChannelRole::*;
+        match self {
+            Guest => true,
+            Admin | Member | Banned => false,
+        }
+    }
+}
+
+impl From<proto::ChannelRole> for ChannelRole {
+    fn from(value: proto::ChannelRole) -> Self {
+        match value {
+            proto::ChannelRole::Admin => ChannelRole::Admin,
+            proto::ChannelRole::Member => ChannelRole::Member,
+            proto::ChannelRole::Guest => ChannelRole::Guest,
+            proto::ChannelRole::Banned => ChannelRole::Banned,
+        }
+    }
+}
+
+impl Into<proto::ChannelRole> for ChannelRole {
+    fn into(self) -> proto::ChannelRole {
+        match self {
+            ChannelRole::Admin => proto::ChannelRole::Admin,
+            ChannelRole::Member => proto::ChannelRole::Member,
+            ChannelRole::Guest => proto::ChannelRole::Guest,
+            ChannelRole::Banned => proto::ChannelRole::Banned,
+        }
+    }
+}
+
+impl Into<i32> for ChannelRole {
+    fn into(self) -> i32 {
+        let proto: proto::ChannelRole = self.into();
+        proto.into()
+    }
+}
+
+#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
+#[sea_orm(rs_type = "String", db_type = "String(None)")]
+pub enum ChannelVisibility {
+    #[sea_orm(string_value = "public")]
+    Public,
+    #[sea_orm(string_value = "members")]
+    #[default]
+    Members,
+}
+
+impl From<proto::ChannelVisibility> for ChannelVisibility {
+    fn from(value: proto::ChannelVisibility) -> Self {
+        match value {
+            proto::ChannelVisibility::Public => ChannelVisibility::Public,
+            proto::ChannelVisibility::Members => ChannelVisibility::Members,
+        }
+    }
+}
+
+impl Into<proto::ChannelVisibility> for ChannelVisibility {
+    fn into(self) -> proto::ChannelVisibility {
+        match self {
+            ChannelVisibility::Public => proto::ChannelVisibility::Public,
+            ChannelVisibility::Members => proto::ChannelVisibility::Members,
+        }
+    }
+}
+
+impl Into<i32> for ChannelVisibility {
+    fn into(self) -> i32 {
+        let proto: proto::ChannelVisibility = self.into();
+        proto.into()
+    }
+}

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

@@ -5,6 +5,7 @@ pub mod buffers;
 pub mod channels;
 pub mod contacts;
 pub mod messages;
+pub mod notifications;
 pub mod projects;
 pub mod rooms;
 pub mod servers;

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

@@ -16,7 +16,8 @@ impl Database {
         connection: ConnectionId,
     ) -> Result<proto::JoinChannelBufferResponse> {
         self.transaction(|tx| async move {
-            self.check_user_is_channel_member(channel_id, user_id, &tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_participant(&channel, user_id, &tx)
                 .await?;
 
             let buffer = channel::Model {
@@ -129,9 +130,11 @@ impl Database {
         self.transaction(|tx| async move {
             let mut results = Vec::new();
             for client_buffer in buffers {
-                let channel_id = ChannelId::from_proto(client_buffer.channel_id);
+                let channel = self
+                    .get_channel_internal(ChannelId::from_proto(client_buffer.channel_id), &*tx)
+                    .await?;
                 if self
-                    .check_user_is_channel_member(channel_id, user_id, &*tx)
+                    .check_user_is_channel_participant(&channel, user_id, &*tx)
                     .await
                     .is_err()
                 {
@@ -139,9 +142,9 @@ impl Database {
                     continue;
                 }
 
-                let buffer = self.get_channel_buffer(channel_id, &*tx).await?;
+                let buffer = self.get_channel_buffer(channel.id, &*tx).await?;
                 let mut collaborators = channel_buffer_collaborator::Entity::find()
-                    .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
+                    .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel.id))
                     .all(&*tx)
                     .await?;
 
@@ -439,7 +442,8 @@ impl Database {
         Vec<proto::VectorClockEntry>,
     )> {
         self.transaction(move |tx| async move {
-            self.check_user_is_channel_member(channel_id, user, &*tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_member(&channel, user, &*tx)
                 .await?;
 
             let buffer = buffer::Entity::find()
@@ -482,7 +486,7 @@ impl Database {
                 )
                 .await?;
 
-                channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
+                channel_members = self.get_channel_participants(&channel, &*tx).await?;
                 let collaborators = self
                     .get_channel_buffer_collaborators_internal(channel_id, &*tx)
                     .await?;

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

@@ -1,8 +1,6 @@
 use super::*;
-use rpc::proto::ChannelEdge;
-use smallvec::SmallVec;
-
-type ChannelDescendants = HashMap<ChannelId, SmallSet<ChannelId>>;
+use rpc::proto::channel_member::Kind;
+use sea_orm::TryGetableMany;
 
 impl Database {
     #[cfg(test)]
@@ -19,112 +17,258 @@ impl Database {
         .await
     }
 
+    #[cfg(test)]
     pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
-        self.create_channel(name, None, creator_id).await
+        Ok(self
+            .create_channel(name, None, creator_id)
+            .await?
+            .channel
+            .id)
     }
 
-    pub async fn create_channel(
+    #[cfg(test)]
+    pub async fn create_sub_channel(
         &self,
         name: &str,
-        parent: Option<ChannelId>,
+        parent: ChannelId,
         creator_id: UserId,
     ) -> Result<ChannelId> {
+        Ok(self
+            .create_channel(name, Some(parent), creator_id)
+            .await?
+            .channel
+            .id)
+    }
+
+    pub async fn create_channel(
+        &self,
+        name: &str,
+        parent_channel_id: Option<ChannelId>,
+        admin_id: UserId,
+    ) -> Result<CreateChannelResult> {
         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)
+            let mut parent = None;
+
+            if let Some(parent_channel_id) = parent_channel_id {
+                let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?;
+                self.check_user_is_channel_admin(&parent_channel, admin_id, &*tx)
                     .await?;
+                parent = Some(parent_channel);
             }
 
             let channel = channel::ActiveModel {
+                id: ActiveValue::NotSet,
                 name: ActiveValue::Set(name.to_string()),
-                ..Default::default()
+                visibility: ActiveValue::Set(ChannelVisibility::Members),
+                parent_path: ActiveValue::Set(
+                    parent
+                        .as_ref()
+                        .map_or(String::new(), |parent| parent.path()),
+                ),
             }
             .insert(&*tx)
             .await?;
 
-            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
-                "#;
-                let 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?;
+            let participants_to_update;
+            if let Some(parent) = &parent {
+                participants_to_update = self
+                    .participants_to_notify_for_channel_change(parent, &*tx)
+                    .await?;
             } else {
-                channel_path::Entity::insert(channel_path::ActiveModel {
+                participants_to_update = vec![];
+
+                channel_member::ActiveModel {
+                    id: ActiveValue::NotSet,
                     channel_id: ActiveValue::Set(channel.id),
-                    id_path: ActiveValue::Set(format!("/{}/", channel.id)),
+                    user_id: ActiveValue::Set(admin_id),
+                    accepted: ActiveValue::Set(true),
+                    role: ActiveValue::Set(ChannelRole::Admin),
+                }
+                .insert(&*tx)
+                .await?;
+            };
+
+            Ok(CreateChannelResult {
+                channel: Channel::from_model(channel, ChannelRole::Admin),
+                participants_to_update,
+            })
+        })
+        .await
+    }
+
+    pub async fn join_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        connection: ConnectionId,
+        environment: &str,
+    ) -> Result<(JoinRoom, Option<MembershipUpdated>, ChannelRole)> {
+        self.transaction(move |tx| async move {
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            let mut role = self.channel_role_for_user(&channel, user_id, &*tx).await?;
+
+            let mut accept_invite_result = None;
+
+            if role.is_none() {
+                if let Some(invitation) = self
+                    .pending_invite_for_channel(&channel, user_id, &*tx)
+                    .await?
+                {
+                    // note, this may be a parent channel
+                    role = Some(invitation.role);
+                    channel_member::Entity::update(channel_member::ActiveModel {
+                        accepted: ActiveValue::Set(true),
+                        ..invitation.into_active_model()
+                    })
+                    .exec(&*tx)
+                    .await?;
+
+                    accept_invite_result = Some(
+                        self.calculate_membership_updated(&channel, user_id, &*tx)
+                            .await?,
+                    );
+
+                    debug_assert!(
+                        self.channel_role_for_user(&channel, user_id, &*tx).await? == role
+                    );
+                }
+            }
+
+            if channel.visibility == ChannelVisibility::Public {
+                role = Some(ChannelRole::Guest);
+                let channel_to_join = self
+                    .public_ancestors_including_self(&channel, &*tx)
+                    .await?
+                    .first()
+                    .cloned()
+                    .unwrap_or(channel.clone());
+
+                channel_member::Entity::insert(channel_member::ActiveModel {
+                    id: ActiveValue::NotSet,
+                    channel_id: ActiveValue::Set(channel_to_join.id),
+                    user_id: ActiveValue::Set(user_id),
+                    accepted: ActiveValue::Set(true),
+                    role: ActiveValue::Set(ChannelRole::Guest),
                 })
                 .exec(&*tx)
                 .await?;
+
+                accept_invite_result = Some(
+                    self.calculate_membership_updated(&channel_to_join, user_id, &*tx)
+                        .await?,
+                );
+
+                debug_assert!(self.channel_role_for_user(&channel, user_id, &*tx).await? == role);
             }
 
-            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()
+            if role.is_none() || role == Some(ChannelRole::Banned) {
+                Err(anyhow!("not allowed"))?
             }
-            .insert(&*tx)
-            .await?;
 
-            Ok(channel.id)
+            let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
+            let room_id = self
+                .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx)
+                .await?;
+
+            self.join_channel_room_internal(room_id, user_id, connection, &*tx)
+                .await
+                .map(|jr| (jr, accept_invite_result, role.unwrap()))
         })
         .await
     }
 
-    pub async fn delete_channel(
+    pub async fn set_channel_visibility(
         &self,
         channel_id: ChannelId,
-        user_id: UserId,
-    ) -> Result<(Vec<ChannelId>, Vec<UserId>)> {
+        visibility: ChannelVisibility,
+        admin_id: UserId,
+    ) -> Result<SetChannelVisibilityResult> {
         self.transaction(move |tx| async move {
-            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+
+            self.check_user_is_channel_admin(&channel, admin_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 previous_members = self
+                .get_channel_participant_details_internal(&channel, &*tx)
+                .await?;
+
+            let mut model = channel.into_active_model();
+            model.visibility = ActiveValue::Set(visibility);
+            let channel = model.update(&*tx).await?;
+
+            let mut participants_to_update: HashMap<UserId, ChannelsForUser> = self
+                .participants_to_notify_for_channel_change(&channel, &*tx)
+                .await?
+                .into_iter()
+                .collect();
+
+            let mut channels_to_remove: Vec<ChannelId> = vec![];
+            let mut participants_to_remove: HashSet<UserId> = HashSet::default();
+            match visibility {
+                ChannelVisibility::Members => {
+                    let all_descendents: Vec<ChannelId> = self
+                        .get_channel_descendants_including_self(vec![channel_id], &*tx)
+                        .await?
+                        .into_iter()
+                        .map(|channel| channel.id)
+                        .collect();
+
+                    channels_to_remove = channel::Entity::find()
+                        .filter(
+                            channel::Column::Id
+                                .is_in(all_descendents)
+                                .and(channel::Column::Visibility.eq(ChannelVisibility::Public)),
+                        )
+                        .all(&*tx)
+                        .await?
+                        .into_iter()
+                        .map(|channel| channel.id)
+                        .collect();
+
+                    channels_to_remove.push(channel_id);
+
+                    for member in previous_members {
+                        if member.role.can_only_see_public_descendants() {
+                            participants_to_remove.insert(member.user_id);
+                        }
+                    }
+                }
+                ChannelVisibility::Public => {
+                    if let Some(public_parent) = self.public_parent_channel(&channel, &*tx).await? {
+                        let parent_updates = self
+                            .participants_to_notify_for_channel_change(&public_parent, &*tx)
+                            .await?;
+
+                        for (user_id, channels) in parent_updates {
+                            participants_to_update.insert(user_id, channels);
+                        }
+                    }
                 }
             }
 
-            let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?;
+            Ok(SetChannelVisibilityResult {
+                participants_to_update,
+                participants_to_remove,
+                channels_to_remove,
+            })
+        })
+        .await
+    }
+
+    pub async fn delete_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+    ) -> Result<(Vec<ChannelId>, Vec<UserId>)> {
+        self.transaction(move |tx| async move {
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_admin(&channel, user_id, &*tx)
+                .await?;
+
             let members_to_notify: Vec<UserId> = channel_member::Entity::find()
-                .filter(channel_member::Column::ChannelId.is_in(channel_ancestors))
+                .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
                 .select_only()
                 .column(channel_member::Column::UserId)
                 .distinct()
@@ -132,25 +276,19 @@ impl Database {
                 .all(&*tx)
                 .await?;
 
+            let channels_to_remove = self
+                .get_channel_descendants_including_self(vec![channel.id], &*tx)
+                .await?
+                .into_iter()
+                .map(|channel| channel.id)
+                .collect::<Vec<_>>();
+
             channel::Entity::delete_many()
-                .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied()))
+                .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied()))
                 .exec(&*tx)
                 .await?;
 
-            // Delete any other paths that include this channel
-            let sql = r#"
-                    DELETE FROM channel_paths
-                    WHERE
-                        id_path LIKE '%' || $1 || '%'
-                "#;
-            let channel_paths_stmt = Statement::from_sql_and_values(
-                self.pool.get_database_backend(),
-                sql,
-                [channel_id.to_proto().into()],
-            );
-            tx.execute(channel_paths_stmt).await?;
-
-            Ok((channels_to_remove.into_keys().collect(), members_to_notify))
+            Ok((channels_to_remove, members_to_notify))
         })
         .await
     }
@@ -160,23 +298,44 @@ impl Database {
         channel_id: ChannelId,
         invitee_id: UserId,
         inviter_id: UserId,
-        is_admin: bool,
-    ) -> Result<()> {
+        role: ChannelRole,
+    ) -> Result<InviteMemberResult> {
         self.transaction(move |tx| async move {
-            self.check_user_is_channel_admin(channel_id, inviter_id, &*tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_admin(&channel, inviter_id, &*tx)
                 .await?;
 
             channel_member::ActiveModel {
+                id: ActiveValue::NotSet,
                 channel_id: ActiveValue::Set(channel_id),
                 user_id: ActiveValue::Set(invitee_id),
                 accepted: ActiveValue::Set(false),
-                admin: ActiveValue::Set(is_admin),
-                ..Default::default()
+                role: ActiveValue::Set(role),
             }
             .insert(&*tx)
             .await?;
 
-            Ok(())
+            let channel = Channel::from_model(channel, role);
+
+            let notifications = self
+                .create_notification(
+                    invitee_id,
+                    rpc::Notification::ChannelInvitation {
+                        channel_id: channel_id.to_proto(),
+                        channel_name: channel.name.clone(),
+                        inviter_id: inviter_id.to_proto(),
+                    },
+                    true,
+                    &*tx,
+                )
+                .await?
+                .into_iter()
+                .collect();
+
+            Ok(InviteMemberResult {
+                channel,
+                notifications,
+            })
         })
         .await
     }
@@ -192,24 +351,37 @@ impl Database {
     pub async fn rename_channel(
         &self,
         channel_id: ChannelId,
-        user_id: UserId,
+        admin_id: UserId,
         new_name: &str,
-    ) -> Result<String> {
+    ) -> Result<RenameChannelResult> {
         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)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            let role = self
+                .check_user_is_channel_admin(&channel, admin_id, &*tx)
                 .await?;
 
-            channel::ActiveModel {
-                id: ActiveValue::Unchanged(channel_id),
-                name: ActiveValue::Set(new_name.clone()),
-                ..Default::default()
-            }
-            .update(&*tx)
-            .await?;
+            let mut model = channel.into_active_model();
+            model.name = ActiveValue::Set(new_name.clone());
+            let channel = model.update(&*tx).await?;
+
+            let participants = self
+                .get_channel_participant_details_internal(&channel, &*tx)
+                .await?;
 
-            Ok(new_name)
+            Ok(RenameChannelResult {
+                channel: Channel::from_model(channel.clone(), role),
+                participants_to_update: participants
+                    .iter()
+                    .map(|participant| {
+                        (
+                            participant.user_id,
+                            Channel::from_model(channel.clone(), participant.role),
+                        )
+                    })
+                    .collect(),
+            })
         })
         .await
     }
@@ -219,10 +391,12 @@ impl Database {
         channel_id: ChannelId,
         user_id: UserId,
         accept: bool,
-    ) -> Result<()> {
+    ) -> Result<RespondToChannelInvite> {
         self.transaction(move |tx| async move {
-            let rows_affected = if accept {
-                channel_member::Entity::update_many()
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+
+            let membership_update = if accept {
+                let rows_affected = channel_member::Entity::update_many()
                     .set(channel_member::ActiveModel {
                         accepted: ActiveValue::Set(accept),
                         ..Default::default()
@@ -235,35 +409,91 @@ impl Database {
                     )
                     .exec(&*tx)
                     .await?
-                    .rows_affected
+                    .rows_affected;
+
+                if rows_affected == 0 {
+                    Err(anyhow!("no such invitation"))?;
+                }
+
+                Some(
+                    self.calculate_membership_updated(&channel, user_id, &*tx)
+                        .await?,
+                )
             } else {
-                channel_member::ActiveModel {
-                    channel_id: ActiveValue::Unchanged(channel_id),
-                    user_id: ActiveValue::Unchanged(user_id),
-                    ..Default::default()
+                let rows_affected = channel_member::Entity::delete_many()
+                    .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;
+                if rows_affected == 0 {
+                    Err(anyhow!("no such invitation"))?;
                 }
-                .delete(&*tx)
-                .await?
-                .rows_affected
-            };
 
-            if rows_affected == 0 {
-                Err(anyhow!("no such invitation"))?;
-            }
+                None
+            };
 
-            Ok(())
+            Ok(RespondToChannelInvite {
+                membership_update,
+                notifications: self
+                    .mark_notification_as_read_with_response(
+                        user_id,
+                        &rpc::Notification::ChannelInvitation {
+                            channel_id: channel_id.to_proto(),
+                            channel_name: Default::default(),
+                            inviter_id: Default::default(),
+                        },
+                        accept,
+                        &*tx,
+                    )
+                    .await?
+                    .into_iter()
+                    .collect(),
+            })
         })
         .await
     }
 
+    async fn calculate_membership_updated(
+        &self,
+        channel: &channel::Model,
+        user_id: UserId,
+        tx: &DatabaseTransaction,
+    ) -> Result<MembershipUpdated> {
+        let new_channels = self.get_user_channels(user_id, Some(channel), &*tx).await?;
+        let removed_channels = self
+            .get_channel_descendants_including_self(vec![channel.id], &*tx)
+            .await?
+            .into_iter()
+            .filter_map(|channel| {
+                if !new_channels.channels.iter().any(|c| c.id == channel.id) {
+                    Some(channel.id)
+                } else {
+                    None
+                }
+            })
+            .collect::<Vec<_>>();
+
+        Ok(MembershipUpdated {
+            channel_id: channel.id,
+            new_channels,
+            removed_channels,
+        })
+    }
+
     pub async fn remove_channel_member(
         &self,
         channel_id: ChannelId,
         member_id: UserId,
-        remover_id: UserId,
-    ) -> Result<()> {
+        admin_id: UserId,
+    ) -> Result<RemoveChannelMemberResult> {
         self.transaction(|tx| async move {
-            self.check_user_is_channel_admin(channel_id, remover_id, &*tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_admin(&channel, admin_id, &*tx)
                 .await?;
 
             let result = channel_member::Entity::delete_many()
@@ -279,13 +509,30 @@ impl Database {
                 Err(anyhow!("no such member"))?;
             }
 
-            Ok(())
+            Ok(RemoveChannelMemberResult {
+                membership_update: self
+                    .calculate_membership_updated(&channel, member_id, &*tx)
+                    .await?,
+                notification_id: self
+                    .remove_notification(
+                        member_id,
+                        rpc::Notification::ChannelInvitation {
+                            channel_id: channel_id.to_proto(),
+                            channel_name: Default::default(),
+                            inviter_id: Default::default(),
+                        },
+                        &*tx,
+                    )
+                    .await?,
+            })
         })
         .await
     }
 
     pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
         self.transaction(|tx| async move {
+            let mut role_for_channel: HashMap<ChannelId, ChannelRole> = HashMap::default();
+
             let channel_invites = channel_member::Entity::find()
                 .filter(
                     channel_member::Column::UserId
@@ -295,22 +542,20 @@ impl Database {
                 .all(&*tx)
                 .await?;
 
+            for invite in channel_invites {
+                role_for_channel.insert(invite.channel_id, invite.role);
+            }
+
             let channels = channel::Entity::find()
-                .filter(
-                    channel::Column::Id.is_in(
-                        channel_invites
-                            .into_iter()
-                            .map(|channel_member| channel_member.channel_id),
-                    ),
-                )
+                .filter(channel::Column::Id.is_in(role_for_channel.keys().copied()))
                 .all(&*tx)
                 .await?;
 
             let channels = channels
                 .into_iter()
-                .map(|channel| Channel {
-                    id: channel.id,
-                    name: channel.name,
+                .filter_map(|channel| {
+                    let role = *role_for_channel.get(&channel.id)?;
+                    Some(Channel::from_model(channel, role))
                 })
                 .collect();
 
@@ -319,88 +564,11 @@ impl Database {
         .await
     }
 
-    async fn get_channel_graph(
-        &self,
-        parents_by_child_id: ChannelDescendants,
-        trim_dangling_parents: bool,
-        tx: &DatabaseTransaction,
-    ) -> Result<ChannelGraph> {
-        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,
-                })
-            }
-        }
-
-        let mut edges = Vec::with_capacity(parents_by_child_id.len());
-        for (channel, parents) in parents_by_child_id.iter() {
-            for parent in parents.into_iter() {
-                if trim_dangling_parents {
-                    if parents_by_child_id.contains_key(parent) {
-                        edges.push(ChannelEdge {
-                            channel_id: channel.to_proto(),
-                            parent_id: parent.to_proto(),
-                        });
-                    }
-                } else {
-                    edges.push(ChannelEdge {
-                        channel_id: channel.to_proto(),
-                        parent_id: parent.to_proto(),
-                    });
-                }
-            }
-        }
-
-        Ok(ChannelGraph { channels, edges })
-    }
-
     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?;
-
-            self.get_user_channels(user_id, channel_memberships, &tx)
-                .await
-        })
-        .await
-    }
-
-    pub async fn get_channel_for_user(
-        &self,
-        channel_id: ChannelId,
-        user_id: UserId,
-    ) -> Result<ChannelsForUser> {
-        self.transaction(|tx| async move {
-            let tx = tx;
-
-            let channel_membership = channel_member::Entity::find()
-                .filter(
-                    channel_member::Column::UserId
-                        .eq(user_id)
-                        .and(channel_member::Column::ChannelId.eq(channel_id))
-                        .and(channel_member::Column::Accepted.eq(true)),
-                )
-                .all(&*tx)
-                .await?;
-
-            self.get_user_channels(user_id, channel_membership, &tx)
-                .await
+            self.get_user_channels(user_id, None, &tx).await
         })
         .await
     }
@@ -408,22 +576,78 @@ impl Database {
     pub async fn get_user_channels(
         &self,
         user_id: UserId,
-        channel_memberships: Vec<channel_member::Model>,
+        ancestor_channel: Option<&channel::Model>,
         tx: &DatabaseTransaction,
     ) -> Result<ChannelsForUser> {
-        let parents_by_child_id = self
-            .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*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 channels_with_admin_privileges = channel_memberships
-            .iter()
-            .filter_map(|membership| membership.admin.then_some(membership.channel_id))
-            .collect();
-
-        let graph = self
-            .get_channel_graph(parents_by_child_id, true, &tx)
+        let descendants = self
+            .get_channel_descendants_including_self(
+                channel_memberships.iter().map(|m| m.channel_id),
+                &*tx,
+            )
             .await?;
 
+        let mut roles_by_channel_id: HashMap<ChannelId, ChannelRole> = HashMap::default();
+        for membership in channel_memberships.iter() {
+            roles_by_channel_id.insert(membership.channel_id, membership.role);
+        }
+
+        let mut visible_channel_ids: HashSet<ChannelId> = HashSet::default();
+
+        let channels: Vec<Channel> = descendants
+            .into_iter()
+            .filter_map(|channel| {
+                let parent_role = channel
+                    .parent_id()
+                    .and_then(|parent_id| roles_by_channel_id.get(&parent_id));
+
+                let role = if let Some(parent_role) = parent_role {
+                    let role = if let Some(existing_role) = roles_by_channel_id.get(&channel.id) {
+                        existing_role.max(*parent_role)
+                    } else {
+                        *parent_role
+                    };
+                    roles_by_channel_id.insert(channel.id, role);
+                    role
+                } else {
+                    *roles_by_channel_id.get(&channel.id)?
+                };
+
+                let can_see_parent_paths = role.can_see_all_descendants()
+                    || role.can_only_see_public_descendants()
+                        && channel.visibility == ChannelVisibility::Public;
+                if !can_see_parent_paths {
+                    return None;
+                }
+
+                visible_channel_ids.insert(channel.id);
+
+                if let Some(ancestor) = ancestor_channel {
+                    if !channel
+                        .ancestors_including_self()
+                        .any(|id| id == ancestor.id)
+                    {
+                        return None;
+                    }
+                }
+
+                let mut channel = Channel::from_model(channel, role);
+                channel
+                    .parent_path
+                    .retain(|id| visible_channel_ids.contains(&id));
+
+                Some(channel)
+            })
+            .collect();
+
         #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
         enum QueryUserIdsAndChannelIds {
             ChannelId,
@@ -434,7 +658,7 @@ impl Database {
         {
             let mut rows = room_participant::Entity::find()
                 .inner_join(room::Entity)
-                .filter(room::Column::ChannelId.is_in(graph.channels.iter().map(|c| c.id)))
+                .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id)))
                 .select_only()
                 .column(room::Column::ChannelId)
                 .column(room_participant::Column::UserId)
@@ -447,7 +671,7 @@ impl Database {
             }
         }
 
-        let channel_ids = graph.channels.iter().map(|c| c.id).collect::<Vec<_>>();
+        let channel_ids = channels.iter().map(|c| c.id).collect::<Vec<_>>();
         let channel_buffer_changes = self
             .unseen_channel_buffer_changes(user_id, &channel_ids, &*tx)
             .await?;
@@ -457,634 +681,632 @@ impl Database {
             .await?;
 
         Ok(ChannelsForUser {
-            channels: graph,
+            channels,
             channel_participants,
-            channels_with_admin_privileges,
             unseen_buffer_changes: channel_buffer_changes,
             channel_messages: unseen_messages,
         })
     }
 
-    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
+    async fn participants_to_notify_for_channel_change(
+        &self,
+        new_parent: &channel::Model,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<(UserId, ChannelsForUser)>> {
+        let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new();
+
+        let members = self
+            .get_channel_participant_details_internal(new_parent, &*tx)
+            .await?;
+
+        for member in members.iter() {
+            if !member.role.can_see_all_descendants() {
+                continue;
+            }
+            results.push((
+                member.user_id,
+                self.get_user_channels(member.user_id, Some(new_parent), &*tx)
+                    .await?,
+            ))
+        }
+
+        let public_parents = self
+            .public_ancestors_including_self(new_parent, &*tx)
+            .await?;
+        let public_parent = public_parents.last();
+
+        let Some(public_parent) = public_parent else {
+            return Ok(results);
+        };
+
+        // could save some time in the common case by skipping this if the
+        // new channel is not public and has no public descendants.
+        let public_members = if public_parent == new_parent {
+            members
+        } else {
+            self.get_channel_participant_details_internal(public_parent, &*tx)
+                .await?
+        };
+
+        for member in public_members {
+            if !member.role.can_only_see_public_descendants() {
+                continue;
+            };
+            results.push((
+                member.user_id,
+                self.get_user_channels(member.user_id, Some(public_parent), &*tx)
+                    .await?,
+            ))
+        }
+
+        Ok(results)
     }
 
-    pub async fn set_channel_member_admin(
+    pub async fn set_channel_member_role(
         &self,
         channel_id: ChannelId,
-        from: UserId,
+        admin_id: UserId,
         for_user: UserId,
-        admin: bool,
-    ) -> Result<()> {
+        role: ChannelRole,
+    ) -> Result<SetMemberRoleResult> {
         self.transaction(|tx| async move {
-            self.check_user_is_channel_admin(channel_id, from, &*tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_admin(&channel, admin_id, &*tx)
                 .await?;
 
-            let result = channel_member::Entity::update_many()
+            let membership = channel_member::Entity::find()
                 .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)
+                .one(&*tx)
                 .await?;
 
-            if result.rows_affected == 0 {
-                Err(anyhow!("no such member"))?;
-            }
+            let Some(membership) = membership else {
+                Err(anyhow!("no such member"))?
+            };
 
-            Ok(())
+            let mut update = membership.into_active_model();
+            update.role = ActiveValue::Set(role);
+            let updated = channel_member::Entity::update(update).exec(&*tx).await?;
+
+            if updated.accepted {
+                Ok(SetMemberRoleResult::MembershipUpdated(
+                    self.calculate_membership_updated(&channel, for_user, &*tx)
+                        .await?,
+                ))
+            } else {
+                Ok(SetMemberRoleResult::InviteUpdated(Channel::from_model(
+                    channel, role,
+                )))
+            }
         })
         .await
     }
 
-    pub async fn get_channel_member_details(
+    pub async fn get_channel_participant_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 (role, members) = self
+            .transaction(move |tx| async move {
+                let channel = self.get_channel_internal(channel_id, &*tx).await?;
+                let role = self
+                    .check_user_is_channel_participant(&channel, user_id, &*tx)
+                    .await?;
+                Ok((
+                    role,
+                    self.get_channel_participant_details_internal(&channel, &*tx)
+                        .await?,
+                ))
+            })
+            .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;
+        if role == ChannelRole::Admin {
+            Ok(members
+                .into_iter()
+                .map(|channel_member| channel_member.to_proto())
+                .collect())
+        } else {
+            return Ok(members
+                .into_iter()
+                .filter_map(|member| {
+                    if member.kind == proto::channel_member::Kind::Invitee {
+                        return None;
                     }
-                }
-                rows.push(proto::ChannelMember {
-                    user_id,
-                    kind,
-                    admin: is_admin,
-                });
-            }
-
-            Ok(rows)
-        })
-        .await
+                    Some(ChannelMember {
+                        role: member.role,
+                        user_id: member.user_id,
+                        kind: proto::channel_member::Kind::Member,
+                    })
+                })
+                .map(|channel_member| channel_member.to_proto())
+                .collect());
+        }
     }
 
-    pub async fn get_channel_members_internal(
+    async fn get_channel_participant_details_internal(
         &self,
-        id: ChannelId,
+        channel: &channel::Model,
         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)),
-            )
+    ) -> Result<Vec<ChannelMember>> {
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryMemberDetails {
+            UserId,
+            Role,
+            IsDirectMember,
+            Accepted,
+            Visibility,
+        }
+
+        let mut stream = channel_member::Entity::find()
+            .left_join(channel::Entity)
+            .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
             .select_only()
             .column(channel_member::Column::UserId)
-            .into_values::<_, QueryUserIds>()
-            .all(&*tx)
+            .column(channel_member::Column::Role)
+            .column_as(
+                channel_member::Column::ChannelId.eq(channel.id),
+                QueryMemberDetails::IsDirectMember,
+            )
+            .column(channel_member::Column::Accepted)
+            .column(channel::Column::Visibility)
+            .into_values::<_, QueryMemberDetails>()
+            .stream(&*tx)
             .await?;
-        Ok(user_ids)
+
+        let mut user_details: HashMap<UserId, ChannelMember> = HashMap::default();
+
+        while let Some(user_membership) = stream.next().await {
+            let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): (
+                UserId,
+                ChannelRole,
+                bool,
+                bool,
+                ChannelVisibility,
+            ) = user_membership?;
+            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,
+            };
+
+            if channel_role == ChannelRole::Guest
+                && visibility != ChannelVisibility::Public
+                && channel.visibility != ChannelVisibility::Public
+            {
+                continue;
+            }
+
+            if let Some(details_mut) = user_details.get_mut(&user_id) {
+                if channel_role.should_override(details_mut.role) {
+                    details_mut.role = channel_role;
+                }
+                if kind == Kind::Member {
+                    details_mut.kind = kind;
+                // the UI is going to be a bit confusing if you already have permissions
+                // that are greater than or equal to the ones you're being invited to.
+                } else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember {
+                    details_mut.kind = kind;
+                }
+            } else {
+                user_details.insert(
+                    user_id,
+                    ChannelMember {
+                        user_id,
+                        kind,
+                        role: channel_role,
+                    },
+                );
+            }
+        }
+
+        Ok(user_details
+            .into_iter()
+            .map(|(_, details)| details)
+            .collect())
     }
 
-    pub async fn check_user_is_channel_member(
+    pub async fn get_channel_participants(
         &self,
-        channel_id: ChannelId,
-        user_id: UserId,
+        channel: &channel::Model,
         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(())
+    ) -> Result<Vec<UserId>> {
+        let participants = self
+            .get_channel_participant_details_internal(channel, &*tx)
+            .await?;
+        Ok(participants
+            .into_iter()
+            .map(|member| member.user_id)
+            .collect())
     }
 
     pub async fn check_user_is_channel_admin(
         &self,
-        channel_id: ChannelId,
+        channel: &channel::Model,
         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(())
+    ) -> Result<ChannelRole> {
+        let role = self.channel_role_for_user(channel, user_id, tx).await?;
+        match role {
+            Some(ChannelRole::Admin) => Ok(role.unwrap()),
+            Some(ChannelRole::Member)
+            | Some(ChannelRole::Banned)
+            | Some(ChannelRole::Guest)
+            | None => Err(anyhow!(
+                "user is not a channel admin or channel does not exist"
+            ))?,
+        }
     }
 
-    /// Returns the channel ancestors, deepest first
-    pub async fn get_channel_ancestors(
+    pub async fn check_user_is_channel_member(
         &self,
-        channel_id: ChannelId,
+        channel: &channel::Model,
+        user_id: UserId,
         tx: &DatabaseTransaction,
-    ) -> Result<Vec<ChannelId>> {
-        let paths = channel_path::Entity::find()
-            .filter(channel_path::Column::ChannelId.eq(channel_id))
-            .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc)
-            .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);
-                    }
-                }
-            }
+    ) -> Result<ChannelRole> {
+        let channel_role = self.channel_role_for_user(channel, user_id, tx).await?;
+        match channel_role {
+            Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()),
+            Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
+                "user is not a channel member or channel does not exist"
+            ))?,
         }
-        Ok(channel_ids)
     }
 
-    /// Returns the channel descendants,
-    /// Structured as a map from child ids to their parent ids
-    /// For example, the descendants of 'a' in this DAG:
-    ///
-    ///   /- b -\
-    /// a -- c -- d
-    ///
-    /// would be:
-    /// {
-    ///     a: [],
-    ///     b: [a],
-    ///     c: [a],
-    ///     d: [a, c],
-    /// }
-    async fn get_channel_descendants(
+    pub async fn check_user_is_channel_participant(
         &self,
-        channel_ids: impl IntoIterator<Item = ChannelId>,
+        channel: &channel::Model,
+        user_id: UserId,
         tx: &DatabaseTransaction,
-    ) -> Result<ChannelDescendants> {
-        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: ChannelDescendants = 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);
-                }
-            }
-            let entry = parents_by_child_id.entry(path.channel_id).or_default();
-            if let Some(parent_id) = parent_id {
-                entry.insert(parent_id);
+    ) -> Result<ChannelRole> {
+        let role = self.channel_role_for_user(channel, user_id, tx).await?;
+        match role {
+            Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => {
+                Ok(role.unwrap())
             }
+            Some(ChannelRole::Banned) | None => Err(anyhow!(
+                "user is not a channel participant or channel does not exist"
+            ))?,
         }
-
-        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(
+    pub async fn pending_invite_for_channel(
         &self,
-        channel_id: ChannelId,
+        channel: &channel::Model,
         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);
+        tx: &DatabaseTransaction,
+    ) -> Result<Option<channel_member::Model>> {
+        let row = channel_member::Entity::find()
+            .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
+            .filter(channel_member::Column::UserId.eq(user_id))
+            .filter(channel_member::Column::Accepted.eq(false))
+            .one(&*tx)
+            .await?;
 
-                Ok(Some((
-                    Channel {
-                        id: channel.id,
-                        name: channel.name,
-                    },
-                    is_accepted,
-                )))
-            } else {
-                Ok(None)
-            }
-        })
-        .await
+        Ok(row)
     }
 
-    pub async fn get_or_create_channel_room(
+    pub async fn public_parent_channel(
         &self,
-        channel_id: ChannelId,
-        live_kit_room: &str,
-        enviroment: &str,
-    ) -> Result<RoomId> {
-        self.transaction(|tx| async move {
-            let tx = tx;
-
-            let room = room::Entity::find()
-                .filter(room::Column::ChannelId.eq(channel_id))
-                .one(&*tx)
-                .await?;
-
-            let room_id = if let Some(room) = room {
-                room.id
-            } else {
-                let result = room::Entity::insert(room::ActiveModel {
-                    channel_id: ActiveValue::Set(Some(channel_id)),
-                    live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
-                    enviroment: ActiveValue::Set(Some(enviroment.to_string())),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-
-                result.last_insert_id
-            };
-
-            Ok(room_id)
-        })
-        .await
+        channel: &channel::Model,
+        tx: &DatabaseTransaction,
+    ) -> Result<Option<channel::Model>> {
+        let mut path = self.public_ancestors_including_self(channel, &*tx).await?;
+        if path.last().unwrap().id == channel.id {
+            path.pop();
+        }
+        Ok(path.pop())
     }
 
-    // Insert an edge from the given channel to the given other channel.
-    pub async fn link_channel(
+    pub async fn public_ancestors_including_self(
         &self,
-        user: UserId,
-        channel: ChannelId,
-        to: ChannelId,
-    ) -> Result<ChannelGraph> {
-        self.transaction(|tx| async move {
-            // Note that even with these maxed permissions, this linking operation
-            // is still insecure because you can't remove someone's permissions to a
-            // channel if they've linked the channel to one where they're an admin.
-            self.check_user_is_channel_admin(channel, user, &*tx)
-                .await?;
+        channel: &channel::Model,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<channel::Model>> {
+        let visible_channels = channel::Entity::find()
+            .filter(channel::Column::Id.is_in(channel.ancestors_including_self()))
+            .filter(channel::Column::Visibility.eq(ChannelVisibility::Public))
+            .order_by_asc(channel::Column::ParentPath)
+            .all(&*tx)
+            .await?;
 
-            self.link_channel_internal(user, channel, to, &*tx).await
-        })
-        .await
+        Ok(visible_channels)
     }
 
-    pub async fn link_channel_internal(
+    pub async fn channel_role_for_user(
         &self,
-        user: UserId,
-        channel: ChannelId,
-        to: ChannelId,
+        channel: &channel::Model,
+        user_id: UserId,
         tx: &DatabaseTransaction,
-    ) -> Result<ChannelGraph> {
-        self.check_user_is_channel_admin(to, user, &*tx).await?;
+    ) -> Result<Option<ChannelRole>> {
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryChannelMembership {
+            ChannelId,
+            Role,
+            Visibility,
+        }
 
-        let paths = channel_path::Entity::find()
-            .filter(channel_path::Column::IdPath.like(&format!("%/{}/%", channel)))
-            .all(tx)
+        let mut rows = channel_member::Entity::find()
+            .left_join(channel::Entity)
+            .filter(
+                channel_member::Column::ChannelId
+                    .is_in(channel.ancestors_including_self())
+                    .and(channel_member::Column::UserId.eq(user_id))
+                    .and(channel_member::Column::Accepted.eq(true)),
+            )
+            .select_only()
+            .column(channel_member::Column::ChannelId)
+            .column(channel_member::Column::Role)
+            .column(channel::Column::Visibility)
+            .into_values::<_, QueryChannelMembership>()
+            .stream(&*tx)
             .await?;
 
-        let mut new_path_suffixes = HashSet::default();
-        for path in paths {
-            if let Some(start_offset) = path.id_path.find(&format!("/{}/", channel)) {
-                new_path_suffixes.insert((
-                    path.channel_id,
-                    path.id_path[(start_offset + 1)..].to_string(),
-                ));
+        let mut user_role: Option<ChannelRole> = None;
+
+        let mut is_participant = false;
+        let mut current_channel_visibility = None;
+
+        // note these channels are not iterated in any particular order,
+        // our current logic takes the highest permission available.
+        while let Some(row) = rows.next().await {
+            let (membership_channel, role, visibility): (
+                ChannelId,
+                ChannelRole,
+                ChannelVisibility,
+            ) = row?;
+
+            match role {
+                ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => {
+                    if let Some(users_role) = user_role {
+                        user_role = Some(users_role.max(role));
+                    } else {
+                        user_role = Some(role)
+                    }
+                }
+                ChannelRole::Guest if visibility == ChannelVisibility::Public => {
+                    is_participant = true
+                }
+                ChannelRole::Guest => {}
+            }
+            if channel.id == membership_channel {
+                current_channel_visibility = Some(visibility);
             }
         }
+        // free up database connection
+        drop(rows);
 
-        let paths_to_new_parent = channel_path::Entity::find()
-            .filter(channel_path::Column::ChannelId.eq(to))
-            .all(tx)
-            .await?;
-
-        let mut new_paths = Vec::new();
-        for path in paths_to_new_parent {
-            if path.id_path.contains(&format!("/{}/", channel)) {
-                Err(anyhow!("cycle"))?;
+        if is_participant && user_role.is_none() {
+            if current_channel_visibility.is_none() {
+                current_channel_visibility = channel::Entity::find()
+                    .filter(channel::Column::Id.eq(channel.id))
+                    .one(&*tx)
+                    .await?
+                    .map(|channel| channel.visibility);
+            }
+            if current_channel_visibility == Some(ChannelVisibility::Public) {
+                user_role = Some(ChannelRole::Guest);
             }
-
-            new_paths.extend(new_path_suffixes.iter().map(|(channel_id, path_suffix)| {
-                channel_path::ActiveModel {
-                    channel_id: ActiveValue::Set(*channel_id),
-                    id_path: ActiveValue::Set(format!("{}{}", &path.id_path, path_suffix)),
-                }
-            }));
         }
 
-        channel_path::Entity::insert_many(new_paths)
-            .exec(&*tx)
-            .await?;
+        Ok(user_role)
+    }
 
-        // remove any root edges for the channel we just linked
-        {
-            channel_path::Entity::delete_many()
-                .filter(channel_path::Column::IdPath.like(&format!("/{}/%", channel)))
-                .exec(&*tx)
-                .await?;
+    // Get the descendants of the given set if channels, ordered by their
+    // path.
+    async fn get_channel_descendants_including_self(
+        &self,
+        channel_ids: impl IntoIterator<Item = ChannelId>,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<channel::Model>> {
+        let mut values = String::new();
+        for id in channel_ids {
+            if !values.is_empty() {
+                values.push_str(", ");
+            }
+            write!(&mut values, "({})", id).unwrap();
         }
 
-        let mut channel_descendants = self.get_channel_descendants([channel], &*tx).await?;
-        if let Some(channel) = channel_descendants.get_mut(&channel) {
-            // Remove the other parents
-            channel.clear();
-            channel.insert(to);
+        if values.is_empty() {
+            return Ok(vec![]);
         }
 
-        let channels = self
-            .get_channel_graph(channel_descendants, false, &*tx)
-            .await?;
+        let sql = format!(
+            r#"
+            SELECT DISTINCT
+                descendant_channels.*,
+                descendant_channels.parent_path || descendant_channels.id as full_path
+            FROM
+                channels parent_channels, channels descendant_channels
+            WHERE
+                descendant_channels.id IN ({values}) OR
+                (
+                    parent_channels.id IN ({values}) AND
+                    descendant_channels.parent_path LIKE (parent_channels.parent_path || parent_channels.id || '/%')
+                )
+            ORDER BY
+                full_path ASC
+            "#
+        );
 
-        Ok(channels)
+        Ok(channel::Entity::find()
+            .from_raw_sql(Statement::from_string(
+                self.pool.get_database_backend(),
+                sql,
+            ))
+            .all(tx)
+            .await?)
     }
 
-    /// Unlink a channel from a given parent. This will add in a root edge if
-    /// the channel has no other parents after this operation.
-    pub async fn unlink_channel(
-        &self,
-        user: UserId,
-        channel: ChannelId,
-        from: ChannelId,
-    ) -> Result<()> {
+    /// Returns the channel with the given ID
+    pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result<Channel> {
         self.transaction(|tx| async move {
-            // Note that even with these maxed permissions, this linking operation
-            // is still insecure because you can't remove someone's permissions to a
-            // channel if they've linked the channel to one where they're an admin.
-            self.check_user_is_channel_admin(channel, user, &*tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            let role = self
+                .check_user_is_channel_participant(&channel, user_id, &*tx)
                 .await?;
 
-            self.unlink_channel_internal(user, channel, from, &*tx)
-                .await?;
-
-            Ok(())
+            Ok(Channel::from_model(channel, role))
         })
         .await
     }
 
-    pub async fn unlink_channel_internal(
+    pub async fn get_channel_internal(
         &self,
-        user: UserId,
-        channel: ChannelId,
-        from: ChannelId,
+        channel_id: ChannelId,
         tx: &DatabaseTransaction,
-    ) -> Result<()> {
-        self.check_user_is_channel_admin(from, user, &*tx).await?;
+    ) -> Result<channel::Model> {
+        Ok(channel::Entity::find_by_id(channel_id)
+            .one(&*tx)
+            .await?
+            .ok_or_else(|| anyhow!("no such channel"))?)
+    }
 
-        let sql = r#"
-            DELETE FROM channel_paths
-            WHERE
-                id_path LIKE '%/' || $1 || '/' || $2 || '/%'
-            RETURNING id_path, channel_id
-        "#;
+    pub(crate) async fn get_or_create_channel_room(
+        &self,
+        channel_id: ChannelId,
+        live_kit_room: &str,
+        environment: &str,
+        tx: &DatabaseTransaction,
+    ) -> Result<RoomId> {
+        let room = room::Entity::find()
+            .filter(room::Column::ChannelId.eq(channel_id))
+            .one(&*tx)
+            .await?;
 
-        let paths = channel_path::Entity::find()
-            .from_raw_sql(Statement::from_sql_and_values(
-                self.pool.get_database_backend(),
-                sql,
-                [from.to_proto().into(), channel.to_proto().into()],
-            ))
-            .all(&*tx)
+        let room_id = if let Some(room) = room {
+            if let Some(env) = room.enviroment {
+                if &env != environment {
+                    Err(anyhow!("must join using the {} release", env))?;
+                }
+            }
+            room.id
+        } else {
+            let result = room::Entity::insert(room::ActiveModel {
+                channel_id: ActiveValue::Set(Some(channel_id)),
+                live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
+                enviroment: ActiveValue::Set(Some(environment.to_string())),
+                ..Default::default()
+            })
+            .exec(&*tx)
             .await?;
 
-        let is_stranded = channel_path::Entity::find()
-            .filter(channel_path::Column::ChannelId.eq(channel))
-            .count(&*tx)
-            .await?
-            == 0;
-
-        // Make sure that there is always at least one path to the channel
-        if is_stranded {
-            let root_paths: Vec<_> = paths
-                .iter()
-                .map(|path| {
-                    let start_offset = path.id_path.find(&format!("/{}/", channel)).unwrap();
-                    channel_path::ActiveModel {
-                        channel_id: ActiveValue::Set(path.channel_id),
-                        id_path: ActiveValue::Set(path.id_path[start_offset..].to_string()),
-                    }
-                })
-                .collect();
-            channel_path::Entity::insert_many(root_paths)
-                .exec(&*tx)
-                .await?;
-        }
+            result.last_insert_id
+        };
 
-        Ok(())
+        Ok(room_id)
     }
 
-    /// Move a channel from one parent to another, returns the
-    /// Channels that were moved for notifying clients
+    /// Move a channel from one parent to another
     pub async fn move_channel(
         &self,
-        user: UserId,
-        channel: ChannelId,
-        from: ChannelId,
-        to: ChannelId,
-    ) -> Result<ChannelGraph> {
-        if from == to {
-            return Ok(ChannelGraph {
-                channels: vec![],
-                edges: vec![],
-            });
-        }
-
+        channel_id: ChannelId,
+        new_parent_id: Option<ChannelId>,
+        admin_id: UserId,
+    ) -> Result<Option<MoveChannelResult>> {
         self.transaction(|tx| async move {
-            self.check_user_is_channel_admin(channel, user, &*tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_admin(&channel, admin_id, &*tx)
                 .await?;
 
-            let moved_channels = self.link_channel_internal(user, channel, to, &*tx).await?;
+            let new_parent_path;
+            let new_parent_channel;
+            if let Some(new_parent_id) = new_parent_id {
+                let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
+                self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
+                    .await?;
 
-            self.unlink_channel_internal(user, channel, from, &*tx)
+                new_parent_path = new_parent.path();
+                new_parent_channel = Some(new_parent);
+            } else {
+                new_parent_path = String::new();
+                new_parent_channel = None;
+            };
+
+            let previous_participants = self
+                .get_channel_participant_details_internal(&channel, &*tx)
                 .await?;
 
-            Ok(moved_channels)
-        })
-        .await
-    }
-}
+            let old_path = format!("{}{}/", channel.parent_path, channel.id);
+            let new_path = format!("{}{}/", new_parent_path, channel.id);
 
-#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-enum QueryUserIds {
-    UserId,
-}
+            if old_path == new_path {
+                return Ok(None);
+            }
 
-#[derive(Debug)]
-pub struct ChannelGraph {
-    pub channels: Vec<Channel>,
-    pub edges: Vec<ChannelEdge>,
-}
+            let mut model = channel.into_active_model();
+            model.parent_path = ActiveValue::Set(new_parent_path);
+            let channel = model.update(&*tx).await?;
 
-impl ChannelGraph {
-    pub fn is_empty(&self) -> bool {
-        self.channels.is_empty() && self.edges.is_empty()
-    }
-}
+            if new_parent_channel.is_none() {
+                channel_member::ActiveModel {
+                    id: ActiveValue::NotSet,
+                    channel_id: ActiveValue::Set(channel_id),
+                    user_id: ActiveValue::Set(admin_id),
+                    accepted: ActiveValue::Set(true),
+                    role: ActiveValue::Set(ChannelRole::Admin),
+                }
+                .insert(&*tx)
+                .await?;
+            }
 
-#[cfg(test)]
-impl PartialEq for ChannelGraph {
-    fn eq(&self, other: &Self) -> bool {
-        // Order independent comparison for tests
-        let channels_set = self.channels.iter().collect::<HashSet<_>>();
-        let other_channels_set = other.channels.iter().collect::<HashSet<_>>();
-        let edges_set = self
-            .edges
-            .iter()
-            .map(|edge| (edge.channel_id, edge.parent_id))
-            .collect::<HashSet<_>>();
-        let other_edges_set = other
-            .edges
-            .iter()
-            .map(|edge| (edge.channel_id, edge.parent_id))
-            .collect::<HashSet<_>>();
-
-        channels_set == other_channels_set && edges_set == other_edges_set
-    }
-}
+            let descendent_ids =
+                ChannelId::find_by_statement::<QueryIds>(Statement::from_sql_and_values(
+                    self.pool.get_database_backend(),
+                    "
+                    UPDATE channels SET parent_path = REPLACE(parent_path, $1, $2)
+                    WHERE parent_path LIKE $3 || '%'
+                    RETURNING id
+                ",
+                    [old_path.clone().into(), new_path.into(), old_path.into()],
+                ))
+                .all(&*tx)
+                .await?;
 
-#[cfg(not(test))]
-impl PartialEq for ChannelGraph {
-    fn eq(&self, other: &Self) -> bool {
-        self.channels == other.channels && self.edges == other.edges
-    }
-}
+            let participants_to_update: HashMap<_, _> = self
+                .participants_to_notify_for_channel_change(
+                    new_parent_channel.as_ref().unwrap_or(&channel),
+                    &*tx,
+                )
+                .await?
+                .into_iter()
+                .collect();
 
-struct SmallSet<T>(SmallVec<[T; 1]>);
+            let mut moved_channels: HashSet<ChannelId> = HashSet::default();
+            for id in descendent_ids {
+                moved_channels.insert(id);
+            }
+            moved_channels.insert(channel_id);
 
-impl<T> Deref for SmallSet<T> {
-    type Target = [T];
+            let mut participants_to_remove: HashSet<UserId> = HashSet::default();
+            for participant in previous_participants {
+                if participant.kind == proto::channel_member::Kind::AncestorMember {
+                    if !participants_to_update.contains_key(&participant.user_id) {
+                        participants_to_remove.insert(participant.user_id);
+                    }
+                }
+            }
 
-    fn deref(&self) -> &Self::Target {
-        self.0.deref()
+            Ok(Some(MoveChannelResult {
+                participants_to_remove,
+                participants_to_update,
+                moved_channels,
+            }))
+        })
+        .await
     }
 }
 
-impl<T> Default for SmallSet<T> {
-    fn default() -> Self {
-        Self(SmallVec::new())
-    }
+#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+enum QueryIds {
+    Id,
 }
 
-impl<T> SmallSet<T> {
-    fn insert(&mut self, value: T) -> bool
-    where
-        T: Ord,
-    {
-        match self.binary_search(&value) {
-            Ok(_) => false,
-            Err(ix) => {
-                self.0.insert(ix, value);
-                true
-            }
-        }
-    }
-
-    fn clear(&mut self) {
-        self.0.clear();
-    }
+#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+enum QueryUserIds {
+    UserId,
 }

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

@@ -8,7 +8,6 @@ impl Database {
             user_id_b: UserId,
             a_to_b: bool,
             accepted: bool,
-            should_notify: bool,
             user_a_busy: bool,
             user_b_busy: bool,
         }
@@ -53,7 +52,6 @@ impl Database {
                     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 {
@@ -63,19 +61,16 @@ impl Database {
                     } 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 {
@@ -124,7 +119,11 @@ impl Database {
         .await
     }
 
-    pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> {
+    pub async fn send_contact_request(
+        &self,
+        sender_id: UserId,
+        receiver_id: UserId,
+    ) -> Result<NotificationBatch> {
         self.transaction(|tx| async move {
             let (id_a, id_b, a_to_b) = if sender_id < receiver_id {
                 (sender_id, receiver_id, true)
@@ -161,11 +160,22 @@ impl Database {
             .exec_without_returning(&*tx)
             .await?;
 
-            if rows_affected == 1 {
-                Ok(())
-            } else {
-                Err(anyhow!("contact already requested"))?
+            if rows_affected == 0 {
+                Err(anyhow!("contact already requested"))?;
             }
+
+            Ok(self
+                .create_notification(
+                    receiver_id,
+                    rpc::Notification::ContactRequest {
+                        sender_id: sender_id.to_proto(),
+                    },
+                    true,
+                    &*tx,
+                )
+                .await?
+                .into_iter()
+                .collect())
         })
         .await
     }
@@ -179,7 +189,11 @@ impl Database {
     ///
     /// * `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> {
+    pub async fn remove_contact(
+        &self,
+        requester_id: UserId,
+        responder_id: UserId,
+    ) -> Result<(bool, Option<NotificationId>)> {
         self.transaction(|tx| async move {
             let (id_a, id_b) = if responder_id < requester_id {
                 (responder_id, requester_id)
@@ -198,7 +212,21 @@ impl Database {
                 .ok_or_else(|| anyhow!("no such contact"))?;
 
             contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
-            Ok(contact.accepted)
+
+            let mut deleted_notification_id = None;
+            if !contact.accepted {
+                deleted_notification_id = self
+                    .remove_notification(
+                        responder_id,
+                        rpc::Notification::ContactRequest {
+                            sender_id: requester_id.to_proto(),
+                        },
+                        &*tx,
+                    )
+                    .await?;
+            }
+
+            Ok((contact.accepted, deleted_notification_id))
         })
         .await
     }
@@ -249,7 +277,7 @@ impl Database {
         responder_id: UserId,
         requester_id: UserId,
         accept: bool,
-    ) -> Result<()> {
+    ) -> Result<NotificationBatch> {
         self.transaction(|tx| async move {
             let (id_a, id_b, a_to_b) = if responder_id < requester_id {
                 (responder_id, requester_id, false)
@@ -287,11 +315,38 @@ impl Database {
                 result.rows_affected
             };
 
-            if rows_affected == 1 {
-                Ok(())
-            } else {
+            if rows_affected == 0 {
                 Err(anyhow!("no such contact request"))?
             }
+
+            let mut notifications = Vec::new();
+            notifications.extend(
+                self.mark_notification_as_read_with_response(
+                    responder_id,
+                    &rpc::Notification::ContactRequest {
+                        sender_id: requester_id.to_proto(),
+                    },
+                    accept,
+                    &*tx,
+                )
+                .await?,
+            );
+
+            if accept {
+                notifications.extend(
+                    self.create_notification(
+                        requester_id,
+                        rpc::Notification::ContactRequestAccepted {
+                            responder_id: responder_id.to_proto(),
+                        },
+                        true,
+                        &*tx,
+                    )
+                    .await?,
+                );
+            }
+
+            Ok(notifications)
         })
         .await
     }

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

@@ -1,4 +1,6 @@
 use super::*;
+use rpc::Notification;
+use sea_orm::TryInsertResult;
 use time::OffsetDateTime;
 
 impl Database {
@@ -9,7 +11,8 @@ impl Database {
         user_id: UserId,
     ) -> Result<()> {
         self.transaction(|tx| async move {
-            self.check_user_is_channel_member(channel_id, user_id, &*tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_participant(&channel, user_id, &*tx)
                 .await?;
             channel_chat_participant::ActiveModel {
                 id: ActiveValue::NotSet,
@@ -77,7 +80,8 @@ impl Database {
         before_message_id: Option<MessageId>,
     ) -> Result<Vec<proto::ChannelMessage>> {
         self.transaction(|tx| async move {
-            self.check_user_is_channel_member(channel_id, user_id, &*tx)
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_participant(&channel, user_id, &*tx)
                 .await?;
 
             let mut condition =
@@ -87,33 +91,103 @@ impl Database {
                 condition = condition.add(channel_message::Column::Id.lt(before_message_id));
             }
 
-            let mut rows = channel_message::Entity::find()
+            let rows = channel_message::Entity::find()
                 .filter(condition)
                 .order_by_desc(channel_message::Column::Id)
                 .limit(count as u64)
-                .stream(&*tx)
+                .all(&*tx)
                 .await?;
 
-            let mut messages = Vec::new();
-            while let Some(row) = rows.next().await {
-                let row = row?;
+            self.load_channel_messages(rows, &*tx).await
+        })
+        .await
+    }
+
+    pub async fn get_channel_messages_by_id(
+        &self,
+        user_id: UserId,
+        message_ids: &[MessageId],
+    ) -> Result<Vec<proto::ChannelMessage>> {
+        self.transaction(|tx| async move {
+            let rows = channel_message::Entity::find()
+                .filter(channel_message::Column::Id.is_in(message_ids.iter().copied()))
+                .order_by_desc(channel_message::Column::Id)
+                .all(&*tx)
+                .await?;
+
+            let mut channels = HashMap::<ChannelId, channel::Model>::default();
+            for row in &rows {
+                channels.insert(
+                    row.channel_id,
+                    self.get_channel_internal(row.channel_id, &*tx).await?,
+                );
+            }
+
+            for (_, channel) in channels {
+                self.check_user_is_channel_participant(&channel, user_id, &*tx)
+                    .await?;
+            }
+
+            let messages = self.load_channel_messages(rows, &*tx).await?;
+            Ok(messages)
+        })
+        .await
+    }
+
+    async fn load_channel_messages(
+        &self,
+        rows: Vec<channel_message::Model>,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<proto::ChannelMessage>> {
+        let mut messages = rows
+            .into_iter()
+            .map(|row| {
                 let nonce = row.nonce.as_u64_pair();
-                messages.push(proto::ChannelMessage {
+                proto::ChannelMessage {
                     id: row.id.to_proto(),
                     sender_id: row.sender_id.to_proto(),
                     body: row.body,
                     timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
+                    mentions: vec![],
                     nonce: Some(proto::Nonce {
                         upper_half: nonce.0,
                         lower_half: nonce.1,
                     }),
-                });
+                }
+            })
+            .collect::<Vec<_>>();
+        messages.reverse();
+
+        let mut mentions = channel_message_mention::Entity::find()
+            .filter(channel_message_mention::Column::MessageId.is_in(messages.iter().map(|m| m.id)))
+            .order_by_asc(channel_message_mention::Column::MessageId)
+            .order_by_asc(channel_message_mention::Column::StartOffset)
+            .stream(&*tx)
+            .await?;
+
+        let mut message_ix = 0;
+        while let Some(mention) = mentions.next().await {
+            let mention = mention?;
+            let message_id = mention.message_id.to_proto();
+            while let Some(message) = messages.get_mut(message_ix) {
+                if message.id < message_id {
+                    message_ix += 1;
+                } else {
+                    if message.id == message_id {
+                        message.mentions.push(proto::ChatMention {
+                            range: Some(proto::Range {
+                                start: mention.start_offset as u64,
+                                end: mention.end_offset as u64,
+                            }),
+                            user_id: mention.user_id.to_proto(),
+                        });
+                    }
+                    break;
+                }
             }
-            drop(rows);
-            messages.reverse();
-            Ok(messages)
-        })
-        .await
+        }
+
+        Ok(messages)
     }
 
     pub async fn create_channel_message(
@@ -121,10 +195,15 @@ impl Database {
         channel_id: ChannelId,
         user_id: UserId,
         body: &str,
+        mentions: &[proto::ChatMention],
         timestamp: OffsetDateTime,
         nonce: u128,
-    ) -> Result<(MessageId, Vec<ConnectionId>, Vec<UserId>)> {
+    ) -> Result<CreatedChannelMessage> {
         self.transaction(|tx| async move {
+            let channel = self.get_channel_internal(channel_id, &*tx).await?;
+            self.check_user_is_channel_participant(&channel, user_id, &*tx)
+                .await?;
+
             let mut rows = channel_chat_participant::Entity::find()
                 .filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
                 .stream(&*tx)
@@ -150,7 +229,7 @@ impl Database {
             let timestamp = timestamp.to_offset(time::UtcOffset::UTC);
             let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
 
-            let message = channel_message::Entity::insert(channel_message::ActiveModel {
+            let result = channel_message::Entity::insert(channel_message::ActiveModel {
                 channel_id: ActiveValue::Set(channel_id),
                 sender_id: ActiveValue::Set(user_id),
                 body: ActiveValue::Set(body.to_string()),
@@ -159,35 +238,85 @@ impl Database {
                 id: ActiveValue::NotSet,
             })
             .on_conflict(
-                OnConflict::column(channel_message::Column::Nonce)
-                    .update_column(channel_message::Column::Nonce)
-                    .to_owned(),
+                OnConflict::columns([
+                    channel_message::Column::SenderId,
+                    channel_message::Column::Nonce,
+                ])
+                .do_nothing()
+                .to_owned(),
             )
+            .do_nothing()
             .exec(&*tx)
             .await?;
 
-            #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
-            enum QueryConnectionId {
-                ConnectionId,
+            let message_id;
+            let mut notifications = Vec::new();
+            match result {
+                TryInsertResult::Inserted(result) => {
+                    message_id = result.last_insert_id;
+                    let mentioned_user_ids =
+                        mentions.iter().map(|m| m.user_id).collect::<HashSet<_>>();
+                    let mentions = mentions
+                        .iter()
+                        .filter_map(|mention| {
+                            let range = mention.range.as_ref()?;
+                            if !body.is_char_boundary(range.start as usize)
+                                || !body.is_char_boundary(range.end as usize)
+                            {
+                                return None;
+                            }
+                            Some(channel_message_mention::ActiveModel {
+                                message_id: ActiveValue::Set(message_id),
+                                start_offset: ActiveValue::Set(range.start as i32),
+                                end_offset: ActiveValue::Set(range.end as i32),
+                                user_id: ActiveValue::Set(UserId::from_proto(mention.user_id)),
+                            })
+                        })
+                        .collect::<Vec<_>>();
+                    if !mentions.is_empty() {
+                        channel_message_mention::Entity::insert_many(mentions)
+                            .exec(&*tx)
+                            .await?;
+                    }
+
+                    for mentioned_user in mentioned_user_ids {
+                        notifications.extend(
+                            self.create_notification(
+                                UserId::from_proto(mentioned_user),
+                                rpc::Notification::ChannelMessageMention {
+                                    message_id: message_id.to_proto(),
+                                    sender_id: user_id.to_proto(),
+                                    channel_id: channel_id.to_proto(),
+                                },
+                                false,
+                                &*tx,
+                            )
+                            .await?,
+                        );
+                    }
+
+                    self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
+                        .await?;
+                }
+                _ => {
+                    message_id = channel_message::Entity::find()
+                        .filter(channel_message::Column::Nonce.eq(Uuid::from_u128(nonce)))
+                        .one(&*tx)
+                        .await?
+                        .ok_or_else(|| anyhow!("failed to insert message"))?
+                        .id;
+                }
             }
 
-            // Observe this message for the sender
-            self.observe_channel_message_internal(
-                channel_id,
-                user_id,
-                message.last_insert_id,
-                &*tx,
-            )
-            .await?;
-
-            let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
+            let mut channel_members = self.get_channel_participants(&channel, &*tx).await?;
             channel_members.retain(|member| !participant_user_ids.contains(member));
 
-            Ok((
-                message.last_insert_id,
+            Ok(CreatedChannelMessage {
+                message_id,
                 participant_connection_ids,
                 channel_members,
-            ))
+                notifications,
+            })
         })
         .await
     }
@@ -197,11 +326,24 @@ impl Database {
         channel_id: ChannelId,
         user_id: UserId,
         message_id: MessageId,
-    ) -> Result<()> {
+    ) -> Result<NotificationBatch> {
         self.transaction(|tx| async move {
             self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
                 .await?;
-            Ok(())
+            let mut batch = NotificationBatch::default();
+            batch.extend(
+                self.mark_notification_as_read(
+                    user_id,
+                    &Notification::ChannelMessageMention {
+                        message_id: message_id.to_proto(),
+                        sender_id: Default::default(),
+                        channel_id: Default::default(),
+                    },
+                    &*tx,
+                )
+                .await?,
+            );
+            Ok(batch)
         })
         .await
     }
@@ -337,8 +479,23 @@ impl Database {
                 .filter(channel_message::Column::SenderId.eq(user_id))
                 .exec(&*tx)
                 .await?;
+
             if result.rows_affected == 0 {
-                Err(anyhow!("no such message"))?;
+                let channel = self.get_channel_internal(channel_id, &*tx).await?;
+                if self
+                    .check_user_is_channel_admin(&channel, user_id, &*tx)
+                    .await
+                    .is_ok()
+                {
+                    let result = channel_message::Entity::delete_by_id(message_id)
+                        .exec(&*tx)
+                        .await?;
+                    if result.rows_affected == 0 {
+                        Err(anyhow!("no such message"))?;
+                    }
+                } else {
+                    Err(anyhow!("operation could not be completed"))?;
+                }
             }
 
             Ok(participant_connection_ids)

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

@@ -0,0 +1,262 @@
+use super::*;
+use rpc::Notification;
+
+impl Database {
+    pub async fn initialize_notification_kinds(&mut self) -> Result<()> {
+        notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map(
+            |kind| notification_kind::ActiveModel {
+                name: ActiveValue::Set(kind.to_string()),
+                ..Default::default()
+            },
+        ))
+        .on_conflict(OnConflict::new().do_nothing().to_owned())
+        .exec_without_returning(&self.pool)
+        .await?;
+
+        let mut rows = notification_kind::Entity::find().stream(&self.pool).await?;
+        while let Some(row) = rows.next().await {
+            let row = row?;
+            self.notification_kinds_by_name.insert(row.name, row.id);
+        }
+
+        for name in Notification::all_variant_names() {
+            if let Some(id) = self.notification_kinds_by_name.get(*name).copied() {
+                self.notification_kinds_by_id.insert(id, name);
+            }
+        }
+
+        Ok(())
+    }
+
+    pub async fn get_notifications(
+        &self,
+        recipient_id: UserId,
+        limit: usize,
+        before_id: Option<NotificationId>,
+    ) -> Result<Vec<proto::Notification>> {
+        self.transaction(|tx| async move {
+            let mut result = Vec::new();
+            let mut condition =
+                Condition::all().add(notification::Column::RecipientId.eq(recipient_id));
+
+            if let Some(before_id) = before_id {
+                condition = condition.add(notification::Column::Id.lt(before_id));
+            }
+
+            let mut rows = notification::Entity::find()
+                .filter(condition)
+                .order_by_desc(notification::Column::Id)
+                .limit(limit as u64)
+                .stream(&*tx)
+                .await?;
+            while let Some(row) = rows.next().await {
+                let row = row?;
+                let kind = row.kind;
+                if let Some(proto) = model_to_proto(self, row) {
+                    result.push(proto);
+                } else {
+                    log::warn!("unknown notification kind {:?}", kind);
+                }
+            }
+            result.reverse();
+            Ok(result)
+        })
+        .await
+    }
+
+    /// Create a notification. If `avoid_duplicates` is set to true, then avoid
+    /// creating a new notification if the given recipient already has an
+    /// unread notification with the given kind and entity id.
+    pub async fn create_notification(
+        &self,
+        recipient_id: UserId,
+        notification: Notification,
+        avoid_duplicates: bool,
+        tx: &DatabaseTransaction,
+    ) -> Result<Option<(UserId, proto::Notification)>> {
+        if avoid_duplicates {
+            if self
+                .find_notification(recipient_id, &notification, tx)
+                .await?
+                .is_some()
+            {
+                return Ok(None);
+            }
+        }
+
+        let proto = notification.to_proto();
+        let kind = notification_kind_from_proto(self, &proto)?;
+        let model = notification::ActiveModel {
+            recipient_id: ActiveValue::Set(recipient_id),
+            kind: ActiveValue::Set(kind),
+            entity_id: ActiveValue::Set(proto.entity_id.map(|id| id as i32)),
+            content: ActiveValue::Set(proto.content.clone()),
+            ..Default::default()
+        }
+        .save(&*tx)
+        .await?;
+
+        Ok(Some((
+            recipient_id,
+            proto::Notification {
+                id: model.id.as_ref().to_proto(),
+                kind: proto.kind,
+                timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64,
+                is_read: false,
+                response: None,
+                content: proto.content,
+                entity_id: proto.entity_id,
+            },
+        )))
+    }
+
+    /// Remove an unread notification with the given recipient, kind and
+    /// entity id.
+    pub async fn remove_notification(
+        &self,
+        recipient_id: UserId,
+        notification: Notification,
+        tx: &DatabaseTransaction,
+    ) -> Result<Option<NotificationId>> {
+        let id = self
+            .find_notification(recipient_id, &notification, tx)
+            .await?;
+        if let Some(id) = id {
+            notification::Entity::delete_by_id(id).exec(tx).await?;
+        }
+        Ok(id)
+    }
+
+    /// Populate the response for the notification with the given kind and
+    /// entity id.
+    pub async fn mark_notification_as_read_with_response(
+        &self,
+        recipient_id: UserId,
+        notification: &Notification,
+        response: bool,
+        tx: &DatabaseTransaction,
+    ) -> Result<Option<(UserId, proto::Notification)>> {
+        self.mark_notification_as_read_internal(recipient_id, notification, Some(response), tx)
+            .await
+    }
+
+    pub async fn mark_notification_as_read(
+        &self,
+        recipient_id: UserId,
+        notification: &Notification,
+        tx: &DatabaseTransaction,
+    ) -> Result<Option<(UserId, proto::Notification)>> {
+        self.mark_notification_as_read_internal(recipient_id, notification, None, tx)
+            .await
+    }
+
+    pub async fn mark_notification_as_read_by_id(
+        &self,
+        recipient_id: UserId,
+        notification_id: NotificationId,
+    ) -> Result<NotificationBatch> {
+        self.transaction(|tx| async move {
+            let row = notification::Entity::update(notification::ActiveModel {
+                id: ActiveValue::Unchanged(notification_id),
+                recipient_id: ActiveValue::Unchanged(recipient_id),
+                is_read: ActiveValue::Set(true),
+                ..Default::default()
+            })
+            .exec(&*tx)
+            .await?;
+            Ok(model_to_proto(self, row)
+                .map(|notification| (recipient_id, notification))
+                .into_iter()
+                .collect())
+        })
+        .await
+    }
+
+    async fn mark_notification_as_read_internal(
+        &self,
+        recipient_id: UserId,
+        notification: &Notification,
+        response: Option<bool>,
+        tx: &DatabaseTransaction,
+    ) -> Result<Option<(UserId, proto::Notification)>> {
+        if let Some(id) = self
+            .find_notification(recipient_id, notification, &*tx)
+            .await?
+        {
+            let row = notification::Entity::update(notification::ActiveModel {
+                id: ActiveValue::Unchanged(id),
+                recipient_id: ActiveValue::Unchanged(recipient_id),
+                is_read: ActiveValue::Set(true),
+                response: if let Some(response) = response {
+                    ActiveValue::Set(Some(response))
+                } else {
+                    ActiveValue::NotSet
+                },
+                ..Default::default()
+            })
+            .exec(tx)
+            .await?;
+            Ok(model_to_proto(self, row).map(|notification| (recipient_id, notification)))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Find an unread notification by its recipient, kind and entity id.
+    async fn find_notification(
+        &self,
+        recipient_id: UserId,
+        notification: &Notification,
+        tx: &DatabaseTransaction,
+    ) -> Result<Option<NotificationId>> {
+        let proto = notification.to_proto();
+        let kind = notification_kind_from_proto(self, &proto)?;
+
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryIds {
+            Id,
+        }
+
+        Ok(notification::Entity::find()
+            .select_only()
+            .column(notification::Column::Id)
+            .filter(
+                Condition::all()
+                    .add(notification::Column::RecipientId.eq(recipient_id))
+                    .add(notification::Column::IsRead.eq(false))
+                    .add(notification::Column::Kind.eq(kind))
+                    .add(if proto.entity_id.is_some() {
+                        notification::Column::EntityId.eq(proto.entity_id)
+                    } else {
+                        notification::Column::EntityId.is_null()
+                    }),
+            )
+            .into_values::<_, QueryIds>()
+            .one(&*tx)
+            .await?)
+    }
+}
+
+fn model_to_proto(this: &Database, row: notification::Model) -> Option<proto::Notification> {
+    let kind = this.notification_kinds_by_id.get(&row.kind)?;
+    Some(proto::Notification {
+        id: row.id.to_proto(),
+        kind: kind.to_string(),
+        timestamp: row.created_at.assume_utc().unix_timestamp() as u64,
+        is_read: row.is_read,
+        response: row.response,
+        content: row.content,
+        entity_id: row.entity_id.map(|id| id as u64),
+    })
+}
+
+fn notification_kind_from_proto(
+    this: &Database,
+    proto: &proto::Notification,
+) -> Result<NotificationKindId> {
+    Ok(this
+        .notification_kinds_by_name
+        .get(&proto.kind)
+        .copied()
+        .ok_or_else(|| anyhow!("invalid notification kind {:?}", proto.kind))?)
+}

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

@@ -50,10 +50,10 @@ impl Database {
                     .map(|participant| participant.user_id),
             );
 
-            let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
+            let (channel, 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?;
+            if let Some(channel) = &channel {
+                channel_members = self.get_channel_participants(channel, &tx).await?;
             } else {
                 channel_members = Vec::new();
 
@@ -69,7 +69,7 @@ impl Database {
 
             Ok(RefreshedRoom {
                 room,
-                channel_id,
+                channel_id: channel.map(|channel| channel.id),
                 channel_members,
                 stale_participant_user_ids,
                 canceled_calls_to_user_ids,
@@ -298,98 +298,137 @@ impl Database {
                 }
             }
 
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryParticipantIndices {
-                ParticipantIndex,
-            }
-            let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
-                .filter(
-                    room_participant::Column::RoomId
-                        .eq(room_id)
-                        .and(room_participant::Column::ParticipantIndex.is_not_null()),
-                )
-                .select_only()
-                .column(room_participant::Column::ParticipantIndex)
-                .into_values::<_, QueryParticipantIndices>()
-                .all(&*tx)
-                .await?;
-
-            let mut participant_index = 0;
-            while existing_participant_indices.contains(&participant_index) {
-                participant_index += 1;
+            if channel_id.is_some() {
+                Err(anyhow!("tried to join channel call directly"))?
             }
 
-            if let Some(channel_id) = channel_id {
-                self.check_user_is_channel_member(channel_id, user_id, &*tx)
-                    .await?;
+            let participant_index = self
+                .get_next_participant_index_internal(room_id, &*tx)
+                .await?;
 
-                room_participant::Entity::insert_many([room_participant::ActiveModel {
-                    room_id: ActiveValue::set(room_id),
-                    user_id: ActiveValue::set(user_id),
+            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 {
+                    participant_index: ActiveValue::Set(Some(participant_index)),
                     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,
-                    ))),
-                    participant_index: ActiveValue::Set(Some(participant_index)),
                     ..Default::default()
-                }])
-                .on_conflict(
-                    OnConflict::columns([room_participant::Column::UserId])
-                        .update_columns([
-                            room_participant::Column::AnsweringConnectionId,
-                            room_participant::Column::AnsweringConnectionServerId,
-                            room_participant::Column::AnsweringConnectionLost,
-                            room_participant::Column::ParticipantIndex,
-                        ])
-                        .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 {
-                        participant_index: ActiveValue::Set(Some(participant_index)),
-                        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"))?;
-                }
+            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,
+                channel_id: None,
+                channel_members: vec![],
             })
         })
         .await
     }
 
+    async fn get_next_participant_index_internal(
+        &self,
+        room_id: RoomId,
+        tx: &DatabaseTransaction,
+    ) -> Result<i32> {
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryParticipantIndices {
+            ParticipantIndex,
+        }
+        let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
+            .filter(
+                room_participant::Column::RoomId
+                    .eq(room_id)
+                    .and(room_participant::Column::ParticipantIndex.is_not_null()),
+            )
+            .select_only()
+            .column(room_participant::Column::ParticipantIndex)
+            .into_values::<_, QueryParticipantIndices>()
+            .all(&*tx)
+            .await?;
+
+        let mut participant_index = 0;
+        while existing_participant_indices.contains(&participant_index) {
+            participant_index += 1;
+        }
+
+        Ok(participant_index)
+    }
+
+    pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result<Option<ChannelId>> {
+        self.transaction(|tx| async move {
+            let room: Option<room::Model> = room::Entity::find()
+                .filter(room::Column::Id.eq(room_id))
+                .one(&*tx)
+                .await?;
+
+            Ok(room.and_then(|room| room.channel_id))
+        })
+        .await
+    }
+
+    pub(crate) async fn join_channel_room_internal(
+        &self,
+        room_id: RoomId,
+        user_id: UserId,
+        connection: ConnectionId,
+        tx: &DatabaseTransaction,
+    ) -> Result<JoinRoom> {
+        let participant_index = self
+            .get_next_participant_index_internal(room_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,
+            ))),
+            participant_index: ActiveValue::Set(Some(participant_index)),
+            ..Default::default()
+        }])
+        .on_conflict(
+            OnConflict::columns([room_participant::Column::UserId])
+                .update_columns([
+                    room_participant::Column::AnsweringConnectionId,
+                    room_participant::Column::AnsweringConnectionServerId,
+                    room_participant::Column::AnsweringConnectionLost,
+                    room_participant::Column::ParticipantIndex,
+                ])
+                .to_owned(),
+        )
+        .exec(&*tx)
+        .await?;
+
+        let (channel, room) = self.get_channel_room(room_id, &tx).await?;
+        let channel = channel.ok_or_else(|| anyhow!("no channel for room"))?;
+        let channel_members = self.get_channel_participants(&channel, &*tx).await?;
+        Ok(JoinRoom {
+            room,
+            channel_id: Some(channel.id),
+            channel_members,
+        })
+    }
+
     pub async fn rejoin_room(
         &self,
         rejoin_room: proto::RejoinRoom,
@@ -679,16 +718,16 @@ impl Database {
                 });
             }
 
-            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?
+            let (channel, room) = self.get_channel_room(room_id, &tx).await?;
+            let channel_members = if let Some(channel) = &channel {
+                self.get_channel_participants(&channel, &tx).await?
             } else {
                 Vec::new()
             };
 
             Ok(RejoinedRoom {
                 room,
-                channel_id,
+                channel_id: channel.map(|channel| channel.id),
                 channel_members,
                 rejoined_projects,
                 reshared_projects,
@@ -830,7 +869,7 @@ impl Database {
                     .exec(&*tx)
                     .await?;
 
-                let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
+                let (channel, 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).exec(&*tx).await?;
                     result.rows_affected > 0
@@ -838,14 +877,14 @@ impl Database {
                     false
                 };
 
-                let channel_members = if let Some(channel_id) = channel_id {
-                    self.get_channel_members_internal(channel_id, &tx).await?
+                let channel_members = if let Some(channel) = &channel {
+                    self.get_channel_participants(channel, &tx).await?
                 } else {
                     Vec::new()
                 };
                 let left_room = LeftRoom {
                     room,
-                    channel_id,
+                    channel_id: channel.map(|channel| channel.id),
                     channel_members,
                     left_projects,
                     canceled_calls_to_user_ids,
@@ -1033,7 +1072,7 @@ impl Database {
         &self,
         room_id: RoomId,
         tx: &DatabaseTransaction,
-    ) -> Result<(Option<ChannelId>, proto::Room)> {
+    ) -> Result<(Option<channel::Model>, proto::Room)> {
         let db_room = room::Entity::find_by_id(room_id)
             .one(tx)
             .await?
@@ -1142,9 +1181,16 @@ impl Database {
                 project_id: db_follower.project_id.to_proto(),
             });
         }
+        drop(db_followers);
+
+        let channel = if let Some(channel_id) = db_room.channel_id {
+            Some(self.get_channel_internal(channel_id, &*tx).await?)
+        } else {
+            None
+        };
 
         Ok((
-            db_room.channel_id,
+            channel,
             proto::Room {
                 id: db_room.id.to_proto(),
                 live_kit_room: db_room.live_kit_room,

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

@@ -7,11 +7,13 @@ pub mod channel_buffer_collaborator;
 pub mod channel_chat_participant;
 pub mod channel_member;
 pub mod channel_message;
-pub mod channel_path;
+pub mod channel_message_mention;
 pub mod contact;
 pub mod feature_flag;
 pub mod follower;
 pub mod language_server;
+pub mod notification;
+pub mod notification_kind;
 pub mod observed_buffer_edits;
 pub mod observed_channel_messages;
 pub mod project;

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

@@ -1,4 +1,4 @@
-use crate::db::ChannelId;
+use crate::db::{ChannelId, ChannelVisibility};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
@@ -7,6 +7,29 @@ pub struct Model {
     #[sea_orm(primary_key)]
     pub id: ChannelId,
     pub name: String,
+    pub visibility: ChannelVisibility,
+    pub parent_path: String,
+}
+
+impl Model {
+    pub fn parent_id(&self) -> Option<ChannelId> {
+        self.ancestors().last()
+    }
+
+    pub fn ancestors(&self) -> impl Iterator<Item = ChannelId> + '_ {
+        self.parent_path
+            .trim_end_matches('/')
+            .split('/')
+            .filter_map(|id| Some(ChannelId::from_proto(id.parse().ok()?)))
+    }
+
+    pub fn ancestors_including_self(&self) -> impl Iterator<Item = ChannelId> + '_ {
+        self.ancestors().chain(Some(self.id))
+    }
+
+    pub fn path(&self) -> String {
+        format!("{}{}/", self.parent_path, self.id)
+    }
 }
 
 impl ActiveModelBehavior for ActiveModel {}

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

@@ -1,7 +1,7 @@
-use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId};
+use crate::db::{channel_member, ChannelId, ChannelMemberId, ChannelRole, UserId};
 use sea_orm::entity::prelude::*;
 
-#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
 #[sea_orm(table_name = "channel_members")]
 pub struct Model {
     #[sea_orm(primary_key)]
@@ -9,7 +9,7 @@ pub struct Model {
     pub channel_id: ChannelId,
     pub user_id: UserId,
     pub accepted: bool,
-    pub admin: bool,
+    pub role: ChannelRole,
 }
 
 impl ActiveModelBehavior for ActiveModel {}

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

@@ -0,0 +1,43 @@
+use crate::db::{MessageId, UserId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "channel_message_mentions")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub message_id: MessageId,
+    #[sea_orm(primary_key)]
+    pub start_offset: i32,
+    pub end_offset: i32,
+    pub user_id: UserId,
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::channel_message::Entity",
+        from = "Column::MessageId",
+        to = "super::channel_message::Column::Id"
+    )]
+    Message,
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::UserId",
+        to = "super::user::Column::Id"
+    )]
+    MentionedUser,
+}
+
+impl Related<super::channel::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Message.def()
+    }
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::MentionedUser.def()
+    }
+}

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

@@ -0,0 +1,29 @@
+use crate::db::{NotificationId, NotificationKindId, UserId};
+use sea_orm::entity::prelude::*;
+use time::PrimitiveDateTime;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "notifications")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: NotificationId,
+    pub created_at: PrimitiveDateTime,
+    pub recipient_id: UserId,
+    pub kind: NotificationKindId,
+    pub entity_id: Option<i32>,
+    pub content: String,
+    pub is_read: bool,
+    pub response: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::RecipientId",
+        to = "super::user::Column::Id"
+    )]
+    Recipient,
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

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

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

@@ -7,10 +7,12 @@ mod message_tests;
 use super::*;
 use gpui::executor::Background;
 use parking_lot::Mutex;
-use rpc::proto::ChannelEdge;
 use sea_orm::ConnectionTrait;
 use sqlx::migrate::MigrateDatabase;
-use std::sync::Arc;
+use std::sync::{
+    atomic::{AtomicI32, AtomicU32, Ordering::SeqCst},
+    Arc,
+};
 
 const TEST_RELEASE_CHANNEL: &'static str = "test";
 
@@ -31,7 +33,7 @@ impl TestDb {
         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))
+            let mut db = Database::new(options, Executor::Deterministic(background))
                 .await
                 .unwrap();
             let sql = include_str!(concat!(
@@ -45,6 +47,7 @@ impl TestDb {
                 ))
                 .await
                 .unwrap();
+            db.initialize_notification_kinds().await.unwrap();
             db
         });
 
@@ -79,11 +82,12 @@ impl TestDb {
             options
                 .max_connections(5)
                 .idle_timeout(Duration::from_secs(0));
-            let db = Database::new(options, Executor::Deterministic(background))
+            let mut 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.initialize_notification_kinds().await.unwrap();
             db
         });
 
@@ -148,26 +152,39 @@ impl Drop for TestDb {
     }
 }
 
-/// The second tuples are (channel_id, parent)
-fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)]) -> ChannelGraph {
-    let mut graph = ChannelGraph {
-        channels: vec![],
-        edges: vec![],
-    };
-
-    for (id, name) in channels {
-        graph.channels.push(Channel {
+fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str, ChannelRole)]) -> Vec<Channel> {
+    channels
+        .iter()
+        .map(|(id, parent_path, name, role)| Channel {
             id: *id,
             name: name.to_string(),
+            visibility: ChannelVisibility::Members,
+            role: *role,
+            parent_path: parent_path.to_vec(),
         })
-    }
+        .collect()
+}
 
-    for (channel, parent) in edges {
-        graph.edges.push(ChannelEdge {
-            channel_id: channel.to_proto(),
-            parent_id: parent.to_proto(),
-        })
-    }
+static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
+
+async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
+    db.create_user(
+        email,
+        false,
+        NewUserParams {
+            github_login: email[0..email.find("@").unwrap()].to_string(),
+            github_user_id: GITHUB_USER_ID.fetch_add(1, SeqCst),
+        },
+    )
+    .await
+    .unwrap()
+    .user_id
+}
 
-    graph
+static TEST_CONNECTION_ID: AtomicU32 = AtomicU32::new(1);
+fn new_test_connection(server: ServerId) -> ConnectionId {
+    ConnectionId {
+        id: TEST_CONNECTION_ID.fetch_add(1, SeqCst),
+        owner_id: server.0 as u32,
+    }
 }

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

@@ -17,7 +17,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user_a".into(),
                 github_user_id: 101,
-                invite_count: 0,
             },
         )
         .await
@@ -30,7 +29,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user_b".into(),
                 github_user_id: 102,
-                invite_count: 0,
             },
         )
         .await
@@ -45,7 +43,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user_c".into(),
                 github_user_id: 102,
-                invite_count: 0,
             },
         )
         .await
@@ -56,7 +53,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
 
     let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
 
-    db.invite_channel_member(zed_id, b_id, a_id, false)
+    db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
         .await
         .unwrap();
 
@@ -178,7 +175,6 @@ async fn test_channel_buffers_last_operations(db: &Database) {
             NewUserParams {
                 github_login: "user_a".into(),
                 github_user_id: 101,
-                invite_count: 0,
             },
         )
         .await
@@ -191,7 +187,6 @@ async fn test_channel_buffers_last_operations(db: &Database) {
             NewUserParams {
                 github_login: "user_b".into(),
                 github_user_id: 102,
-                invite_count: 0,
             },
         )
         .await
@@ -211,7 +206,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
             .await
             .unwrap();
 
-        db.invite_channel_member(channel, observer_id, user_id, false)
+        db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member)
             .await
             .unwrap();
         db.respond_to_channel_invite(channel, observer_id, true)

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

@@ -1,56 +1,28 @@
-use collections::{HashMap, HashSet};
-use rpc::{
-    proto::{self},
-    ConnectionId,
-};
-
 use crate::{
     db::{
-        queries::channels::ChannelGraph,
-        tests::{graph, TEST_RELEASE_CHANNEL},
-        ChannelId, Database, NewUserParams,
+        tests::{channel_tree, new_test_connection, new_test_user, TEST_RELEASE_CHANNEL},
+        Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId,
     },
     test_both_dbs,
 };
+use rpc::{
+    proto::{self},
+    ConnectionId,
+};
 use std::sync::Arc;
 
 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 a_id = new_test_user(db, "user1@example.com").await;
+    let b_id = new_test_user(db, "user2@example.com").await;
 
     let zed_id = db.create_root_channel("zed", 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());
+    assert!(db.get_channel(zed_id, b_id).await.is_err());
 
-    db.invite_channel_member(zed_id, b_id, a_id, false)
+    db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
         .await
         .unwrap();
 
@@ -58,99 +30,103 @@ async fn test_channels(db: &Arc<Database>) {
         .await
         .unwrap();
 
-    let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
+    let crdb_id = db.create_sub_channel("crdb", zed_id, a_id).await.unwrap();
     let livestreaming_id = db
-        .create_channel("livestreaming", Some(zed_id), a_id)
+        .create_sub_channel("livestreaming", zed_id, a_id)
         .await
         .unwrap();
     let replace_id = db
-        .create_channel("replace", Some(zed_id), a_id)
+        .create_sub_channel("replace", zed_id, a_id)
         .await
         .unwrap();
 
-    let mut members = db.get_channel_members(replace_id).await.unwrap();
+    let mut members = db
+        .transaction(|tx| async move {
+            let channel = db.get_channel_internal(replace_id, &*tx).await?;
+            Ok(db.get_channel_participants(&channel, &*tx).await?)
+        })
+        .await
+        .unwrap();
     members.sort();
     assert_eq!(members, &[a_id, b_id]);
 
     let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
-    let cargo_id = db
-        .create_channel("cargo", Some(rust_id), a_id)
-        .await
-        .unwrap();
+    let cargo_id = db.create_sub_channel("cargo", rust_id, a_id).await.unwrap();
 
     let cargo_ra_id = db
-        .create_channel("cargo-ra", Some(cargo_id), a_id)
+        .create_sub_channel("cargo-ra", cargo_id, a_id)
         .await
         .unwrap();
 
     let result = db.get_channels_for_user(a_id).await.unwrap();
     assert_eq!(
         result.channels,
-        graph(
-            &[
-                (zed_id, "zed"),
-                (crdb_id, "crdb"),
-                (livestreaming_id, "livestreaming"),
-                (replace_id, "replace"),
-                (rust_id, "rust"),
-                (cargo_id, "cargo"),
-                (cargo_ra_id, "cargo-ra")
-            ],
-            &[
-                (crdb_id, zed_id),
-                (livestreaming_id, zed_id),
-                (replace_id, zed_id),
-                (cargo_id, rust_id),
-                (cargo_ra_id, cargo_id),
-            ]
-        )
+        channel_tree(&[
+            (zed_id, &[], "zed", ChannelRole::Admin),
+            (crdb_id, &[zed_id], "crdb", ChannelRole::Admin),
+            (
+                livestreaming_id,
+                &[zed_id],
+                "livestreaming",
+                ChannelRole::Admin
+            ),
+            (replace_id, &[zed_id], "replace", ChannelRole::Admin),
+            (rust_id, &[], "rust", ChannelRole::Admin),
+            (cargo_id, &[rust_id], "cargo", ChannelRole::Admin),
+            (
+                cargo_ra_id,
+                &[rust_id, cargo_id],
+                "cargo-ra",
+                ChannelRole::Admin
+            )
+        ],)
     );
 
     let result = db.get_channels_for_user(b_id).await.unwrap();
     assert_eq!(
         result.channels,
-        graph(
-            &[
-                (zed_id, "zed"),
-                (crdb_id, "crdb"),
-                (livestreaming_id, "livestreaming"),
-                (replace_id, "replace")
-            ],
-            &[
-                (crdb_id, zed_id),
-                (livestreaming_id, zed_id),
-                (replace_id, zed_id)
-            ]
-        )
+        channel_tree(&[
+            (zed_id, &[], "zed", ChannelRole::Member),
+            (crdb_id, &[zed_id], "crdb", ChannelRole::Member),
+            (
+                livestreaming_id,
+                &[zed_id],
+                "livestreaming",
+                ChannelRole::Member
+            ),
+            (replace_id, &[zed_id], "replace", ChannelRole::Member)
+        ],)
     );
 
     // Update member permissions
-    let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
+    let set_subchannel_admin = db
+        .set_channel_member_role(crdb_id, a_id, b_id, ChannelRole::Admin)
+        .await;
     assert!(set_subchannel_admin.is_err());
-    let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
+    let set_channel_admin = db
+        .set_channel_member_role(zed_id, a_id, b_id, ChannelRole::Admin)
+        .await;
     assert!(set_channel_admin.is_ok());
 
     let result = db.get_channels_for_user(b_id).await.unwrap();
     assert_eq!(
         result.channels,
-        graph(
-            &[
-                (zed_id, "zed"),
-                (crdb_id, "crdb"),
-                (livestreaming_id, "livestreaming"),
-                (replace_id, "replace")
-            ],
-            &[
-                (crdb_id, zed_id),
-                (livestreaming_id, zed_id),
-                (replace_id, zed_id)
-            ]
-        )
+        channel_tree(&[
+            (zed_id, &[], "zed", ChannelRole::Admin),
+            (crdb_id, &[zed_id], "crdb", ChannelRole::Admin),
+            (
+                livestreaming_id,
+                &[zed_id],
+                "livestreaming",
+                ChannelRole::Admin
+            ),
+            (replace_id, &[zed_id], "replace", ChannelRole::Admin)
+        ],)
     );
 
     // Remove a single channel
     db.delete_channel(crdb_id, a_id).await.unwrap();
-    assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
+    assert!(db.get_channel(crdb_id, a_id).await.is_err());
 
     // Remove a channel tree
     let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
@@ -158,9 +134,9 @@ async fn test_channels(db: &Arc<Database>) {
     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());
+    assert!(db.get_channel(rust_id, a_id).await.is_err());
+    assert!(db.get_channel(cargo_id, a_id).await.is_err());
+    assert!(db.get_channel(cargo_ra_id, a_id).await.is_err());
 }
 
 test_both_dbs!(
@@ -172,43 +148,15 @@ test_both_dbs!(
 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 user_1 = new_test_user(db, "user1@example.com").await;
+    let user_2 = new_test_user(db, "user2@example.com").await;
 
     let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
-    let room_1 = db
-        .get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL)
-        .await
-        .unwrap();
 
     // can join a room with membership to its channel
-    let joined_room = db
-        .join_room(
-            room_1,
+    let (joined_room, _, _) = db
+        .join_channel(
+            channel_1,
             user_1,
             ConnectionId { owner_id, id: 1 },
             TEST_RELEASE_CHANNEL,
@@ -217,11 +165,12 @@ async fn test_joining_channels(db: &Arc<Database>) {
         .unwrap();
     assert_eq!(joined_room.room.participants.len(), 1);
 
+    let room_id = RoomId::from_proto(joined_room.room.id);
     drop(joined_room);
     // cannot join a room without membership to its channel
     assert!(db
         .join_room(
-            room_1,
+            room_id,
             user_2,
             ConnectionId { owner_id, id: 1 },
             TEST_RELEASE_CHANNEL
@@ -239,58 +188,21 @@ test_both_dbs!(
 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 user_1 = new_test_user(db, "user1@example.com").await;
+    let user_2 = new_test_user(db, "user2@example.com").await;
+    let user_3 = new_test_user(db, "user3@example.com").await;
 
     let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
 
     let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
 
-    db.invite_channel_member(channel_1_1, user_2, user_1, false)
+    db.invite_channel_member(channel_1_1, user_2, user_1, ChannelRole::Member)
         .await
         .unwrap();
-    db.invite_channel_member(channel_1_2, user_2, user_1, false)
+    db.invite_channel_member(channel_1_2, user_2, user_1, ChannelRole::Member)
         .await
         .unwrap();
-    db.invite_channel_member(channel_1_1, user_3, user_1, true)
+    db.invite_channel_member(channel_1_1, user_3, user_1, ChannelRole::Admin)
         .await
         .unwrap();
 
@@ -314,27 +226,29 @@ async fn test_channel_invites(db: &Arc<Database>) {
 
     assert_eq!(user_3_invites, &[channel_1_1]);
 
-    let members = db
-        .get_channel_member_details(channel_1_1, user_1)
+    let mut members = db
+        .get_channel_participant_details(channel_1_1, user_1)
         .await
         .unwrap();
+
+    members.sort_by_key(|member| member.user_id);
     assert_eq!(
         members,
         &[
             proto::ChannelMember {
                 user_id: user_1.to_proto(),
                 kind: proto::channel_member::Kind::Member.into(),
-                admin: true,
+                role: proto::ChannelRole::Admin.into(),
             },
             proto::ChannelMember {
                 user_id: user_2.to_proto(),
                 kind: proto::channel_member::Kind::Invitee.into(),
-                admin: false,
+                role: proto::ChannelRole::Member.into(),
             },
             proto::ChannelMember {
                 user_id: user_3.to_proto(),
                 kind: proto::channel_member::Kind::Invitee.into(),
-                admin: true,
+                role: proto::ChannelRole::Admin.into(),
             },
         ]
     );
@@ -344,12 +258,12 @@ async fn test_channel_invites(db: &Arc<Database>) {
         .unwrap();
 
     let channel_1_3 = db
-        .create_channel("channel_3", Some(channel_1_1), user_1)
+        .create_sub_channel("channel_3", channel_1_1, user_1)
         .await
         .unwrap();
 
     let members = db
-        .get_channel_member_details(channel_1_3, user_1)
+        .get_channel_participant_details(channel_1_3, user_1)
         .await
         .unwrap();
     assert_eq!(
@@ -357,13 +271,13 @@ async fn test_channel_invites(db: &Arc<Database>) {
         &[
             proto::ChannelMember {
                 user_id: user_1.to_proto(),
-                kind: proto::channel_member::Kind::Member.into(),
-                admin: true,
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Admin.into(),
             },
             proto::ChannelMember {
                 user_id: user_2.to_proto(),
                 kind: proto::channel_member::Kind::AncestorMember.into(),
-                admin: false,
+                role: proto::ChannelRole::Member.into(),
             },
         ]
     );
@@ -385,7 +299,6 @@ async fn test_channel_renames(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user1".into(),
                 github_user_id: 5,
-                invite_count: 0,
             },
         )
         .await
@@ -399,7 +312,6 @@ async fn test_channel_renames(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user2".into(),
                 github_user_id: 6,
-                invite_count: 0,
             },
         )
         .await
@@ -412,18 +324,10 @@ async fn test_channel_renames(db: &Arc<Database>) {
         .await
         .unwrap();
 
-    let zed_archive_id = zed_id;
-
-    let (channel, _) = db
-        .get_channel(zed_archive_id, user_1)
-        .await
-        .unwrap()
-        .unwrap();
+    let channel = db.get_channel(zed_id, user_1).await.unwrap();
     assert_eq!(channel.name, "zed-archive");
 
-    let non_permissioned_rename = db
-        .rename_channel(zed_archive_id, user_2, "hacked-lol")
-        .await;
+    let non_permissioned_rename = db.rename_channel(zed_id, user_2, "hacked-lol").await;
     assert!(non_permissioned_rename.is_err());
 
     let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
@@ -444,7 +348,6 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user1".into(),
                 github_user_id: 5,
-                invite_count: 0,
             },
         )
         .await
@@ -453,20 +356,17 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
 
     let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
 
-    let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
+    let crdb_id = db.create_sub_channel("crdb", zed_id, a_id).await.unwrap();
 
-    let gpui2_id = db
-        .create_channel("gpui2", Some(zed_id), a_id)
-        .await
-        .unwrap();
+    let gpui2_id = db.create_sub_channel("gpui2", zed_id, a_id).await.unwrap();
 
     let livestreaming_id = db
-        .create_channel("livestreaming", Some(crdb_id), a_id)
+        .create_sub_channel("livestreaming", crdb_id, a_id)
         .await
         .unwrap();
 
     let livestreaming_dag_id = db
-        .create_channel("livestreaming_dag", Some(livestreaming_id), a_id)
+        .create_sub_channel("livestreaming_dag", livestreaming_id, a_id)
         .await
         .unwrap();
 
@@ -476,397 +376,444 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
     //     /- gpui2
     // zed -- crdb - livestreaming - livestreaming_dag
     let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
+    assert_channel_tree(
         result.channels,
         &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (gpui2_id, Some(zed_id)),
-            (livestreaming_id, Some(crdb_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
+            (zed_id, &[]),
+            (crdb_id, &[zed_id]),
+            (livestreaming_id, &[zed_id, crdb_id]),
+            (livestreaming_dag_id, &[zed_id, crdb_id, livestreaming_id]),
+            (gpui2_id, &[zed_id]),
         ],
     );
+}
 
-    // Attempt to make a cycle
-    assert!(db
-        .link_channel(a_id, zed_id, livestreaming_id)
-        .await
-        .is_err());
+test_both_dbs!(
+    test_db_channel_moving_bugs,
+    test_db_channel_moving_bugs_postgres,
+    test_db_channel_moving_bugs_sqlite
+);
 
-    // ========================================================================
-    // Make a link
-    db.link_channel(a_id, livestreaming_id, zed_id)
+async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
+    let user_id = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+            },
+        )
         .await
-        .unwrap();
+        .unwrap()
+        .user_id;
 
-    // DAG is now:
-    //     /- gpui2
-    // zed -- crdb - livestreaming - livestreaming_dag
-    //    \---------/
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
-        &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (gpui2_id, Some(zed_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_id, Some(crdb_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-        ],
-    );
+    let zed_id = db.create_root_channel("zed", user_id).await.unwrap();
 
-    // ========================================================================
-    // Create a new channel below a channel with multiple parents
-    let livestreaming_dag_sub_id = db
-        .create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id)
+    let projects_id = db
+        .create_sub_channel("projects", zed_id, user_id)
         .await
         .unwrap();
 
-    // DAG is now:
-    //     /- gpui2
-    // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id
-    //    \---------/
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
-        &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (gpui2_id, Some(zed_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_id, Some(crdb_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
-        ],
-    );
-
-    // ========================================================================
-    // Test a complex DAG by making another link
-    let returned_channels = db
-        .link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id)
+    let livestreaming_id = db
+        .create_sub_channel("livestreaming", projects_id, user_id)
         .await
         .unwrap();
 
-    // DAG is now:
-    //    /- gpui2                /---------------------\
-    // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id
-    //    \--------/
+    // Dag is: zed - projects - livestreaming
+
+    // Move to same parent should be a no-op
+    assert!(db
+        .move_channel(projects_id, Some(zed_id), user_id)
+        .await
+        .unwrap()
+        .is_none());
 
-    // make sure we're getting just the new link
-    // Not using the assert_dag helper because we want to make sure we're returning the full data
-    pretty_assertions::assert_eq!(
-        returned_channels,
-        graph(
-            &[(livestreaming_dag_sub_id, "livestreaming_dag_sub")],
-            &[(livestreaming_dag_sub_id, livestreaming_id)]
-        )
+    let result = db.get_channels_for_user(user_id).await.unwrap();
+    assert_channel_tree(
+        result.channels,
+        &[
+            (zed_id, &[]),
+            (projects_id, &[zed_id]),
+            (livestreaming_id, &[zed_id, projects_id]),
+        ],
     );
 
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
+    // Move the project channel to the root
+    db.move_channel(projects_id, None, user_id).await.unwrap();
+    let result = db.get_channels_for_user(user_id).await.unwrap();
+    assert_channel_tree(
         result.channels,
         &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (gpui2_id, Some(zed_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_id, Some(crdb_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+            (zed_id, &[]),
+            (projects_id, &[]),
+            (livestreaming_id, &[projects_id]),
         ],
     );
+}
 
-    // ========================================================================
-    // Test a complex DAG by making another link
-    let returned_channels = db
-        .link_channel(a_id, livestreaming_id, gpui2_id)
+test_both_dbs!(
+    test_user_is_channel_participant,
+    test_user_is_channel_participant_postgres,
+    test_user_is_channel_participant_sqlite
+);
+
+async fn test_user_is_channel_participant(db: &Arc<Database>) {
+    let admin = new_test_user(db, "admin@example.com").await;
+    let member = new_test_user(db, "member@example.com").await;
+    let guest = new_test_user(db, "guest@example.com").await;
+
+    let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
+    let active_channel_id = db
+        .create_sub_channel("active", zed_channel, admin)
+        .await
+        .unwrap();
+    let vim_channel_id = db
+        .create_sub_channel("vim", active_channel_id, admin)
+        .await
+        .unwrap();
+
+    db.set_channel_visibility(vim_channel_id, crate::db::ChannelVisibility::Public, admin)
+        .await
+        .unwrap();
+    db.invite_channel_member(active_channel_id, member, admin, ChannelRole::Member)
+        .await
+        .unwrap();
+    db.invite_channel_member(vim_channel_id, guest, admin, ChannelRole::Guest)
         .await
         .unwrap();
 
-    // DAG is now:
-    //    /- gpui2 -\             /---------------------\
-    // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id
-    //    \---------/
+    db.respond_to_channel_invite(active_channel_id, member, true)
+        .await
+        .unwrap();
 
-    // Make sure that we're correctly getting the full sub-dag
-    pretty_assertions::assert_eq!(
-        returned_channels,
-        graph(
-            &[
-                (livestreaming_id, "livestreaming"),
-                (livestreaming_dag_id, "livestreaming_dag"),
-                (livestreaming_dag_sub_id, "livestreaming_dag_sub"),
-            ],
-            &[
-                (livestreaming_id, gpui2_id),
-                (livestreaming_dag_id, livestreaming_id),
-                (livestreaming_dag_sub_id, livestreaming_id),
-                (livestreaming_dag_sub_id, livestreaming_dag_id),
-            ]
+    db.transaction(|tx| async move {
+        db.check_user_is_channel_participant(
+            &db.get_channel_internal(vim_channel_id, &*tx).await?,
+            admin,
+            &*tx,
         )
-    );
-
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
-        &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (gpui2_id, Some(zed_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_id, Some(crdb_id)),
-            (livestreaming_id, Some(gpui2_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
-        ],
-    );
+        .await
+    })
+    .await
+    .unwrap();
+    db.transaction(|tx| async move {
+        db.check_user_is_channel_participant(
+            &db.get_channel_internal(vim_channel_id, &*tx).await?,
+            member,
+            &*tx,
+        )
+        .await
+    })
+    .await
+    .unwrap();
 
-    // ========================================================================
-    // Test unlinking in a complex DAG by removing the inner link
-    db.unlink_channel(a_id, livestreaming_dag_sub_id, livestreaming_id)
+    let mut members = db
+        .get_channel_participant_details(vim_channel_id, admin)
         .await
         .unwrap();
 
-    // DAG is now:
-    //    /- gpui2 -\
-    // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
-    //    \---------/
+    members.sort_by_key(|member| member.user_id);
 
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
+    assert_eq!(
+        members,
         &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (gpui2_id, Some(zed_id)),
-            (livestreaming_id, Some(gpui2_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_id, Some(crdb_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
-        ],
+            proto::ChannelMember {
+                user_id: admin.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Admin.into(),
+            },
+            proto::ChannelMember {
+                user_id: member.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Member.into(),
+            },
+            proto::ChannelMember {
+                user_id: guest.to_proto(),
+                kind: proto::channel_member::Kind::Invitee.into(),
+                role: proto::ChannelRole::Guest.into(),
+            },
+        ]
     );
 
-    // ========================================================================
-    // Test unlinking in a complex DAG by removing the inner link
-    db.unlink_channel(a_id, livestreaming_id, gpui2_id)
+    db.respond_to_channel_invite(vim_channel_id, guest, true)
         .await
         .unwrap();
 
-    // DAG is now:
-    //    /- gpui2
-    // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
-    //    \---------/
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
+    db.transaction(|tx| async move {
+        db.check_user_is_channel_participant(
+            &db.get_channel_internal(vim_channel_id, &*tx).await?,
+            guest,
+            &*tx,
+        )
+        .await
+    })
+    .await
+    .unwrap();
+
+    let channels = db.get_channels_for_user(guest).await.unwrap().channels;
+    assert_channel_tree(channels, &[(vim_channel_id, &[])]);
+    let channels = db.get_channels_for_user(member).await.unwrap().channels;
+    assert_channel_tree(
+        channels,
         &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (gpui2_id, Some(zed_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_id, Some(crdb_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+            (active_channel_id, &[]),
+            (vim_channel_id, &[active_channel_id]),
         ],
     );
 
-    // ========================================================================
-    // Test moving DAG nodes by moving livestreaming to be below gpui2
-    db.move_channel(a_id, livestreaming_id, crdb_id, gpui2_id)
+    db.set_channel_member_role(vim_channel_id, admin, guest, ChannelRole::Banned)
         .await
         .unwrap();
+    assert!(db
+        .transaction(|tx| async move {
+            db.check_user_is_channel_participant(
+                &db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(),
+                guest,
+                &*tx,
+            )
+            .await
+        })
+        .await
+        .is_err());
 
-    // DAG is now:
-    //    /- gpui2 -- livestreaming - livestreaming_dag - livestreaming_dag_sub
-    // zed - crdb    /
-    //    \---------/
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
-        &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (gpui2_id, Some(zed_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_id, Some(gpui2_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
-        ],
-    );
+    let mut members = db
+        .get_channel_participant_details(vim_channel_id, admin)
+        .await
+        .unwrap();
 
-    // ========================================================================
-    // Deleting a channel should not delete children that still have other parents
-    db.delete_channel(gpui2_id, a_id).await.unwrap();
+    members.sort_by_key(|member| member.user_id);
 
-    // DAG is now:
-    // zed - crdb
-    //    \- livestreaming - livestreaming_dag - livestreaming_dag_sub
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
+    assert_eq!(
+        members,
         &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
-        ],
+            proto::ChannelMember {
+                user_id: admin.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Admin.into(),
+            },
+            proto::ChannelMember {
+                user_id: member.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Member.into(),
+            },
+            proto::ChannelMember {
+                user_id: guest.to_proto(),
+                kind: proto::channel_member::Kind::Member.into(),
+                role: proto::ChannelRole::Banned.into(),
+            },
+        ]
     );
 
-    // ========================================================================
-    // Unlinking a channel from it's parent should automatically promote it to a root channel
-    db.unlink_channel(a_id, crdb_id, zed_id).await.unwrap();
+    db.remove_channel_member(vim_channel_id, guest, admin)
+        .await
+        .unwrap();
 
-    // DAG is now:
-    // crdb
-    // zed
-    //    \- livestreaming - livestreaming_dag - livestreaming_dag_sub
+    db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
+        .await
+        .unwrap();
 
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
-        &[
-            (zed_id, None),
-            (crdb_id, None),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
-        ],
-    );
+    db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
+        .await
+        .unwrap();
 
-    // ========================================================================
-    // You should be able to move a root channel into a non-root channel
-    db.link_channel(a_id, crdb_id, zed_id).await.unwrap();
+    // currently people invited to parent channels are not shown here
+    let mut members = db
+        .get_channel_participant_details(vim_channel_id, admin)
+        .await
+        .unwrap();
 
-    // DAG is now:
-    // zed - crdb
-    //    \- livestreaming - livestreaming_dag - livestreaming_dag_sub
+    members.sort_by_key(|member| member.user_id);
 
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
+    assert_eq!(
+        members,
         &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
-        ],
+            proto::ChannelMember {
+                user_id: admin.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Admin.into(),
+            },
+            proto::ChannelMember {
+                user_id: member.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Member.into(),
+            },
+        ]
     );
 
-    // ========================================================================
-    // Prep for DAG deletion test
-    db.link_channel(a_id, livestreaming_id, crdb_id)
+    db.respond_to_channel_invite(zed_channel, guest, true)
         .await
         .unwrap();
 
-    // DAG is now:
-    // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub
-    //    \--------/
+    db.transaction(|tx| async move {
+        db.check_user_is_channel_participant(
+            &db.get_channel_internal(zed_channel, &*tx).await.unwrap(),
+            guest,
+            &*tx,
+        )
+        .await
+    })
+    .await
+    .unwrap();
+    assert!(db
+        .transaction(|tx| async move {
+            db.check_user_is_channel_participant(
+                &db.get_channel_internal(active_channel_id, &*tx)
+                    .await
+                    .unwrap(),
+                guest,
+                &*tx,
+            )
+            .await
+        })
+        .await
+        .is_err(),);
+
+    db.transaction(|tx| async move {
+        db.check_user_is_channel_participant(
+            &db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(),
+            guest,
+            &*tx,
+        )
+        .await
+    })
+    .await
+    .unwrap();
 
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_dag(
-        result.channels,
+    let mut members = db
+        .get_channel_participant_details(vim_channel_id, admin)
+        .await
+        .unwrap();
+
+    members.sort_by_key(|member| member.user_id);
+
+    assert_eq!(
+        members,
         &[
-            (zed_id, None),
-            (crdb_id, Some(zed_id)),
-            (livestreaming_id, Some(zed_id)),
-            (livestreaming_id, Some(crdb_id)),
-            (livestreaming_dag_id, Some(livestreaming_id)),
-            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
-        ],
+            proto::ChannelMember {
+                user_id: admin.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Admin.into(),
+            },
+            proto::ChannelMember {
+                user_id: member.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Member.into(),
+            },
+            proto::ChannelMember {
+                user_id: guest.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                role: proto::ChannelRole::Guest.into(),
+            },
+        ]
     );
 
-    // Deleting the parent of a DAG should delete the whole DAG:
-    db.delete_channel(zed_id, a_id).await.unwrap();
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-
-    assert!(result.channels.is_empty())
+    let channels = db.get_channels_for_user(guest).await.unwrap().channels;
+    assert_channel_tree(
+        channels,
+        &[(zed_channel, &[]), (vim_channel_id, &[zed_channel])],
+    )
 }
 
 test_both_dbs!(
-    test_db_channel_moving_bugs,
-    test_db_channel_moving_bugs_postgres,
-    test_db_channel_moving_bugs_sqlite
+    test_user_joins_correct_channel,
+    test_user_joins_correct_channel_postgres,
+    test_user_joins_correct_channel_sqlite
 );
 
-async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
-    let user_id = db
-        .create_user(
-            "user1@example.com",
-            false,
-            NewUserParams {
-                github_login: "user1".into(),
-                github_user_id: 5,
-                invite_count: 0,
-            },
-        )
+async fn test_user_joins_correct_channel(db: &Arc<Database>) {
+    let admin = new_test_user(db, "admin@example.com").await;
+
+    let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
+
+    let active_channel = db
+        .create_sub_channel("active", zed_channel, admin)
         .await
-        .unwrap()
-        .user_id;
+        .unwrap();
 
-    let zed_id = db.create_root_channel("zed", user_id).await.unwrap();
+    let vim_channel = db
+        .create_sub_channel("vim", active_channel, admin)
+        .await
+        .unwrap();
 
-    let projects_id = db
-        .create_channel("projects", Some(zed_id), user_id)
+    let vim2_channel = db
+        .create_sub_channel("vim2", vim_channel, admin)
         .await
         .unwrap();
 
-    let livestreaming_id = db
-        .create_channel("livestreaming", Some(projects_id), user_id)
+    db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
         .await
         .unwrap();
 
-    // Dag is: zed - projects - livestreaming
+    db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
+        .await
+        .unwrap();
 
-    // Move to same parent should be a no-op
-    assert!(db
-        .move_channel(user_id, projects_id, zed_id, zed_id)
+    db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin)
         .await
+        .unwrap();
+
+    let most_public = db
+        .transaction(|tx| async move {
+            Ok(db
+                .public_ancestors_including_self(
+                    &db.get_channel_internal(vim_channel, &*tx).await.unwrap(),
+                    &tx,
+                )
+                .await?
+                .first()
+                .cloned())
+        })
+        .await
+        .unwrap()
         .unwrap()
-        .is_empty());
+        .id;
+
+    assert_eq!(most_public, zed_channel)
+}
 
-    // Stranding a channel should retain it's sub channels
-    db.unlink_channel(user_id, projects_id, zed_id)
+test_both_dbs!(
+    test_guest_access,
+    test_guest_access_postgres,
+    test_guest_access_sqlite
+);
+
+async fn test_guest_access(db: &Arc<Database>) {
+    let server = db.create_server("test").await.unwrap();
+
+    let admin = new_test_user(db, "admin@example.com").await;
+    let guest = new_test_user(db, "guest@example.com").await;
+    let guest_connection = new_test_connection(server);
+
+    let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
+    db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
         .await
         .unwrap();
 
-    let result = db.get_channels_for_user(user_id).await.unwrap();
-    assert_dag(
-        result.channels,
-        &[
-            (zed_id, None),
-            (projects_id, None),
-            (livestreaming_id, Some(projects_id)),
-        ],
-    );
+    assert!(db
+        .join_channel_chat(zed_channel, guest_connection, guest)
+        .await
+        .is_err());
+
+    db.join_channel(zed_channel, guest, guest_connection, TEST_RELEASE_CHANNEL)
+        .await
+        .unwrap();
+
+    assert!(db
+        .join_channel_chat(zed_channel, guest_connection, guest)
+        .await
+        .is_ok())
 }
 
 #[track_caller]
-fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
-    let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
-    for channel in actual.channels {
-        actual_map.insert(channel.id, HashSet::default());
-    }
-    for edge in actual.edges {
-        actual_map
-            .get_mut(&ChannelId::from_proto(edge.channel_id))
-            .unwrap()
-            .insert(ChannelId::from_proto(edge.parent_id));
-    }
-
-    let mut expected_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
-
-    for (child, parent) in expected {
-        let entry = expected_map.entry(*child).or_default();
-        if let Some(parent) = parent {
-            entry.insert(*parent);
-        }
-    }
-
-    pretty_assertions::assert_eq!(actual_map, expected_map)
+fn assert_channel_tree(actual: Vec<Channel>, expected: &[(ChannelId, &[ChannelId])]) {
+    let actual = actual
+        .iter()
+        .map(|channel| (channel.id, channel.parent_path.as_slice()))
+        .collect::<Vec<_>>();
+    pretty_assertions::assert_eq!(
+        actual,
+        expected.to_vec(),
+        "wrong channel ids and parent paths"
+    );
 }

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

@@ -22,7 +22,6 @@ async fn test_get_users(db: &Arc<Database>) {
                 NewUserParams {
                     github_login: format!("user{i}"),
                     github_user_id: i,
-                    invite_count: 0,
                 },
             )
             .await
@@ -88,7 +87,6 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "login1".into(),
                 github_user_id: 101,
-                invite_count: 0,
             },
         )
         .await
@@ -101,7 +99,6 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "login2".into(),
                 github_user_id: 102,
-                invite_count: 0,
             },
         )
         .await
@@ -156,7 +153,6 @@ async fn test_create_access_tokens(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "u1".into(),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await
@@ -238,7 +234,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
                 NewUserParams {
                     github_login: format!("user{i}"),
                     github_user_id: i,
-                    invite_count: 0,
                 },
             )
             .await
@@ -264,10 +259,7 @@ async fn test_add_contacts(db: &Arc<Database>) {
     );
     assert_eq!(
         db.get_contacts(user_2).await.unwrap(),
-        &[Contact::Incoming {
-            user_id: user_1,
-            should_notify: true
-        }]
+        &[Contact::Incoming { user_id: user_1 }]
     );
 
     // User 2 dismisses the contact request notification without accepting or rejecting.
@@ -280,10 +272,7 @@ async fn test_add_contacts(db: &Arc<Database>) {
         .unwrap();
     assert_eq!(
         db.get_contacts(user_2).await.unwrap(),
-        &[Contact::Incoming {
-            user_id: user_1,
-            should_notify: false
-        }]
+        &[Contact::Incoming { user_id: user_1 }]
     );
 
     // User can't accept their own contact request
@@ -299,7 +288,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
         db.get_contacts(user_1).await.unwrap(),
         &[Contact::Accepted {
             user_id: user_2,
-            should_notify: true,
             busy: false,
         }],
     );
@@ -309,7 +297,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
         db.get_contacts(user_2).await.unwrap(),
         &[Contact::Accepted {
             user_id: user_1,
-            should_notify: false,
             busy: false,
         }]
     );
@@ -326,7 +313,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
         db.get_contacts(user_1).await.unwrap(),
         &[Contact::Accepted {
             user_id: user_2,
-            should_notify: true,
             busy: false,
         }]
     );
@@ -339,7 +325,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
         db.get_contacts(user_1).await.unwrap(),
         &[Contact::Accepted {
             user_id: user_2,
-            should_notify: false,
             busy: false,
         }]
     );
@@ -353,12 +338,10 @@ async fn test_add_contacts(db: &Arc<Database>) {
         &[
             Contact::Accepted {
                 user_id: user_2,
-                should_notify: false,
                 busy: false,
             },
             Contact::Accepted {
                 user_id: user_3,
-                should_notify: false,
                 busy: false,
             }
         ]
@@ -367,7 +350,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
         db.get_contacts(user_3).await.unwrap(),
         &[Contact::Accepted {
             user_id: user_1,
-            should_notify: false,
             busy: false,
         }],
     );
@@ -383,7 +365,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
         db.get_contacts(user_2).await.unwrap(),
         &[Contact::Accepted {
             user_id: user_1,
-            should_notify: false,
             busy: false,
         }]
     );
@@ -391,7 +372,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
         db.get_contacts(user_3).await.unwrap(),
         &[Contact::Accepted {
             user_id: user_1,
-            should_notify: false,
             busy: false,
         }],
     );
@@ -415,7 +395,6 @@ async fn test_metrics_id(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "person1".into(),
                 github_user_id: 101,
-                invite_count: 5,
             },
         )
         .await
@@ -431,7 +410,6 @@ async fn test_metrics_id(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "person2".into(),
                 github_user_id: 102,
-                invite_count: 5,
             },
         )
         .await
@@ -460,7 +438,6 @@ async fn test_project_count(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "admin".into(),
                 github_user_id: 0,
-                invite_count: 0,
             },
         )
         .await
@@ -472,7 +449,6 @@ async fn test_project_count(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user".into(),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await
@@ -554,7 +530,6 @@ async fn test_fuzzy_search_users() {
             NewUserParams {
                 github_login: github_login.into(),
                 github_user_id: i as i32,
-                invite_count: 0,
             },
         )
         .await
@@ -596,7 +571,6 @@ async fn test_non_matching_release_channels(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "admin".into(),
                 github_user_id: 0,
-                invite_count: 0,
             },
         )
         .await
@@ -608,7 +582,6 @@ async fn test_non_matching_release_channels(db: &Arc<Database>) {
             NewUserParams {
                 github_login: "user".into(),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await

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

@@ -18,7 +18,6 @@ async fn test_get_user_flags(db: &Arc<Database>) {
             NewUserParams {
                 github_login: format!("user1"),
                 github_user_id: 1,
-                invite_count: 0,
             },
         )
         .await
@@ -32,7 +31,6 @@ async fn test_get_user_flags(db: &Arc<Database>) {
             NewUserParams {
                 github_login: format!("user2"),
                 github_user_id: 2,
-                invite_count: 0,
             },
         )
         .await

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

@@ -1,7 +1,9 @@
+use super::new_test_user;
 use crate::{
-    db::{Database, MessageId, NewUserParams},
+    db::{ChannelRole, Database, MessageId},
     test_both_dbs,
 };
+use channel::mentions_to_proto;
 use std::sync::Arc;
 use time::OffsetDateTime;
 
@@ -12,39 +14,38 @@ test_both_dbs!(
 );
 
 async fn test_channel_message_retrieval(db: &Arc<Database>) {
-    let user = db
-        .create_user(
-            "user@example.com",
-            false,
-            NewUserParams {
-                github_login: "user".into(),
-                github_user_id: 1,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-    let channel = db.create_channel("channel", None, user).await.unwrap();
+    let user = new_test_user(db, "user@example.com").await;
+    let result = db.create_channel("channel", None, user).await.unwrap();
 
     let owner_id = db.create_server("test").await.unwrap().0 as u32;
-    db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
-        .await
-        .unwrap();
+    db.join_channel_chat(
+        result.channel.id,
+        rpc::ConnectionId { owner_id, id: 0 },
+        user,
+    )
+    .await
+    .unwrap();
 
     let mut all_messages = Vec::new();
     for i in 0..10 {
         all_messages.push(
-            db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
-                .await
-                .unwrap()
-                .0
-                .to_proto(),
+            db.create_channel_message(
+                result.channel.id,
+                user,
+                &i.to_string(),
+                &[],
+                OffsetDateTime::now_utc(),
+                i,
+            )
+            .await
+            .unwrap()
+            .message_id
+            .to_proto(),
         );
     }
 
     let messages = db
-        .get_channel_messages(channel, user, 3, None)
+        .get_channel_messages(result.channel.id, user, 3, None)
         .await
         .unwrap()
         .into_iter()
@@ -54,7 +55,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
 
     let messages = db
         .get_channel_messages(
-            channel,
+            result.channel.id,
             user,
             4,
             Some(MessageId::from_proto(all_messages[6])),
@@ -74,99 +75,154 @@ test_both_dbs!(
 );
 
 async fn test_channel_message_nonces(db: &Arc<Database>) {
-    let user = db
-        .create_user(
-            "user@example.com",
-            false,
-            NewUserParams {
-                github_login: "user".into(),
-                github_user_id: 1,
-                invite_count: 0,
-            },
-        )
+    let user_a = new_test_user(db, "user_a@example.com").await;
+    let user_b = new_test_user(db, "user_b@example.com").await;
+    let user_c = new_test_user(db, "user_c@example.com").await;
+    let channel = db.create_root_channel("channel", user_a).await.unwrap();
+    db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
         .await
-        .unwrap()
-        .user_id;
-    let channel = db.create_channel("channel", None, user).await.unwrap();
-
-    let owner_id = db.create_server("test").await.unwrap().0 as u32;
-
-    db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
+        .unwrap();
+    db.invite_channel_member(channel, user_c, user_a, ChannelRole::Member)
         .await
         .unwrap();
-
-    let msg1_id = db
-        .create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
+    db.respond_to_channel_invite(channel, user_b, true)
         .await
         .unwrap();
-    let msg2_id = db
-        .create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2)
+    db.respond_to_channel_invite(channel, user_c, true)
         .await
         .unwrap();
-    let msg3_id = db
-        .create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1)
+
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+    db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user_a)
         .await
         .unwrap();
-    let msg4_id = db
-        .create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2)
+    db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 1 }, user_b)
         .await
         .unwrap();
 
-    assert_ne!(msg1_id, msg2_id);
-    assert_eq!(msg1_id, msg3_id);
-    assert_eq!(msg2_id, msg4_id);
-}
-
-test_both_dbs!(
-    test_channel_message_new_notification,
-    test_channel_message_new_notification_postgres,
-    test_channel_message_new_notification_sqlite
-);
-
-async fn test_channel_message_new_notification(db: &Arc<Database>) {
-    let user = db
-        .create_user(
-            "user_a@example.com",
-            false,
-            NewUserParams {
-                github_login: "user_a".into(),
-                github_user_id: 1,
-                invite_count: 0,
-            },
+    // As user A, create messages that re-use the same nonces. The requests
+    // succeed, but return the same ids.
+    let id1 = db
+        .create_channel_message(
+            channel,
+            user_a,
+            "hi @user_b",
+            &mentions_to_proto(&[(3..10, user_b.to_proto())]),
+            OffsetDateTime::now_utc(),
+            100,
         )
         .await
         .unwrap()
-        .user_id;
-    let observer = db
-        .create_user(
-            "user_b@example.com",
-            false,
-            NewUserParams {
-                github_login: "user_b".into(),
-                github_user_id: 1,
-                invite_count: 0,
-            },
+        .message_id;
+    let id2 = db
+        .create_channel_message(
+            channel,
+            user_a,
+            "hello, fellow users",
+            &mentions_to_proto(&[]),
+            OffsetDateTime::now_utc(),
+            200,
+        )
+        .await
+        .unwrap()
+        .message_id;
+    let id3 = db
+        .create_channel_message(
+            channel,
+            user_a,
+            "bye @user_c (same nonce as first message)",
+            &mentions_to_proto(&[(4..11, user_c.to_proto())]),
+            OffsetDateTime::now_utc(),
+            100,
+        )
+        .await
+        .unwrap()
+        .message_id;
+    let id4 = db
+        .create_channel_message(
+            channel,
+            user_a,
+            "omg (same nonce as second message)",
+            &mentions_to_proto(&[]),
+            OffsetDateTime::now_utc(),
+            200,
         )
         .await
         .unwrap()
-        .user_id;
+        .message_id;
 
-    let channel_1 = db.create_channel("channel", None, user).await.unwrap();
+    // As a different user, reuse one of the same nonces. This request succeeds
+    // and returns a different id.
+    let id5 = db
+        .create_channel_message(
+            channel,
+            user_b,
+            "omg @user_a (same nonce as user_a's first message)",
+            &mentions_to_proto(&[(4..11, user_a.to_proto())]),
+            OffsetDateTime::now_utc(),
+            100,
+        )
+        .await
+        .unwrap()
+        .message_id;
 
-    let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
+    assert_ne!(id1, id2);
+    assert_eq!(id1, id3);
+    assert_eq!(id2, id4);
+    assert_ne!(id5, id1);
 
-    db.invite_channel_member(channel_1, observer, user, false)
+    let messages = db
+        .get_channel_messages(channel, user_a, 5, None)
         .await
-        .unwrap();
+        .unwrap()
+        .into_iter()
+        .map(|m| (m.id, m.body, m.mentions))
+        .collect::<Vec<_>>();
+    assert_eq!(
+        messages,
+        &[
+            (
+                id1.to_proto(),
+                "hi @user_b".into(),
+                mentions_to_proto(&[(3..10, user_b.to_proto())]),
+            ),
+            (
+                id2.to_proto(),
+                "hello, fellow users".into(),
+                mentions_to_proto(&[])
+            ),
+            (
+                id5.to_proto(),
+                "omg @user_a (same nonce as user_a's first message)".into(),
+                mentions_to_proto(&[(4..11, user_a.to_proto())]),
+            ),
+        ]
+    );
+}
 
-    db.respond_to_channel_invite(channel_1, observer, true)
+test_both_dbs!(
+    test_unseen_channel_messages,
+    test_unseen_channel_messages_postgres,
+    test_unseen_channel_messages_sqlite
+);
+
+async fn test_unseen_channel_messages(db: &Arc<Database>) {
+    let user = new_test_user(db, "user_a@example.com").await;
+    let observer = new_test_user(db, "user_b@example.com").await;
+
+    let channel_1 = db.create_root_channel("channel", user).await.unwrap();
+    let channel_2 = db.create_root_channel("channel-2", user).await.unwrap();
+
+    db.invite_channel_member(channel_1, observer, user, ChannelRole::Member)
         .await
         .unwrap();
-
-    db.invite_channel_member(channel_2, observer, user, false)
+    db.invite_channel_member(channel_2, observer, user, ChannelRole::Member)
         .await
         .unwrap();
 
+    db.respond_to_channel_invite(channel_1, observer, true)
+        .await
+        .unwrap();
     db.respond_to_channel_invite(channel_2, observer, true)
         .await
         .unwrap();
@@ -179,28 +235,31 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
         .unwrap();
 
     let _ = db
-        .create_channel_message(channel_1, user, "1_1", OffsetDateTime::now_utc(), 1)
+        .create_channel_message(channel_1, user, "1_1", &[], OffsetDateTime::now_utc(), 1)
         .await
         .unwrap();
 
-    let (second_message, _, _) = db
-        .create_channel_message(channel_1, user, "1_2", OffsetDateTime::now_utc(), 2)
+    let second_message = db
+        .create_channel_message(channel_1, user, "1_2", &[], OffsetDateTime::now_utc(), 2)
         .await
-        .unwrap();
+        .unwrap()
+        .message_id;
 
-    let (third_message, _, _) = db
-        .create_channel_message(channel_1, user, "1_3", OffsetDateTime::now_utc(), 3)
+    let third_message = db
+        .create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3)
         .await
-        .unwrap();
+        .unwrap()
+        .message_id;
 
     db.join_channel_chat(channel_2, user_connection_id, user)
         .await
         .unwrap();
 
-    let (fourth_message, _, _) = db
-        .create_channel_message(channel_2, user, "2_1", OffsetDateTime::now_utc(), 4)
+    let fourth_message = db
+        .create_channel_message(channel_2, user, "2_1", &[], OffsetDateTime::now_utc(), 4)
         .await
-        .unwrap();
+        .unwrap()
+        .message_id;
 
     // Check that observer has new messages
     let unseen_messages = db
@@ -295,3 +354,101 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
         }]
     );
 }
+
+test_both_dbs!(
+    test_channel_message_mentions,
+    test_channel_message_mentions_postgres,
+    test_channel_message_mentions_sqlite
+);
+
+async fn test_channel_message_mentions(db: &Arc<Database>) {
+    let user_a = new_test_user(db, "user_a@example.com").await;
+    let user_b = new_test_user(db, "user_b@example.com").await;
+    let user_c = new_test_user(db, "user_c@example.com").await;
+
+    let channel = db
+        .create_channel("channel", None, user_a)
+        .await
+        .unwrap()
+        .channel
+        .id;
+    db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
+        .await
+        .unwrap();
+    db.respond_to_channel_invite(channel, user_b, true)
+        .await
+        .unwrap();
+
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+    let connection_id = rpc::ConnectionId { owner_id, id: 0 };
+    db.join_channel_chat(channel, connection_id, user_a)
+        .await
+        .unwrap();
+
+    db.create_channel_message(
+        channel,
+        user_a,
+        "hi @user_b and @user_c",
+        &mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]),
+        OffsetDateTime::now_utc(),
+        1,
+    )
+    .await
+    .unwrap();
+    db.create_channel_message(
+        channel,
+        user_a,
+        "bye @user_c",
+        &mentions_to_proto(&[(4..11, user_c.to_proto())]),
+        OffsetDateTime::now_utc(),
+        2,
+    )
+    .await
+    .unwrap();
+    db.create_channel_message(
+        channel,
+        user_a,
+        "umm",
+        &mentions_to_proto(&[]),
+        OffsetDateTime::now_utc(),
+        3,
+    )
+    .await
+    .unwrap();
+    db.create_channel_message(
+        channel,
+        user_a,
+        "@user_b, stop.",
+        &mentions_to_proto(&[(0..7, user_b.to_proto())]),
+        OffsetDateTime::now_utc(),
+        4,
+    )
+    .await
+    .unwrap();
+
+    let messages = db
+        .get_channel_messages(channel, user_b, 5, None)
+        .await
+        .unwrap()
+        .into_iter()
+        .map(|m| (m.body, m.mentions))
+        .collect::<Vec<_>>();
+    assert_eq!(
+        &messages,
+        &[
+            (
+                "hi @user_b and @user_c".into(),
+                mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]),
+            ),
+            (
+                "bye @user_c".into(),
+                mentions_to_proto(&[(4..11, user_c.to_proto())]),
+            ),
+            ("umm".into(), mentions_to_proto(&[]),),
+            (
+                "@user_b, stop.".into(),
+                mentions_to_proto(&[(0..7, user_b.to_proto())]),
+            ),
+        ]
+    );
+}

crates/collab/src/lib.rs 🔗

@@ -119,7 +119,9 @@ impl AppState {
     pub async fn new(config: Config) -> Result<Arc<Self>> {
         let mut db_options = db::ConnectOptions::new(config.database_url.clone());
         db_options.max_connections(config.database_max_connections);
-        let db = Database::new(db_options, Executor::Production).await?;
+        let mut db = Database::new(db_options, Executor::Production).await?;
+        db.initialize_notification_kinds().await?;
+
         let live_kit_client = if let Some(((server, key), secret)) = config
             .live_kit_server
             .as_ref()

crates/collab/src/rpc.rs 🔗

@@ -3,8 +3,11 @@ mod connection_pool;
 use crate::{
     auth,
     db::{
-        self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId,
-        ServerId, User, UserId,
+        self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult,
+        CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
+        MoveChannelResult, NotificationId, ProjectId, RemoveChannelMemberResult,
+        RenameChannelResult, RespondToChannelInvite, RoomId, ServerId, SetChannelVisibilityResult,
+        User, UserId,
     },
     executor::Executor,
     AppState, Result,
@@ -38,8 +41,8 @@ use lazy_static::lazy_static;
 use prometheus::{register_int_gauge, IntGauge};
 use rpc::{
     proto::{
-        self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage,
-        LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
+        self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
+        RequestMessage, UpdateChannelBufferCollaborators,
     },
     Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
 };
@@ -70,6 +73,7 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
 
 const MESSAGE_COUNT_PER_PAGE: usize = 100;
 const MAX_MESSAGE_LEN: usize = 1024;
+const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
 
 lazy_static! {
     static ref METRIC_CONNECTIONS: IntGauge =
@@ -225,6 +229,7 @@ impl Server {
             .add_request_handler(forward_project_request::<proto::OpenBufferByPath>)
             .add_request_handler(forward_project_request::<proto::GetCompletions>)
             .add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
+            .add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
             .add_request_handler(forward_project_request::<proto::GetCodeActions>)
             .add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
             .add_request_handler(forward_project_request::<proto::PrepareRename>)
@@ -254,7 +259,8 @@ impl Server {
             .add_request_handler(delete_channel)
             .add_request_handler(invite_channel_member)
             .add_request_handler(remove_channel_member)
-            .add_request_handler(set_channel_member_admin)
+            .add_request_handler(set_channel_member_role)
+            .add_request_handler(set_channel_visibility)
             .add_request_handler(rename_channel)
             .add_request_handler(join_channel_buffer)
             .add_request_handler(leave_channel_buffer)
@@ -268,8 +274,9 @@ impl Server {
             .add_request_handler(send_channel_message)
             .add_request_handler(remove_channel_message)
             .add_request_handler(get_channel_messages)
-            .add_request_handler(link_channel)
-            .add_request_handler(unlink_channel)
+            .add_request_handler(get_channel_messages_by_id)
+            .add_request_handler(get_notifications)
+            .add_request_handler(mark_notification_as_read)
             .add_request_handler(move_channel)
             .add_request_handler(follow)
             .add_message_handler(unfollow)
@@ -387,7 +394,7 @@ impl Server {
                             let contacts = app_state.db.get_contacts(user_id).await.trace_err();
                             if let Some((busy, contacts)) = busy.zip(contacts) {
                                 let pool = pool.lock();
-                                let updated_contact = contact_for_user(user_id, false, busy, &pool);
+                                let updated_contact = contact_for_user(user_id, busy, &pool);
                                 for contact in contacts {
                                     if let db::Contact::Accepted {
                                         user_id: contact_user_id,
@@ -581,14 +588,14 @@ impl Server {
             let (contacts, channels_for_user, channel_invites) = future::try_join3(
                 this.app_state.db.get_contacts(user_id),
                 this.app_state.db.get_channels_for_user(user_id),
-                this.app_state.db.get_channel_invites_for_user(user_id)
+                this.app_state.db.get_channel_invites_for_user(user_id),
             ).await?;
 
             {
                 let mut pool = this.connection_pool.lock();
                 pool.add_connection(connection_id, user_id, user.admin);
                 this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
-                this.peer.send(connection_id, build_initial_channels_update(
+                this.peer.send(connection_id, build_channels_update(
                     channels_for_user,
                     channel_invites
                 ))?;
@@ -687,7 +694,7 @@ impl Server {
         if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? {
             if let Some(code) = &user.invite_code {
                 let pool = self.connection_pool.lock();
-                let invitee_contact = contact_for_user(invitee_id, true, false, &pool);
+                let invitee_contact = contact_for_user(invitee_id, false, &pool);
                 for connection_id in pool.user_connection_ids(inviter_id) {
                     self.peer.send(
                         connection_id,
@@ -935,7 +942,7 @@ async fn create_room(
         let live_kit_room = live_kit_room.clone();
         let live_kit = session.live_kit_client.as_ref();
 
-        util::async_iife!({
+        util::async_maybe!({
             let live_kit = live_kit?;
 
             let token = live_kit
@@ -945,6 +952,7 @@ async fn create_room(
             Some(proto::LiveKitConnectionInfo {
                 server_url: live_kit.url().into(),
                 token,
+                can_publish: true,
             })
         })
     }
@@ -976,6 +984,13 @@ async fn join_room(
     session: Session,
 ) -> Result<()> {
     let room_id = RoomId::from_proto(request.id);
+
+    let channel_id = session.db().await.channel_id_for_room(room_id).await?;
+
+    if let Some(channel_id) = channel_id {
+        return join_channel_internal(channel_id, Box::new(response), session).await;
+    }
+
     let joined_room = {
         let room = session
             .db()
@@ -991,16 +1006,6 @@ async fn join_room(
         room.into_inner()
     };
 
-    if let Some(channel_id) = joined_room.channel_id {
-        channel_updated(
-            channel_id,
-            &joined_room.room,
-            &joined_room.channel_members,
-            &session.peer,
-            &*session.connection_pool().await,
-        )
-    }
-
     for connection_id in session
         .connection_pool()
         .await
@@ -1028,6 +1033,7 @@ async fn join_room(
             Some(proto::LiveKitConnectionInfo {
                 server_url: live_kit.url().into(),
                 token,
+                can_publish: true,
             })
         } else {
             None
@@ -1038,7 +1044,7 @@ async fn join_room(
 
     response.send(proto::JoinRoomResponse {
         room: Some(joined_room.room),
-        channel_id: joined_room.channel_id.map(|id| id.to_proto()),
+        channel_id: None,
         live_kit_connection_info,
     })?;
 
@@ -2064,7 +2070,7 @@ async fn request_contact(
         return Err(anyhow!("cannot add yourself as a contact"))?;
     }
 
-    session
+    let notifications = session
         .db()
         .await
         .send_contact_request(requester_id, responder_id)
@@ -2087,16 +2093,14 @@ async fn request_contact(
         .incoming_requests
         .push(proto::IncomingContactRequest {
             requester_id: requester_id.to_proto(),
-            should_notify: true,
         });
-    for connection_id in session
-        .connection_pool()
-        .await
-        .user_connection_ids(responder_id)
-    {
+    let connection_pool = session.connection_pool().await;
+    for connection_id in connection_pool.user_connection_ids(responder_id) {
         session.peer.send(connection_id, update.clone())?;
     }
 
+    send_notifications(&*connection_pool, &session.peer, notifications);
+
     response.send(proto::Ack {})?;
     Ok(())
 }
@@ -2115,7 +2119,8 @@ async fn respond_to_contact_request(
     } else {
         let accept = request.response == proto::ContactRequestResponse::Accept as i32;
 
-        db.respond_to_contact_request(responder_id, requester_id, accept)
+        let notifications = db
+            .respond_to_contact_request(responder_id, requester_id, accept)
             .await?;
         let requester_busy = db.is_user_busy(requester_id).await?;
         let responder_busy = db.is_user_busy(responder_id).await?;
@@ -2126,7 +2131,7 @@ async fn respond_to_contact_request(
         if accept {
             update
                 .contacts
-                .push(contact_for_user(requester_id, false, requester_busy, &pool));
+                .push(contact_for_user(requester_id, requester_busy, &pool));
         }
         update
             .remove_incoming_requests
@@ -2140,14 +2145,17 @@ async fn respond_to_contact_request(
         if accept {
             update
                 .contacts
-                .push(contact_for_user(responder_id, true, responder_busy, &pool));
+                .push(contact_for_user(responder_id, responder_busy, &pool));
         }
         update
             .remove_outgoing_requests
             .push(responder_id.to_proto());
+
         for connection_id in pool.user_connection_ids(requester_id) {
             session.peer.send(connection_id, update.clone())?;
         }
+
+        send_notifications(&*pool, &session.peer, notifications);
     }
 
     response.send(proto::Ack {})?;
@@ -2162,7 +2170,8 @@ async fn remove_contact(
     let requester_id = session.user_id;
     let responder_id = UserId::from_proto(request.user_id);
     let db = session.db().await;
-    let contact_accepted = db.remove_contact(requester_id, responder_id).await?;
+    let (contact_accepted, deleted_notification_id) =
+        db.remove_contact(requester_id, responder_id).await?;
 
     let pool = session.connection_pool().await;
     // Update outgoing contact requests of requester
@@ -2189,6 +2198,14 @@ async fn remove_contact(
     }
     for connection_id in pool.user_connection_ids(responder_id) {
         session.peer.send(connection_id, update.clone())?;
+        if let Some(notification_id) = deleted_notification_id {
+            session.peer.send(
+                connection_id,
+                proto::DeleteNotification {
+                    notification_id: notification_id.to_proto(),
+                },
+            )?;
+        }
     }
 
     response.send(proto::Ack {})?;
@@ -2203,37 +2220,21 @@ async fn create_channel(
     let db = session.db().await;
 
     let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
-    let id = db
+    let CreateChannelResult {
+        channel,
+        participants_to_update,
+    } = db
         .create_channel(&request.name, parent_id, session.user_id)
         .await?;
 
-    let channel = proto::Channel {
-        id: id.to_proto(),
-        name: request.name,
-    };
-
     response.send(proto::CreateChannelResponse {
-        channel: Some(channel.clone()),
+        channel: Some(channel.to_proto()),
         parent_id: request.parent_id,
     })?;
 
-    let Some(parent_id) = parent_id else {
-        return Ok(());
-    };
-
-    let update = proto::UpdateChannels {
-        channels: vec![channel],
-        insert_edge: vec![ChannelEdge {
-            parent_id: parent_id.to_proto(),
-            channel_id: id.to_proto(),
-        }],
-        ..Default::default()
-    };
-
-    let user_ids_to_notify = db.get_channel_members(parent_id).await?;
-
     let connection_pool = session.connection_pool().await;
-    for user_id in user_ids_to_notify {
+    for (user_id, channels) in participants_to_update {
+        let update = build_channels_update(channels, vec![]);
         for connection_id in connection_pool.user_connection_ids(user_id) {
             if user_id == session.user_id {
                 continue;
@@ -2282,27 +2283,30 @@ async fn invite_channel_member(
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
     let invitee_id = UserId::from_proto(request.user_id);
-    db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin)
+    let InviteMemberResult {
+        channel,
+        notifications,
+    } = db
+        .invite_channel_member(
+            channel_id,
+            invitee_id,
+            session.user_id,
+            request.role().into(),
+        )
         .await?;
 
-    let (channel, _) = db
-        .get_channel(channel_id, session.user_id)
-        .await?
-        .ok_or_else(|| anyhow!("channel not found"))?;
+    let update = proto::UpdateChannels {
+        channel_invitations: vec![channel.to_proto()],
+        ..Default::default()
+    };
 
-    let mut update = proto::UpdateChannels::default();
-    update.channel_invitations.push(proto::Channel {
-        id: channel.id.to_proto(),
-        name: channel.name,
-    });
-    for connection_id in session
-        .connection_pool()
-        .await
-        .user_connection_ids(invitee_id)
-    {
+    let connection_pool = session.connection_pool().await;
+    for connection_id in connection_pool.user_connection_ids(invitee_id) {
         session.peer.send(connection_id, update.clone())?;
     }
 
+    send_notifications(&*connection_pool, &session.peer, notifications);
+
     response.send(proto::Ack {})?;
     Ok(())
 }
@@ -2316,157 +2320,153 @@ async fn remove_channel_member(
     let channel_id = ChannelId::from_proto(request.channel_id);
     let member_id = UserId::from_proto(request.user_id);
 
-    db.remove_channel_member(channel_id, member_id, session.user_id)
+    let RemoveChannelMemberResult {
+        membership_update,
+        notification_id,
+    } = db
+        .remove_channel_member(channel_id, member_id, session.user_id)
         .await?;
 
-    let mut update = proto::UpdateChannels::default();
-    update.delete_channels.push(channel_id.to_proto());
-
-    for connection_id in session
-        .connection_pool()
-        .await
-        .user_connection_ids(member_id)
-    {
-        session.peer.send(connection_id, update.clone())?;
+    let connection_pool = &session.connection_pool().await;
+    notify_membership_updated(
+        &connection_pool,
+        membership_update,
+        member_id,
+        &session.peer,
+    );
+    for connection_id in connection_pool.user_connection_ids(member_id) {
+        if let Some(notification_id) = notification_id {
+            session
+                .peer
+                .send(
+                    connection_id,
+                    proto::DeleteNotification {
+                        notification_id: notification_id.to_proto(),
+                    },
+                )
+                .trace_err();
+        }
     }
 
     response.send(proto::Ack {})?;
     Ok(())
 }
 
-async fn set_channel_member_admin(
-    request: proto::SetChannelMemberAdmin,
-    response: Response<proto::SetChannelMemberAdmin>,
+async fn set_channel_visibility(
+    request: proto::SetChannelVisibility,
+    response: Response<proto::SetChannelVisibility>,
     session: Session,
 ) -> Result<()> {
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
-    let member_id = UserId::from_proto(request.user_id);
-    db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin)
+    let visibility = request.visibility().into();
+
+    let SetChannelVisibilityResult {
+        participants_to_update,
+        participants_to_remove,
+        channels_to_remove,
+    } = db
+        .set_channel_visibility(channel_id, visibility, session.user_id)
         .await?;
 
-    let (channel, has_accepted) = db
-        .get_channel(channel_id, member_id)
-        .await?
-        .ok_or_else(|| anyhow!("channel not found"))?;
-
-    let mut update = proto::UpdateChannels::default();
-    if has_accepted {
-        update.channel_permissions.push(proto::ChannelPermission {
-            channel_id: channel.id.to_proto(),
-            is_admin: request.admin,
-        });
+    let connection_pool = session.connection_pool().await;
+    for (user_id, channels) in participants_to_update {
+        let update = build_channels_update(channels, vec![]);
+        for connection_id in connection_pool.user_connection_ids(user_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
     }
-
-    for connection_id in session
-        .connection_pool()
-        .await
-        .user_connection_ids(member_id)
-    {
-        session.peer.send(connection_id, update.clone())?;
+    for user_id in participants_to_remove {
+        let update = proto::UpdateChannels {
+            delete_channels: channels_to_remove.iter().map(|id| id.to_proto()).collect(),
+            ..Default::default()
+        };
+        for connection_id in connection_pool.user_connection_ids(user_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
     }
 
     response.send(proto::Ack {})?;
     Ok(())
 }
 
-async fn rename_channel(
-    request: proto::RenameChannel,
-    response: Response<proto::RenameChannel>,
+async fn set_channel_member_role(
+    request: proto::SetChannelMemberRole,
+    response: Response<proto::SetChannelMemberRole>,
     session: Session,
 ) -> Result<()> {
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
-    let new_name = db
-        .rename_channel(channel_id, session.user_id, &request.name)
+    let member_id = UserId::from_proto(request.user_id);
+    let result = db
+        .set_channel_member_role(
+            channel_id,
+            session.user_id,
+            member_id,
+            request.role().into(),
+        )
         .await?;
 
-    let channel = proto::Channel {
-        id: request.channel_id,
-        name: new_name,
-    };
-    response.send(proto::RenameChannelResponse {
-        channel: Some(channel.clone()),
-    })?;
-    let mut update = proto::UpdateChannels::default();
-    update.channels.push(channel);
-
-    let member_ids = db.get_channel_members(channel_id).await?;
-
-    let connection_pool = session.connection_pool().await;
-    for member_id in member_ids {
-        for connection_id in connection_pool.user_connection_ids(member_id) {
-            session.peer.send(connection_id, update.clone())?;
+    match result {
+        db::SetMemberRoleResult::MembershipUpdated(membership_update) => {
+            let connection_pool = session.connection_pool().await;
+            notify_membership_updated(
+                &connection_pool,
+                membership_update,
+                member_id,
+                &session.peer,
+            )
         }
-    }
-
-    Ok(())
-}
-
-async fn link_channel(
-    request: proto::LinkChannel,
-    response: Response<proto::LinkChannel>,
-    session: Session,
-) -> Result<()> {
-    let db = session.db().await;
-    let channel_id = ChannelId::from_proto(request.channel_id);
-    let to = ChannelId::from_proto(request.to);
-    let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?;
+        db::SetMemberRoleResult::InviteUpdated(channel) => {
+            let update = proto::UpdateChannels {
+                channel_invitations: vec![channel.to_proto()],
+                ..Default::default()
+            };
 
-    let members = db.get_channel_members(to).await?;
-    let connection_pool = session.connection_pool().await;
-    let update = proto::UpdateChannels {
-        channels: channels_to_send
-            .channels
-            .into_iter()
-            .map(|channel| proto::Channel {
-                id: channel.id.to_proto(),
-                name: channel.name,
-            })
-            .collect(),
-        insert_edge: channels_to_send.edges,
-        ..Default::default()
-    };
-    for member_id in members {
-        for connection_id in connection_pool.user_connection_ids(member_id) {
-            session.peer.send(connection_id, update.clone())?;
+            for connection_id in session
+                .connection_pool()
+                .await
+                .user_connection_ids(member_id)
+            {
+                session.peer.send(connection_id, update.clone())?;
+            }
         }
     }
 
-    response.send(Ack {})?;
-
+    response.send(proto::Ack {})?;
     Ok(())
 }
 
-async fn unlink_channel(
-    request: proto::UnlinkChannel,
-    response: Response<proto::UnlinkChannel>,
+async fn rename_channel(
+    request: proto::RenameChannel,
+    response: Response<proto::RenameChannel>,
     session: Session,
 ) -> Result<()> {
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
-    let from = ChannelId::from_proto(request.from);
-
-    db.unlink_channel(session.user_id, channel_id, from).await?;
+    let RenameChannelResult {
+        channel,
+        participants_to_update,
+    } = db
+        .rename_channel(channel_id, session.user_id, &request.name)
+        .await?;
 
-    let members = db.get_channel_members(from).await?;
+    response.send(proto::RenameChannelResponse {
+        channel: Some(channel.to_proto()),
+    })?;
 
-    let update = proto::UpdateChannels {
-        delete_edge: vec![proto::ChannelEdge {
-            channel_id: channel_id.to_proto(),
-            parent_id: from.to_proto(),
-        }],
-        ..Default::default()
-    };
     let connection_pool = session.connection_pool().await;
-    for member_id in members {
-        for connection_id in connection_pool.user_connection_ids(member_id) {
+    for (user_id, channel) in participants_to_update {
+        for connection_id in connection_pool.user_connection_ids(user_id) {
+            let update = proto::UpdateChannels {
+                channels: vec![channel.to_proto()],
+                ..Default::default()
+            };
+
             session.peer.send(connection_id, update.clone())?;
         }
     }
 
-    response.send(Ack {})?;
-
     Ok(())
 }
 
@@ -2475,57 +2475,50 @@ async fn move_channel(
     response: Response<proto::MoveChannel>,
     session: Session,
 ) -> Result<()> {
-    let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
-    let from_parent = ChannelId::from_proto(request.from);
-    let to = ChannelId::from_proto(request.to);
+    let to = request.to.map(ChannelId::from_proto);
 
-    let channels_to_send = db
-        .move_channel(session.user_id, channel_id, from_parent, to)
+    let result = session
+        .db()
+        .await
+        .move_channel(channel_id, to, session.user_id)
         .await?;
 
-    if channels_to_send.is_empty() {
-        response.send(Ack {})?;
-        return Ok(());
-    }
+    notify_channel_moved(result, session).await?;
 
-    let members_from = db.get_channel_members(from_parent).await?;
-    let members_to = db.get_channel_members(to).await?;
+    response.send(Ack {})?;
+    Ok(())
+}
 
-    let update = proto::UpdateChannels {
-        delete_edge: vec![proto::ChannelEdge {
-            channel_id: channel_id.to_proto(),
-            parent_id: from_parent.to_proto(),
-        }],
-        ..Default::default()
+async fn notify_channel_moved(result: Option<MoveChannelResult>, session: Session) -> Result<()> {
+    let Some(MoveChannelResult {
+        participants_to_remove,
+        participants_to_update,
+        moved_channels,
+    }) = result
+    else {
+        return Ok(());
     };
+    let moved_channels: Vec<u64> = moved_channels.iter().map(|id| id.to_proto()).collect();
+
     let connection_pool = session.connection_pool().await;
-    for member_id in members_from {
-        for connection_id in connection_pool.user_connection_ids(member_id) {
+    for (user_id, channels) in participants_to_update {
+        let mut update = build_channels_update(channels, vec![]);
+        update.delete_channels = moved_channels.clone();
+        for connection_id in connection_pool.user_connection_ids(user_id) {
             session.peer.send(connection_id, update.clone())?;
         }
     }
 
-    let update = proto::UpdateChannels {
-        channels: channels_to_send
-            .channels
-            .into_iter()
-            .map(|channel| proto::Channel {
-                id: channel.id.to_proto(),
-                name: channel.name,
-            })
-            .collect(),
-        insert_edge: channels_to_send.edges,
-        ..Default::default()
-    };
-    for member_id in members_to {
-        for connection_id in connection_pool.user_connection_ids(member_id) {
+    for user_id in participants_to_remove {
+        let update = proto::UpdateChannels {
+            delete_channels: moved_channels.clone(),
+            ..Default::default()
+        };
+        for connection_id in connection_pool.user_connection_ids(user_id) {
             session.peer.send(connection_id, update.clone())?;
         }
     }
-
-    response.send(Ack {})?;
-
     Ok(())
 }
 
@@ -2537,7 +2530,7 @@ async fn get_channel_members(
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
     let members = db
-        .get_channel_member_details(channel_id, session.user_id)
+        .get_channel_participant_details(channel_id, session.user_id)
         .await?;
     response.send(proto::GetChannelMembersResponse { members })?;
     Ok(())
@@ -2550,54 +2543,34 @@ async fn respond_to_channel_invite(
 ) -> Result<()> {
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
-    db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
+    let RespondToChannelInvite {
+        membership_update,
+        notifications,
+    } = db
+        .respond_to_channel_invite(channel_id, session.user_id, request.accept)
         .await?;
 
-    let mut update = proto::UpdateChannels::default();
-    update
-        .remove_channel_invitations
-        .push(channel_id.to_proto());
-    if request.accept {
-        let result = db.get_channel_for_user(channel_id, session.user_id).await?;
-        update
-            .channels
-            .extend(
-                result
-                    .channels
-                    .channels
-                    .into_iter()
-                    .map(|channel| proto::Channel {
-                        id: channel.id.to_proto(),
-                        name: channel.name,
-                    }),
-            );
-        update.unseen_channel_messages = result.channel_messages;
-        update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
-        update.insert_edge = result.channels.edges;
-        update
-            .channel_participants
-            .extend(
-                result
-                    .channel_participants
-                    .into_iter()
-                    .map(|(channel_id, user_ids)| proto::ChannelParticipants {
-                        channel_id: channel_id.to_proto(),
-                        participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
-                    }),
-            );
-        update
-            .channel_permissions
-            .extend(
-                result
-                    .channels_with_admin_privileges
-                    .into_iter()
-                    .map(|channel_id| proto::ChannelPermission {
-                        channel_id: channel_id.to_proto(),
-                        is_admin: true,
-                    }),
-            );
-    }
-    session.peer.send(session.connection_id, update)?;
+    let connection_pool = session.connection_pool().await;
+    if let Some(membership_update) = membership_update {
+        notify_membership_updated(
+            &connection_pool,
+            membership_update,
+            session.user_id,
+            &session.peer,
+        );
+    } else {
+        let update = proto::UpdateChannels {
+            remove_channel_invitations: vec![channel_id.to_proto()],
+            ..Default::default()
+        };
+
+        for connection_id in connection_pool.user_connection_ids(session.user_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    };
+
+    send_notifications(&*connection_pool, &session.peer, notifications);
+
     response.send(proto::Ack {})?;
 
     Ok(())
@@ -2609,19 +2582,35 @@ async fn join_channel(
     session: Session,
 ) -> Result<()> {
     let channel_id = ChannelId::from_proto(request.channel_id);
-    let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
+    join_channel_internal(channel_id, Box::new(response), session).await
+}
+
+trait JoinChannelInternalResponse {
+    fn send(self, result: proto::JoinRoomResponse) -> Result<()>;
+}
+impl JoinChannelInternalResponse for Response<proto::JoinChannel> {
+    fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
+        Response::<proto::JoinChannel>::send(self, result)
+    }
+}
+impl JoinChannelInternalResponse for Response<proto::JoinRoom> {
+    fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
+        Response::<proto::JoinRoom>::send(self, result)
+    }
+}
 
+async fn join_channel_internal(
+    channel_id: ChannelId,
+    response: Box<impl JoinChannelInternalResponse>,
+    session: Session,
+) -> Result<()> {
     let joined_room = {
         leave_room_for_session(&session).await?;
         let db = session.db().await;
 
-        let room_id = db
-            .get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME)
-            .await?;
-
-        let joined_room = db
-            .join_room(
-                room_id,
+        let (joined_room, membership_updated, role) = db
+            .join_channel(
+                channel_id,
                 session.user_id,
                 session.connection_id,
                 RELEASE_CHANNEL_NAME.as_str(),
@@ -2629,16 +2618,32 @@ async fn join_channel(
             .await?;
 
         let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
-            let token = live_kit
-                .room_token(
-                    &joined_room.room.live_kit_room,
-                    &session.user_id.to_string(),
+            let (can_publish, token) = if role == ChannelRole::Guest {
+                (
+                    false,
+                    live_kit
+                        .guest_token(
+                            &joined_room.room.live_kit_room,
+                            &session.user_id.to_string(),
+                        )
+                        .trace_err()?,
                 )
-                .trace_err()?;
+            } else {
+                (
+                    true,
+                    live_kit
+                        .room_token(
+                            &joined_room.room.live_kit_room,
+                            &session.user_id.to_string(),
+                        )
+                        .trace_err()?,
+                )
+            };
 
             Some(LiveKitConnectionInfo {
                 server_url: live_kit.url().into(),
                 token,
+                can_publish,
             })
         });
 
@@ -2648,9 +2653,19 @@ async fn join_channel(
             live_kit_connection_info,
         })?;
 
+        let connection_pool = session.connection_pool().await;
+        if let Some(membership_updated) = membership_updated {
+            notify_membership_updated(
+                &connection_pool,
+                membership_updated,
+                session.user_id,
+                &session.peer,
+            );
+        }
+
         room_updated(&joined_room.room, &session.peer);
 
-        joined_room.into_inner()
+        joined_room
     };
 
     channel_updated(
@@ -2662,7 +2677,6 @@ async fn join_channel(
     );
 
     update_user_contacts(session.user_id, &session).await?;
-
     Ok(())
 }
 
@@ -2815,6 +2829,29 @@ fn channel_buffer_updated<T: EnvelopedMessage>(
     });
 }
 
+fn send_notifications(
+    connection_pool: &ConnectionPool,
+    peer: &Peer,
+    notifications: db::NotificationBatch,
+) {
+    for (user_id, notification) in notifications {
+        for connection_id in connection_pool.user_connection_ids(user_id) {
+            if let Err(error) = peer.send(
+                connection_id,
+                proto::AddNotification {
+                    notification: Some(notification.clone()),
+                },
+            ) {
+                tracing::error!(
+                    "failed to send notification to {:?} {}",
+                    connection_id,
+                    error
+                );
+            }
+        }
+    }
+}
+
 async fn send_channel_message(
     request: proto::SendChannelMessage,
     response: Response<proto::SendChannelMessage>,
@@ -2829,19 +2866,27 @@ async fn send_channel_message(
         return Err(anyhow!("message can't be blank"))?;
     }
 
+    // TODO: adjust mentions if body is trimmed
+
     let timestamp = OffsetDateTime::now_utc();
     let nonce = request
         .nonce
         .ok_or_else(|| anyhow!("nonce can't be blank"))?;
 
     let channel_id = ChannelId::from_proto(request.channel_id);
-    let (message_id, connection_ids, non_participants) = session
+    let CreatedChannelMessage {
+        message_id,
+        participant_connection_ids,
+        channel_members,
+        notifications,
+    } = session
         .db()
         .await
         .create_channel_message(
             channel_id,
             session.user_id,
             &body,
+            &request.mentions,
             timestamp,
             nonce.clone().into(),
         )
@@ -2850,18 +2895,23 @@ async fn send_channel_message(
         sender_id: session.user_id.to_proto(),
         id: message_id.to_proto(),
         body,
+        mentions: request.mentions,
         timestamp: timestamp.unix_timestamp() as u64,
         nonce: Some(nonce),
     };
-    broadcast(Some(session.connection_id), connection_ids, |connection| {
-        session.peer.send(
-            connection,
-            proto::ChannelMessageSent {
-                channel_id: channel_id.to_proto(),
-                message: Some(message.clone()),
-            },
-        )
-    });
+    broadcast(
+        Some(session.connection_id),
+        participant_connection_ids,
+        |connection| {
+            session.peer.send(
+                connection,
+                proto::ChannelMessageSent {
+                    channel_id: channel_id.to_proto(),
+                    message: Some(message.clone()),
+                },
+            )
+        },
+    );
     response.send(proto::SendChannelMessageResponse {
         message: Some(message),
     })?;
@@ -2869,7 +2919,7 @@ async fn send_channel_message(
     let pool = &*session.connection_pool().await;
     broadcast(
         None,
-        non_participants
+        channel_members
             .iter()
             .flat_map(|user_id| pool.user_connection_ids(*user_id)),
         |peer_id| {
@@ -2885,6 +2935,7 @@ async fn send_channel_message(
             )
         },
     );
+    send_notifications(pool, &session.peer, notifications);
 
     Ok(())
 }

crates/collab/src/tests.rs 🔗

@@ -6,6 +6,7 @@ mod channel_message_tests;
 mod channel_tests;
 mod following_tests;
 mod integration_tests;
+mod notification_tests;
 mod random_channel_buffer_tests;
 mod random_project_collaboration_tests;
 mod randomized_test_helpers;
@@ -39,3 +40,7 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
         RoomParticipants { remote, pending }
     })
 }
+
+fn channel_id(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> Option<u64> {
+    cx.read(|cx| room.read(cx).channel_id())
+}

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

@@ -3,7 +3,7 @@ use crate::{
     tests::TestServer,
 };
 use call::ActiveCall;
-use channel::{Channel, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
+use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL;
 use client::ParticipantIndex;
 use client::{Collaborator, UserId};
 use collab_ui::channel_view::ChannelView;
@@ -407,11 +407,8 @@ async fn test_channel_buffer_disconnect(
     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(channel_id, "the-channel")
-        );
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        assert_eq!(buffer.channel(cx).unwrap().name, "the-channel");
         assert!(!buffer.is_connected());
     });
 
@@ -432,24 +429,12 @@ async fn test_channel_buffer_disconnect(
     deterministic.run_until_parked();
 
     // Channel buffer observed the deletion
-    channel_buffer_b.update(cx_b, |buffer, _| {
-        assert_eq!(
-            buffer.channel().as_ref(),
-            &channel(channel_id, "the-channel")
-        );
+    channel_buffer_b.update(cx_b, |buffer, cx| {
+        assert!(buffer.channel(cx).is_none());
         assert!(!buffer.is_connected());
     });
 }
 
-fn channel(id: u64, name: &'static str) -> Channel {
-    Channel {
-        id,
-        name: name.to_string(),
-        unseen_note_version: None,
-        unseen_message_id: None,
-    }
-}
-
 #[gpui::test]
 async fn test_rejoin_channel_buffer(
     deterministic: Arc<Deterministic>,
@@ -694,7 +679,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
         .await
         .unwrap();
     channel_view_1_a.update(cx_a, |notes, cx| {
-        assert_eq!(notes.channel(cx).name, "channel-1");
+        assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
         notes.editor.update(cx, |editor, cx| {
             editor.insert("Hello from A.", cx);
             editor.change_selections(None, cx, |selections| {
@@ -726,7 +711,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
             .expect("active item is not a channel view")
     });
     channel_view_1_b.read_with(cx_b, |notes, cx| {
-        assert_eq!(notes.channel(cx).name, "channel-1");
+        assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
         let editor = notes.editor.read(cx);
         assert_eq!(editor.text(cx), "Hello from A.");
         assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
@@ -738,7 +723,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
         .await
         .unwrap();
     channel_view_2_a.read_with(cx_a, |notes, cx| {
-        assert_eq!(notes.channel(cx).name, "channel-2");
+        assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
     });
 
     // Client B is taken to the notes for channel 2.
@@ -755,7 +740,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
             .expect("active item is not a channel view")
     });
     channel_view_2_b.read_with(cx_b, |notes, cx| {
-        assert_eq!(notes.channel(cx).name, "channel-2");
+        assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
     });
 }
 

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

@@ -1,27 +1,30 @@
 use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
-use channel::{ChannelChat, ChannelMessageId};
+use channel::{ChannelChat, ChannelMessageId, MessageParams};
 use collab_ui::chat_panel::ChatPanel;
 use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext};
+use rpc::Notification;
 use std::sync::Arc;
 use workspace::dock::Panel;
 
 #[gpui::test]
 async fn test_basic_channel_messages(
     deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
+    mut cx_a: &mut TestAppContext,
+    mut cx_b: &mut TestAppContext,
+    mut 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(
             "the-channel",
             None,
             (&client_a, cx_a),
-            &mut [(&client_b, cx_b)],
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
         )
         .await;
 
@@ -36,8 +39,17 @@ async fn test_basic_channel_messages(
         .await
         .unwrap();
 
-    channel_chat_a
-        .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
+    let message_id = channel_chat_a
+        .update(cx_a, |c, cx| {
+            c.send_message(
+                MessageParams {
+                    text: "hi @user_c!".into(),
+                    mentions: vec![(3..10, client_c.id())],
+                },
+                cx,
+            )
+            .unwrap()
+        })
         .await
         .unwrap();
     channel_chat_a
@@ -52,15 +64,55 @@ async fn test_basic_channel_messages(
         .unwrap();
 
     deterministic.run_until_parked();
-    channel_chat_a.update(cx_a, |c, _| {
+
+    let channel_chat_c = client_c
+        .channel_store()
+        .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+
+    for (chat, cx) in [
+        (&channel_chat_a, &mut cx_a),
+        (&channel_chat_b, &mut cx_b),
+        (&channel_chat_c, &mut cx_c),
+    ] {
+        chat.update(*cx, |c, _| {
+            assert_eq!(
+                c.messages()
+                    .iter()
+                    .map(|m| (m.body.as_str(), m.mentions.as_slice()))
+                    .collect::<Vec<_>>(),
+                vec![
+                    ("hi @user_c!", [(3..10, client_c.id())].as_slice()),
+                    ("two", &[]),
+                    ("three", &[])
+                ],
+                "results for user {}",
+                c.client().id(),
+            );
+        });
+    }
+
+    client_c.notification_store().update(cx_c, |store, _| {
+        assert_eq!(store.notification_count(), 2);
+        assert_eq!(store.unread_notification_count(), 1);
         assert_eq!(
-            c.messages()
-                .iter()
-                .map(|m| m.body.as_str())
-                .collect::<Vec<_>>(),
-            vec!["one", "two", "three"]
+            store.notification_at(0).unwrap().notification,
+            Notification::ChannelMessageMention {
+                message_id,
+                sender_id: client_a.id(),
+                channel_id,
+            }
         );
-    })
+        assert_eq!(
+            store.notification_at(1).unwrap().notification,
+            Notification::ChannelInvitation {
+                channel_id,
+                channel_name: "the-channel".to_string(),
+                inviter_id: client_a.id()
+            }
+        );
+    });
 }
 
 #[gpui::test]
@@ -280,7 +332,7 @@ async fn test_channel_message_changes(
     chat_panel_b
         .update(cx_b, |chat_panel, cx| {
             chat_panel.set_active(true, cx);
-            chat_panel.select_channel(channel_id, cx)
+            chat_panel.select_channel(channel_id, None, cx)
         })
         .await
         .unwrap();

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

@@ -1,12 +1,17 @@
 use crate::{
+    db::{self, UserId},
     rpc::RECONNECT_TIMEOUT,
     tests::{room_participants, RoomParticipants, TestServer},
 };
 use call::ActiveCall;
 use channel::{ChannelId, ChannelMembership, ChannelStore};
 use client::User;
+use futures::future::try_join_all;
 use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
-use rpc::{proto, RECEIVE_TIMEOUT};
+use rpc::{
+    proto::{self, ChannelRole},
+    RECEIVE_TIMEOUT,
+};
 use std::sync::Arc;
 
 #[gpui::test]
@@ -44,22 +49,19 @@ async fn test_core_channels(
                 id: channel_a_id,
                 name: "channel-a".to_string(),
                 depth: 0,
-                user_is_admin: true,
+                role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 id: channel_b_id,
                 name: "channel-b".to_string(),
                 depth: 1,
-                user_is_admin: true,
+                role: ChannelRole::Admin,
             },
         ],
     );
 
     client_b.channel_store().read_with(cx_b, |channels, _| {
-        assert!(channels
-            .channel_dag_entries()
-            .collect::<Vec<_>>()
-            .is_empty())
+        assert!(channels.ordered_channels().collect::<Vec<_>>().is_empty())
     });
 
     // Invite client B to channel A as client A.
@@ -68,7 +70,12 @@ async fn test_core_channels(
         .update(cx_a, |store, cx| {
             assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
 
-            let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
+            let invite = store.invite_member(
+                channel_a_id,
+                client_b.user_id().unwrap(),
+                proto::ChannelRole::Member,
+                cx,
+            );
 
             // Make sure we're synchronously storing the pending invite
             assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
@@ -86,7 +93,7 @@ async fn test_core_channels(
             id: channel_a_id,
             name: "channel-a".to_string(),
             depth: 0,
-            user_is_admin: false,
+            role: ChannelRole::Member,
         }],
     );
 
@@ -103,12 +110,12 @@ async fn test_core_channels(
         &[
             (
                 client_a.user_id().unwrap(),
-                true,
+                proto::ChannelRole::Admin,
                 proto::channel_member::Kind::Member,
             ),
             (
                 client_b.user_id().unwrap(),
-                false,
+                proto::ChannelRole::Member,
                 proto::channel_member::Kind::Invitee,
             ),
         ],
@@ -117,8 +124,8 @@ async fn test_core_channels(
     // Client B accepts the invitation.
     client_b
         .channel_store()
-        .update(cx_b, |channels, _| {
-            channels.respond_to_channel_invite(channel_a_id, true)
+        .update(cx_b, |channels, cx| {
+            channels.respond_to_channel_invite(channel_a_id, true, cx)
         })
         .await
         .unwrap();
@@ -133,13 +140,13 @@ async fn test_core_channels(
             ExpectedChannel {
                 id: channel_a_id,
                 name: "channel-a".to_string(),
-                user_is_admin: false,
+                role: ChannelRole::Member,
                 depth: 0,
             },
             ExpectedChannel {
                 id: channel_b_id,
                 name: "channel-b".to_string(),
-                user_is_admin: false,
+                role: ChannelRole::Member,
                 depth: 1,
             },
         ],
@@ -161,19 +168,19 @@ async fn test_core_channels(
             ExpectedChannel {
                 id: channel_a_id,
                 name: "channel-a".to_string(),
-                user_is_admin: false,
+                role: ChannelRole::Member,
                 depth: 0,
             },
             ExpectedChannel {
                 id: channel_b_id,
                 name: "channel-b".to_string(),
-                user_is_admin: false,
+                role: ChannelRole::Member,
                 depth: 1,
             },
             ExpectedChannel {
                 id: channel_c_id,
                 name: "channel-c".to_string(),
-                user_is_admin: false,
+                role: ChannelRole::Member,
                 depth: 2,
             },
         ],
@@ -183,7 +190,12 @@ async fn test_core_channels(
     client_a
         .channel_store()
         .update(cx_a, |store, cx| {
-            store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx)
+            store.set_member_role(
+                channel_a_id,
+                client_b.user_id().unwrap(),
+                proto::ChannelRole::Admin,
+                cx,
+            )
         })
         .await
         .unwrap();
@@ -200,19 +212,19 @@ async fn test_core_channels(
                 id: channel_a_id,
                 name: "channel-a".to_string(),
                 depth: 0,
-                user_is_admin: true,
+                role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 id: channel_b_id,
                 name: "channel-b".to_string(),
                 depth: 1,
-                user_is_admin: true,
+                role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 id: channel_c_id,
                 name: "channel-c".to_string(),
                 depth: 2,
-                user_is_admin: true,
+                role: ChannelRole::Admin,
             },
         ],
     );
@@ -234,7 +246,7 @@ async fn test_core_channels(
             id: channel_a_id,
             name: "channel-a".to_string(),
             depth: 0,
-            user_is_admin: true,
+            role: ChannelRole::Admin,
         }],
     );
     assert_channels(
@@ -244,7 +256,7 @@ async fn test_core_channels(
             id: channel_a_id,
             name: "channel-a".to_string(),
             depth: 0,
-            user_is_admin: true,
+            role: ChannelRole::Admin,
         }],
     );
 
@@ -267,18 +279,27 @@ async fn test_core_channels(
             id: channel_a_id,
             name: "channel-a".to_string(),
             depth: 0,
-            user_is_admin: true,
+            role: ChannelRole::Admin,
         }],
     );
 
     // Client B no longer has access to the channel
     assert_channels(client_b.channel_store(), cx_b, &[]);
 
-    // When disconnected, client A sees no channels.
     server.forbid_connections();
     server.disconnect_client(client_a.peer_id().unwrap());
     deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-    assert_channels(client_a.channel_store(), cx_a, &[]);
+
+    server
+        .app_state
+        .db
+        .rename_channel(
+            db::ChannelId::from_proto(channel_a_id),
+            UserId::from_proto(client_a.id()),
+            "channel-a-renamed",
+        )
+        .await
+        .unwrap();
 
     server.allow_connections();
     deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
@@ -287,9 +308,9 @@ async fn test_core_channels(
         cx_a,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a".to_string(),
+            name: "channel-a-renamed".to_string(),
             depth: 0,
-            user_is_admin: true,
+            role: ChannelRole::Admin,
         }],
     );
 }
@@ -305,12 +326,12 @@ fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u
 #[track_caller]
 fn assert_members_eq(
     members: &[ChannelMembership],
-    expected_members: &[(u64, bool, proto::channel_member::Kind)],
+    expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)],
 ) {
     assert_eq!(
         members
             .iter()
-            .map(|member| (member.user.id, member.admin, member.kind))
+            .map(|member| (member.user.id, member.role, member.kind))
             .collect::<Vec<_>>(),
         expected_members
     );
@@ -397,7 +418,7 @@ async fn test_channel_room(
             id: zed_id,
             name: "zed".to_string(),
             depth: 0,
-            user_is_admin: false,
+            role: ChannelRole::Member,
         }],
     );
     client_b.channel_store().read_with(cx_b, |channels, _| {
@@ -611,7 +632,12 @@ async fn test_permissions_update_while_invited(
     client_a
         .channel_store()
         .update(cx_a, |channel_store, cx| {
-            channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx)
+            channel_store.invite_member(
+                rust_id,
+                client_b.user_id().unwrap(),
+                proto::ChannelRole::Member,
+                cx,
+            )
         })
         .await
         .unwrap();
@@ -625,7 +651,7 @@ async fn test_permissions_update_while_invited(
             depth: 0,
             id: rust_id,
             name: "rust".to_string(),
-            user_is_admin: false,
+            role: ChannelRole::Member,
         }],
     );
     assert_channels(client_b.channel_store(), cx_b, &[]);
@@ -634,7 +660,12 @@ async fn test_permissions_update_while_invited(
     client_a
         .channel_store()
         .update(cx_a, |channel_store, cx| {
-            channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx)
+            channel_store.set_member_role(
+                rust_id,
+                client_b.user_id().unwrap(),
+                proto::ChannelRole::Admin,
+                cx,
+            )
         })
         .await
         .unwrap();
@@ -648,7 +679,7 @@ async fn test_permissions_update_while_invited(
             depth: 0,
             id: rust_id,
             name: "rust".to_string(),
-            user_is_admin: false,
+            role: ChannelRole::Member,
         }],
     );
     assert_channels(client_b.channel_store(), cx_b, &[]);
@@ -688,7 +719,7 @@ async fn test_channel_rename(
             depth: 0,
             id: rust_id,
             name: "rust-archive".to_string(),
-            user_is_admin: true,
+            role: ChannelRole::Admin,
         }],
     );
 
@@ -700,7 +731,7 @@ async fn test_channel_rename(
             depth: 0,
             id: rust_id,
             name: "rust-archive".to_string(),
-            user_is_admin: false,
+            role: ChannelRole::Member,
         }],
     );
 }
@@ -803,7 +834,12 @@ async fn test_lost_channel_creation(
     client_a
         .channel_store()
         .update(cx_a, |channel_store, cx| {
-            channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx)
+            channel_store.invite_member(
+                channel_id,
+                client_b.user_id().unwrap(),
+                proto::ChannelRole::Member,
+                cx,
+            )
         })
         .await
         .unwrap();
@@ -818,7 +854,7 @@ async fn test_lost_channel_creation(
             depth: 0,
             id: channel_id,
             name: "x".to_string(),
-            user_is_admin: false,
+            role: ChannelRole::Member,
         }],
     );
 
@@ -842,13 +878,13 @@ async fn test_lost_channel_creation(
                 depth: 0,
                 id: channel_id,
                 name: "x".to_string(),
-                user_is_admin: true,
+                role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 depth: 1,
                 id: subchannel_id,
                 name: "subchannel".to_string(),
-                user_is_admin: true,
+                role: ChannelRole::Admin,
             },
         ],
     );
@@ -856,8 +892,8 @@ async fn test_lost_channel_creation(
     // Client B accepts the invite
     client_b
         .channel_store()
-        .update(cx_b, |channel_store, _| {
-            channel_store.respond_to_channel_invite(channel_id, true)
+        .update(cx_b, |channel_store, cx| {
+            channel_store.respond_to_channel_invite(channel_id, true, cx)
         })
         .await
         .unwrap();
@@ -873,234 +909,507 @@ async fn test_lost_channel_creation(
                 depth: 0,
                 id: channel_id,
                 name: "x".to_string(),
-                user_is_admin: false,
+                role: ChannelRole::Member,
             },
             ExpectedChannel {
                 depth: 1,
                 id: subchannel_id,
                 name: "subchannel".to_string(),
-                user_is_admin: false,
+                role: ChannelRole::Member,
             },
         ],
     );
 }
 
 #[gpui::test]
-async fn test_channel_moving(
+async fn test_channel_link_notifications(
     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 user_b = client_b.user_id().unwrap();
+    let user_c = client_c.user_id().unwrap();
+
     let channels = server
-        .make_channel_tree(
-            &[
-                ("channel-a", None),
-                ("channel-b", Some("channel-a")),
-                ("channel-c", Some("channel-b")),
-                ("channel-d", Some("channel-c")),
-            ],
-            (&client_a, cx_a),
-        )
+        .make_channel_tree(&[("zed", None)], (&client_a, cx_a))
         .await;
-    let channel_a_id = channels[0];
-    let channel_b_id = channels[1];
-    let channel_c_id = channels[2];
-    let channel_d_id = channels[3];
+    let zed_channel = channels[0];
+
+    try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| {
+        [
+            channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx),
+            channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Member, cx),
+            channel_store.invite_member(zed_channel, user_c, proto::ChannelRole::Guest, cx),
+        ]
+    }))
+    .await
+    .unwrap();
 
-    // Current shape:
-    // a - b - c - d
+    deterministic.run_until_parked();
+
+    client_b
+        .channel_store()
+        .update(cx_b, |channel_store, cx| {
+            channel_store.respond_to_channel_invite(zed_channel, true, cx)
+        })
+        .await
+        .unwrap();
+
+    client_c
+        .channel_store()
+        .update(cx_c, |channel_store, cx| {
+            channel_store.respond_to_channel_invite(zed_channel, true, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // we have an admin (a), member (b) and guest (c) all part of the zed channel.
+
+    // create a new private channel, make it public, and move it under the previous one, and verify it shows for b and not c
+    let active_channel = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("active", Some(zed_channel), cx)
+        })
+        .await
+        .unwrap();
+
+    // the new channel shows for b and not c
     assert_channels_list_shape(
         client_a.channel_store(),
         cx_a,
-        &[
-            (channel_a_id, 0),
-            (channel_b_id, 1),
-            (channel_c_id, 2),
-            (channel_d_id, 3),
-        ],
+        &[(zed_channel, 0), (active_channel, 1)],
     );
+    assert_channels_list_shape(
+        client_b.channel_store(),
+        cx_b,
+        &[(zed_channel, 0), (active_channel, 1)],
+    );
+    assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]);
+
+    let vim_channel = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("vim", None, cx)
+        })
+        .await
+        .unwrap();
 
     client_a
         .channel_store()
         .update(cx_a, |channel_store, cx| {
-            channel_store.move_channel(channel_d_id, channel_c_id, channel_b_id, cx)
+            channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx)
         })
         .await
         .unwrap();
 
-    // Current shape:
-    //       /- d
-    // a - b -- c
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.move_channel(vim_channel, Some(active_channel), cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // the new channel shows for b and c
     assert_channels_list_shape(
         client_a.channel_store(),
         cx_a,
-        &[
-            (channel_a_id, 0),
-            (channel_b_id, 1),
-            (channel_c_id, 2),
-            (channel_d_id, 2),
-        ],
+        &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
+    );
+    assert_channels_list_shape(
+        client_b.channel_store(),
+        cx_b,
+        &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
     );
+    assert_channels_list_shape(
+        client_c.channel_store(),
+        cx_c,
+        &[(zed_channel, 0), (vim_channel, 1)],
+    );
+
+    let helix_channel = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("helix", None, cx)
+        })
+        .await
+        .unwrap();
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.move_channel(helix_channel, Some(vim_channel), cx)
+        })
+        .await
+        .unwrap();
 
     client_a
         .channel_store()
         .update(cx_a, |channel_store, cx| {
-            channel_store.link_channel(channel_d_id, channel_c_id, cx)
+            channel_store.set_channel_visibility(
+                helix_channel,
+                proto::ChannelVisibility::Public,
+                cx,
+            )
         })
         .await
         .unwrap();
 
-    // Current shape for A:
-    //      /------\
-    // a - b -- c -- d
+    // the new channel shows for b and c
     assert_channels_list_shape(
-        client_a.channel_store(),
-        cx_a,
+        client_b.channel_store(),
+        cx_b,
         &[
-            (channel_a_id, 0),
-            (channel_b_id, 1),
-            (channel_c_id, 2),
-            (channel_d_id, 3),
-            (channel_d_id, 2),
+            (zed_channel, 0),
+            (active_channel, 1),
+            (vim_channel, 2),
+            (helix_channel, 3),
         ],
     );
-
-    let b_channels = server
-        .make_channel_tree(
-            &[
-                ("channel-mu", None),
-                ("channel-gamma", Some("channel-mu")),
-                ("channel-epsilon", Some("channel-mu")),
-            ],
-            (&client_b, cx_b),
-        )
-        .await;
-    let channel_mu_id = b_channels[0];
-    let channel_ga_id = b_channels[1];
-    let channel_ep_id = b_channels[2];
-
-    // Current shape for B:
-    //    /- ep
-    // mu -- ga
     assert_channels_list_shape(
-        client_b.channel_store(),
-        cx_b,
-        &[(channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1)],
+        client_c.channel_store(),
+        cx_c,
+        &[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)],
     );
 
     client_a
-        .add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a)
-        .await;
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Members, cx)
+        })
+        .await
+        .unwrap();
 
-    // Current shape for B:
-    //    /- ep
-    // mu -- ga
-    //  /---------\
-    // b  -- c  -- d
+    // the members-only channel is still shown for c, but hidden for b
     assert_channels_list_shape(
         client_b.channel_store(),
         cx_b,
         &[
-            // New channels from a
-            (channel_b_id, 0),
-            (channel_c_id, 1),
-            (channel_d_id, 2),
-            (channel_d_id, 1),
-            // B's old channels
-            (channel_mu_id, 0),
-            (channel_ep_id, 1),
-            (channel_ga_id, 1),
+            (zed_channel, 0),
+            (active_channel, 1),
+            (vim_channel, 2),
+            (helix_channel, 3),
         ],
     );
-
     client_b
-        .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b)
+        .channel_store()
+        .read_with(cx_b, |channel_store, _| {
+            assert_eq!(
+                channel_store
+                    .channel_for_id(vim_channel)
+                    .unwrap()
+                    .visibility,
+                proto::ChannelVisibility::Members
+            )
+        });
+
+    assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]);
+}
+
+#[gpui::test]
+async fn test_channel_membership_notifications(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+
+    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_c").await;
+
+    let user_b = client_b.user_id().unwrap();
+
+    let channels = server
+        .make_channel_tree(
+            &[
+                ("zed", None),
+                ("active", Some("zed")),
+                ("vim", Some("active")),
+            ],
+            (&client_a, cx_a),
+        )
         .await;
+    let zed_channel = channels[0];
+    let _active_channel = channels[1];
+    let vim_channel = channels[2];
+
+    try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| {
+        [
+            channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx),
+            channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx),
+            channel_store.invite_member(vim_channel, user_b, proto::ChannelRole::Member, cx),
+            channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Guest, cx),
+        ]
+    }))
+    .await
+    .unwrap();
 
-    // Current shape for C:
-    // - ep
-    assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]);
+    deterministic.run_until_parked();
 
     client_b
         .channel_store()
         .update(cx_b, |channel_store, cx| {
-            channel_store.link_channel(channel_b_id, channel_ep_id, cx)
+            channel_store.respond_to_channel_invite(zed_channel, true, cx)
         })
         .await
         .unwrap();
 
-    // Current shape for B:
-    //              /---------\
-    //    /- ep -- b  -- c  -- d
-    // mu -- ga
-    assert_channels_list_shape(
+    client_b
+        .channel_store()
+        .update(cx_b, |channel_store, cx| {
+            channel_store.respond_to_channel_invite(vim_channel, true, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // we have an admin (a), and a guest (b) with access to all of zed, and membership in vim.
+    assert_channels(
         client_b.channel_store(),
         cx_b,
         &[
-            (channel_mu_id, 0),
-            (channel_ep_id, 1),
-            (channel_b_id, 2),
-            (channel_c_id, 3),
-            (channel_d_id, 4),
-            (channel_d_id, 3),
-            (channel_ga_id, 1),
+            ExpectedChannel {
+                depth: 0,
+                id: zed_channel,
+                name: "zed".to_string(),
+                role: ChannelRole::Guest,
+            },
+            ExpectedChannel {
+                depth: 1,
+                id: vim_channel,
+                name: "vim".to_string(),
+                role: ChannelRole::Member,
+            },
         ],
     );
 
-    // Current shape for C:
-    //        /---------\
-    // ep -- b  -- c  -- d
-    assert_channels_list_shape(
-        client_c.channel_store(),
-        cx_c,
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.remove_member(vim_channel, user_b, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    assert_channels(
+        client_b.channel_store(),
+        cx_b,
         &[
-            (channel_ep_id, 0),
-            (channel_b_id, 1),
-            (channel_c_id, 2),
-            (channel_d_id, 3),
-            (channel_d_id, 2),
+            ExpectedChannel {
+                depth: 0,
+                id: zed_channel,
+                name: "zed".to_string(),
+                role: ChannelRole::Guest,
+            },
+            ExpectedChannel {
+                depth: 1,
+                id: vim_channel,
+                name: "vim".to_string(),
+                role: ChannelRole::Guest,
+            },
         ],
-    );
+    )
+}
 
-    client_b
+#[gpui::test]
+async fn test_guest_access(
+    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 channels = server
+        .make_channel_tree(
+            &[("channel-a", None), ("channel-b", Some("channel-a"))],
+            (&client_a, cx_a),
+        )
+        .await;
+    let channel_a = channels[0];
+    let channel_b = channels[1];
+
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    // Non-members should not be allowed to join
+    assert!(active_call_b
+        .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
+        .await
+        .is_err());
+
+    // Make channels A and B public
+    client_a
         .channel_store()
-        .update(cx_b, |channel_store, cx| {
-            channel_store.link_channel(channel_ga_id, channel_b_id, cx)
+        .update(cx_a, |channel_store, cx| {
+            channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Public, cx)
         })
         .await
         .unwrap();
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.set_channel_visibility(channel_b, proto::ChannelVisibility::Public, cx)
+        })
+        .await
+        .unwrap();
+
+    // Client B joins channel A as a guest
+    active_call_b
+        .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
+        .await
+        .unwrap();
 
-    // Current shape for B:
-    //              /---------\
-    //    /- ep -- b  -- c  -- d
-    //   /          \
-    // mu ---------- ga
+    deterministic.run_until_parked();
+    assert_channels_list_shape(
+        client_a.channel_store(),
+        cx_a,
+        &[(channel_a, 0), (channel_b, 1)],
+    );
     assert_channels_list_shape(
         client_b.channel_store(),
         cx_b,
-        &[
-            (channel_mu_id, 0),
-            (channel_ep_id, 1),
-            (channel_b_id, 2),
-            (channel_c_id, 3),
-            (channel_d_id, 4),
-            (channel_d_id, 3),
-            (channel_ga_id, 3),
-            (channel_ga_id, 1),
-        ],
+        &[(channel_a, 0), (channel_b, 1)],
     );
 
-    // Current shape for A:
-    //      /------\
-    // a - b -- c -- d
-    //      \-- ga
+    client_a.channel_store().update(cx_a, |channel_store, _| {
+        let participants = channel_store.channel_participants(channel_a);
+        assert_eq!(participants.len(), 1);
+        assert_eq!(participants[0].id, client_b.user_id().unwrap());
+    });
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Members, cx)
+        })
+        .await
+        .unwrap();
+
+    assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
+
+    active_call_b
+        .update(cx_b, |call, cx| call.join_channel(channel_b, cx))
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    assert_channels_list_shape(client_b.channel_store(), cx_b, &[(channel_b, 0)]);
+}
+
+#[gpui::test]
+async fn test_invite_access(
+    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 channels = server
+        .make_channel_tree(
+            &[("channel-a", None), ("channel-b", Some("channel-a"))],
+            (&client_a, cx_a),
+        )
+        .await;
+    let channel_a_id = channels[0];
+    let channel_b_id = channels[0];
+
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    // should not be allowed to join
+    assert!(active_call_b
+        .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
+        .await
+        .is_err());
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.invite_member(
+                channel_a_id,
+                client_b.user_id().unwrap(),
+                ChannelRole::Member,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    active_call_b
+        .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    client_b.channel_store().update(cx_b, |channel_store, _| {
+        assert!(channel_store.channel_for_id(channel_b_id).is_some());
+        assert!(channel_store.channel_for_id(channel_a_id).is_some());
+    });
+
+    client_a.channel_store().update(cx_a, |channel_store, _| {
+        let participants = channel_store.channel_participants(channel_b_id);
+        assert_eq!(participants.len(), 1);
+        assert_eq!(participants[0].id, client_b.user_id().unwrap());
+    })
+}
+
+#[gpui::test]
+async fn test_channel_moving(
+    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 channels = server
+        .make_channel_tree(
+            &[
+                ("channel-a", None),
+                ("channel-b", Some("channel-a")),
+                ("channel-c", Some("channel-b")),
+                ("channel-d", Some("channel-c")),
+            ],
+            (&client_a, cx_a),
+        )
+        .await;
+    let channel_a_id = channels[0];
+    let channel_b_id = channels[1];
+    let channel_c_id = channels[2];
+    let channel_d_id = channels[3];
+
+    // Current shape:
+    // a - b - c - d
     assert_channels_list_shape(
         client_a.channel_store(),
         cx_a,

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

@@ -1,6 +1,6 @@
 use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
 use call::ActiveCall;
-use collab_ui::project_shared_notification::ProjectSharedNotification;
+use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
 use editor::{Editor, ExcerptRange, MultiBuffer};
 use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
 use live_kit_client::MacOSDisplay;

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

@@ -1,6 +1,6 @@
 use crate::{
     rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
-    tests::{room_participants, RoomParticipants, TestClient, TestServer},
+    tests::{channel_id, room_participants, RoomParticipants, TestClient, TestServer},
 };
 use call::{room, ActiveCall, ParticipantLocation, Room};
 use client::{User, RECEIVE_TIMEOUT};
@@ -15,8 +15,8 @@ use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, Te
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
-    tree_sitter_rust, Anchor, BundledFormatter, Diagnostic, DiagnosticEntry, FakeLspAdapter,
-    Language, LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
+    tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
+    LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
 };
 use live_kit_client::MacOSDisplay;
 use lsp::LanguageServerId;
@@ -469,6 +469,119 @@ async fn test_calling_multiple_users_simultaneously(
     );
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_joining_channels_and_calling_multiple_users_simultaneously(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+    server
+        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
+        .await;
+
+    let channel_1 = server
+        .make_channel(
+            "channel1",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+
+    let channel_2 = server
+        .make_channel(
+            "channel2",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+
+    let active_call_a = cx_a.read(ActiveCall::global);
+
+    // Simultaneously join channel 1 and then channel 2
+    active_call_a
+        .update(cx_a, |call, cx| call.join_channel(channel_1, cx))
+        .detach();
+    let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
+
+    join_channel_2.await.unwrap();
+
+    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
+    deterministic.run_until_parked();
+
+    assert_eq!(channel_id(&room_a, cx_a), Some(channel_2));
+
+    // Leave the room
+    active_call_a
+        .update(cx_a, |call, cx| {
+            let hang_up = call.hang_up(cx);
+            hang_up
+        })
+        .await
+        .unwrap();
+
+    // Initiating invites and then joining a channel should fail gracefully
+    let b_invite = active_call_a.update(cx_a, |call, cx| {
+        call.invite(client_b.user_id().unwrap(), None, cx)
+    });
+    let c_invite = active_call_a.update(cx_a, |call, cx| {
+        call.invite(client_c.user_id().unwrap(), None, cx)
+    });
+
+    let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
+
+    b_invite.await.unwrap();
+    c_invite.await.unwrap();
+    join_channel.await.unwrap();
+
+    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
+    deterministic.run_until_parked();
+
+    assert_eq!(
+        room_participants(&room_a, cx_a),
+        RoomParticipants {
+            remote: Default::default(),
+            pending: vec!["user_b".to_string(), "user_c".to_string()]
+        }
+    );
+
+    assert_eq!(channel_id(&room_a, cx_a), None);
+
+    // Leave the room
+    active_call_a
+        .update(cx_a, |call, cx| {
+            let hang_up = call.hang_up(cx);
+            hang_up
+        })
+        .await
+        .unwrap();
+
+    // Simultaneously join channel 1 and call user B and user C from client A.
+    let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
+
+    let b_invite = active_call_a.update(cx_a, |call, cx| {
+        call.invite(client_b.user_id().unwrap(), None, cx)
+    });
+    let c_invite = active_call_a.update(cx_a, |call, cx| {
+        call.invite(client_c.user_id().unwrap(), None, cx)
+    });
+
+    join_channel.await.unwrap();
+    b_invite.await.unwrap();
+    c_invite.await.unwrap();
+
+    active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
+    deterministic.run_until_parked();
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_room_uniqueness(
     deterministic: Arc<Deterministic>,
@@ -4530,6 +4643,7 @@ async fn test_prettier_formatting_buffer(
         LanguageConfig {
             name: "Rust".into(),
             path_suffixes: vec!["rs".to_string()],
+            prettier_parser_name: Some("test_parser".to_string()),
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
@@ -4537,10 +4651,7 @@ async fn test_prettier_formatting_buffer(
     let test_plugin = "test_plugin";
     let mut fake_language_servers = language
         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            enabled_formatters: vec![BundledFormatter::Prettier {
-                parser_name: Some("test_parser"),
-                plugin_names: vec![test_plugin],
-            }],
+            prettier_plugins: vec![test_plugin],
             ..Default::default()
         }))
         .await;
@@ -4557,11 +4668,7 @@ async fn test_prettier_formatting_buffer(
         .insert_tree(&directory, json!({ "a.rs": buffer_text }))
         .await;
     let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
-    let prettier_format_suffix = project_a.update(cx_a, |project, _| {
-        let suffix = project.enable_test_prettier(&[test_plugin]);
-        project.languages().add(language);
-        suffix
-    });
+    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
     let buffer_a = cx_a
         .background()
         .spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))

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

@@ -0,0 +1,159 @@
+use crate::tests::TestServer;
+use gpui::{executor::Deterministic, TestAppContext};
+use notifications::NotificationEvent;
+use parking_lot::Mutex;
+use rpc::{proto, Notification};
+use std::sync::Arc;
+
+#[gpui::test]
+async fn test_notifications(
+    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 notification_events_a = Arc::new(Mutex::new(Vec::new()));
+    let notification_events_b = Arc::new(Mutex::new(Vec::new()));
+    client_a.notification_store().update(cx_a, |_, cx| {
+        let events = notification_events_a.clone();
+        cx.subscribe(&cx.handle(), move |_, _, event, _| {
+            events.lock().push(event.clone());
+        })
+        .detach()
+    });
+    client_b.notification_store().update(cx_b, |_, cx| {
+        let events = notification_events_b.clone();
+        cx.subscribe(&cx.handle(), move |_, _, event, _| {
+            events.lock().push(event.clone());
+        })
+        .detach()
+    });
+
+    // Client A sends a contact request to client B.
+    client_a
+        .user_store()
+        .update(cx_a, |store, cx| store.request_contact(client_b.id(), cx))
+        .await
+        .unwrap();
+
+    // Client B receives a contact request notification and responds to the
+    // request, accepting it.
+    deterministic.run_until_parked();
+    client_b.notification_store().update(cx_b, |store, cx| {
+        assert_eq!(store.notification_count(), 1);
+        assert_eq!(store.unread_notification_count(), 1);
+
+        let entry = store.notification_at(0).unwrap();
+        assert_eq!(
+            entry.notification,
+            Notification::ContactRequest {
+                sender_id: client_a.id()
+            }
+        );
+        assert!(!entry.is_read);
+        assert_eq!(
+            &notification_events_b.lock()[0..],
+            &[
+                NotificationEvent::NewNotification {
+                    entry: entry.clone(),
+                },
+                NotificationEvent::NotificationsUpdated {
+                    old_range: 0..0,
+                    new_count: 1
+                }
+            ]
+        );
+
+        store.respond_to_notification(entry.notification.clone(), true, cx);
+    });
+
+    // Client B sees the notification is now read, and that they responded.
+    deterministic.run_until_parked();
+    client_b.notification_store().read_with(cx_b, |store, _| {
+        assert_eq!(store.notification_count(), 1);
+        assert_eq!(store.unread_notification_count(), 0);
+
+        let entry = store.notification_at(0).unwrap();
+        assert!(entry.is_read);
+        assert_eq!(entry.response, Some(true));
+        assert_eq!(
+            &notification_events_b.lock()[2..],
+            &[
+                NotificationEvent::NotificationRead {
+                    entry: entry.clone(),
+                },
+                NotificationEvent::NotificationsUpdated {
+                    old_range: 0..1,
+                    new_count: 1
+                }
+            ]
+        );
+    });
+
+    // Client A receives a notification that client B accepted their request.
+    client_a.notification_store().read_with(cx_a, |store, _| {
+        assert_eq!(store.notification_count(), 1);
+        assert_eq!(store.unread_notification_count(), 1);
+
+        let entry = store.notification_at(0).unwrap();
+        assert_eq!(
+            entry.notification,
+            Notification::ContactRequestAccepted {
+                responder_id: client_b.id()
+            }
+        );
+        assert!(!entry.is_read);
+    });
+
+    // Client A creates a channel and invites client B to be a member.
+    let channel_id = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| {
+            store.create_channel("the-channel", None, cx)
+        })
+        .await
+        .unwrap();
+    client_a
+        .channel_store()
+        .update(cx_a, |store, cx| {
+            store.invite_member(channel_id, client_b.id(), proto::ChannelRole::Member, cx)
+        })
+        .await
+        .unwrap();
+
+    // Client B receives a channel invitation notification and responds to the
+    // invitation, accepting it.
+    deterministic.run_until_parked();
+    client_b.notification_store().update(cx_b, |store, cx| {
+        assert_eq!(store.notification_count(), 2);
+        assert_eq!(store.unread_notification_count(), 1);
+
+        let entry = store.notification_at(0).unwrap();
+        assert_eq!(
+            entry.notification,
+            Notification::ChannelInvitation {
+                channel_id,
+                channel_name: "the-channel".to_string(),
+                inviter_id: client_a.id()
+            }
+        );
+        assert!(!entry.is_read);
+
+        store.respond_to_notification(entry.notification.clone(), true, cx);
+    });
+
+    // Client B sees the notification is now read, and that they responded.
+    deterministic.run_until_parked();
+    client_b.notification_store().read_with(cx_b, |store, _| {
+        assert_eq!(store.notification_count(), 2);
+        assert_eq!(store.unread_notification_count(), 0);
+
+        let entry = store.notification_at(0).unwrap();
+        assert!(entry.is_read);
+        assert_eq!(entry.response, Some(true));
+    });
+}

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

@@ -1,3 +1,5 @@
+use crate::db::ChannelRole;
+
 use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
 use anyhow::Result;
 use async_trait::async_trait;
@@ -46,11 +48,11 @@ impl RandomizedTest for RandomChannelBufferTest {
         let db = &server.app_state.db;
         for ix in 0..CHANNEL_COUNT {
             let id = db
-                .create_channel(&format!("channel-{ix}"), None, users[0].user_id)
+                .create_root_channel(&format!("channel-{ix}"), users[0].user_id)
                 .await
                 .unwrap();
             for user in &users[1..] {
-                db.invite_channel_member(id, user.user_id, users[0].user_id, false)
+                db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member)
                     .await
                     .unwrap();
                 db.respond_to_channel_invite(id, user.user_id, true)
@@ -81,7 +83,7 @@ impl RandomizedTest for RandomChannelBufferTest {
             match rng.gen_range(0..100_u32) {
                 0..=29 => {
                     let channel_name = client.channel_store().read_with(cx, |store, cx| {
-                        store.channel_dag_entries().find_map(|(_, channel)| {
+                        store.ordered_channels().find_map(|(_, channel)| {
                             if store.has_open_channel_buffer(channel.id, cx) {
                                 None
                             } else {
@@ -96,15 +98,16 @@ impl RandomizedTest for RandomChannelBufferTest {
 
                 30..=40 => {
                     if let Some(buffer) = channel_buffers.iter().choose(rng) {
-                        let channel_name = buffer.read_with(cx, |b, _| b.channel().name.clone());
+                        let channel_name =
+                            buffer.read_with(cx, |b, cx| b.channel(cx).unwrap().name.clone());
                         break ChannelBufferOperation::LeaveChannelNotes { channel_name };
                     }
                 }
 
                 _ => {
                     if let Some(buffer) = channel_buffers.iter().choose(rng) {
-                        break buffer.read_with(cx, |b, _| {
-                            let channel_name = b.channel().name.clone();
+                        break buffer.read_with(cx, |b, cx| {
+                            let channel_name = b.channel(cx).unwrap().name.clone();
                             let edits = b
                                 .buffer()
                                 .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3));
@@ -128,7 +131,7 @@ impl RandomizedTest for RandomChannelBufferTest {
             ChannelBufferOperation::JoinChannelNotes { channel_name } => {
                 let buffer = client.channel_store().update(cx, |store, cx| {
                     let channel_id = store
-                        .channel_dag_entries()
+                        .ordered_channels()
                         .find(|(_, c)| c.name == channel_name)
                         .unwrap()
                         .1
@@ -151,7 +154,7 @@ impl RandomizedTest for RandomChannelBufferTest {
                 let buffer = cx.update(|cx| {
                     let mut left_buffer = Err(TestError::Inapplicable);
                     client.channel_buffers().retain(|buffer| {
-                        if buffer.read(cx).channel().name == channel_name {
+                        if buffer.read(cx).channel(cx).unwrap().name == channel_name {
                             left_buffer = Ok(buffer.clone());
                             false
                         } else {
@@ -177,7 +180,9 @@ impl RandomizedTest for RandomChannelBufferTest {
                         client
                             .channel_buffers()
                             .iter()
-                            .find(|buffer| buffer.read(cx).channel().name == channel_name)
+                            .find(|buffer| {
+                                buffer.read(cx).channel(cx).unwrap().name == channel_name
+                            })
                             .cloned()
                     })
                     .ok_or_else(|| TestError::Inapplicable)?;
@@ -248,7 +253,7 @@ impl RandomizedTest for RandomChannelBufferTest {
                     if let Some(channel_buffer) = client
                         .channel_buffers()
                         .iter()
-                        .find(|b| b.read(cx).channel().id == channel_id.to_proto())
+                        .find(|b| b.read(cx).channel_id == channel_id.to_proto())
                     {
                         let channel_buffer = channel_buffer.read(cx);
 

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

@@ -16,9 +16,10 @@ use futures::{channel::oneshot, StreamExt as _};
 use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
 use language::LanguageRegistry;
 use node_runtime::FakeNodeRuntime;
+use notifications::NotificationStore;
 use parking_lot::Mutex;
 use project::{Project, WorktreeId};
-use rpc::RECEIVE_TIMEOUT;
+use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
 use settings::SettingsStore;
 use std::{
     cell::{Ref, RefCell, RefMut},
@@ -46,6 +47,7 @@ pub struct TestClient {
     pub username: String,
     pub app_state: Arc<workspace::AppState>,
     channel_store: ModelHandle<ChannelStore>,
+    notification_store: ModelHandle<NotificationStore>,
     state: RefCell<TestClientState>,
 }
 
@@ -138,7 +140,6 @@ impl TestServer {
                     NewUserParams {
                         github_login: name.into(),
                         github_user_id: 0,
-                        invite_count: 0,
                     },
                 )
                 .await
@@ -231,7 +232,8 @@ impl TestServer {
             workspace::init(app_state.clone(), cx);
             audio::init((), cx);
             call::init(client.clone(), user_store.clone(), cx);
-            channel::init(&client, user_store, cx);
+            channel::init(&client, user_store.clone(), cx);
+            notifications::init(client.clone(), user_store, cx);
         });
 
         client
@@ -243,6 +245,7 @@ impl TestServer {
             app_state,
             username: name.to_string(),
             channel_store: cx.read(ChannelStore::global).clone(),
+            notification_store: cx.read(NotificationStore::global).clone(),
             state: Default::default(),
         };
         client.wait_for_current_user(cx).await;
@@ -327,7 +330,7 @@ impl TestServer {
                     channel_store.invite_member(
                         channel_id,
                         member_client.user_id().unwrap(),
-                        false,
+                        ChannelRole::Member,
                         cx,
                     )
                 })
@@ -338,8 +341,8 @@ impl TestServer {
 
             member_cx
                 .read(ChannelStore::global)
-                .update(*member_cx, |channels, _| {
-                    channels.respond_to_channel_invite(channel_id, true)
+                .update(*member_cx, |channels, cx| {
+                    channels.respond_to_channel_invite(channel_id, true, cx)
                 })
                 .await
                 .unwrap();
@@ -448,6 +451,10 @@ impl TestClient {
         &self.channel_store
     }
 
+    pub fn notification_store(&self) -> &ModelHandle<NotificationStore> {
+        &self.notification_store
+    }
+
     pub fn user_store(&self) -> &ModelHandle<UserStore> {
         &self.app_state.user_store
     }
@@ -604,33 +611,6 @@ impl TestClient {
     ) -> WindowHandle<Workspace> {
         cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
     }
-
-    pub async fn add_admin_to_channel(
-        &self,
-        user: (&TestClient, &mut TestAppContext),
-        channel: u64,
-        cx_self: &mut TestAppContext,
-    ) {
-        let (other_client, other_cx) = user;
-
-        cx_self
-            .read(ChannelStore::global)
-            .update(cx_self, |channel_store, cx| {
-                channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
-            })
-            .await
-            .unwrap();
-
-        cx_self.foreground().run_until_parked();
-
-        other_cx
-            .read(ChannelStore::global)
-            .update(other_cx, |channel_store, _| {
-                channel_store.respond_to_channel_invite(channel, true)
-            })
-            .await
-            .unwrap();
-    }
 }
 
 impl Drop for TestClient {

crates/collab_ui/Cargo.toml 🔗

@@ -37,10 +37,12 @@ fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 menu = { path = "../menu" }
+notifications = { path = "../notifications" }
 rich_text = { path = "../rich_text" }
 picker = { path = "../picker" }
 project = { path = "../project" }
-recent_projects = {path = "../recent_projects"}
+recent_projects = { path = "../recent_projects" }
+rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 feature_flags = {path = "../feature_flags"}
 theme = { path = "../theme" }
@@ -52,12 +54,14 @@ zed-actions = {path = "../zed-actions"}
 
 anyhow.workspace = true
 futures.workspace = true
+lazy_static.workspace = true
 log.workspace = true
 schemars.workspace = true
 postage.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 time.workspace = true
+smallvec.workspace = true
 
 [dev-dependencies]
 call = { path = "../call", features = ["test-support"] }
@@ -65,7 +69,12 @@ client = { path = "../client", features = ["test-support"] }
 collections = { path = "../collections", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
+notifications = { path = "../notifications", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
+
+pretty_assertions.workspace = true
+tree-sitter-markdown.workspace = true

crates/collab_ui/src/channel_view.rs 🔗

@@ -15,13 +15,14 @@ use gpui::{
     ViewContext, ViewHandle,
 };
 use project::Project;
+use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
     sync::Arc,
 };
 use util::ResultExt;
 use workspace::{
-    item::{FollowableItem, Item, ItemHandle},
+    item::{FollowableItem, Item, ItemEvent, ItemHandle},
     register_followable_item,
     searchable::SearchableItemHandle,
     ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
@@ -140,6 +141,12 @@ impl ChannelView {
             editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
                 channel_buffer.clone(),
             )));
+            editor.set_read_only(
+                !channel_buffer
+                    .read(cx)
+                    .channel(cx)
+                    .is_some_and(|c| c.can_edit_notes()),
+            );
             editor
         });
         let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
@@ -157,8 +164,8 @@ impl ChannelView {
         }
     }
 
-    pub fn channel(&self, cx: &AppContext) -> Arc<Channel> {
-        self.channel_buffer.read(cx).channel()
+    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+        self.channel_buffer.read(cx).channel(cx)
     }
 
     fn handle_channel_buffer_event(
@@ -172,6 +179,13 @@ impl ChannelView {
                 editor.set_read_only(true);
                 cx.notify();
             }),
+            ChannelBufferEvent::ChannelChanged => {
+                self.editor.update(cx, |editor, cx| {
+                    editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
+                    cx.emit(editor::Event::TitleChanged);
+                    cx.notify()
+                });
+            }
             ChannelBufferEvent::BufferEdited => {
                 if cx.is_self_focused() || self.editor.is_focused(cx) {
                     self.acknowledge_buffer_version(cx);
@@ -179,7 +193,7 @@ impl ChannelView {
                     self.channel_store.update(cx, |store, cx| {
                         let channel_buffer = self.channel_buffer.read(cx);
                         store.notes_changed(
-                            channel_buffer.channel().id,
+                            channel_buffer.channel_id,
                             channel_buffer.epoch(),
                             &channel_buffer.buffer().read(cx).version(),
                             cx,
@@ -187,7 +201,7 @@ impl ChannelView {
                     });
                 }
             }
-            _ => {}
+            ChannelBufferEvent::CollaboratorsChanged => {}
         }
     }
 
@@ -195,7 +209,7 @@ impl ChannelView {
         self.channel_store.update(cx, |store, cx| {
             let channel_buffer = self.channel_buffer.read(cx);
             store.acknowledge_notes_version(
-                channel_buffer.channel().id,
+                channel_buffer.channel_id,
                 channel_buffer.epoch(),
                 &channel_buffer.buffer().read(cx).version(),
                 cx,
@@ -250,11 +264,17 @@ impl Item for ChannelView {
         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)
+        let label = if let Some(channel) = self.channel(cx) {
+            match (
+                channel.can_edit_notes(),
+                self.channel_buffer.read(cx).is_connected(),
+            ) {
+                (true, true) => format!("#{}", channel.name),
+                (false, true) => format!("#{} (read-only)", channel.name),
+                (_, false) => format!("#{} (disconnected)", channel.name),
+            }
         } else {
-            format!("#{} (disconnected)", channel_name)
+            format!("channel notes (disconnected)")
         };
         Label::new(label, style.label.to_owned()).into_any()
     }
@@ -298,6 +318,10 @@ impl Item for ChannelView {
     fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
         self.editor.read(cx).pixel_position_of_cursor(cx)
     }
+
+    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+        editor::Editor::to_item_events(event)
+    }
 }
 
 impl FollowableItem for ChannelView {
@@ -313,7 +337,7 @@ impl FollowableItem for ChannelView {
 
         Some(proto::view::Variant::ChannelView(
             proto::view::ChannelView {
-                channel_id: channel_buffer.channel().id,
+                channel_id: channel_buffer.channel_id,
                 editor: if let Some(proto::view::Variant::Editor(proto)) =
                     self.editor.read(cx).to_state_proto(cx)
                 {

crates/collab_ui/src/chat_panel.rs 🔗

@@ -1,4 +1,6 @@
-use crate::{channel_view::ChannelView, ChatPanelSettings};
+use crate::{
+    channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
+};
 use anyhow::Result;
 use call::ActiveCall;
 use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
@@ -6,18 +8,18 @@ use client::Client;
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
 use gpui::{
     actions,
     elements::*,
     platform::{CursorStyle, MouseButton},
     serde_json,
     views::{ItemType, Select, SelectStyle},
-    AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task,
-    View, ViewContext, ViewHandle, WeakViewHandle,
+    AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
-use language::{language_settings::SoftWrap, LanguageRegistry};
+use language::LanguageRegistry;
 use menu::Confirm;
+use message_editor::MessageEditor;
 use project::Fs;
 use rich_text::RichText;
 use serde::{Deserialize, Serialize};
@@ -31,6 +33,8 @@ use workspace::{
     Workspace,
 };
 
+mod message_editor;
+
 const MESSAGE_LOADING_THRESHOLD: usize = 50;
 const CHAT_PANEL_KEY: &'static str = "ChatPanel";
 
@@ -40,7 +44,7 @@ pub struct ChatPanel {
     languages: Arc<LanguageRegistry>,
     active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
     message_list: ListState<ChatPanel>,
-    input_editor: ViewHandle<Editor>,
+    input_editor: ViewHandle<MessageEditor>,
     channel_select: ViewHandle<Select>,
     local_timezone: UtcOffset,
     fs: Arc<dyn Fs>,
@@ -49,6 +53,7 @@ pub struct ChatPanel {
     pending_serialization: Task<Option<()>>,
     subscriptions: Vec<gpui::Subscription>,
     workspace: WeakViewHandle<Workspace>,
+    is_scrolled_to_bottom: bool,
     has_focus: bool,
     markdown_data: HashMap<ChannelMessageId, RichText>,
 }
@@ -85,13 +90,18 @@ impl ChatPanel {
         let languages = workspace.app_state().languages.clone();
 
         let input_editor = cx.add_view(|cx| {
-            let mut editor = Editor::auto_height(
-                4,
-                Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
+            MessageEditor::new(
+                languages.clone(),
+                channel_store.clone(),
+                cx.add_view(|cx| {
+                    Editor::auto_height(
+                        4,
+                        Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
+                        cx,
+                    )
+                }),
                 cx,
-            );
-            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
-            editor
+            )
         });
 
         let workspace_handle = workspace.weak_handle();
@@ -121,13 +131,14 @@ impl ChatPanel {
         });
 
         let mut message_list =
-            ListState::<Self>::new(0, Orientation::Bottom, 1000., move |this, ix, cx| {
+            ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
                 this.render_message(ix, cx)
             });
-        message_list.set_scroll_handler(|visible_range, this, cx| {
+        message_list.set_scroll_handler(|visible_range, count, this, cx| {
             if visible_range.start < MESSAGE_LOADING_THRESHOLD {
                 this.load_more_messages(&LoadMoreMessages, cx);
             }
+            this.is_scrolled_to_bottom = visible_range.end == count;
         });
 
         cx.add_view(|cx| {
@@ -136,7 +147,6 @@ impl ChatPanel {
                 client,
                 channel_store,
                 languages,
-
                 active_chat: Default::default(),
                 pending_serialization: Task::ready(None),
                 message_list,
@@ -146,6 +156,7 @@ impl ChatPanel {
                 has_focus: false,
                 subscriptions: Vec::new(),
                 workspace: workspace_handle,
+                is_scrolled_to_bottom: true,
                 active: false,
                 width: None,
                 markdown_data: Default::default(),
@@ -179,35 +190,20 @@ impl ChatPanel {
                     .channel_at(selected_ix)
                     .map(|e| e.id);
                 if let Some(selected_channel_id) = selected_channel_id {
-                    this.select_channel(selected_channel_id, cx)
+                    this.select_channel(selected_channel_id, None, cx)
                         .detach_and_log_err(cx);
                 }
             })
             .detach();
 
-            let markdown = this.languages.language_for_name("Markdown");
-            cx.spawn(|this, mut cx| async move {
-                let markdown = markdown.await?;
-
-                this.update(&mut cx, |this, cx| {
-                    this.input_editor.update(cx, |editor, cx| {
-                        editor.buffer().update(cx, |multi_buffer, cx| {
-                            multi_buffer
-                                .as_singleton()
-                                .unwrap()
-                                .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx))
-                        })
-                    })
-                })?;
-
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
-
             this
         })
     }
 
+    pub fn is_scrolled_to_bottom(&self) -> bool {
+        self.is_scrolled_to_bottom
+    }
+
     pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
         self.active_chat.as_ref().map(|(chat, _)| chat.clone())
     }
@@ -267,20 +263,22 @@ impl ChatPanel {
 
     fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
         if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
-            let id = chat.read(cx).channel().id;
+            let channel_id = chat.read(cx).channel_id;
             {
+                self.markdown_data.clear();
                 let chat = chat.read(cx);
                 self.message_list.reset(chat.message_count());
-                let placeholder = format!("Message #{}", chat.channel().name);
-                self.input_editor.update(cx, move |editor, cx| {
-                    editor.set_placeholder_text(placeholder, cx);
+
+                let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
+                self.input_editor.update(cx, |editor, cx| {
+                    editor.set_channel(channel_id, channel_name, cx);
                 });
-            }
+            };
             let subscription = cx.subscribe(&chat, Self::channel_did_change);
             self.active_chat = Some((chat, subscription));
             self.acknowledge_last_message(cx);
             self.channel_select.update(cx, |select, cx| {
-                if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) {
+                if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
                     select.set_selected_index(ix, cx);
                 }
             });
@@ -319,7 +317,7 @@ impl ChatPanel {
     }
 
     fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
-        if self.active {
+        if self.active && self.is_scrolled_to_bottom {
             if let Some((chat, _)) = &self.active_chat {
                 chat.update(cx, |chat, cx| {
                     chat.acknowledge_last_message(cx);
@@ -355,28 +353,48 @@ impl ChatPanel {
     }
 
     fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let (message, is_continuation, is_last) = {
-            let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
-            let last_message = active_chat.message(ix.saturating_sub(1));
-            let this_message = active_chat.message(ix);
-            let is_continuation = last_message.id != this_message.id
-                && this_message.sender.id == last_message.sender.id;
-
-            (
-                active_chat.message(ix).clone(),
-                is_continuation,
-                active_chat.message_count() == ix + 1,
-            )
-        };
+        let (message, is_continuation, is_last, is_admin) = self
+            .active_chat
+            .as_ref()
+            .unwrap()
+            .0
+            .update(cx, |active_chat, cx| {
+                let is_admin = self
+                    .channel_store
+                    .read(cx)
+                    .is_channel_admin(active_chat.channel_id);
+
+                let last_message = active_chat.message(ix.saturating_sub(1));
+                let this_message = active_chat.message(ix).clone();
+                let is_continuation = last_message.id != this_message.id
+                    && this_message.sender.id == last_message.sender.id;
+
+                if let ChannelMessageId::Saved(id) = this_message.id {
+                    if this_message
+                        .mentions
+                        .iter()
+                        .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
+                    {
+                        active_chat.acknowledge_message(id);
+                    }
+                }
+
+                (
+                    this_message,
+                    is_continuation,
+                    active_chat.message_count() == ix + 1,
+                    is_admin,
+                )
+            });
 
         let is_pending = message.is_pending();
-        let text = self
-            .markdown_data
-            .entry(message.id)
-            .or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None));
+        let theme = theme::current(cx);
+        let text = self.markdown_data.entry(message.id).or_insert_with(|| {
+            Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
+        });
 
         let now = OffsetDateTime::now_utc();
-        let theme = theme::current(cx);
+
         let style = if is_pending {
             &theme.chat_panel.pending_message
         } else if is_continuation {
@@ -386,23 +404,23 @@ impl ChatPanel {
         };
 
         let belongs_to_user = Some(message.sender.id) == self.client.user_id();
-        let message_id_to_remove =
-            if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) {
-                Some(id)
-            } else {
-                None
-            };
+        let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
+            (message.id, belongs_to_user || is_admin)
+        {
+            Some(id)
+        } else {
+            None
+        };
 
         enum MessageBackgroundHighlight {}
         MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
-            let container = style.container.style_for(state);
+            let container = style.style_for(state);
             if is_continuation {
                 Flex::row()
                     .with_child(
                         text.element(
                             theme.editor.syntax.clone(),
-                            style.body.clone(),
-                            theme.editor.document_highlight_read_background,
+                            theme.chat_panel.rich_text.clone(),
                             cx,
                         )
                         .flex(1., true),
@@ -424,15 +442,16 @@ impl ChatPanel {
                                 Flex::row()
                                     .with_child(render_avatar(
                                         message.sender.avatar.clone(),
-                                        &theme,
+                                        &theme.chat_panel.avatar,
+                                        theme.chat_panel.avatar_container,
                                     ))
                                     .with_child(
                                         Label::new(
                                             message.sender.github_login.clone(),
-                                            style.sender.text.clone(),
+                                            theme.chat_panel.message_sender.text.clone(),
                                         )
                                         .contained()
-                                        .with_style(style.sender.container),
+                                        .with_style(theme.chat_panel.message_sender.container),
                                     )
                                     .with_child(
                                         Label::new(
@@ -441,10 +460,10 @@ impl ChatPanel {
                                                 now,
                                                 self.local_timezone,
                                             ),
-                                            style.timestamp.text.clone(),
+                                            theme.chat_panel.message_timestamp.text.clone(),
                                         )
                                         .contained()
-                                        .with_style(style.timestamp.container),
+                                        .with_style(theme.chat_panel.message_timestamp.container),
                                     )
                                     .align_children_center()
                                     .flex(1., true),
@@ -457,8 +476,7 @@ impl ChatPanel {
                             .with_child(
                                 text.element(
                                     theme.editor.syntax.clone(),
-                                    style.body.clone(),
-                                    theme.editor.document_highlight_read_background,
+                                    theme.chat_panel.rich_text.clone(),
                                     cx,
                                 )
                                 .flex(1., true),
@@ -479,6 +497,23 @@ impl ChatPanel {
         .into_any()
     }
 
+    fn render_markdown_with_mentions(
+        language_registry: &Arc<LanguageRegistry>,
+        current_user_id: u64,
+        message: &channel::ChannelMessage,
+    ) -> RichText {
+        let mentions = message
+            .mentions
+            .iter()
+            .map(|(range, user_id)| rich_text::Mention {
+                range: range.clone(),
+                is_self_mention: *user_id == current_user_id,
+            })
+            .collect::<Vec<_>>();
+
+        rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
+    }
+
     fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
         ChildView::new(&self.input_editor, cx)
             .contained()
@@ -604,14 +639,12 @@ impl ChatPanel {
 
     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some((chat, _)) = self.active_chat.as_ref() {
-            let body = self.input_editor.update(cx, |editor, cx| {
-                let body = editor.text(cx);
-                editor.clear(cx);
-                body
-            });
+            let message = self
+                .input_editor
+                .update(cx, |editor, cx| editor.take_message(cx));
 
             if let Some(task) = chat
-                .update(cx, |chat, cx| chat.send_message(body, cx))
+                .update(cx, |chat, cx| chat.send_message(message, cx))
                 .log_err()
             {
                 task.detach();
@@ -628,7 +661,9 @@ impl ChatPanel {
     fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
         if let Some((chat, _)) = self.active_chat.as_ref() {
             chat.update(cx, |channel, cx| {
-                channel.load_more_messages(cx);
+                if let Some(task) = channel.load_more_messages(cx) {
+                    task.detach();
+                }
             })
         }
     }
@@ -636,29 +671,52 @@ impl ChatPanel {
     pub fn select_channel(
         &mut self,
         selected_channel_id: u64,
+        scroll_to_message_id: Option<u64>,
         cx: &mut ViewContext<ChatPanel>,
     ) -> Task<Result<()>> {
-        if let Some((chat, _)) = &self.active_chat {
-            if chat.read(cx).channel().id == selected_channel_id {
-                return Task::ready(Ok(()));
-            }
-        }
+        let open_chat = self
+            .active_chat
+            .as_ref()
+            .and_then(|(chat, _)| {
+                (chat.read(cx).channel_id == selected_channel_id)
+                    .then(|| Task::ready(anyhow::Ok(chat.clone())))
+            })
+            .unwrap_or_else(|| {
+                self.channel_store.update(cx, |store, cx| {
+                    store.open_channel_chat(selected_channel_id, cx)
+                })
+            });
 
-        let open_chat = self.channel_store.update(cx, |store, cx| {
-            store.open_channel_chat(selected_channel_id, cx)
-        });
         cx.spawn(|this, mut cx| async move {
             let chat = open_chat.await?;
             this.update(&mut cx, |this, cx| {
-                this.markdown_data = Default::default();
-                this.set_active_chat(chat, cx);
-            })
+                this.set_active_chat(chat.clone(), cx);
+            })?;
+
+            if let Some(message_id) = scroll_to_message_id {
+                if let Some(item_ix) =
+                    ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
+                        .await
+                {
+                    this.update(&mut cx, |this, cx| {
+                        if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
+                            this.message_list.scroll_to(ListOffset {
+                                item_ix,
+                                offset_in_item: 0.,
+                            });
+                            cx.notify();
+                        }
+                    })?;
+                }
+            }
+
+            Ok(())
         })
     }
 
     fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
         if let Some((chat, _)) = &self.active_chat {
-            let channel_id = chat.read(cx).channel().id;
+            let channel_id = chat.read(cx).channel_id;
             if let Some(workspace) = self.workspace.upgrade(cx) {
                 ChannelView::open(channel_id, workspace, cx).detach();
             }
@@ -667,7 +725,7 @@ impl ChatPanel {
 
     fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
         if let Some((chat, _)) = &self.active_chat {
-            let channel_id = chat.read(cx).channel().id;
+            let channel_id = chat.read(cx).channel_id;
             ActiveCall::global(cx)
                 .update(cx, |call, cx| call.join_channel(channel_id, cx))
                 .detach_and_log_err(cx);
@@ -675,32 +733,6 @@ impl ChatPanel {
     }
 }
 
-fn render_avatar(avatar: Option<Arc<ImageData>>, theme: &Arc<Theme>) -> AnyElement<ChatPanel> {
-    let avatar_style = theme.chat_panel.avatar;
-
-    avatar
-        .map(|avatar| {
-            Image::from_data(avatar)
-                .with_style(avatar_style.image)
-                .aligned()
-                .contained()
-                .with_corner_radius(avatar_style.outer_corner_radius)
-                .constrained()
-                .with_width(avatar_style.outer_width)
-                .with_height(avatar_style.outer_width)
-                .into_any()
-        })
-        .unwrap_or_else(|| {
-            Empty::new()
-                .constrained()
-                .with_width(avatar_style.outer_width)
-                .into_any()
-        })
-        .contained()
-        .with_style(theme.chat_panel.avatar_container)
-        .into_any()
-}
-
 fn render_remove(
     message_id_to_remove: Option<u64>,
     cx: &mut ViewContext<'_, '_, ChatPanel>,
@@ -771,7 +803,8 @@ impl View for ChatPanel {
             *self.client.status().borrow(),
             client::Status::Connected { .. }
         ) {
-            cx.focus(&self.input_editor);
+            let editor = self.input_editor.read(cx).editor.clone();
+            cx.focus(&editor);
         }
     }
 
@@ -810,14 +843,14 @@ impl Panel for ChatPanel {
         self.active = active;
         if active {
             self.acknowledge_last_message(cx);
-            if !is_chat_feature_enabled(cx) {
+            if !is_channels_feature_enabled(cx) {
                 cx.emit(Event::Dismissed);
             }
         }
     }
 
     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-        (settings::get::<ChatPanelSettings>(cx).button && is_chat_feature_enabled(cx))
+        (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
             .then(|| "icons/conversations.svg")
     }
 
@@ -842,10 +875,6 @@ impl Panel for ChatPanel {
     }
 }
 
-fn is_chat_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
-    cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
-}
-
 fn format_timestamp(
     mut timestamp: OffsetDateTime,
     mut now: OffsetDateTime,
@@ -883,3 +912,72 @@ fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> im
         .contained()
         .with_style(style.container)
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::fonts::HighlightStyle;
+    use pretty_assertions::assert_eq;
+    use rich_text::{BackgroundKind, Highlight, RenderedRegion};
+    use util::test::marked_text_ranges;
+
+    #[gpui::test]
+    fn test_render_markdown_with_mentions() {
+        let language_registry = Arc::new(LanguageRegistry::test());
+        let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
+        let message = channel::ChannelMessage {
+            id: ChannelMessageId::Saved(0),
+            body,
+            timestamp: OffsetDateTime::now_utc(),
+            sender: Arc::new(client::User {
+                github_login: "fgh".into(),
+                avatar: None,
+                id: 103,
+            }),
+            nonce: 5,
+            mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+        };
+
+        let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
+
+        // Note that the "'" was replaced with ’ due to smart punctuation.
+        let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
+        assert_eq!(message.text, body);
+        assert_eq!(
+            message.highlights,
+            vec![
+                (
+                    ranges[0].clone(),
+                    HighlightStyle {
+                        italic: Some(true),
+                        ..Default::default()
+                    }
+                    .into()
+                ),
+                (ranges[1].clone(), Highlight::Mention),
+                (
+                    ranges[2].clone(),
+                    HighlightStyle {
+                        weight: Some(gpui::fonts::Weight::BOLD),
+                        ..Default::default()
+                    }
+                    .into()
+                ),
+                (ranges[3].clone(), Highlight::SelfMention)
+            ]
+        );
+        assert_eq!(
+            message.regions,
+            vec![
+                RenderedRegion {
+                    background_kind: Some(BackgroundKind::Mention),
+                    link_url: None
+                },
+                RenderedRegion {
+                    background_kind: Some(BackgroundKind::SelfMention),
+                    link_url: None
+                },
+            ]
+        );
+    }
+}

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

@@ -0,0 +1,313 @@
+use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
+use client::UserId;
+use collections::HashMap;
+use editor::{AnchorRangeExt, Editor};
+use gpui::{
+    elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
+};
+use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
+use lazy_static::lazy_static;
+use project::search::SearchQuery;
+use std::{sync::Arc, time::Duration};
+
+const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
+
+lazy_static! {
+    static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
+        "@[-_\\w]+",
+        false,
+        false,
+        Default::default(),
+        Default::default()
+    )
+    .unwrap();
+}
+
+pub struct MessageEditor {
+    pub editor: ViewHandle<Editor>,
+    channel_store: ModelHandle<ChannelStore>,
+    users: HashMap<String, UserId>,
+    mentions: Vec<UserId>,
+    mentions_task: Option<Task<()>>,
+    channel_id: Option<ChannelId>,
+}
+
+impl MessageEditor {
+    pub fn new(
+        language_registry: Arc<LanguageRegistry>,
+        channel_store: ModelHandle<ChannelStore>,
+        editor: ViewHandle<Editor>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        editor.update(cx, |editor, cx| {
+            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+        });
+
+        let buffer = editor
+            .read(cx)
+            .buffer()
+            .read(cx)
+            .as_singleton()
+            .expect("message editor must be singleton");
+
+        cx.subscribe(&buffer, Self::on_buffer_event).detach();
+
+        let markdown = language_registry.language_for_name("Markdown");
+        cx.app_context()
+            .spawn(|mut cx| async move {
+                let markdown = markdown.await?;
+                buffer.update(&mut cx, |buffer, cx| {
+                    buffer.set_language(Some(markdown), cx)
+                });
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+
+        Self {
+            editor,
+            channel_store,
+            users: HashMap::default(),
+            channel_id: None,
+            mentions: Vec::new(),
+            mentions_task: None,
+        }
+    }
+
+    pub fn set_channel(
+        &mut self,
+        channel_id: u64,
+        channel_name: Option<String>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.editor.update(cx, |editor, cx| {
+            if let Some(channel_name) = channel_name {
+                editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
+            } else {
+                editor.set_placeholder_text(format!("Message Channel"), cx);
+            }
+        });
+        self.channel_id = Some(channel_id);
+        self.refresh_users(cx);
+    }
+
+    pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(channel_id) = self.channel_id {
+            let members = self.channel_store.update(cx, |store, cx| {
+                store.get_channel_member_details(channel_id, cx)
+            });
+            cx.spawn(|this, mut cx| async move {
+                let members = members.await?;
+                this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    }
+
+    pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
+        self.users.clear();
+        self.users.extend(
+            members
+                .into_iter()
+                .map(|member| (member.user.github_login.clone(), member.user.id)),
+        );
+    }
+
+    pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
+        self.editor.update(cx, |editor, cx| {
+            let highlights = editor.text_highlights::<Self>(cx);
+            let text = editor.text(cx);
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            let mentions = if let Some((_, ranges)) = highlights {
+                ranges
+                    .iter()
+                    .map(|range| range.to_offset(&snapshot))
+                    .zip(self.mentions.iter().copied())
+                    .collect()
+            } else {
+                Vec::new()
+            };
+
+            editor.clear(cx);
+            self.mentions.clear();
+
+            MessageParams { text, mentions }
+        })
+    }
+
+    fn on_buffer_event(
+        &mut self,
+        buffer: ModelHandle<Buffer>,
+        event: &language::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let language::Event::Reparsed | language::Event::Edited = event {
+            let buffer = buffer.read(cx).snapshot();
+            self.mentions_task = Some(cx.spawn(|this, cx| async move {
+                cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
+                Self::find_mentions(this, buffer, cx).await;
+            }));
+        }
+    }
+
+    async fn find_mentions(
+        this: WeakViewHandle<MessageEditor>,
+        buffer: BufferSnapshot,
+        mut cx: AsyncAppContext,
+    ) {
+        let (buffer, ranges) = cx
+            .background()
+            .spawn(async move {
+                let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
+                (buffer, ranges)
+            })
+            .await;
+
+        this.update(&mut cx, |this, cx| {
+            let mut anchor_ranges = Vec::new();
+            let mut mentioned_user_ids = Vec::new();
+            let mut text = String::new();
+
+            this.editor.update(cx, |editor, cx| {
+                let multi_buffer = editor.buffer().read(cx).snapshot(cx);
+                for range in ranges {
+                    text.clear();
+                    text.extend(buffer.text_for_range(range.clone()));
+                    if let Some(username) = text.strip_prefix("@") {
+                        if let Some(user_id) = this.users.get(username) {
+                            let start = multi_buffer.anchor_after(range.start);
+                            let end = multi_buffer.anchor_after(range.end);
+
+                            mentioned_user_ids.push(*user_id);
+                            anchor_ranges.push(start..end);
+                        }
+                    }
+                }
+
+                editor.clear_highlights::<Self>(cx);
+                editor.highlight_text::<Self>(
+                    anchor_ranges,
+                    theme::current(cx).chat_panel.rich_text.mention_highlight,
+                    cx,
+                )
+            });
+
+            this.mentions = mentioned_user_ids;
+            this.mentions_task.take();
+        })
+        .ok();
+    }
+}
+
+impl Entity for MessageEditor {
+    type Event = ();
+}
+
+impl View for MessageEditor {
+    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
+        ChildView::new(&self.editor, cx).into_any()
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if cx.is_self_focused() {
+            cx.focus(&self.editor);
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use client::{Client, User, UserStore};
+    use gpui::{TestAppContext, WindowHandle};
+    use language::{Language, LanguageConfig};
+    use rpc::proto;
+    use settings::SettingsStore;
+    use util::{http::FakeHttpClient, test::marked_text_ranges};
+
+    #[gpui::test]
+    async fn test_message_editor(cx: &mut TestAppContext) {
+        let editor = init_test(cx);
+        let editor = editor.root(cx);
+
+        editor.update(cx, |editor, cx| {
+            editor.set_members(
+                vec![
+                    ChannelMembership {
+                        user: Arc::new(User {
+                            github_login: "a-b".into(),
+                            id: 101,
+                            avatar: None,
+                        }),
+                        kind: proto::channel_member::Kind::Member,
+                        role: proto::ChannelRole::Member,
+                    },
+                    ChannelMembership {
+                        user: Arc::new(User {
+                            github_login: "C_D".into(),
+                            id: 102,
+                            avatar: None,
+                        }),
+                        kind: proto::channel_member::Kind::Member,
+                        role: proto::ChannelRole::Member,
+                    },
+                ],
+                cx,
+            );
+
+            editor.editor.update(cx, |editor, cx| {
+                editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
+            });
+        });
+
+        cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
+
+        editor.update(cx, |editor, cx| {
+            let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
+            assert_eq!(
+                editor.take_message(cx),
+                MessageParams {
+                    text,
+                    mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+                }
+            );
+        });
+    }
+
+    fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
+        cx.foreground().forbid_parking();
+
+        cx.update(|cx| {
+            let http = FakeHttpClient::with_404_response();
+            let client = Client::new(http.clone(), cx);
+            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            language::init(cx);
+            editor::init(cx);
+            client::init(&client, cx);
+            channel::init(&client, user_store, cx);
+        });
+
+        let language_registry = Arc::new(LanguageRegistry::test());
+        language_registry.add(Arc::new(Language::new(
+            LanguageConfig {
+                name: "Markdown".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_markdown::language()),
+        )));
+
+        let editor = cx.add_window(|cx| {
+            MessageEditor::new(
+                language_registry,
+                ChannelStore::global(cx),
+                cx.add_view(|cx| Editor::auto_height(4, None, cx)),
+                cx,
+            )
+        });
+        cx.foreground().run_until_parked();
+        editor
+    }
+}

crates/collab_ui/src/collab_panel.rs 🔗

@@ -9,9 +9,12 @@ use crate::{
 };
 use anyhow::Result;
 use call::ActiveCall;
-use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
+use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
 use channel_modal::ChannelModal;
-use client::{proto::PeerId, Client, Contact, User, UserStore};
+use client::{
+    proto::{self, PeerId},
+    Client, Contact, User, UserStore,
+};
 use contact_finder::ContactFinder;
 use context_menu::{ContextMenu, ContextMenuItem};
 use db::kvp::KEY_VALUE_STORE;
@@ -43,7 +46,7 @@ use serde_derive::{Deserialize, Serialize};
 use settings::SettingsStore;
 use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
 use theme::{components::ComponentExt, IconButton, Interactive};
-use util::{iife, ResultExt, TryFutureExt};
+use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
     item::ItemHandle,
@@ -52,17 +55,17 @@ use workspace::{
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct ToggleCollapse {
-    location: ChannelPath,
+    location: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct NewChannel {
-    location: ChannelPath,
+    location: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct RenameChannel {
-    location: ChannelPath,
+    channel_id: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -108,18 +111,6 @@ pub struct CopyChannelLink {
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct StartMoveChannelFor {
     channel_id: ChannelId,
-    parent_id: Option<ChannelId>,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct StartLinkChannelFor {
-    channel_id: ChannelId,
-    parent_id: Option<ChannelId>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct LinkChannel {
-    to: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -127,14 +118,6 @@ struct MoveChannel {
     to: ChannelId,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct UnlinkChannel {
-    channel_id: ChannelId,
-    parent_id: ChannelId,
-}
-
-type DraggedChannel = (Channel, Option<ChannelId>);
-
 actions!(
     collab_panel,
     [
@@ -144,8 +127,7 @@ actions!(
         CollapseSelectedChannel,
         ExpandSelectedChannel,
         StartMoveChannel,
-        StartLinkChannel,
-        MoveOrLinkToSelected,
+        MoveSelected,
         InsertSpace,
     ]
 );
@@ -163,11 +145,8 @@ impl_actions!(
         JoinChannelCall,
         JoinChannelChat,
         CopyChannelLink,
-        LinkChannel,
         StartMoveChannelFor,
-        StartLinkChannelFor,
         MoveChannel,
-        UnlinkChannel,
         ToggleSelectedIx
     ]
 );
@@ -175,14 +154,6 @@ impl_actions!(
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 struct ChannelMoveClipboard {
     channel_id: ChannelId,
-    parent_id: Option<ChannelId>,
-    intent: ClipboardIntent,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-enum ClipboardIntent {
-    Move,
-    Link,
 }
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
@@ -229,87 +200,35 @@ pub fn init(cx: &mut AppContext) {
          _: &mut ViewContext<CollabPanel>| {
             panel.channel_clipboard = Some(ChannelMoveClipboard {
                 channel_id: action.channel_id,
-                parent_id: action.parent_id,
-                intent: ClipboardIntent::Move,
             });
         },
     );
 
-    cx.add_action(
-        |panel: &mut CollabPanel,
-         action: &StartLinkChannelFor,
-         _: &mut ViewContext<CollabPanel>| {
-            panel.channel_clipboard = Some(ChannelMoveClipboard {
-                channel_id: action.channel_id,
-                parent_id: action.parent_id,
-                intent: ClipboardIntent::Link,
-            })
-        },
-    );
-
     cx.add_action(
         |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
-            if let Some((_, path)) = panel.selected_channel() {
-                panel.channel_clipboard = Some(ChannelMoveClipboard {
-                    channel_id: path.channel_id(),
-                    parent_id: path.parent_id(),
-                    intent: ClipboardIntent::Move,
-                })
-            }
-        },
-    );
-
-    cx.add_action(
-        |panel: &mut CollabPanel, _: &StartLinkChannel, _: &mut ViewContext<CollabPanel>| {
-            if let Some((_, path)) = panel.selected_channel() {
+            if let Some(channel) = panel.selected_channel() {
                 panel.channel_clipboard = Some(ChannelMoveClipboard {
-                    channel_id: path.channel_id(),
-                    parent_id: path.parent_id(),
-                    intent: ClipboardIntent::Link,
+                    channel_id: channel.id,
                 })
             }
         },
     );
 
     cx.add_action(
-        |panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext<CollabPanel>| {
-            let clipboard = panel.channel_clipboard.take();
-            if let Some(((selected_channel, _), clipboard)) =
-                panel.selected_channel().zip(clipboard)
-            {
-                match clipboard.intent {
-                    ClipboardIntent::Move if clipboard.parent_id.is_some() => {
-                        let parent_id = clipboard.parent_id.unwrap();
-                        panel.channel_store.update(cx, |channel_store, cx| {
-                            channel_store
-                                .move_channel(
-                                    clipboard.channel_id,
-                                    parent_id,
-                                    selected_channel.id,
-                                    cx,
-                                )
-                                .detach_and_log_err(cx)
-                        })
-                    }
-                    _ => panel.channel_store.update(cx, |channel_store, cx| {
-                        channel_store
-                            .link_channel(clipboard.channel_id, selected_channel.id, cx)
-                            .detach_and_log_err(cx)
-                    }),
-                }
-            }
-        },
-    );
+        |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
+            let Some(clipboard) = panel.channel_clipboard.take() else {
+                return;
+            };
+            let Some(selected_channel) = panel.selected_channel() else {
+                return;
+            };
 
-    cx.add_action(
-        |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext<CollabPanel>| {
-            if let Some(clipboard) = panel.channel_clipboard.take() {
-                panel.channel_store.update(cx, |channel_store, cx| {
-                    channel_store
-                        .link_channel(clipboard.channel_id, action.to, cx)
-                        .detach_and_log_err(cx)
+            panel
+                .channel_store
+                .update(cx, |channel_store, cx| {
+                    channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
                 })
-            }
+                .detach_and_log_err(cx)
         },
     );
 
@@ -317,39 +236,23 @@ pub fn init(cx: &mut AppContext) {
         |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
             if let Some(clipboard) = panel.channel_clipboard.take() {
                 panel.channel_store.update(cx, |channel_store, cx| {
-                    if let Some(parent) = clipboard.parent_id {
-                        channel_store
-                            .move_channel(clipboard.channel_id, parent, action.to, cx)
-                            .detach_and_log_err(cx)
-                    } else {
-                        channel_store
-                            .link_channel(clipboard.channel_id, action.to, cx)
-                            .detach_and_log_err(cx)
-                    }
+                    channel_store
+                        .move_channel(clipboard.channel_id, Some(action.to), cx)
+                        .detach_and_log_err(cx)
                 })
             }
         },
     );
-
-    cx.add_action(
-        |panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext<CollabPanel>| {
-            panel.channel_store.update(cx, |channel_store, cx| {
-                channel_store
-                    .unlink_channel(action.channel_id, action.parent_id, cx)
-                    .detach_and_log_err(cx)
-            })
-        },
-    );
 }
 
 #[derive(Debug)]
 pub enum ChannelEditingState {
     Create {
-        location: Option<ChannelPath>,
+        location: Option<ChannelId>,
         pending_name: Option<String>,
     },
     Rename {
-        location: ChannelPath,
+        location: ChannelId,
         pending_name: Option<String>,
     },
 }
@@ -383,16 +286,23 @@ pub struct CollabPanel {
     list_state: ListState<Self>,
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
-    collapsed_channels: Vec<ChannelPath>,
-    drag_target_channel: Option<ChannelData>,
+    collapsed_channels: Vec<ChannelId>,
+    drag_target_channel: ChannelDragTarget,
     workspace: WeakViewHandle<Workspace>,
     context_menu_on_selected: bool,
 }
 
+#[derive(PartialEq, Eq)]
+enum ChannelDragTarget {
+    None,
+    Root,
+    Channel(ChannelId),
+}
+
 #[derive(Serialize, Deserialize)]
 struct SerializedCollabPanel {
     width: Option<f32>,
-    collapsed_channels: Option<Vec<ChannelPath>>,
+    collapsed_channels: Option<Vec<ChannelId>>,
 }
 
 #[derive(Debug)]
@@ -428,7 +338,7 @@ enum ListEntry {
         is_last: bool,
     },
     ParticipantScreen {
-        peer_id: PeerId,
+        peer_id: Option<PeerId>,
         is_last: bool,
     },
     IncomingRequest(Arc<User>),
@@ -437,11 +347,14 @@ enum ListEntry {
     Channel {
         channel: Arc<Channel>,
         depth: usize,
-        path: ChannelPath,
+        has_children: bool,
     },
     ChannelNotes {
         channel_id: ChannelId,
     },
+    ChannelChat {
+        channel_id: ChannelId,
+    },
     ChannelEditor {
         depth: usize,
     },
@@ -569,14 +482,14 @@ impl CollabPanel {
                         ListEntry::Channel {
                             channel,
                             depth,
-                            path,
+                            has_children,
                         } => {
                             let channel_row = this.render_channel(
                                 &*channel,
                                 *depth,
-                                path.to_owned(),
                                 &theme,
                                 is_selected,
+                                *has_children,
                                 ix,
                                 cx,
                             );
@@ -602,6 +515,13 @@ impl CollabPanel {
                             ix,
                             cx,
                         ),
+                        ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
+                            *channel_id,
+                            &theme.collab_panel,
+                            is_selected,
+                            ix,
+                            cx,
+                        ),
                         ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
                             channel.clone(),
                             this.channel_store.clone(),
@@ -664,7 +584,7 @@ impl CollabPanel {
                 workspace: workspace.weak_handle(),
                 client: workspace.app_state().client.clone(),
                 context_menu_on_selected: true,
-                drag_target_channel: None,
+                drag_target_channel: ChannelDragTarget::None,
                 list_state,
             };
 
@@ -804,7 +724,8 @@ impl CollabPanel {
                 let room = room.read(cx);
 
                 if let Some(channel_id) = room.channel_id() {
-                    self.entries.push(ListEntry::ChannelNotes { channel_id })
+                    self.entries.push(ListEntry::ChannelNotes { channel_id });
+                    self.entries.push(ListEntry::ChannelChat { channel_id })
                 }
 
                 // Populate the active user.
@@ -836,7 +757,13 @@ impl CollabPanel {
                                 project_id: project.id,
                                 worktree_root_names: project.worktree_root_names.clone(),
                                 host_user_id: user_id,
-                                is_last: projects.peek().is_none(),
+                                is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+                            });
+                        }
+                        if room.is_screen_sharing() {
+                            self.entries.push(ListEntry::ParticipantScreen {
+                                peer_id: None,
+                                is_last: true,
                             });
                         }
                     }
@@ -880,7 +807,7 @@ impl CollabPanel {
                     }
                     if !participant.video_tracks.is_empty() {
                         self.entries.push(ListEntry::ParticipantScreen {
-                            peer_id: participant.peer_id,
+                            peer_id: Some(participant.peer_id),
                             is_last: true,
                         });
                     }
@@ -921,7 +848,7 @@ impl CollabPanel {
             if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
                 self.match_candidates.clear();
                 self.match_candidates
-                    .extend(channel_store.channel_dag_entries().enumerate().map(
+                    .extend(channel_store.ordered_channels().enumerate().map(
                         |(ix, (_, channel))| StringMatchCandidate {
                             id: ix,
                             string: channel.name.clone(),
@@ -943,48 +870,52 @@ impl CollabPanel {
                 }
                 let mut collapse_depth = None;
                 for mat in matches {
-                    let (channel, path) = channel_store
-                        .channel_dag_entry_at(mat.candidate_id)
-                        .unwrap();
-                    let depth = path.len() - 1;
+                    let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
+                    let depth = channel.parent_path.len();
 
-                    if collapse_depth.is_none() && self.is_channel_collapsed(path) {
+                    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(path) {
+                        if self.is_channel_collapsed(channel.id) {
                             collapse_depth = Some(depth);
                         } else {
                             collapse_depth = None;
                         }
                     }
 
+                    let has_children = channel_store
+                        .channel_at_index(mat.candidate_id + 1)
+                        .map_or(false, |next_channel| {
+                            next_channel.parent_path.ends_with(&[channel.id])
+                        });
+
                     match &self.channel_editing_state {
                         Some(ChannelEditingState::Create {
-                            location: parent_path,
+                            location: parent_id,
                             ..
-                        }) if parent_path.as_ref() == Some(path) => {
+                        }) if *parent_id == Some(channel.id) => {
                             self.entries.push(ListEntry::Channel {
                                 channel: channel.clone(),
                                 depth,
-                                path: path.clone(),
+                                has_children: false,
                             });
                             self.entries
                                 .push(ListEntry::ChannelEditor { depth: depth + 1 });
                         }
                         Some(ChannelEditingState::Rename {
-                            location: parent_path,
+                            location: parent_id,
                             ..
-                        }) if parent_path == path => {
+                        }) if parent_id == &channel.id => {
                             self.entries.push(ListEntry::ChannelEditor { depth });
                         }
                         _ => {
                             self.entries.push(ListEntry::Channel {
                                 channel: channel.clone(),
                                 depth,
-                                path: path.clone(),
+                                has_children,
                             });
                         }
                     }
@@ -1225,14 +1156,18 @@ impl CollabPanel {
     ) -> AnyElement<Self> {
         enum CallParticipant {}
         enum CallParticipantTooltip {}
+        enum LeaveCallButton {}
+        enum LeaveCallTooltip {}
 
         let collab_theme = &theme.collab_panel;
 
         let is_current_user =
             user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
 
-        let content =
-            MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
+        let content = MouseEventHandler::new::<CallParticipant, _>(
+            user.id as usize,
+            cx,
+            |mouse_state, cx| {
                 let style = if is_current_user {
                     *collab_theme
                         .contact_row
@@ -1268,14 +1203,32 @@ impl CollabPanel {
                             Label::new("Calling", collab_theme.calling_indicator.text.clone())
                                 .contained()
                                 .with_style(collab_theme.calling_indicator.container)
-                                .aligned(),
+                                .aligned()
+                                .into_any(),
                         )
                     } else if is_current_user {
                         Some(
-                            Label::new("You", collab_theme.calling_indicator.text.clone())
-                                .contained()
-                                .with_style(collab_theme.calling_indicator.container)
-                                .aligned(),
+                            MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
+                                render_icon_button(
+                                    theme
+                                        .collab_panel
+                                        .leave_call_button
+                                        .style_for(is_selected, state),
+                                    "icons/exit.svg",
+                                )
+                            })
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .on_click(MouseButton::Left, |_, _, cx| {
+                                Self::leave_call(cx);
+                            })
+                            .with_tooltip::<LeaveCallTooltip>(
+                                0,
+                                "Leave call",
+                                None,
+                                theme.tooltip.clone(),
+                                cx,
+                            )
+                            .into_any(),
                         )
                     } else {
                         None
@@ -1284,7 +1237,8 @@ impl CollabPanel {
                     .with_height(collab_theme.row_height)
                     .contained()
                     .with_style(style)
-            });
+            },
+        );
 
         if is_current_user || is_pending || peer_id.is_none() {
             return content.into_any();
@@ -1406,7 +1360,7 @@ impl CollabPanel {
     }
 
     fn render_participant_screen(
-        peer_id: PeerId,
+        peer_id: Option<PeerId>,
         is_last: bool,
         is_selected: bool,
         theme: &theme::CollabPanel,
@@ -1421,8 +1375,8 @@ impl CollabPanel {
             .unwrap_or(0.);
         let tree_branch = theme.tree_branch;
 
-        MouseEventHandler::new::<OpenSharedScreen, _>(
-            peer_id.as_u64() as usize,
+        let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
+            peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
             cx,
             |mouse_state, cx| {
                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
@@ -1460,16 +1414,20 @@ impl CollabPanel {
                     .contained()
                     .with_style(row.container)
             },
-        )
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            if let Some(workspace) = this.workspace.upgrade(cx) {
-                workspace.update(cx, |workspace, cx| {
-                    workspace.open_shared_screen(peer_id, cx)
-                });
-            }
-        })
-        .into_any()
+        );
+        if peer_id.is_none() {
+            return handler.into_any();
+        }
+        handler
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                if let Some(workspace) = this.workspace.upgrade(cx) {
+                    workspace.update(cx, |workspace, cx| {
+                        workspace.open_shared_screen(peer_id.unwrap(), cx)
+                    });
+                }
+            })
+            .into_any()
     }
 
     fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
@@ -1496,23 +1454,33 @@ impl CollabPanel {
         enum AddChannel {}
 
         let tooltip_style = &theme.tooltip;
+        let mut channel_link = None;
+        let mut channel_tooltip_text = None;
+        let mut channel_icon = None;
+        let mut is_dragged_over = false;
+
         let text = match section {
             Section::ActiveCall => {
-                let channel_name = iife!({
+                let channel_name = maybe!({
                     let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
 
-                    let name = self
-                        .channel_store
-                        .read(cx)
-                        .channel_for_id(channel_id)?
-                        .name
-                        .as_str();
+                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
+
+                    channel_link = Some(channel.link());
+                    (channel_icon, channel_tooltip_text) = match channel.visibility {
+                        proto::ChannelVisibility::Public => {
+                            (Some("icons/public.svg"), Some("Copy public channel link."))
+                        }
+                        proto::ChannelVisibility::Members => {
+                            (Some("icons/hash.svg"), Some("Copy private channel link."))
+                        }
+                    };
 
-                    Some(name)
+                    Some(channel.name.as_str())
                 });
 
                 if let Some(name) = channel_name {
-                    Cow::Owned(format!("#{}", name))
+                    Cow::Owned(format!("{}", name))
                 } else {
                     Cow::Borrowed("Current Call")
                 }
@@ -1527,28 +1495,30 @@ impl CollabPanel {
 
         enum AddContact {}
         let button = match section {
-            Section::ActiveCall => Some(
+            Section::ActiveCall => channel_link.map(|channel_link| {
+                let channel_link_copy = channel_link.clone();
                 MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
                     render_icon_button(
                         theme
                             .collab_panel
                             .leave_call_button
                             .style_for(is_selected, state),
-                        "icons/exit.svg",
+                        "icons/link.svg",
                     )
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, _, cx| {
-                    Self::leave_call(cx);
+                .on_click(MouseButton::Left, move |_, _, cx| {
+                    let item = ClipboardItem::new(channel_link_copy.clone());
+                    cx.write_to_clipboard(item)
                 })
                 .with_tooltip::<AddContact>(
                     0,
-                    "Leave call",
+                    channel_tooltip_text.unwrap(),
                     None,
                     tooltip_style.clone(),
                     cx,
-                ),
-            ),
+                )
+            }),
             Section::Contacts => Some(
                 MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
                     render_icon_button(
@@ -1571,26 +1541,37 @@ impl CollabPanel {
                     cx,
                 ),
             ),
-            Section::Channels => Some(
-                MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
-                    render_icon_button(
-                        theme
-                            .collab_panel
-                            .add_contact_button
-                            .style_for(is_selected, state),
-                        "icons/plus.svg",
-                    )
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
-                .with_tooltip::<AddChannel>(
-                    0,
-                    "Create a channel",
-                    None,
-                    tooltip_style.clone(),
-                    cx,
-                ),
-            ),
+            Section::Channels => {
+                if cx
+                    .global::<DragAndDrop<Workspace>>()
+                    .currently_dragged::<Channel>(cx.window())
+                    .is_some()
+                    && self.drag_target_channel == ChannelDragTarget::Root
+                {
+                    is_dragged_over = true;
+                }
+
+                Some(
+                    MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
+                        render_icon_button(
+                            theme
+                                .collab_panel
+                                .add_contact_button
+                                .style_for(is_selected, state),
+                            "icons/plus.svg",
+                        )
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
+                    .with_tooltip::<AddChannel>(
+                        0,
+                        "Create a channel",
+                        None,
+                        tooltip_style.clone(),
+                        cx,
+                    ),
+                )
+            }
             _ => None,
         };
 
@@ -1633,6 +1614,21 @@ impl CollabPanel {
                             theme.collab_panel.contact_username.container.margin.left,
                         ),
                     )
+                } else if let Some(channel_icon) = channel_icon {
+                    Some(
+                        Svg::new(channel_icon)
+                            .with_color(header_style.text.color)
+                            .constrained()
+                            .with_max_width(icon_size)
+                            .with_max_height(icon_size)
+                            .aligned()
+                            .constrained()
+                            .with_width(icon_size)
+                            .contained()
+                            .with_margin_right(
+                                theme.collab_panel.contact_username.container.margin.left,
+                            ),
+                    )
                 } else {
                     None
                 })
@@ -1646,9 +1642,37 @@ impl CollabPanel {
                 .constrained()
                 .with_height(theme.collab_panel.row_height)
                 .contained()
-                .with_style(header_style.container)
+                .with_style(if is_dragged_over {
+                    theme.collab_panel.dragged_over_header
+                } else {
+                    header_style.container
+                })
         });
 
+        result = result
+            .on_move(move |_, this, cx| {
+                if cx
+                    .global::<DragAndDrop<Workspace>>()
+                    .currently_dragged::<Channel>(cx.window())
+                    .is_some()
+                {
+                    this.drag_target_channel = ChannelDragTarget::Root;
+                    cx.notify()
+                }
+            })
+            .on_up(MouseButton::Left, move |_, this, cx| {
+                if let Some((_, dragged_channel)) = cx
+                    .global::<DragAndDrop<Workspace>>()
+                    .currently_dragged::<Channel>(cx.window())
+                {
+                    this.channel_store
+                        .update(cx, |channel_store, cx| {
+                            channel_store.move_channel(dragged_channel.id, None, cx)
+                        })
+                        .detach_and_log_err(cx)
+                }
+            });
+
         if can_collapse {
             result = result
                 .with_cursor_style(CursorStyle::PointingHand)
@@ -1899,20 +1923,25 @@ impl CollabPanel {
         &self,
         channel: &Channel,
         depth: usize,
-        path: ChannelPath,
         theme: &theme::Theme,
         is_selected: bool,
+        has_children: bool,
         ix: usize,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
         let channel_id = channel.id;
         let collab_theme = &theme.collab_panel;
-        let has_children = self.channel_store.read(cx).has_children(channel_id);
-        let other_selected =
-            self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
-        let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
-
-        let is_active = iife!({
+        let is_public = self
+            .channel_store
+            .read(cx)
+            .channel_for_id(channel_id)
+            .map(|channel| channel.visibility)
+            == Some(proto::ChannelVisibility::Public);
+        let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
+        let disclosed =
+            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
+
+        let is_active = maybe!({
             let call_channel = ActiveCall::global(cx)
                 .read(cx)
                 .room()?
@@ -1933,13 +1962,9 @@ impl CollabPanel {
         let mut is_dragged_over = false;
         if cx
             .global::<DragAndDrop<Workspace>>()
-            .currently_dragged::<DraggedChannel>(cx.window())
+            .currently_dragged::<Channel>(cx.window())
             .is_some()
-            && self
-                .drag_target_channel
-                .as_ref()
-                .filter(|(_, dragged_path)| path.starts_with(dragged_path))
-                .is_some()
+            && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
         {
             is_dragged_over = true;
         }
@@ -1965,12 +1990,16 @@ impl CollabPanel {
 
             Flex::<Self>::row()
                 .with_child(
-                    Svg::new("icons/hash.svg")
-                        .with_color(collab_theme.channel_hash.color)
-                        .constrained()
-                        .with_width(collab_theme.channel_hash.width)
-                        .aligned()
-                        .left(),
+                    Svg::new(if is_public {
+                        "icons/public.svg"
+                    } else {
+                        "icons/hash.svg"
+                    })
+                    .with_color(collab_theme.channel_hash.color)
+                    .constrained()
+                    .with_width(collab_theme.channel_hash.width)
+                    .aligned()
+                    .left(),
                 )
                 .with_child({
                     let style = collab_theme.channel_name.inactive_state();
@@ -2118,7 +2147,7 @@ impl CollabPanel {
                 .disclosable(
                     disclosed,
                     Box::new(ToggleCollapse {
-                        location: path.clone(),
+                        location: channel.id.clone(),
                     }),
                 )
                 .with_id(ix)
@@ -2138,7 +2167,7 @@ impl CollabPanel {
                 )
         })
         .on_click(MouseButton::Left, move |_, this, cx| {
-            if this.drag_target_channel.take().is_none() {
+            if this.drag_target_channel == ChannelDragTarget::None {
                 if is_active {
                     this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
                 } else {
@@ -2147,76 +2176,43 @@ impl CollabPanel {
             }
         })
         .on_click(MouseButton::Right, {
-            let path = path.clone();
+            let channel = channel.clone();
             move |e, this, cx| {
-                this.deploy_channel_context_menu(Some(e.position), &path, ix, cx);
+                this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
             }
         })
-        .on_up(MouseButton::Left, move |e, this, cx| {
+        .on_up(MouseButton::Left, move |_, this, cx| {
             if let Some((_, dragged_channel)) = cx
                 .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<DraggedChannel>(cx.window())
+                .currently_dragged::<Channel>(cx.window())
             {
-                if e.modifiers.alt {
-                    this.channel_store.update(cx, |channel_store, cx| {
-                        channel_store
-                            .link_channel(dragged_channel.0.id, channel_id, cx)
-                            .detach_and_log_err(cx)
+                this.channel_store
+                    .update(cx, |channel_store, cx| {
+                        channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
                     })
-                } else {
-                    this.channel_store.update(cx, |channel_store, cx| {
-                        match dragged_channel.1 {
-                            Some(parent_id) => channel_store.move_channel(
-                                dragged_channel.0.id,
-                                parent_id,
-                                channel_id,
-                                cx,
-                            ),
-                            None => {
-                                channel_store.link_channel(dragged_channel.0.id, channel_id, cx)
-                            }
-                        }
-                        .detach_and_log_err(cx)
-                    })
-                }
+                    .detach_and_log_err(cx)
             }
         })
         .on_move({
             let channel = channel.clone();
-            let path = path.clone();
             move |_, this, cx| {
-                if let Some((_, _dragged_channel)) =
-                    cx.global::<DragAndDrop<Workspace>>()
-                        .currently_dragged::<DraggedChannel>(cx.window())
+                if let Some((_, dragged_channel)) = cx
+                    .global::<DragAndDrop<Workspace>>()
+                    .currently_dragged::<Channel>(cx.window())
                 {
-                    match &this.drag_target_channel {
-                        Some(current_target)
-                            if current_target.0 == channel && current_target.1 == path =>
-                        {
-                            return
-                        }
-                        _ => {
-                            this.drag_target_channel = Some((channel.clone(), path.clone()));
-                            cx.notify();
-                        }
+                    if channel.id != dragged_channel.id {
+                        this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
                     }
+                    cx.notify()
                 }
             }
         })
-        .as_draggable(
-            (channel.clone(), path.parent_id()),
-            move |modifiers, (channel, _), cx: &mut ViewContext<Workspace>| {
+        .as_draggable::<_, Channel>(
+            channel.clone(),
+            move |_, channel, cx: &mut ViewContext<Workspace>| {
                 let theme = &theme::current(cx).collab_panel;
 
                 Flex::<Workspace>::row()
-                    .with_children(modifiers.alt.then(|| {
-                        Svg::new("icons/plus.svg")
-                            .with_color(theme.channel_hash.color)
-                            .constrained()
-                            .with_width(theme.channel_hash.width)
-                            .aligned()
-                            .left()
-                    }))
                     .with_child(
                         Svg::new("icons/hash.svg")
                             .with_color(theme.channel_hash.color)
@@ -2275,7 +2271,7 @@ impl CollabPanel {
                 .with_child(render_tree_branch(
                     tree_branch,
                     &row.name.text,
-                    true,
+                    false,
                     vec2f(host_avatar_width, theme.row_height),
                     cx.font_cache(),
                 ))
@@ -2308,6 +2304,62 @@ impl CollabPanel {
         .into_any()
     }
 
+    fn render_channel_chat(
+        &self,
+        channel_id: ChannelId,
+        theme: &theme::CollabPanel,
+        is_selected: bool,
+        ix: usize,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum ChannelChat {}
+        let host_avatar_width = theme
+            .contact_avatar
+            .width
+            .or(theme.contact_avatar.height)
+            .unwrap_or(0.);
+
+        MouseEventHandler::new::<ChannelChat, _>(ix 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/conversations.svg")
+                        .with_color(theme.channel_hash.color)
+                        .constrained()
+                        .with_width(theme.channel_hash.width)
+                        .aligned()
+                        .left(),
+                )
+                .with_child(
+                    Label::new("chat", 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.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .into_any()
+    }
+
     fn render_channel_invite(
         channel: Arc<Channel>,
         channel_store: ModelHandle<ChannelStore>,

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

@@ -1,12 +1,16 @@
 use channel::{ChannelId, ChannelMembership, ChannelStore};
-use client::{proto, User, UserId, UserStore};
+use client::{
+    proto::{self, ChannelRole, ChannelVisibility},
+    User, UserId, UserStore,
+};
 use context_menu::{ContextMenu, ContextMenuItem};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
     actions,
     elements::*,
     platform::{CursorStyle, MouseButton},
-    AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+    AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
+    ViewHandle,
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::sync::Arc;
@@ -96,11 +100,14 @@ impl ChannelModal {
         let channel_id = self.channel_id;
         cx.spawn(|this, mut cx| async move {
             if mode == Mode::ManageMembers {
-                let members = channel_store
+                let mut members = channel_store
                     .update(&mut cx, |channel_store, cx| {
                         channel_store.get_channel_member_details(channel_id, cx)
                     })
                     .await?;
+
+                members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
+
                 this.update(&mut cx, |this, cx| {
                     this.picker
                         .update(cx, |picker, _| picker.delegate_mut().members = members);
@@ -182,6 +189,81 @@ impl View for ChannelModal {
             .into_any()
         }
 
+        fn render_visibility(
+            channel_id: ChannelId,
+            visibility: ChannelVisibility,
+            theme: &theme::TabbedModal,
+            cx: &mut ViewContext<ChannelModal>,
+        ) -> AnyElement<ChannelModal> {
+            enum TogglePublic {}
+
+            if visibility == ChannelVisibility::Members {
+                return Flex::row()
+                    .with_child(
+                        MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+                            let style = theme.visibility_toggle.style_for(state);
+                            Label::new(format!("{}", "Public access: OFF"), style.text.clone())
+                                .contained()
+                                .with_style(style.container.clone())
+                        })
+                        .on_click(MouseButton::Left, move |_, this, cx| {
+                            this.channel_store
+                                .update(cx, |channel_store, cx| {
+                                    channel_store.set_channel_visibility(
+                                        channel_id,
+                                        ChannelVisibility::Public,
+                                        cx,
+                                    )
+                                })
+                                .detach_and_log_err(cx);
+                        })
+                        .with_cursor_style(CursorStyle::PointingHand),
+                    )
+                    .into_any();
+            }
+
+            Flex::row()
+                .with_child(
+                    MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+                        let style = theme.visibility_toggle.style_for(state);
+                        Label::new(format!("{}", "Public access: ON"), style.text.clone())
+                            .contained()
+                            .with_style(style.container.clone())
+                    })
+                    .on_click(MouseButton::Left, move |_, this, cx| {
+                        this.channel_store
+                            .update(cx, |channel_store, cx| {
+                                channel_store.set_channel_visibility(
+                                    channel_id,
+                                    ChannelVisibility::Members,
+                                    cx,
+                                )
+                            })
+                            .detach_and_log_err(cx);
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand),
+                )
+                .with_spacing(14.0)
+                .with_child(
+                    MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
+                        let style = theme.channel_link.style_for(state);
+                        Label::new(format!("{}", "copy link"), style.text.clone())
+                            .contained()
+                            .with_style(style.container.clone())
+                    })
+                    .on_click(MouseButton::Left, move |_, this, cx| {
+                        if let Some(channel) =
+                            this.channel_store.read(cx).channel_for_id(channel_id)
+                        {
+                            let item = ClipboardItem::new(channel.link());
+                            cx.write_to_clipboard(item);
+                        }
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand),
+                )
+                .into_any()
+        }
+
         Flex::column()
             .with_child(
                 Flex::column()
@@ -190,6 +272,7 @@ impl View for ChannelModal {
                             .contained()
                             .with_style(theme.title.container.clone()),
                     )
+                    .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
                     .with_child(Flex::row().with_children([
                         render_mode_button::<InviteMembers>(
                             Mode::InviteMembers,
@@ -343,9 +426,11 @@ impl PickerDelegate for ChannelModalDelegate {
     }
 
     fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
-        if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
+        if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
             match self.mode {
-                Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
+                Mode::ManageMembers => {
+                    self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
+                }
                 Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
                     Some(proto::channel_member::Kind::Invitee) => {
                         self.remove_selected_member(cx);
@@ -373,7 +458,7 @@ impl PickerDelegate for ChannelModalDelegate {
         let full_theme = &theme::current(cx);
         let theme = &full_theme.collab_panel.channel_modal;
         let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
-        let (user, admin) = self.user_at_index(ix).unwrap();
+        let (user, role) = self.user_at_index(ix).unwrap();
         let request_status = self.member_status(user.id, cx);
 
         let style = tabbed_modal
@@ -409,15 +494,25 @@ impl PickerDelegate for ChannelModalDelegate {
                     },
                 )
             })
-            .with_children(admin.and_then(|admin| {
-                (in_manage && admin).then(|| {
+            .with_children(if in_manage && role == Some(ChannelRole::Admin) {
+                Some(
                     Label::new("Admin", theme.member_tag.text.clone())
                         .contained()
                         .with_style(theme.member_tag.container)
                         .aligned()
-                        .left()
-                })
-            }))
+                        .left(),
+                )
+            } else if in_manage && role == Some(ChannelRole::Guest) {
+                Some(
+                    Label::new("Guest", theme.member_tag.text.clone())
+                        .contained()
+                        .with_style(theme.member_tag.container)
+                        .aligned()
+                        .left(),
+                )
+            } else {
+                None
+            })
             .with_children({
                 let svg = match self.mode {
                     Mode::ManageMembers => Some(
@@ -502,13 +597,13 @@ impl ChannelModalDelegate {
             })
     }
 
-    fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
+    fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
         match self.mode {
             Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
                 let channel_membership = self.members.get(*ix)?;
                 Some((
                     channel_membership.user.clone(),
-                    Some(channel_membership.admin),
+                    Some(channel_membership.role),
                 ))
             }),
             Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
@@ -516,17 +611,21 @@ impl ChannelModalDelegate {
     }
 
     fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
-        let (user, admin) = self.user_at_index(self.selected_index)?;
-        let admin = !admin.unwrap_or(false);
+        let (user, role) = self.user_at_index(self.selected_index)?;
+        let new_role = if role == Some(ChannelRole::Admin) {
+            ChannelRole::Member
+        } else {
+            ChannelRole::Admin
+        };
         let update = self.channel_store.update(cx, |store, cx| {
-            store.set_member_admin(self.channel_id, user.id, admin, cx)
+            store.set_member_role(self.channel_id, user.id, new_role, cx)
         });
         cx.spawn(|picker, mut cx| async move {
             update.await?;
             picker.update(&mut cx, |picker, cx| {
                 let this = picker.delegate_mut();
                 if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
-                    member.admin = admin;
+                    member.role = new_role;
                 }
                 cx.focus_self();
                 cx.notify();
@@ -572,25 +671,30 @@ impl ChannelModalDelegate {
 
     fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
         let invite_member = self.channel_store.update(cx, |store, cx| {
-            store.invite_member(self.channel_id, user.id, false, cx)
+            store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
         });
 
         cx.spawn(|this, mut cx| async move {
             invite_member.await?;
 
             this.update(&mut cx, |this, cx| {
-                this.delegate_mut().members.push(ChannelMembership {
+                let new_member = ChannelMembership {
                     user,
                     kind: proto::channel_member::Kind::Invitee,
-                    admin: false,
-                });
+                    role: ChannelRole::Member,
+                };
+                let members = &mut this.delegate_mut().members;
+                match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
+                    Ok(ix) | Err(ix) => members.insert(ix, new_member),
+                }
+
                 cx.notify();
             })
         })
         .detach_and_log_err(cx);
     }
 
-    fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
+    fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
         self.context_menu.update(cx, |context_menu, cx| {
             context_menu.show(
                 Default::default(),
@@ -598,7 +702,7 @@ impl ChannelModalDelegate {
                 vec![
                     ContextMenuItem::action("Remove", RemoveMember),
                     ContextMenuItem::action(
-                        if user_is_admin {
+                        if role == ChannelRole::Admin {
                             "Make non-admin"
                         } else {
                             "Make admin"

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -1,10 +1,10 @@
 use crate::{
-    contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
-    toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
+    face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall,
+    ToggleDeafen, ToggleMute, ToggleScreenSharing,
 };
 use auto_update::AutoUpdateStatus;
 use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
+use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore};
 use clock::ReplicaId;
 use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
@@ -88,8 +88,10 @@ impl View for CollabTitlebarItem {
             .zip(peer_id)
             .zip(ActiveCall::global(cx).read(cx).room().cloned())
         {
-            right_container
-                .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
+            if room.read(cx).can_publish() {
+                right_container
+                    .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
+            }
             right_container.add_child(self.render_leave_call(&theme, cx));
             let muted = room.read(cx).is_muted(cx);
             let speaking = room.read(cx).is_speaking();
@@ -97,9 +99,14 @@ impl View for CollabTitlebarItem {
                 self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
             );
             left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
-            right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
+            if room.read(cx).can_publish() {
+                right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
+            }
             right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
-            right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
+            if room.read(cx).can_publish() {
+                right_container
+                    .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
+            }
         }
 
         let status = workspace.read(cx).client().status();
@@ -151,28 +158,6 @@ impl CollabTitlebarItem {
             this.window_activation_changed(active, cx)
         }));
         subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
-        subscriptions.push(
-            cx.subscribe(&user_store, move |this, user_store, event, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    workspace.update(cx, |workspace, cx| {
-                        if let client::Event::Contact { user, kind } = event {
-                            if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
-                                workspace.show_notification(user.id as usize, cx, |cx| {
-                                    cx.add_view(|cx| {
-                                        ContactNotification::new(
-                                            user.clone(),
-                                            *kind,
-                                            user_store,
-                                            cx,
-                                        )
-                                    })
-                                })
-                            }
-                        }
-                    });
-                }
-            }),
-        );
 
         Self {
             workspace: workspace.weak_handle(),
@@ -488,7 +473,11 @@ impl CollabTitlebarItem {
     pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
         if self.branch_popover.take().is_none() {
             if let Some(workspace) = self.workspace.upgrade(cx) {
-                let view = cx.add_view(|cx| build_branch_list(workspace, cx));
+                let Some(view) =
+                    cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
+                else {
+                    return;
+                };
                 cx.subscribe(&view, |this, _, event, cx| {
                     match event {
                         PickerEvent::Dismiss => {

crates/collab_ui/src/collab_ui.rs 🔗

@@ -2,30 +2,32 @@ pub mod channel_view;
 pub mod chat_panel;
 pub mod collab_panel;
 mod collab_titlebar_item;
-mod contact_notification;
 mod face_pile;
-mod incoming_call_notification;
-mod notifications;
+pub mod notification_panel;
+pub mod notifications;
 mod panel_settings;
-pub mod project_shared_notification;
-mod sharing_status_indicator;
 
 use call::{report_call_event_for_room, ActiveCall, Room};
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
 use gpui::{
     actions,
+    elements::{ContainerStyle, Empty, Image},
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
     platform::{Screen, WindowBounds, WindowKind, WindowOptions},
-    AppContext, Task,
+    AnyElement, AppContext, Element, ImageData, Task,
 };
 use std::{rc::Rc, sync::Arc};
+use theme::AvatarStyle;
 use util::ResultExt;
 use workspace::AppState;
 
 pub use collab_titlebar_item::CollabTitlebarItem;
-pub use panel_settings::{ChatPanelSettings, CollaborationPanelSettings};
+pub use panel_settings::{
+    ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
+};
 
 actions!(
     collab,
@@ -35,14 +37,13 @@ actions!(
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     settings::register::<CollaborationPanelSettings>(cx);
     settings::register::<ChatPanelSettings>(cx);
+    settings::register::<NotificationPanelSettings>(cx);
 
     vcs_menu::init(cx);
     collab_titlebar_item::init(cx);
     collab_panel::init(cx);
     chat_panel::init(cx);
-    incoming_call_notification::init(&app_state, cx);
-    project_shared_notification::init(&app_state, cx);
-    sharing_status_indicator::init(cx);
+    notifications::init(&app_state, cx);
 
     cx.add_global_action(toggle_screen_sharing);
     cx.add_global_action(toggle_mute);
@@ -130,3 +131,35 @@ fn notification_window_options(
         screen: Some(screen),
     }
 }
+
+fn render_avatar<T: 'static>(
+    avatar: Option<Arc<ImageData>>,
+    avatar_style: &AvatarStyle,
+    container: ContainerStyle,
+) -> AnyElement<T> {
+    avatar
+        .map(|avatar| {
+            Image::from_data(avatar)
+                .with_style(avatar_style.image)
+                .aligned()
+                .contained()
+                .with_corner_radius(avatar_style.outer_corner_radius)
+                .constrained()
+                .with_width(avatar_style.outer_width)
+                .with_height(avatar_style.outer_width)
+                .into_any()
+        })
+        .unwrap_or_else(|| {
+            Empty::new()
+                .constrained()
+                .with_width(avatar_style.outer_width)
+                .into_any()
+        })
+        .contained()
+        .with_style(container)
+        .into_any()
+}
+
+fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
+    cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
+}

crates/collab_ui/src/contact_notification.rs 🔗

@@ -1,121 +0,0 @@
-use std::sync::Arc;
-
-use crate::notifications::render_user_notification;
-use client::{ContactEventKind, User, UserStore};
-use gpui::{elements::*, Entity, ModelHandle, View, ViewContext};
-use workspace::notifications::Notification;
-
-pub struct ContactNotification {
-    user_store: ModelHandle<UserStore>,
-    user: Arc<User>,
-    kind: client::ContactEventKind,
-}
-
-#[derive(Clone, PartialEq)]
-struct Dismiss(u64);
-
-#[derive(Clone, PartialEq)]
-pub struct RespondToContactRequest {
-    pub user_id: u64,
-    pub accept: bool,
-}
-
-pub enum Event {
-    Dismiss,
-}
-
-impl Entity for ContactNotification {
-    type Event = Event;
-}
-
-impl View for ContactNotification {
-    fn ui_name() -> &'static str {
-        "ContactNotification"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        match self.kind {
-            ContactEventKind::Requested => render_user_notification(
-                self.user.clone(),
-                "wants to add you as a contact",
-                Some("They won't be alerted if you decline."),
-                |notification, cx| notification.dismiss(cx),
-                vec![
-                    (
-                        "Decline",
-                        Box::new(|notification, cx| {
-                            notification.respond_to_contact_request(false, cx)
-                        }),
-                    ),
-                    (
-                        "Accept",
-                        Box::new(|notification, cx| {
-                            notification.respond_to_contact_request(true, cx)
-                        }),
-                    ),
-                ],
-                cx,
-            ),
-            ContactEventKind::Accepted => render_user_notification(
-                self.user.clone(),
-                "accepted your contact request",
-                None,
-                |notification, cx| notification.dismiss(cx),
-                vec![],
-                cx,
-            ),
-            _ => unreachable!(),
-        }
-    }
-}
-
-impl Notification for ContactNotification {
-    fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
-        matches!(event, Event::Dismiss)
-    }
-}
-
-impl ContactNotification {
-    pub fn new(
-        user: Arc<User>,
-        kind: client::ContactEventKind,
-        user_store: ModelHandle<UserStore>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        cx.subscribe(&user_store, move |this, _, event, cx| {
-            if let client::Event::Contact {
-                kind: ContactEventKind::Cancelled,
-                user,
-            } = event
-            {
-                if user.id == this.user.id {
-                    cx.emit(Event::Dismiss);
-                }
-            }
-        })
-        .detach();
-
-        Self {
-            user,
-            kind,
-            user_store,
-        }
-    }
-
-    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
-        self.user_store.update(cx, |store, cx| {
-            store
-                .dismiss_contact_request(self.user.id, cx)
-                .detach_and_log_err(cx);
-        });
-        cx.emit(Event::Dismiss);
-    }
-
-    fn respond_to_contact_request(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
-        self.user_store
-            .update(cx, |store, cx| {
-                store.respond_to_contact_request(self.user.id, accept, cx)
-            })
-            .detach();
-    }
-}

crates/collab_ui/src/notification_panel.rs 🔗

@@ -0,0 +1,884 @@
+use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
+use anyhow::Result;
+use channel::ChannelStore;
+use client::{Client, Notification, User, UserStore};
+use collections::HashMap;
+use db::kvp::KEY_VALUE_STORE;
+use futures::StreamExt;
+use gpui::{
+    actions,
+    elements::*,
+    platform::{CursorStyle, MouseButton},
+    serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+};
+use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
+use project::Fs;
+use rpc::proto;
+use serde::{Deserialize, Serialize};
+use settings::SettingsStore;
+use std::{sync::Arc, time::Duration};
+use theme::{ui, Theme};
+use time::{OffsetDateTime, UtcOffset};
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel},
+    Workspace,
+};
+
+const LOADING_THRESHOLD: usize = 30;
+const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
+const TOAST_DURATION: Duration = Duration::from_secs(5);
+const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
+
+pub struct NotificationPanel {
+    client: Arc<Client>,
+    user_store: ModelHandle<UserStore>,
+    channel_store: ModelHandle<ChannelStore>,
+    notification_store: ModelHandle<NotificationStore>,
+    fs: Arc<dyn Fs>,
+    width: Option<f32>,
+    active: bool,
+    notification_list: ListState<Self>,
+    pending_serialization: Task<Option<()>>,
+    subscriptions: Vec<gpui::Subscription>,
+    workspace: WeakViewHandle<Workspace>,
+    current_notification_toast: Option<(u64, Task<()>)>,
+    local_timezone: UtcOffset,
+    has_focus: bool,
+    mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedNotificationPanel {
+    width: Option<f32>,
+}
+
+#[derive(Debug)]
+pub enum Event {
+    DockPositionChanged,
+    Focus,
+    Dismissed,
+}
+
+pub struct NotificationPresenter {
+    pub actor: Option<Arc<client::User>>,
+    pub text: String,
+    pub icon: &'static str,
+    pub needs_response: bool,
+    pub can_navigate: bool,
+}
+
+actions!(notification_panel, [ToggleFocus]);
+
+pub fn init(_cx: &mut AppContext) {}
+
+impl NotificationPanel {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+        let fs = workspace.app_state().fs.clone();
+        let client = workspace.app_state().client.clone();
+        let user_store = workspace.app_state().user_store.clone();
+        let workspace_handle = workspace.weak_handle();
+
+        cx.add_view(|cx| {
+            let mut status = client.status();
+            cx.spawn(|this, mut cx| async move {
+                while let Some(_) = status.next().await {
+                    if this
+                        .update(&mut cx, |_, cx| {
+                            cx.notify();
+                        })
+                        .is_err()
+                    {
+                        break;
+                    }
+                }
+            })
+            .detach();
+
+            let mut notification_list =
+                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
+                    this.render_notification(ix, cx)
+                        .unwrap_or_else(|| Empty::new().into_any())
+                });
+            notification_list.set_scroll_handler(|visible_range, count, this, cx| {
+                if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
+                    if let Some(task) = this
+                        .notification_store
+                        .update(cx, |store, cx| store.load_more_notifications(false, cx))
+                    {
+                        task.detach();
+                    }
+                }
+            });
+
+            let mut this = Self {
+                fs,
+                client,
+                user_store,
+                local_timezone: cx.platform().local_timezone(),
+                channel_store: ChannelStore::global(cx),
+                notification_store: NotificationStore::global(cx),
+                notification_list,
+                pending_serialization: Task::ready(None),
+                workspace: workspace_handle,
+                has_focus: false,
+                current_notification_toast: None,
+                subscriptions: Vec::new(),
+                active: false,
+                mark_as_read_tasks: HashMap::default(),
+                width: None,
+            };
+
+            let mut old_dock_position = this.position(cx);
+            this.subscriptions.extend([
+                cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
+                cx.subscribe(&this.notification_store, Self::on_notification_event),
+                cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
+                    let new_dock_position = this.position(cx);
+                    if new_dock_position != old_dock_position {
+                        old_dock_position = new_dock_position;
+                        cx.emit(Event::DockPositionChanged);
+                    }
+                    cx.notify();
+                }),
+            ]);
+            this
+        })
+    }
+
+    pub fn load(
+        workspace: WeakViewHandle<Workspace>,
+        cx: AsyncAppContext,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        cx.spawn(|mut cx| async move {
+            let serialized_panel = if let Some(panel) = cx
+                .background()
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
+            } else {
+                None
+            };
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = Self::new(workspace, cx);
+                if let Some(serialized_panel) = serialized_panel {
+                    panel.update(cx, |panel, cx| {
+                        panel.width = serialized_panel.width;
+                        cx.notify();
+                    });
+                }
+                panel
+            })
+        })
+    }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        self.pending_serialization = cx.background().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        NOTIFICATION_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedNotificationPanel { width })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+
+    fn render_notification(
+        &mut self,
+        ix: usize,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<AnyElement<Self>> {
+        let entry = self.notification_store.read(cx).notification_at(ix)?;
+        let notification_id = entry.id;
+        let now = OffsetDateTime::now_utc();
+        let timestamp = entry.timestamp;
+        let NotificationPresenter {
+            actor,
+            text,
+            needs_response,
+            can_navigate,
+            ..
+        } = self.present_notification(entry, cx)?;
+
+        let theme = theme::current(cx);
+        let style = &theme.notification_panel;
+        let response = entry.response;
+        let notification = entry.notification.clone();
+
+        let message_style = if entry.is_read {
+            style.read_text.clone()
+        } else {
+            style.unread_text.clone()
+        };
+
+        if self.active && !entry.is_read {
+            self.did_render_notification(notification_id, &notification, cx);
+        }
+
+        enum Decline {}
+        enum Accept {}
+
+        Some(
+            MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
+                let container = message_style.container;
+
+                Flex::row()
+                    .with_children(actor.map(|actor| {
+                        render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
+                    }))
+                    .with_child(
+                        Flex::column()
+                            .with_child(Text::new(text, message_style.text.clone()))
+                            .with_child(
+                                Flex::row()
+                                    .with_child(
+                                        Label::new(
+                                            format_timestamp(timestamp, now, self.local_timezone),
+                                            style.timestamp.text.clone(),
+                                        )
+                                        .contained()
+                                        .with_style(style.timestamp.container),
+                                    )
+                                    .with_children(if let Some(is_accepted) = response {
+                                        Some(
+                                            Label::new(
+                                                if is_accepted {
+                                                    "You accepted"
+                                                } else {
+                                                    "You declined"
+                                                },
+                                                style.read_text.text.clone(),
+                                            )
+                                            .flex_float()
+                                            .into_any(),
+                                        )
+                                    } else if needs_response {
+                                        Some(
+                                            Flex::row()
+                                                .with_children([
+                                                    MouseEventHandler::new::<Decline, _>(
+                                                        ix,
+                                                        cx,
+                                                        |state, _| {
+                                                            let button =
+                                                                style.button.style_for(state);
+                                                            Label::new(
+                                                                "Decline",
+                                                                button.text.clone(),
+                                                            )
+                                                            .contained()
+                                                            .with_style(button.container)
+                                                        },
+                                                    )
+                                                    .with_cursor_style(CursorStyle::PointingHand)
+                                                    .on_click(MouseButton::Left, {
+                                                        let notification = notification.clone();
+                                                        move |_, view, cx| {
+                                                            view.respond_to_notification(
+                                                                notification.clone(),
+                                                                false,
+                                                                cx,
+                                                            );
+                                                        }
+                                                    }),
+                                                    MouseEventHandler::new::<Accept, _>(
+                                                        ix,
+                                                        cx,
+                                                        |state, _| {
+                                                            let button =
+                                                                style.button.style_for(state);
+                                                            Label::new(
+                                                                "Accept",
+                                                                button.text.clone(),
+                                                            )
+                                                            .contained()
+                                                            .with_style(button.container)
+                                                        },
+                                                    )
+                                                    .with_cursor_style(CursorStyle::PointingHand)
+                                                    .on_click(MouseButton::Left, {
+                                                        let notification = notification.clone();
+                                                        move |_, view, cx| {
+                                                            view.respond_to_notification(
+                                                                notification.clone(),
+                                                                true,
+                                                                cx,
+                                                            );
+                                                        }
+                                                    }),
+                                                ])
+                                                .flex_float()
+                                                .into_any(),
+                                        )
+                                    } else {
+                                        None
+                                    }),
+                            )
+                            .flex(1.0, true),
+                    )
+                    .contained()
+                    .with_style(container)
+                    .into_any()
+            })
+            .with_cursor_style(if can_navigate {
+                CursorStyle::PointingHand
+            } else {
+                CursorStyle::default()
+            })
+            .on_click(MouseButton::Left, {
+                let notification = notification.clone();
+                move |_, this, cx| this.did_click_notification(&notification, cx)
+            })
+            .into_any(),
+        )
+    }
+
+    fn present_notification(
+        &self,
+        entry: &NotificationEntry,
+        cx: &AppContext,
+    ) -> Option<NotificationPresenter> {
+        let user_store = self.user_store.read(cx);
+        let channel_store = self.channel_store.read(cx);
+        match entry.notification {
+            Notification::ContactRequest { sender_id } => {
+                let requester = user_store.get_cached_user(sender_id)?;
+                Some(NotificationPresenter {
+                    icon: "icons/plus.svg",
+                    text: format!("{} wants to add you as a contact", requester.github_login),
+                    needs_response: user_store.has_incoming_contact_request(requester.id),
+                    actor: Some(requester),
+                    can_navigate: false,
+                })
+            }
+            Notification::ContactRequestAccepted { responder_id } => {
+                let responder = user_store.get_cached_user(responder_id)?;
+                Some(NotificationPresenter {
+                    icon: "icons/plus.svg",
+                    text: format!("{} accepted your contact invite", responder.github_login),
+                    needs_response: false,
+                    actor: Some(responder),
+                    can_navigate: false,
+                })
+            }
+            Notification::ChannelInvitation {
+                ref channel_name,
+                channel_id,
+                inviter_id,
+            } => {
+                let inviter = user_store.get_cached_user(inviter_id)?;
+                Some(NotificationPresenter {
+                    icon: "icons/hash.svg",
+                    text: format!(
+                        "{} invited you to join the #{channel_name} channel",
+                        inviter.github_login
+                    ),
+                    needs_response: channel_store.has_channel_invitation(channel_id),
+                    actor: Some(inviter),
+                    can_navigate: false,
+                })
+            }
+            Notification::ChannelMessageMention {
+                sender_id,
+                channel_id,
+                message_id,
+            } => {
+                let sender = user_store.get_cached_user(sender_id)?;
+                let channel = channel_store.channel_for_id(channel_id)?;
+                let message = self
+                    .notification_store
+                    .read(cx)
+                    .channel_message_for_id(message_id)?;
+                Some(NotificationPresenter {
+                    icon: "icons/conversations.svg",
+                    text: format!(
+                        "{} mentioned you in #{}:\n{}",
+                        sender.github_login, channel.name, message.body,
+                    ),
+                    needs_response: false,
+                    actor: Some(sender),
+                    can_navigate: true,
+                })
+            }
+        }
+    }
+
+    fn did_render_notification(
+        &mut self,
+        notification_id: u64,
+        notification: &Notification,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let should_mark_as_read = match notification {
+            Notification::ContactRequestAccepted { .. } => true,
+            Notification::ContactRequest { .. }
+            | Notification::ChannelInvitation { .. }
+            | Notification::ChannelMessageMention { .. } => false,
+        };
+
+        if should_mark_as_read {
+            self.mark_as_read_tasks
+                .entry(notification_id)
+                .or_insert_with(|| {
+                    let client = self.client.clone();
+                    cx.spawn(|this, mut cx| async move {
+                        cx.background().timer(MARK_AS_READ_DELAY).await;
+                        client
+                            .request(proto::MarkNotificationRead { notification_id })
+                            .await?;
+                        this.update(&mut cx, |this, _| {
+                            this.mark_as_read_tasks.remove(&notification_id);
+                        })?;
+                        Ok(())
+                    })
+                });
+        }
+    }
+
+    fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
+        if let Notification::ChannelMessageMention {
+            message_id,
+            channel_id,
+            ..
+        } = notification.clone()
+        {
+            if let Some(workspace) = self.workspace.upgrade(cx) {
+                cx.app_context().defer(move |cx| {
+                    workspace.update(cx, |workspace, cx| {
+                        if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+                            panel.update(cx, |panel, cx| {
+                                panel
+                                    .select_channel(channel_id, Some(message_id), cx)
+                                    .detach_and_log_err(cx);
+                            });
+                        }
+                    });
+                });
+            }
+        }
+    }
+
+    fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
+        if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
+            if let Some(workspace) = self.workspace.upgrade(cx) {
+                return workspace
+                    .read_with(cx, |workspace, cx| {
+                        if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
+                            return panel.read_with(cx, |panel, cx| {
+                                panel.is_scrolled_to_bottom()
+                                    && panel.active_chat().map_or(false, |chat| {
+                                        chat.read(cx).channel_id == *channel_id
+                                    })
+                            });
+                        }
+                        false
+                    })
+                    .unwrap_or_default();
+            }
+        }
+
+        false
+    }
+
+    fn render_sign_in_prompt(
+        &self,
+        theme: &Arc<Theme>,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum SignInPromptLabel {}
+
+        MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
+            Label::new(
+                "Sign in to view your notifications".to_string(),
+                theme
+                    .chat_panel
+                    .sign_in_prompt
+                    .style_for(mouse_state)
+                    .clone(),
+            )
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            let client = this.client.clone();
+            cx.spawn(|_, cx| async move {
+                client.authenticate_and_connect(true, &cx).log_err().await;
+            })
+            .detach();
+        })
+        .aligned()
+        .into_any()
+    }
+
+    fn render_empty_state(
+        &self,
+        theme: &Arc<Theme>,
+        _cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        Label::new(
+            "You have no notifications".to_string(),
+            theme.chat_panel.sign_in_prompt.default.clone(),
+        )
+        .aligned()
+        .into_any()
+    }
+
+    fn on_notification_event(
+        &mut self,
+        _: ModelHandle<NotificationStore>,
+        event: &NotificationEvent,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
+            NotificationEvent::NotificationRemoved { entry }
+            | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
+            NotificationEvent::NotificationsUpdated {
+                old_range,
+                new_count,
+            } => {
+                self.notification_list.splice(old_range.clone(), *new_count);
+                cx.notify();
+            }
+        }
+    }
+
+    fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
+        if self.is_showing_notification(&entry.notification, cx) {
+            return;
+        }
+
+        let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
+        else {
+            return;
+        };
+
+        let notification_id = entry.id;
+        self.current_notification_toast = Some((
+            notification_id,
+            cx.spawn(|this, mut cx| async move {
+                cx.background().timer(TOAST_DURATION).await;
+                this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
+                    .ok();
+            }),
+        ));
+
+        self.workspace
+            .update(cx, |workspace, cx| {
+                workspace.dismiss_notification::<NotificationToast>(0, cx);
+                workspace.show_notification(0, cx, |cx| {
+                    let workspace = cx.weak_handle();
+                    cx.add_view(|_| NotificationToast {
+                        notification_id,
+                        actor,
+                        text,
+                        workspace,
+                    })
+                })
+            })
+            .ok();
+    }
+
+    fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
+        if let Some((current_id, _)) = &self.current_notification_toast {
+            if *current_id == notification_id {
+                self.current_notification_toast.take();
+                self.workspace
+                    .update(cx, |workspace, cx| {
+                        workspace.dismiss_notification::<NotificationToast>(0, cx)
+                    })
+                    .ok();
+            }
+        }
+    }
+
+    fn respond_to_notification(
+        &mut self,
+        notification: Notification,
+        response: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.notification_store.update(cx, |store, cx| {
+            store.respond_to_notification(notification, response, cx);
+        });
+    }
+}
+
+impl Entity for NotificationPanel {
+    type Event = Event;
+}
+
+impl View for NotificationPanel {
+    fn ui_name() -> &'static str {
+        "NotificationPanel"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let theme = theme::current(cx);
+        let style = &theme.notification_panel;
+        let element = if self.client.user_id().is_none() {
+            self.render_sign_in_prompt(&theme, cx)
+        } else if self.notification_list.item_count() == 0 {
+            self.render_empty_state(&theme, cx)
+        } else {
+            Flex::column()
+                .with_child(
+                    Flex::row()
+                        .with_child(Label::new("Notifications", style.title.text.clone()))
+                        .with_child(ui::svg(&style.title_icon).flex_float())
+                        .align_children_center()
+                        .contained()
+                        .with_style(style.title.container)
+                        .constrained()
+                        .with_height(style.title_height),
+                )
+                .with_child(
+                    List::new(self.notification_list.clone())
+                        .contained()
+                        .with_style(style.list)
+                        .flex(1., true),
+                )
+                .into_any()
+        };
+        element
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_min_width(150.)
+            .into_any()
+    }
+
+    fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = true;
+    }
+
+    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
+}
+
+impl Panel for NotificationPanel {
+    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+        settings::get::<NotificationPanelSettings>(cx).dock
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<NotificationPanelSettings>(
+            self.fs.clone(),
+            cx,
+            move |settings| settings.dock = Some(position),
+        );
+    }
+
+    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+        self.active = active;
+        if self.notification_store.read(cx).notification_count() == 0 {
+            cx.emit(Event::Dismissed);
+        }
+    }
+
+    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+        (settings::get::<NotificationPanelSettings>(cx).button
+            && self.notification_store.read(cx).notification_count() > 0)
+            .then(|| "icons/bell.svg")
+    }
+
+    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+        (
+            "Notification Panel".to_string(),
+            Some(Box::new(ToggleFocus)),
+        )
+    }
+
+    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+        let count = self.notification_store.read(cx).unread_notification_count();
+        if count == 0 {
+            None
+        } else {
+            Some(count.to_string())
+        }
+    }
+
+    fn should_change_position_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::DockPositionChanged)
+    }
+
+    fn should_close_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Dismissed)
+    }
+
+    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+        self.has_focus
+    }
+
+    fn is_focus_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Focus)
+    }
+}
+
+pub struct NotificationToast {
+    notification_id: u64,
+    actor: Option<Arc<User>>,
+    text: String,
+    workspace: WeakViewHandle<Workspace>,
+}
+
+pub enum ToastEvent {
+    Dismiss,
+}
+
+impl NotificationToast {
+    fn focus_notification_panel(&self, cx: &mut AppContext) {
+        let workspace = self.workspace.clone();
+        let notification_id = self.notification_id;
+        cx.defer(move |cx| {
+            workspace
+                .update(cx, |workspace, cx| {
+                    if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
+                        panel.update(cx, |panel, cx| {
+                            let store = panel.notification_store.read(cx);
+                            if let Some(entry) = store.notification_for_id(notification_id) {
+                                panel.did_click_notification(&entry.clone().notification, cx);
+                            }
+                        });
+                    }
+                })
+                .ok();
+        })
+    }
+}
+
+impl Entity for NotificationToast {
+    type Event = ToastEvent;
+}
+
+impl View for NotificationToast {
+    fn ui_name() -> &'static str {
+        "ContactNotification"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let user = self.actor.clone();
+        let theme = theme::current(cx).clone();
+        let theme = &theme.contact_notification;
+
+        MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
+            Flex::row()
+                .with_children(user.and_then(|user| {
+                    Some(
+                        Image::from_data(user.avatar.clone()?)
+                            .with_style(theme.header_avatar)
+                            .aligned()
+                            .constrained()
+                            .with_height(
+                                cx.font_cache()
+                                    .line_height(theme.header_message.text.font_size),
+                            )
+                            .aligned()
+                            .top(),
+                    )
+                }))
+                .with_child(
+                    Text::new(self.text.clone(), theme.header_message.text.clone())
+                        .contained()
+                        .with_style(theme.header_message.container)
+                        .aligned()
+                        .top()
+                        .left()
+                        .flex(1., true),
+                )
+                .with_child(
+                    MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
+                        let style = theme.dismiss_button.style_for(state);
+                        Svg::new("icons/x.svg")
+                            .with_color(style.color)
+                            .constrained()
+                            .with_width(style.icon_width)
+                            .aligned()
+                            .contained()
+                            .with_style(style.container)
+                            .constrained()
+                            .with_width(style.button_width)
+                            .with_height(style.button_width)
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .with_padding(Padding::uniform(5.))
+                    .on_click(MouseButton::Left, move |_, _, cx| {
+                        cx.emit(ToastEvent::Dismiss)
+                    })
+                    .aligned()
+                    .constrained()
+                    .with_height(
+                        cx.font_cache()
+                            .line_height(theme.header_message.text.font_size),
+                    )
+                    .aligned()
+                    .top()
+                    .flex_float(),
+                )
+                .contained()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            this.focus_notification_panel(cx);
+            cx.emit(ToastEvent::Dismiss);
+        })
+        .into_any()
+    }
+}
+
+impl workspace::notifications::Notification for NotificationToast {
+    fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
+        matches!(event, ToastEvent::Dismiss)
+    }
+}
+
+fn format_timestamp(
+    mut timestamp: OffsetDateTime,
+    mut now: OffsetDateTime,
+    local_timezone: UtcOffset,
+) -> String {
+    timestamp = timestamp.to_offset(local_timezone);
+    now = now.to_offset(local_timezone);
+
+    let today = now.date();
+    let date = timestamp.date();
+    if date == today {
+        let difference = now - timestamp;
+        if difference >= Duration::from_secs(3600) {
+            format!("{}h", difference.whole_seconds() / 3600)
+        } else if difference >= Duration::from_secs(60) {
+            format!("{}m", difference.whole_seconds() / 60)
+        } else {
+            "just now".to_string()
+        }
+    } else if date.next_day() == Some(today) {
+        format!("yesterday")
+    } else {
+        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+    }
+}

crates/collab_ui/src/notifications.rs 🔗

@@ -1,110 +1,11 @@
-use client::User;
-use gpui::{
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    AnyElement, Element, ViewContext,
-};
+use gpui::AppContext;
 use std::sync::Arc;
+use workspace::AppState;
 
-enum Dismiss {}
-enum Button {}
+pub mod incoming_call_notification;
+pub mod project_shared_notification;
 
-pub fn render_user_notification<F, V: 'static>(
-    user: Arc<User>,
-    title: &'static str,
-    body: Option<&'static str>,
-    on_dismiss: F,
-    buttons: Vec<(&'static str, Box<dyn Fn(&mut V, &mut ViewContext<V>)>)>,
-    cx: &mut ViewContext<V>,
-) -> AnyElement<V>
-where
-    F: 'static + Fn(&mut V, &mut ViewContext<V>),
-{
-    let theme = theme::current(cx).clone();
-    let theme = &theme.contact_notification;
-
-    Flex::column()
-        .with_child(
-            Flex::row()
-                .with_children(user.avatar.clone().map(|avatar| {
-                    Image::from_data(avatar)
-                        .with_style(theme.header_avatar)
-                        .aligned()
-                        .constrained()
-                        .with_height(
-                            cx.font_cache()
-                                .line_height(theme.header_message.text.font_size),
-                        )
-                        .aligned()
-                        .top()
-                }))
-                .with_child(
-                    Text::new(
-                        format!("{} {}", user.github_login, title),
-                        theme.header_message.text.clone(),
-                    )
-                    .contained()
-                    .with_style(theme.header_message.container)
-                    .aligned()
-                    .top()
-                    .left()
-                    .flex(1., true),
-                )
-                .with_child(
-                    MouseEventHandler::new::<Dismiss, _>(user.id as usize, cx, |state, _| {
-                        let style = theme.dismiss_button.style_for(state);
-                        Svg::new("icons/x.svg")
-                            .with_color(style.color)
-                            .constrained()
-                            .with_width(style.icon_width)
-                            .aligned()
-                            .contained()
-                            .with_style(style.container)
-                            .constrained()
-                            .with_width(style.button_width)
-                            .with_height(style.button_width)
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .with_padding(Padding::uniform(5.))
-                    .on_click(MouseButton::Left, move |_, view, cx| on_dismiss(view, cx))
-                    .aligned()
-                    .constrained()
-                    .with_height(
-                        cx.font_cache()
-                            .line_height(theme.header_message.text.font_size),
-                    )
-                    .aligned()
-                    .top()
-                    .flex_float(),
-                )
-                .into_any_named("contact notification header"),
-        )
-        .with_children(body.map(|body| {
-            Label::new(body, theme.body_message.text.clone())
-                .contained()
-                .with_style(theme.body_message.container)
-        }))
-        .with_children(if buttons.is_empty() {
-            None
-        } else {
-            Some(
-                Flex::row()
-                    .with_children(buttons.into_iter().enumerate().map(
-                        |(ix, (message, handler))| {
-                            MouseEventHandler::new::<Button, _>(ix, cx, |state, _| {
-                                let button = theme.button.style_for(state);
-                                Label::new(message, button.text.clone())
-                                    .contained()
-                                    .with_style(button.container)
-                            })
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_click(MouseButton::Left, move |_, view, cx| handler(view, cx))
-                        },
-                    ))
-                    .aligned()
-                    .right(),
-            )
-        })
-        .contained()
-        .into_any()
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    incoming_call_notification::init(app_state, cx);
+    project_shared_notification::init(app_state, cx);
 }

crates/collab_ui/src/panel_settings.rs 🔗

@@ -18,6 +18,13 @@ pub struct ChatPanelSettings {
     pub default_width: f32,
 }
 
+#[derive(Deserialize, Debug)]
+pub struct NotificationPanelSettings {
+    pub button: bool,
+    pub dock: DockPosition,
+    pub default_width: f32,
+}
+
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct PanelSettingsContent {
     pub button: Option<bool>,
@@ -27,9 +34,7 @@ pub struct PanelSettingsContent {
 
 impl Setting for CollaborationPanelSettings {
     const KEY: Option<&'static str> = Some("collaboration_panel");
-
     type FileContent = PanelSettingsContent;
-
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],
@@ -41,9 +46,19 @@ impl Setting for CollaborationPanelSettings {
 
 impl Setting for ChatPanelSettings {
     const KEY: Option<&'static str> = Some("chat_panel");
-
     type FileContent = PanelSettingsContent;
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
 
+impl Setting for NotificationPanelSettings {
+    const KEY: Option<&'static str> = Some("notification_panel");
+    type FileContent = PanelSettingsContent;
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],

crates/collab_ui/src/sharing_status_indicator.rs 🔗

@@ -1,62 +0,0 @@
-use crate::toggle_screen_sharing;
-use call::ActiveCall;
-use gpui::{
-    color::Color,
-    elements::{MouseEventHandler, Svg},
-    platform::{Appearance, MouseButton},
-    AnyElement, AppContext, Element, Entity, View, ViewContext,
-};
-use workspace::WorkspaceSettings;
-
-pub fn init(cx: &mut AppContext) {
-    let active_call = ActiveCall::global(cx);
-
-    let mut status_indicator = None;
-    cx.observe(&active_call, move |call, cx| {
-        if let Some(room) = call.read(cx).room() {
-            if room.read(cx).is_screen_sharing() {
-                if status_indicator.is_none()
-                    && settings::get::<WorkspaceSettings>(cx).show_call_status_icon
-                {
-                    status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
-                }
-            } else if let Some(window) = status_indicator.take() {
-                window.update(cx, |cx| cx.remove_window());
-            }
-        } else if let Some(window) = status_indicator.take() {
-            window.update(cx, |cx| cx.remove_window());
-        }
-    })
-    .detach();
-}
-
-pub struct SharingStatusIndicator;
-
-impl Entity for SharingStatusIndicator {
-    type Event = ();
-}
-
-impl View for SharingStatusIndicator {
-    fn ui_name() -> &'static str {
-        "SharingStatusIndicator"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let color = match cx.window_appearance() {
-            Appearance::Light | Appearance::VibrantLight => Color::black(),
-            Appearance::Dark | Appearance::VibrantDark => Color::white(),
-        };
-
-        MouseEventHandler::new::<Self, _>(0, cx, |_, _| {
-            Svg::new("icons/desktop.svg")
-                .with_color(color)
-                .constrained()
-                .with_width(18.)
-                .aligned()
-        })
-        .on_click(MouseButton::Left, |_, _, cx| {
-            toggle_screen_sharing(&Default::default(), cx)
-        })
-        .into_any()
-    }
-}

crates/command_palette/Cargo.toml 🔗

@@ -19,6 +19,7 @@ settings = { path = "../settings" }
 util = { path = "../util" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
+zed-actions = { path = "../zed-actions" }
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }

crates/command_palette/src/command_palette.rs 🔗

@@ -6,8 +6,12 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::cmp::{self, Reverse};
-use util::ResultExt;
+use util::{
+    channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
+    ResultExt,
+};
 use workspace::Workspace;
+use zed_actions::OpenZedURL;
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(toggle_command_palette);
@@ -167,13 +171,22 @@ impl PickerDelegate for CommandPaletteDelegate {
                 )
                 .await
             };
-            let intercept_result = cx.read(|cx| {
+            let mut intercept_result = cx.read(|cx| {
                 if cx.has_global::<CommandPaletteInterceptor>() {
                     cx.global::<CommandPaletteInterceptor>()(&query, cx)
                 } else {
                     None
                 }
             });
+            if *RELEASE_CHANNEL == ReleaseChannel::Dev {
+                if parse_zed_link(&query).is_some() {
+                    intercept_result = Some(CommandInterceptResult {
+                        action: OpenZedURL { url: query.clone() }.boxed_clone(),
+                        string: query.clone(),
+                        positions: vec![],
+                    })
+                }
+            }
             if let Some(CommandInterceptResult {
                 action,
                 string,

crates/copilot/Cargo.toml 🔗

@@ -36,6 +36,7 @@ serde.workspace = true
 serde_derive.workspace = true
 smol.workspace = true
 futures.workspace = true
+parking_lot.workspace = true
 
 [dev-dependencies]
 clock = { path = "../clock" }

crates/copilot/src/copilot.rs 🔗

@@ -16,6 +16,7 @@ use language::{
 };
 use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
 use node_runtime::NodeRuntime;
+use parking_lot::Mutex;
 use request::StatusNotification;
 use settings::SettingsStore;
 use smol::{fs, io::BufReader, stream::StreamExt};
@@ -387,8 +388,15 @@ impl Copilot {
                     path: node_path,
                     arguments,
                 };
-                let server =
-                    LanguageServer::new(new_server_id, binary, Path::new("/"), None, cx.clone())?;
+
+                let server = LanguageServer::new(
+                    Arc::new(Mutex::new(None)),
+                    new_server_id,
+                    binary,
+                    Path::new("/"),
+                    None,
+                    cx.clone(),
+                )?;
 
                 server
                     .on_notification::<StatusNotification, _>(

crates/db/src/db.rs 🔗

@@ -20,7 +20,7 @@ use std::future::Future;
 use std::path::{Path, PathBuf};
 use std::sync::atomic::{AtomicBool, Ordering};
 use util::channel::ReleaseChannel;
-use util::{async_iife, ResultExt};
+use util::{async_maybe, ResultExt};
 
 const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
     PRAGMA foreign_keys=TRUE;
@@ -57,7 +57,7 @@ pub async fn open_db<M: Migrator + 'static>(
     let release_channel_name = release_channel.dev_name();
     let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
 
-    let connection = async_iife!({
+    let connection = async_maybe!({
         smol::fs::create_dir_all(&main_db_dir)
             .await
             .context("Could not create db directory")

crates/db2/src/db2.rs 🔗

@@ -20,7 +20,7 @@ use std::future::Future;
 use std::path::{Path, PathBuf};
 use std::sync::atomic::{AtomicBool, Ordering};
 use util::channel::ReleaseChannel;
-use util::{async_iife, ResultExt};
+use util::{async_maybe, ResultExt};
 
 const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
     PRAGMA foreign_keys=TRUE;
@@ -57,7 +57,7 @@ pub async fn open_db<M: Migrator + 'static>(
     let release_channel_name = release_channel.dev_name();
     let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
 
-    let connection = async_iife!({
+    let connection = async_maybe!({
         smol::fs::create_dir_all(&main_db_dir)
             .await
             .context("Could not create db directory")

crates/diagnostics/src/items.rs 🔗

@@ -38,6 +38,10 @@ impl DiagnosticIndicator {
                 this.in_progress_checks.remove(language_server_id);
                 cx.notify();
             }
+            project::Event::DiagnosticsUpdated { .. } => {
+                this.summary = project.read(cx).diagnostic_summary(cx);
+                cx.notify();
+            }
             _ => {}
         })
         .detach();

crates/editor/Cargo.toml 🔗

@@ -14,6 +14,7 @@ test-support = [
     "text/test-support",
     "language/test-support",
     "gpui/test-support",
+    "multi_buffer/test-support",
     "project/test-support",
     "util/test-support",
     "workspace/test-support",
@@ -34,6 +35,7 @@ git = { path = "../git" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 lsp = { path = "../lsp" }
+multi_buffer = { path = "../multi_buffer" }
 project = { path = "../project" }
 rpc = { path = "../rpc" }
 rich_text = { path = "../rich_text" }
@@ -57,7 +59,6 @@ log.workspace = true
 ordered-float.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-pulldown-cmark = { version = "0.9.2", default-features = false }
 rand.workspace = true
 schemars.workspace = true
 serde.workspace = true

crates/editor/src/display_map.rs 🔗

@@ -5,22 +5,24 @@ mod tab_map;
 mod wrap_map;
 
 use crate::{
-    link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer,
-    MultiBufferSnapshot, ToOffset, ToPoint,
+    link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
+    EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
 };
 pub use block_map::{BlockMap, BlockPoint};
 use collections::{BTreeMap, HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
     color::Color,
-    fonts::{FontId, HighlightStyle},
+    fonts::{FontId, HighlightStyle, Underline},
+    text_layout::{Line, RunStyle},
     Entity, ModelContext, ModelHandle,
 };
 use inlay_map::InlayMap;
 use language::{
     language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
 };
-use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
+use lsp::DiagnosticSeverity;
+use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
 use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
 use wrap_map::WrapMap;
@@ -316,6 +318,12 @@ pub struct Highlights<'a> {
     pub suggestion_highlight_style: Option<HighlightStyle>,
 }
 
+pub struct HighlightedChunk<'a> {
+    pub chunk: &'a str,
+    pub style: Option<HighlightStyle>,
+    pub is_tab: bool,
+}
+
 pub struct DisplaySnapshot {
     pub buffer_snapshot: MultiBufferSnapshot,
     pub fold_snapshot: fold_map::FoldSnapshot,
@@ -485,7 +493,7 @@ impl DisplaySnapshot {
         language_aware: bool,
         inlay_highlight_style: Option<HighlightStyle>,
         suggestion_highlight_style: Option<HighlightStyle>,
-    ) -> DisplayChunks<'_> {
+    ) -> DisplayChunks<'a> {
         self.block_snapshot.chunks(
             display_rows,
             language_aware,
@@ -498,6 +506,140 @@ impl DisplaySnapshot {
         )
     }
 
+    pub fn highlighted_chunks<'a>(
+        &'a self,
+        display_rows: Range<u32>,
+        language_aware: bool,
+        style: &'a EditorStyle,
+    ) -> impl Iterator<Item = HighlightedChunk<'a>> {
+        self.chunks(
+            display_rows,
+            language_aware,
+            Some(style.theme.hint),
+            Some(style.theme.suggestion),
+        )
+        .map(|chunk| {
+            let mut highlight_style = chunk
+                .syntax_highlight_id
+                .and_then(|id| id.style(&style.syntax));
+
+            if let Some(chunk_highlight) = chunk.highlight_style {
+                if let Some(highlight_style) = highlight_style.as_mut() {
+                    highlight_style.highlight(chunk_highlight);
+                } else {
+                    highlight_style = Some(chunk_highlight);
+                }
+            }
+
+            let mut diagnostic_highlight = HighlightStyle::default();
+
+            if chunk.is_unnecessary {
+                diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
+            }
+
+            if let Some(severity) = chunk.diagnostic_severity {
+                // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
+                if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
+                    let diagnostic_style = super::diagnostic_style(severity, true, style);
+                    diagnostic_highlight.underline = Some(Underline {
+                        color: Some(diagnostic_style.message.text.color),
+                        thickness: 1.0.into(),
+                        squiggly: true,
+                    });
+                }
+            }
+
+            if let Some(highlight_style) = highlight_style.as_mut() {
+                highlight_style.highlight(diagnostic_highlight);
+            } else {
+                highlight_style = Some(diagnostic_highlight);
+            }
+
+            HighlightedChunk {
+                chunk: chunk.text,
+                style: highlight_style,
+                is_tab: chunk.is_tab,
+            }
+        })
+    }
+
+    pub fn lay_out_line_for_row(
+        &self,
+        display_row: u32,
+        TextLayoutDetails {
+            font_cache,
+            text_layout_cache,
+            editor_style,
+        }: &TextLayoutDetails,
+    ) -> Line {
+        let mut styles = Vec::new();
+        let mut line = String::new();
+        let mut ended_in_newline = false;
+
+        let range = display_row..display_row + 1;
+        for chunk in self.highlighted_chunks(range, false, editor_style) {
+            line.push_str(chunk.chunk);
+
+            let text_style = if let Some(style) = chunk.style {
+                editor_style
+                    .text
+                    .clone()
+                    .highlight(style, font_cache)
+                    .map(Cow::Owned)
+                    .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
+            } else {
+                Cow::Borrowed(&editor_style.text)
+            };
+            ended_in_newline = chunk.chunk.ends_with("\n");
+
+            styles.push((
+                chunk.chunk.len(),
+                RunStyle {
+                    font_id: text_style.font_id,
+                    color: text_style.color,
+                    underline: text_style.underline,
+                },
+            ));
+        }
+
+        // our pixel positioning logic assumes each line ends in \n,
+        // this is almost always true except for the last line which
+        // may have no trailing newline.
+        if !ended_in_newline && display_row == self.max_point().row() {
+            line.push_str("\n");
+
+            styles.push((
+                "\n".len(),
+                RunStyle {
+                    font_id: editor_style.text.font_id,
+                    color: editor_style.text_color,
+                    underline: editor_style.text.underline,
+                },
+            ));
+        }
+
+        text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles)
+    }
+
+    pub fn x_for_point(
+        &self,
+        display_point: DisplayPoint,
+        text_layout_details: &TextLayoutDetails,
+    ) -> f32 {
+        let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
+        layout_line.x_for_index(display_point.column() as usize)
+    }
+
+    pub fn column_for_x(
+        &self,
+        display_row: u32,
+        x_coordinate: f32,
+        text_layout_details: &TextLayoutDetails,
+    ) -> u32 {
+        let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
+        layout_line.closest_index_for_x(x_coordinate) as u32
+    }
+
     pub fn chars_at(
         &self,
         mut point: DisplayPoint,
@@ -869,12 +1011,16 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
 #[cfg(test)]
 pub mod tests {
     use super::*;
-    use crate::{movement, test::marked_display_snapshot};
+    use crate::{
+        movement,
+        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+    };
     use gpui::{color::Color, elements::*, test::observe, AppContext};
     use language::{
         language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
         Buffer, Language, LanguageConfig, SelectionGoal,
     };
+    use project::Project;
     use rand::{prelude::*, Rng};
     use settings::SettingsStore;
     use smol::stream::StreamExt;
@@ -1148,95 +1294,120 @@ pub mod tests {
     }
 
     #[gpui::test(retries = 5)]
-    fn test_soft_wraps(cx: &mut AppContext) {
+    async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
-        init_test(cx, |_| {});
+        cx.update(|cx| {
+            init_test(cx, |_| {});
+        });
 
-        let font_cache = cx.font_cache();
+        let mut cx = EditorTestContext::new(cx).await;
+        let editor = cx.editor.clone();
+        let window = cx.window.clone();
 
-        let family_id = font_cache
-            .load_family(&["Helvetica"], &Default::default())
-            .unwrap();
-        let font_id = font_cache
-            .select_font(family_id, &Default::default())
-            .unwrap();
-        let font_size = 12.0;
-        let wrap_width = Some(64.);
+        cx.update_window(window, |cx| {
+            let text_layout_details =
+                editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
 
-        let text = "one two three four five\nsix seven eight";
-        let buffer = MultiBuffer::build_simple(text, cx);
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
-        });
+            let font_cache = cx.font_cache().clone();
 
-        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-        assert_eq!(
-            snapshot.text_chunks(0).collect::<String>(),
-            "one two \nthree four \nfive\nsix seven \neight"
-        );
-        assert_eq!(
-            snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
-            DisplayPoint::new(0, 7)
-        );
-        assert_eq!(
-            snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
-            DisplayPoint::new(1, 0)
-        );
-        assert_eq!(
-            movement::right(&snapshot, DisplayPoint::new(0, 7)),
-            DisplayPoint::new(1, 0)
-        );
-        assert_eq!(
-            movement::left(&snapshot, DisplayPoint::new(1, 0)),
-            DisplayPoint::new(0, 7)
-        );
-        assert_eq!(
-            movement::up(
-                &snapshot,
-                DisplayPoint::new(1, 10),
-                SelectionGoal::None,
-                false
-            ),
-            (DisplayPoint::new(0, 7), SelectionGoal::Column(10))
-        );
-        assert_eq!(
-            movement::down(
-                &snapshot,
-                DisplayPoint::new(0, 7),
-                SelectionGoal::Column(10),
-                false
-            ),
-            (DisplayPoint::new(1, 10), SelectionGoal::Column(10))
-        );
-        assert_eq!(
-            movement::down(
-                &snapshot,
-                DisplayPoint::new(1, 10),
-                SelectionGoal::Column(10),
-                false
-            ),
-            (DisplayPoint::new(2, 4), SelectionGoal::Column(10))
-        );
+            let family_id = font_cache
+                .load_family(&["Helvetica"], &Default::default())
+                .unwrap();
+            let font_id = font_cache
+                .select_font(family_id, &Default::default())
+                .unwrap();
+            let font_size = 12.0;
+            let wrap_width = Some(64.);
 
-        let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
-        buffer.update(cx, |buffer, cx| {
-            buffer.edit([(ix..ix, "and ")], None, cx);
-        });
+            let text = "one two three four five\nsix seven eight";
+            let buffer = MultiBuffer::build_simple(text, cx);
+            let map = cx.add_model(|cx| {
+                DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
+            });
 
-        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-        assert_eq!(
-            snapshot.text_chunks(1).collect::<String>(),
-            "three four \nfive\nsix and \nseven eight"
-        );
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(0).collect::<String>(),
+                "one two \nthree four \nfive\nsix seven \neight"
+            );
+            assert_eq!(
+                snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
+                DisplayPoint::new(0, 7)
+            );
+            assert_eq!(
+                snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
+                DisplayPoint::new(1, 0)
+            );
+            assert_eq!(
+                movement::right(&snapshot, DisplayPoint::new(0, 7)),
+                DisplayPoint::new(1, 0)
+            );
+            assert_eq!(
+                movement::left(&snapshot, DisplayPoint::new(1, 0)),
+                DisplayPoint::new(0, 7)
+            );
 
-        // Re-wrap on font size changes
-        map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
+            let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details);
+            assert_eq!(
+                movement::up(
+                    &snapshot,
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::None,
+                    false,
+                    &text_layout_details,
+                ),
+                (
+                    DisplayPoint::new(0, 7),
+                    SelectionGoal::HorizontalPosition(x)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(0, 7),
+                    SelectionGoal::HorizontalPosition(x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 4),
+                    SelectionGoal::HorizontalPosition(x)
+                )
+            );
 
-        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-        assert_eq!(
-            snapshot.text_chunks(1).collect::<String>(),
-            "three \nfour five\nsix and \nseven \neight"
-        )
+            let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
+            buffer.update(cx, |buffer, cx| {
+                buffer.edit([(ix..ix, "and ")], None, cx);
+            });
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(1).collect::<String>(),
+                "three four \nfive\nsix and \nseven eight"
+            );
+
+            // Re-wrap on font size changes
+            map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(1).collect::<String>(),
+                "three \nfour five\nsix and \nseven \neight"
+            )
+        });
     }
 
     #[gpui::test]
@@ -1731,6 +1902,9 @@ pub mod tests {
         cx.foreground().forbid_parking();
         cx.set_global(SettingsStore::test(cx));
         language::init(cx);
+        crate::init(cx);
+        Project::init_settings(cx);
+        theme::init((), cx);
         cx.update_global::<SettingsStore, _, _>(|store, cx| {
             store.update_user_settings::<AllLanguageSettings>(cx, f);
         });

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

@@ -993,8 +993,8 @@ mod tests {
     use super::*;
     use crate::display_map::inlay_map::InlayMap;
     use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
-    use crate::multi_buffer::MultiBuffer;
     use gpui::{elements::Empty, Element};
+    use multi_buffer::MultiBuffer;
     use rand::prelude::*;
     use settings::SettingsStore;
     use std::env;

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

@@ -91,7 +91,7 @@ impl<'a> FoldMapWriter<'a> {
 
             // For now, ignore any ranges that span an excerpt boundary.
             let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
-            if fold.0.start.excerpt_id() != fold.0.end.excerpt_id() {
+            if fold.0.start.excerpt_id != fold.0.end.excerpt_id {
                 continue;
             }
 

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

@@ -1,10 +1,8 @@
-use crate::{
-    multi_buffer::{MultiBufferChunks, MultiBufferRows},
-    Anchor, InlayId, MultiBufferSnapshot, ToOffset,
-};
+use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset};
 use collections::{BTreeMap, BTreeSet};
 use gpui::fonts::HighlightStyle;
 use language::{Chunk, Edit, Point, TextSummary};
+use multi_buffer::{MultiBufferChunks, MultiBufferRows};
 use std::{
     any::TypeId,
     cmp,

crates/editor/src/editor.rs 🔗

@@ -11,7 +11,6 @@ pub mod items;
 mod link_go_to_definition;
 mod mouse_context_menu;
 pub mod movement;
-pub mod multi_buffer;
 mod persistence;
 pub mod scroll;
 pub mod selections_collection;
@@ -25,7 +24,7 @@ use ::git::diff::DiffHunk;
 use aho_corasick::AhoCorasick;
 use anyhow::{anyhow, Context, Result};
 use blink_manager::BlinkManager;
-use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings};
+use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
 use clock::{Global, ReplicaId};
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
@@ -48,9 +47,9 @@ use gpui::{
     impl_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton},
-    serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element,
-    Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
-    WindowContext,
+    serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem,
+    CursorRegion, Element, Entity, ModelHandle, MouseRegion, Subscription, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -60,10 +59,10 @@ use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
     language_settings::{self, all_language_settings, InlayHintSettings},
-    point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion,
-    CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language,
-    LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal,
-    TransactionId,
+    markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel,
+    Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind,
+    IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point,
+    Selection, SelectionGoal, TransactionId,
 };
 use link_go_to_definition::{
     hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight,
@@ -71,15 +70,17 @@ use link_go_to_definition::{
 };
 use log::error;
 use lsp::LanguageServerId;
+use movement::TextLayoutDetails;
 use multi_buffer::ToOffsetUtf16;
 pub use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
     ToPoint,
 };
 use ordered_float::OrderedFloat;
+use parking_lot::RwLock;
 use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
 use rand::{seq::SliceRandom, thread_rng};
-use rpc::proto::PeerId;
+use rpc::proto::{self, PeerId};
 use scroll::{
     autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
 };
@@ -118,6 +119,67 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 
+pub fn render_parsed_markdown<Tag: 'static>(
+    parsed: &language::ParsedMarkdown,
+    editor_style: &EditorStyle,
+    workspace: Option<WeakViewHandle<Workspace>>,
+    cx: &mut ViewContext<Editor>,
+) -> Text {
+    enum RenderedMarkdown {}
+
+    let parsed = parsed.clone();
+    let view_id = cx.view_id();
+    let code_span_background_color = editor_style.document_highlight_read_background;
+
+    let mut region_id = 0;
+
+    Text::new(parsed.text, editor_style.text.clone())
+        .with_highlights(
+            parsed
+                .highlights
+                .iter()
+                .filter_map(|(range, highlight)| {
+                    let highlight = highlight.to_highlight_style(&editor_style.syntax)?;
+                    Some((range.clone(), highlight))
+                })
+                .collect::<Vec<_>>(),
+        )
+        .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| {
+            region_id += 1;
+            let region = parsed.regions[ix].clone();
+
+            if let Some(link) = region.link {
+                cx.scene().push_cursor_region(CursorRegion {
+                    bounds,
+                    style: CursorStyle::PointingHand,
+                });
+                cx.scene().push_mouse_region(
+                    MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds)
+                        .on_down::<Editor, _>(MouseButton::Left, move |_, _, cx| match &link {
+                            markdown::Link::Web { url } => cx.platform().open_url(url),
+                            markdown::Link::Path { path } => {
+                                if let Some(workspace) = &workspace {
+                                    _ = workspace.update(cx, |workspace, cx| {
+                                        workspace.open_abs_path(path.clone(), false, cx).detach();
+                                    });
+                                }
+                            }
+                        }),
+                );
+            }
+
+            if region.code {
+                cx.scene().push_quad(gpui::Quad {
+                    bounds,
+                    background: Some(code_span_background_color),
+                    border: Default::default(),
+                    corner_radii: (2.0).into(),
+                });
+            }
+        })
+        .with_soft_wrap(true)
+}
+
 #[derive(Clone, Deserialize, PartialEq, Default)]
 pub struct SelectNext {
     #[serde(default)]
@@ -594,7 +656,7 @@ pub struct Editor {
     background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
     inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
     nav_history: Option<ItemNavHistory>,
-    context_menu: Option<ContextMenu>,
+    context_menu: RwLock<Option<ContextMenu>>,
     mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
     completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
     next_completion_id: CompletionId,
@@ -787,10 +849,14 @@ enum ContextMenu {
 }
 
 impl ContextMenu {
-    fn select_first(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+    fn select_first(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_first(cx),
+                ContextMenu::Completions(menu) => menu.select_first(project, cx),
                 ContextMenu::CodeActions(menu) => menu.select_first(cx),
             }
             true
@@ -799,10 +865,14 @@ impl ContextMenu {
         }
     }
 
-    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+    fn select_prev(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_prev(cx),
+                ContextMenu::Completions(menu) => menu.select_prev(project, cx),
                 ContextMenu::CodeActions(menu) => menu.select_prev(cx),
             }
             true
@@ -811,10 +881,14 @@ impl ContextMenu {
         }
     }
 
-    fn select_next(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+    fn select_next(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_next(cx),
+                ContextMenu::Completions(menu) => menu.select_next(project, cx),
                 ContextMenu::CodeActions(menu) => menu.select_next(cx),
             }
             true
@@ -823,10 +897,14 @@ impl ContextMenu {
         }
     }
 
-    fn select_last(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+    fn select_last(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_last(cx),
+                ContextMenu::Completions(menu) => menu.select_last(project, cx),
                 ContextMenu::CodeActions(menu) => menu.select_last(cx),
             }
             true
@@ -846,99 +924,355 @@ impl ContextMenu {
         &self,
         cursor_position: DisplayPoint,
         style: EditorStyle,
+        workspace: Option<WeakViewHandle<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> (DisplayPoint, AnyElement<Editor>) {
         match self {
-            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
+            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
             ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
         }
     }
 }
 
+#[derive(Clone)]
 struct CompletionsMenu {
     id: CompletionId,
     initial_position: Anchor,
     buffer: ModelHandle<Buffer>,
-    project: Option<ModelHandle<Project>>,
-    completions: Arc<[Completion]>,
-    match_candidates: Vec<StringMatchCandidate>,
+    completions: Arc<RwLock<Box<[Completion]>>>,
+    match_candidates: Arc<[StringMatchCandidate]>,
     matches: Arc<[StringMatch]>,
     selected_item: usize,
     list: UniformListState,
 }
 
 impl CompletionsMenu {
-    fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
+    fn select_first(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         self.selected_item = 0;
         self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
 
-    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+    fn select_prev(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         if self.selected_item > 0 {
             self.selected_item -= 1;
+        } else {
+            self.selected_item = self.matches.len() - 1;
             self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         }
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
 
-    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+    fn select_next(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         if self.selected_item + 1 < self.matches.len() {
             self.selected_item += 1;
-            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        } else {
+            self.selected_item = 0;
         }
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
 
-    fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
+    fn select_last(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         self.selected_item = self.matches.len() - 1;
         self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
 
+    fn pre_resolve_completion_documentation(
+        &self,
+        project: Option<ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let settings = settings::get::<EditorSettings>(cx);
+        if !settings.show_completion_documentation {
+            return;
+        }
+
+        let Some(project) = project else {
+            return;
+        };
+        let client = project.read(cx).client();
+        let language_registry = project.read(cx).languages().clone();
+
+        let is_remote = project.read(cx).is_remote();
+        let project_id = project.read(cx).remote_id();
+
+        let completions = self.completions.clone();
+        let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
+
+        cx.spawn(move |this, mut cx| async move {
+            if is_remote {
+                let Some(project_id) = project_id else {
+                    log::error!("Remote project without remote_id");
+                    return;
+                };
+
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
+
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    Self::resolve_completion_documentation_remote(
+                        project_id,
+                        server_id,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        client.clone(),
+                        language_registry.clone(),
+                    )
+                    .await;
+
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            } else {
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
+
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    let server = project.read_with(&mut cx, |project, _| {
+                        project.language_server_for_id(server_id)
+                    });
+                    let Some(server) = server else {
+                        return;
+                    };
+
+                    Self::resolve_completion_documentation_local(
+                        server,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        language_registry.clone(),
+                    )
+                    .await;
+
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            }
+        })
+        .detach();
+    }
+
+    fn attempt_resolve_selected_completion_documentation(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let settings = settings::get::<EditorSettings>(cx);
+        if !settings.show_completion_documentation {
+            return;
+        }
+
+        let completion_index = self.matches[self.selected_item].candidate_id;
+        let Some(project) = project else {
+            return;
+        };
+        let language_registry = project.read(cx).languages().clone();
+
+        let completions = self.completions.clone();
+        let completions_guard = completions.read();
+        let completion = &completions_guard[completion_index];
+        if completion.documentation.is_some() {
+            return;
+        }
+
+        let server_id = completion.server_id;
+        let completion = completion.lsp_completion.clone();
+        drop(completions_guard);
+
+        if project.read(cx).is_remote() {
+            let Some(project_id) = project.read(cx).remote_id() else {
+                log::error!("Remote project without remote_id");
+                return;
+            };
+
+            let client = project.read(cx).client();
+
+            cx.spawn(move |this, mut cx| async move {
+                Self::resolve_completion_documentation_remote(
+                    project_id,
+                    server_id,
+                    completions.clone(),
+                    completion_index,
+                    completion,
+                    client,
+                    language_registry.clone(),
+                )
+                .await;
+
+                _ = this.update(&mut cx, |_, cx| cx.notify());
+            })
+            .detach();
+        } else {
+            let Some(server) = project.read(cx).language_server_for_id(server_id) else {
+                return;
+            };
+
+            cx.spawn(move |this, mut cx| async move {
+                Self::resolve_completion_documentation_local(
+                    server,
+                    completions,
+                    completion_index,
+                    completion,
+                    language_registry,
+                )
+                .await;
+
+                _ = this.update(&mut cx, |_, cx| cx.notify());
+            })
+            .detach();
+        }
+    }
+
+    async fn resolve_completion_documentation_remote(
+        project_id: u64,
+        server_id: LanguageServerId,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        completion_index: usize,
+        completion: lsp::CompletionItem,
+        client: Arc<Client>,
+        language_registry: Arc<LanguageRegistry>,
+    ) {
+        let request = proto::ResolveCompletionDocumentation {
+            project_id,
+            language_server_id: server_id.0 as u64,
+            lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
+        };
+
+        let Some(response) = client
+            .request(request)
+            .await
+            .context("completion documentation resolve proto request")
+            .log_err()
+        else {
+            return;
+        };
+
+        if response.text.is_empty() {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
+
+        let documentation = if response.is_markdown {
+            Documentation::MultiLineMarkdown(
+                markdown::parse_markdown(&response.text, &language_registry, None).await,
+            )
+        } else if response.text.lines().count() <= 1 {
+            Documentation::SingleLine(response.text)
+        } else {
+            Documentation::MultiLinePlainText(response.text)
+        };
+
+        let mut completions = completions.write();
+        let completion = &mut completions[completion_index];
+        completion.documentation = Some(documentation);
+    }
+
+    async fn resolve_completion_documentation_local(
+        server: Arc<lsp::LanguageServer>,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        completion_index: usize,
+        completion: lsp::CompletionItem,
+        language_registry: Arc<LanguageRegistry>,
+    ) {
+        let can_resolve = server
+            .capabilities()
+            .completion_provider
+            .as_ref()
+            .and_then(|options| options.resolve_provider)
+            .unwrap_or(false);
+        if !can_resolve {
+            return;
+        }
+
+        let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
+        let Some(completion_item) = request.await.log_err() else {
+            return;
+        };
+
+        if let Some(lsp_documentation) = completion_item.documentation {
+            let documentation = language::prepare_completion_documentation(
+                &lsp_documentation,
+                &language_registry,
+                None, // TODO: Try to reasonably work out which language the completion is for
+            )
+            .await;
+
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(documentation);
+        } else {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
+    }
+
     fn visible(&self) -> bool {
         !self.matches.is_empty()
     }
 
-    fn render(&self, style: EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
+    fn render(
+        &self,
+        style: EditorStyle,
+        workspace: Option<WeakViewHandle<Workspace>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> AnyElement<Editor> {
         enum CompletionTag {}
 
-        let language_servers = self.project.as_ref().map(|project| {
-            project
-                .read(cx)
-                .language_servers_for_buffer(self.buffer.read(cx), cx)
-                .filter(|(_, server)| server.capabilities().completion_provider.is_some())
-                .map(|(adapter, server)| (server.server_id(), adapter.short_name))
-                .collect::<Vec<_>>()
-        });
-        let needs_server_name = language_servers
-            .as_ref()
-            .map_or(false, |servers| servers.len() > 1);
-
-        let get_server_name =
-            move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> {
-                language_servers
-                    .iter()
-                    .flatten()
-                    .find_map(|(server_id, server_name)| {
-                        if *server_id == lookup_server_id {
-                            Some(*server_name)
-                        } else {
-                            None
-                        }
-                    })
-            };
+        let settings = settings::get::<EditorSettings>(cx);
+        let show_completion_documentation = settings.show_completion_documentation;
 
         let widest_completion_ix = self
             .matches
             .iter()
             .enumerate()
             .max_by_key(|(_, mat)| {
-                let completion = &self.completions[mat.candidate_id];
-                let mut len = completion.label.text.chars().count();
+                let completions = self.completions.read();
+                let completion = &completions[mat.candidate_id];
+                let documentation = &completion.documentation;
 
-                if let Some(server_name) = get_server_name(completion.server_id) {
-                    len += server_name.chars().count();
+                let mut len = completion.label.text.chars().count();
+                if let Some(Documentation::SingleLine(text)) = documentation {
+                    if show_completion_documentation {
+                        len += text.chars().count();
+                    }
                 }
 
                 len
@@ -948,16 +1282,24 @@ impl CompletionsMenu {
         let completions = self.completions.clone();
         let matches = self.matches.clone();
         let selected_item = self.selected_item;
-        let container_style = style.autocomplete.container;
-        UniformList::new(
-            self.list.clone(),
-            matches.len(),
-            cx,
+
+        let list = UniformList::new(self.list.clone(), matches.len(), cx, {
+            let style = style.clone();
             move |_, range, items, cx| {
                 let start_ix = range.start;
+                let completions_guard = completions.read();
+
                 for (ix, mat) in matches[range].iter().enumerate() {
-                    let completion = &completions[mat.candidate_id];
                     let item_ix = start_ix + ix;
+                    let candidate_id = mat.candidate_id;
+                    let completion = &completions_guard[candidate_id];
+
+                    let documentation = if show_completion_documentation {
+                        &completion.documentation
+                    } else {
+                        &None
+                    };
+
                     items.push(
                         MouseEventHandler::new::<CompletionTag, _>(
                             mat.candidate_id,
@@ -986,22 +1328,18 @@ impl CompletionsMenu {
                                             ),
                                         );
 
-                                if let Some(server_name) = get_server_name(completion.server_id) {
+                                if let Some(Documentation::SingleLine(text)) = documentation {
                                     Flex::row()
                                         .with_child(completion_label)
                                         .with_children((|| {
-                                            if !needs_server_name {
-                                                return None;
-                                            }
-
                                             let text_style = TextStyle {
-                                                color: style.autocomplete.server_name_color,
+                                                color: style.autocomplete.inline_docs_color,
                                                 font_size: style.text.font_size
-                                                    * style.autocomplete.server_name_size_percent,
+                                                    * style.autocomplete.inline_docs_size_percent,
                                                 ..style.text.clone()
                                             };
 
-                                            let label = Text::new(server_name, text_style)
+                                            let label = Text::new(text.clone(), text_style)
                                                 .aligned()
                                                 .constrained()
                                                 .dynamically(move |constraint, _, _| {
@@ -1021,7 +1359,7 @@ impl CompletionsMenu {
                                                         .with_style(
                                                             style
                                                                 .autocomplete
-                                                                .server_name_container,
+                                                                .inline_docs_container,
                                                         )
                                                         .into_any(),
                                                 )
@@ -1060,15 +1398,59 @@ impl CompletionsMenu {
                             )
                             .map(|task| task.detach());
                         })
+                        .constrained()
+                        .with_min_width(style.autocomplete.completion_min_width)
+                        .with_max_width(style.autocomplete.completion_max_width)
                         .into_any(),
                     );
                 }
-            },
-        )
-        .with_width_from_item(widest_completion_ix)
-        .contained()
-        .with_style(container_style)
-        .into_any()
+            }
+        })
+        .with_width_from_item(widest_completion_ix);
+
+        enum MultiLineDocumentation {}
+
+        Flex::row()
+            .with_child(list.flex(1., false))
+            .with_children({
+                let mat = &self.matches[selected_item];
+                let completions = self.completions.read();
+                let completion = &completions[mat.candidate_id];
+                let documentation = &completion.documentation;
+
+                match documentation {
+                    Some(Documentation::MultiLinePlainText(text)) => Some(
+                        Flex::column()
+                            .scrollable::<MultiLineDocumentation>(0, None, cx)
+                            .with_child(
+                                Text::new(text.clone(), style.text.clone()).with_soft_wrap(true),
+                            )
+                            .contained()
+                            .with_style(style.autocomplete.alongside_docs_container)
+                            .constrained()
+                            .with_max_width(style.autocomplete.alongside_docs_max_width)
+                            .flex(1., false),
+                    ),
+
+                    Some(Documentation::MultiLineMarkdown(parsed)) => Some(
+                        Flex::column()
+                            .scrollable::<MultiLineDocumentation>(0, None, cx)
+                            .with_child(render_parsed_markdown::<MultiLineDocumentation>(
+                                parsed, &style, workspace, cx,
+                            ))
+                            .contained()
+                            .with_style(style.autocomplete.alongside_docs_container)
+                            .constrained()
+                            .with_max_width(style.autocomplete.alongside_docs_max_width)
+                            .flex(1., false),
+                    ),
+
+                    _ => None,
+                }
+            })
+            .contained()
+            .with_style(style.autocomplete.container)
+            .into_any()
     }
 
     pub async fn filter(&mut self, query: Option<&str>, executor: Arc<executor::Background>) {
@@ -1095,13 +1477,13 @@ impl CompletionsMenu {
                 .collect()
         };
 
-        //Remove all candidates where the query's start does not match the start of any word in the candidate
+        // Remove all candidates where the query's start does not match the start of any word in the candidate
         if let Some(query) = query {
             if let Some(query_start) = query.chars().next() {
                 matches.retain(|string_match| {
                     split_words(&string_match.string).any(|word| {
-                        //Check that the first codepoint of the word as lowercase matches the first
-                        //codepoint of the query as lowercase
+                        // Check that the first codepoint of the word as lowercase matches the first
+                        // codepoint of the query as lowercase
                         word.chars()
                             .flat_map(|codepoint| codepoint.to_lowercase())
                             .zip(query_start.to_lowercase())
@@ -1111,23 +1493,27 @@ impl CompletionsMenu {
             }
         }
 
+        let completions = self.completions.read();
         matches.sort_unstable_by_key(|mat| {
-            let completion = &self.completions[mat.candidate_id];
+            let completion = &completions[mat.candidate_id];
             (
                 completion.lsp_completion.sort_text.as_ref(),
                 Reverse(OrderedFloat(mat.score)),
                 completion.sort_key(),
             )
         });
+        drop(completions);
 
         for mat in &mut matches {
-            let filter_start = self.completions[mat.candidate_id].label.filter_range.start;
+            let completions = self.completions.read();
+            let filter_start = completions[mat.candidate_id].label.filter_range.start;
             for position in &mut mat.positions {
                 *position += filter_start;
             }
         }
 
         self.matches = matches.into();
+        self.selected_item = 0;
     }
 }
 
@@ -1150,17 +1536,23 @@ impl CodeActionsMenu {
     fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
         if self.selected_item > 0 {
             self.selected_item -= 1;
+        } else {
+            self.selected_item = self.actions.len() - 1;
             self.list.scroll_to(ScrollTarget::Show(self.selected_item));
-            cx.notify()
         }
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        cx.notify();
     }
 
     fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
         if self.selected_item + 1 < self.actions.len() {
             self.selected_item += 1;
             self.list.scroll_to(ScrollTarget::Show(self.selected_item));
-            cx.notify()
+        } else {
+            self.selected_item = 0;
+            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         }
+        cx.notify();
     }
 
     fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
@@ -1563,7 +1955,7 @@ impl Editor {
             background_highlights: Default::default(),
             inlay_background_highlights: Default::default(),
             nav_history: None,
-            context_menu: None,
+            context_menu: RwLock::new(None),
             mouse_context_menu: cx
                 .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
             completion_tasks: Default::default(),
@@ -1858,10 +2250,12 @@ impl Editor {
 
         if local {
             let new_cursor_position = self.selections.newest_anchor().head();
-            let completion_menu = match self.context_menu.as_mut() {
+            let mut context_menu = self.context_menu.write();
+            let completion_menu = match context_menu.as_ref() {
                 Some(ContextMenu::Completions(menu)) => Some(menu),
+
                 _ => {
-                    self.context_menu.take();
+                    *context_menu = None;
                     None
                 }
             };
@@ -1873,13 +2267,39 @@ impl Editor {
                 if kind == Some(CharKind::Word)
                     && word_range.to_inclusive().contains(&cursor_position)
                 {
+                    let mut completion_menu = completion_menu.clone();
+                    drop(context_menu);
+
                     let query = Self::completion_query(buffer, cursor_position);
-                    cx.background()
-                        .block(completion_menu.filter(query.as_deref(), cx.background().clone()));
+                    cx.spawn(move |this, mut cx| async move {
+                        completion_menu
+                            .filter(query.as_deref(), cx.background().clone())
+                            .await;
+
+                        this.update(&mut cx, |this, cx| {
+                            let mut context_menu = this.context_menu.write();
+                            let Some(ContextMenu::Completions(menu)) = context_menu.as_ref() else {
+                                return;
+                            };
+
+                            if menu.id > completion_menu.id {
+                                return;
+                            }
+
+                            *context_menu = Some(ContextMenu::Completions(completion_menu));
+                            drop(context_menu);
+                            cx.notify();
+                        })
+                    })
+                    .detach();
+
                     self.show_completions(&ShowCompletions, cx);
                 } else {
+                    drop(context_menu);
                     self.hide_context_menu(cx);
                 }
+            } else {
+                drop(context_menu);
             }
 
             hide_hover(self, cx);
@@ -2877,8 +3297,10 @@ impl Editor {
                     i = 0;
                 } else if pair_state.range.start.to_offset(buffer) > range.end {
                     break;
-                } else if pair_state.selection_id == selection.id {
-                    enclosing = Some(pair_state);
+                } else {
+                    if pair_state.selection_id == selection.id {
+                        enclosing = Some(pair_state);
+                    }
                     i += 1;
                 }
             }
@@ -2912,6 +3334,7 @@ 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);
@@ -3064,6 +3487,14 @@ impl Editor {
             .collect()
     }
 
+    pub fn text_layout_details(&self, cx: &WindowContext) -> TextLayoutDetails {
+        TextLayoutDetails {
+            font_cache: cx.font_cache().clone(),
+            text_layout_cache: cx.text_layout_cache().clone(),
+            editor_style: self.style(cx),
+        }
+    }
+
     fn splice_inlay_hints(
         &self,
         to_remove: Vec<InlayId>,
@@ -3150,7 +3581,6 @@ impl Editor {
         });
 
         let id = post_inc(&mut self.next_completion_id);
-        let project = self.project.clone();
         let task = cx.spawn(|this, mut cx| {
             async move {
                 let menu = if let Some(completions) = completions.await.log_err() {
@@ -3169,8 +3599,7 @@ impl Editor {
                             })
                             .collect(),
                         buffer,
-                        project,
-                        completions: completions.into(),
+                        completions: Arc::new(RwLock::new(completions.into())),
                         matches: Vec::new().into(),
                         selected_item: 0,
                         list: Default::default(),
@@ -3179,6 +3608,9 @@ impl Editor {
                     if menu.matches.is_empty() {
                         None
                     } else {
+                        _ = this.update(&mut cx, |editor, cx| {
+                            menu.pre_resolve_completion_documentation(editor.project.clone(), cx);
+                        });
                         Some(menu)
                     }
                 } else {
@@ -3188,23 +3620,30 @@ impl Editor {
                 this.update(&mut cx, |this, cx| {
                     this.completion_tasks.retain(|(task_id, _)| *task_id > id);
 
-                    match this.context_menu.as_ref() {
+                    let mut context_menu = this.context_menu.write();
+                    match context_menu.as_ref() {
                         None => {}
+
                         Some(ContextMenu::Completions(prev_menu)) => {
                             if prev_menu.id > id {
                                 return;
                             }
                         }
+
                         _ => return,
                     }
 
                     if this.focused && menu.is_some() {
                         let menu = menu.unwrap();
-                        this.show_context_menu(ContextMenu::Completions(menu), cx);
+                        *context_menu = Some(ContextMenu::Completions(menu));
+                        drop(context_menu);
+                        this.discard_copilot_suggestion(cx);
+                        cx.notify();
                     } else if this.completion_tasks.is_empty() {
                         // If there are no more completion tasks and the last menu was
                         // empty, we should hide it. If it was already hidden, we should
                         // also show the copilot suggestion when available.
+                        drop(context_menu);
                         if this.hide_context_menu(cx).is_none() {
                             this.update_visible_copilot_suggestion(cx);
                         }
@@ -3235,7 +3674,8 @@ impl Editor {
             .matches
             .get(action.item_ix.unwrap_or(completions_menu.selected_item))?;
         let buffer_handle = completions_menu.buffer;
-        let completion = completions_menu.completions.get(mat.candidate_id)?;
+        let completions = completions_menu.completions.read();
+        let completion = completions.get(mat.candidate_id)?;
 
         let snippet;
         let text;
@@ -3348,14 +3788,13 @@ impl Editor {
     }
 
     pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
-        if matches!(
-            self.context_menu.as_ref(),
-            Some(ContextMenu::CodeActions(_))
-        ) {
-            self.context_menu.take();
+        let mut context_menu = self.context_menu.write();
+        if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
+            *context_menu = None;
             cx.notify();
             return;
         }
+        drop(context_menu);
 
         let deployed_from_indicator = action.deployed_from_indicator;
         let mut task = self.code_actions_task.take();

crates/editor/src/editor_settings.rs 🔗

@@ -7,6 +7,7 @@ pub struct EditorSettings {
     pub cursor_blink: bool,
     pub hover_popover_enabled: bool,
     pub show_completions_on_input: bool,
+    pub show_completion_documentation: bool,
     pub use_on_type_format: bool,
     pub scrollbar: Scrollbar,
     pub relative_line_numbers: bool,
@@ -33,6 +34,7 @@ pub struct EditorSettingsContent {
     pub cursor_blink: Option<bool>,
     pub hover_popover_enabled: Option<bool>,
     pub show_completions_on_input: Option<bool>,
+    pub show_completion_documentation: Option<bool>,
     pub use_on_type_format: Option<bool>,
     pub scrollbar: Option<ScrollbarContent>,
     pub relative_line_numbers: Option<bool>,

crates/editor/src/editor_tests.rs 🔗

@@ -19,8 +19,8 @@ use gpui::{
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
-    BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride,
-    LanguageRegistry, Override, Point,
+    BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
+    Override, Point,
 };
 use parking_lot::Mutex;
 use project::project_settings::{LspSettings, ProjectSettings};
@@ -851,7 +851,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
 
     let view = cx
         .add_window(|cx| {
-            let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
+            let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx);
             build_editor(buffer.clone(), cx)
         })
         .root(cx);
@@ -869,7 +869,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
             true,
             cx,
         );
-        assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε\n");
+        assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε");
 
         view.move_right(&MoveRight, cx);
         assert_eq!(
@@ -888,6 +888,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
         );
 
         view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "ab⋯e".len())]
+        );
+        view.move_left(&MoveLeft, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
             &[empty_range(1, "ab⋯".len())]
@@ -929,17 +934,18 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
             view.selections.display_ranges(cx),
             &[empty_range(1, "ab⋯e".len())]
         );
-        view.move_up(&MoveUp, cx);
+        view.move_down(&MoveDown, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(0, "ⓐⓑ⋯ⓔ".len())]
+            &[empty_range(2, "αβ⋯ε".len())]
         );
-        view.move_left(&MoveLeft, cx);
+        view.move_up(&MoveUp, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(0, "ⓐⓑ⋯".len())]
+            &[empty_range(1, "ab⋯e".len())]
         );
-        view.move_left(&MoveLeft, cx);
+
+        view.move_up(&MoveUp, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
             &[empty_range(0, "ⓐⓑ".len())]
@@ -949,6 +955,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
             view.selections.display_ranges(cx),
             &[empty_range(0, "ⓐ".len())]
         );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "".len())]
+        );
     });
 }
 
@@ -5084,6 +5095,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
         LanguageConfig {
             name: "Rust".into(),
             path_suffixes: vec!["rs".to_string()],
+            // Enable Prettier formatting for the same buffer, and ensure
+            // LSP is called instead of Prettier.
+            prettier_parser_name: Some("test_parser".to_string()),
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
@@ -5094,12 +5108,6 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
                 document_formatting_provider: Some(lsp::OneOf::Left(true)),
                 ..Default::default()
             },
-            // Enable Prettier formatting for the same buffer, and ensure
-            // LSP is called instead of Prettier.
-            enabled_formatters: vec![BundledFormatter::Prettier {
-                parser_name: Some("test_parser"),
-                plugin_names: Vec::new(),
-            }],
             ..Default::default()
         }))
         .await;
@@ -5109,7 +5117,6 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
 
     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
     project.update(cx, |project, _| {
-        project.enable_test_prettier(&[]);
         project.languages().add(Arc::new(language));
     });
     let buffer = project
@@ -5430,9 +5437,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
         additional edit
     "});
     cx.simulate_keystroke(" ");
-    assert!(cx.editor(|e, _| e.context_menu.is_none()));
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
     cx.simulate_keystroke("s");
-    assert!(cx.editor(|e, _| e.context_menu.is_none()));
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
 
     cx.assert_editor_state(indoc! {"
         one.second_completion
@@ -5494,12 +5501,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
     });
     cx.set_state("editorˇ");
     cx.simulate_keystroke(".");
-    assert!(cx.editor(|e, _| e.context_menu.is_none()));
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
     cx.simulate_keystroke("c");
     cx.simulate_keystroke("l");
     cx.simulate_keystroke("o");
     cx.assert_editor_state("editor.cloˇ");
-    assert!(cx.editor(|e, _| e.context_menu.is_none()));
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
     cx.update_editor(|editor, cx| {
         editor.show_completions(&ShowCompletions, cx);
     });
@@ -6710,6 +6717,102 @@ fn test_combine_syntax_and_fuzzy_match_highlights() {
     );
 }
 
+#[gpui::test]
+async fn go_to_prev_overlapping_diagnostic(
+    deterministic: Arc<Deterministic>,
+    cx: &mut gpui::TestAppContext,
+) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
+
+    cx.set_state(indoc! {"
+        ˇfn func(abc def: i32) -> u32 {
+        }
+    "});
+
+    cx.update(|cx| {
+        project.update(cx, |project, cx| {
+            project
+                .update_diagnostics(
+                    LanguageServerId(0),
+                    lsp::PublishDiagnosticsParams {
+                        uri: lsp::Url::from_file_path("/root/file").unwrap(),
+                        version: None,
+                        diagnostics: vec![
+                            lsp::Diagnostic {
+                                range: lsp::Range::new(
+                                    lsp::Position::new(0, 11),
+                                    lsp::Position::new(0, 12),
+                                ),
+                                severity: Some(lsp::DiagnosticSeverity::ERROR),
+                                ..Default::default()
+                            },
+                            lsp::Diagnostic {
+                                range: lsp::Range::new(
+                                    lsp::Position::new(0, 12),
+                                    lsp::Position::new(0, 15),
+                                ),
+                                severity: Some(lsp::DiagnosticSeverity::ERROR),
+                                ..Default::default()
+                            },
+                            lsp::Diagnostic {
+                                range: lsp::Range::new(
+                                    lsp::Position::new(0, 25),
+                                    lsp::Position::new(0, 28),
+                                ),
+                                severity: Some(lsp::DiagnosticSeverity::ERROR),
+                                ..Default::default()
+                            },
+                        ],
+                    },
+                    &[],
+                    cx,
+                )
+                .unwrap()
+        });
+    });
+
+    deterministic.run_until_parked();
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+    });
+
+    cx.assert_editor_state(indoc! {"
+        fn func(abc def: i32) -> ˇu32 {
+        }
+    "});
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+    });
+
+    cx.assert_editor_state(indoc! {"
+        fn func(abc ˇdef: i32) -> u32 {
+        }
+    "});
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+    });
+
+    cx.assert_editor_state(indoc! {"
+        fn func(abcˇ def: i32) -> u32 {
+        }
+    "});
+
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+    });
+
+    cx.assert_editor_state(indoc! {"
+        fn func(abc def: i32) -> ˇu32 {
+        }
+    "});
+}
+
 #[gpui::test]
 async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
@@ -6792,6 +6895,46 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
         .unindent(),
     );
 
+    cx.update_editor(|editor, cx| {
+        editor.go_to_prev_hunk(&GoToPrevHunk, cx);
+    });
+
+    cx.assert_editor_state(
+        &r#"
+        use some::modified;
+
+        ˇ
+        fn main() {
+            println!("hello there");
+
+            println!("around the");
+            println!("world");
+        }
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        for _ in 0..3 {
+            editor.go_to_prev_hunk(&GoToPrevHunk, cx);
+        }
+    });
+
+    cx.assert_editor_state(
+        &r#"
+        use some::modified;
+
+
+        fn main() {
+        ˇ    println!("hello there");
+
+            println!("around the");
+            println!("world");
+        }
+        "#
+        .unindent(),
+    );
+
     cx.update_editor(|editor, cx| {
         editor.fold(&Fold, cx);
 
@@ -7788,7 +7931,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
     cx.simulate_keystroke("-");
     cx.foreground().run_until_parked();
     cx.update_editor(|editor, _| {
-        if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
             assert_eq!(
                 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
                 &["bg-red", "bg-blue", "bg-yellow"]
@@ -7801,7 +7944,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
     cx.simulate_keystroke("l");
     cx.foreground().run_until_parked();
     cx.update_editor(|editor, _| {
-        if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
             assert_eq!(
                 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
                 &["bg-blue", "bg-yellow"]
@@ -7817,7 +7960,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
     cx.simulate_keystroke("l");
     cx.foreground().run_until_parked();
     cx.update_editor(|editor, _| {
-        if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
             assert_eq!(
                 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
                 &["bg-yellow"]
@@ -7838,6 +7981,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
         LanguageConfig {
             name: "Rust".into(),
             path_suffixes: vec!["rs".to_string()],
+            prettier_parser_name: Some("test_parser".to_string()),
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
@@ -7846,10 +7990,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
     let test_plugin = "test_plugin";
     let _ = language
         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            enabled_formatters: vec![BundledFormatter::Prettier {
-                parser_name: Some("test_parser"),
-                plugin_names: vec![test_plugin],
-            }],
+            prettier_plugins: vec![test_plugin],
             ..Default::default()
         }))
         .await;
@@ -7858,10 +7999,9 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
     fs.insert_file("/file.rs", Default::default()).await;
 
     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-    let prettier_format_suffix = project.update(cx, |project, _| {
-        let suffix = project.enable_test_prettier(&[test_plugin]);
+    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
+    project.update(cx, |project, _| {
         project.languages().add(Arc::new(language));
-        suffix
     });
     let buffer = project
         .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))

crates/editor/src/element.rs 🔗

@@ -4,7 +4,7 @@ use super::{
     MAX_LINE_LEN,
 };
 use crate::{
-    display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
+    display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock},
     editor_settings::ShowScrollbar,
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::{
@@ -22,7 +22,7 @@ use git::diff::DiffHunkStatus;
 use gpui::{
     color::Color,
     elements::*,
-    fonts::{HighlightStyle, TextStyle, Underline},
+    fonts::TextStyle,
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
@@ -37,8 +37,7 @@ use gpui::{
 use itertools::Itertools;
 use json::json;
 use language::{
-    language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
-    Selection,
+    language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection,
 };
 use project::{
     project_settings::{GitGutterSetting, ProjectSettings},
@@ -1584,56 +1583,7 @@ impl EditorElement {
                 .collect()
         } else {
             let style = &self.style;
-            let chunks = snapshot
-                .chunks(
-                    rows.clone(),
-                    true,
-                    Some(style.theme.hint),
-                    Some(style.theme.suggestion),
-                )
-                .map(|chunk| {
-                    let mut highlight_style = chunk
-                        .syntax_highlight_id
-                        .and_then(|id| id.style(&style.syntax));
-
-                    if let Some(chunk_highlight) = chunk.highlight_style {
-                        if let Some(highlight_style) = highlight_style.as_mut() {
-                            highlight_style.highlight(chunk_highlight);
-                        } else {
-                            highlight_style = Some(chunk_highlight);
-                        }
-                    }
-
-                    let mut diagnostic_highlight = HighlightStyle::default();
-
-                    if chunk.is_unnecessary {
-                        diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
-                    }
-
-                    if let Some(severity) = chunk.diagnostic_severity {
-                        // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
-                        if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
-                            let diagnostic_style = super::diagnostic_style(severity, true, style);
-                            diagnostic_highlight.underline = Some(Underline {
-                                color: Some(diagnostic_style.message.text.color),
-                                thickness: 1.0.into(),
-                                squiggly: true,
-                            });
-                        }
-                    }
-
-                    if let Some(highlight_style) = highlight_style.as_mut() {
-                        highlight_style.highlight(diagnostic_highlight);
-                    } else {
-                        highlight_style = Some(diagnostic_highlight);
-                    }
-
-                    HighlightedChunk {
-                        chunk: chunk.text,
-                        style: highlight_style,
-                        is_tab: chunk.is_tab,
-                    }
-                });
+            let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
 
             LineWithInvisibles::from_chunks(
                 chunks,
@@ -1870,12 +1820,6 @@ impl EditorElement {
     }
 }
 
-struct HighlightedChunk<'a> {
-    chunk: &'a str,
-    style: Option<HighlightStyle>,
-    is_tab: bool,
-}
-
 #[derive(Debug)]
 pub struct LineWithInvisibles {
     pub line: Line,
@@ -2428,7 +2372,7 @@ impl Element<Editor> for EditorElement {
                 }
 
                 let active = matches!(
-                    editor.context_menu,
+                    editor.context_menu.read().as_ref(),
                     Some(crate::ContextMenu::CodeActions(_))
                 );
 
@@ -2439,9 +2383,13 @@ impl Element<Editor> for EditorElement {
         }
 
         let visible_rows = start_row..start_row + line_layouts.len() as u32;
-        let mut hover = editor
-            .hover_state
-            .render(&snapshot, &style, visible_rows, cx);
+        let mut hover = editor.hover_state.render(
+            &snapshot,
+            &style,
+            visible_rows,
+            editor.workspace.as_ref().map(|(w, _)| w.clone()),
+            cx,
+        );
         let mode = editor.mode;
 
         let mut fold_indicators = editor.render_fold_indicators(

crates/editor/src/git.rs 🔗

@@ -36,7 +36,7 @@ impl DisplayDiffHunk {
 
             DisplayDiffHunk::Unfolded {
                 display_row_range, ..
-            } => display_row_range.start..=display_row_range.end - 1,
+            } => display_row_range.start..=display_row_range.end,
         };
 
         range.contains(&display_row)
@@ -77,8 +77,8 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
     } else {
         let start = hunk_start_point.to_display_point(snapshot).row();
 
-        let hunk_end_row_inclusive = hunk.buffer_range.end.max(hunk.buffer_range.start);
-        let hunk_end_point = Point::new(hunk_end_row_inclusive, 0);
+        let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start);
+        let hunk_end_point = Point::new(hunk_end_row, 0);
         let end = hunk_end_point.to_display_point(snapshot).row();
 
         DisplayDiffHunk::Unfolded {
@@ -87,3 +87,196 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
         }
     }
 }
+
+#[cfg(any(test, feature = "test_support"))]
+mod tests {
+    use crate::editor_tests::init_test;
+    use crate::Point;
+    use gpui::TestAppContext;
+    use multi_buffer::{ExcerptRange, MultiBuffer};
+    use project::{FakeFs, Project};
+    use unindent::Unindent;
+    #[gpui::test]
+    async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
+        use git::diff::DiffHunkStatus;
+        init_test(cx, |_| {});
+
+        let fs = FakeFs::new(cx.background());
+        let project = Project::test(fs, [], cx).await;
+
+        // buffer has two modified hunks with two rows each
+        let buffer_1 = project
+            .update(cx, |project, cx| {
+                project.create_buffer(
+                    "
+                        1.zero
+                        1.ONE
+                        1.TWO
+                        1.three
+                        1.FOUR
+                        1.FIVE
+                        1.six
+                    "
+                    .unindent()
+                    .as_str(),
+                    None,
+                    cx,
+                )
+            })
+            .unwrap();
+        buffer_1.update(cx, |buffer, cx| {
+            buffer.set_diff_base(
+                Some(
+                    "
+                        1.zero
+                        1.one
+                        1.two
+                        1.three
+                        1.four
+                        1.five
+                        1.six
+                    "
+                    .unindent(),
+                ),
+                cx,
+            );
+        });
+
+        // buffer has a deletion hunk and an insertion hunk
+        let buffer_2 = project
+            .update(cx, |project, cx| {
+                project.create_buffer(
+                    "
+                        2.zero
+                        2.one
+                        2.two
+                        2.three
+                        2.four
+                        2.five
+                        2.six
+                    "
+                    .unindent()
+                    .as_str(),
+                    None,
+                    cx,
+                )
+            })
+            .unwrap();
+        buffer_2.update(cx, |buffer, cx| {
+            buffer.set_diff_base(
+                Some(
+                    "
+                        2.zero
+                        2.one
+                        2.one-and-a-half
+                        2.two
+                        2.three
+                        2.four
+                        2.six
+                    "
+                    .unindent(),
+                ),
+                cx,
+            );
+        });
+
+        cx.foreground().run_until_parked();
+
+        let multibuffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    // excerpt ends in the middle of a modified hunk
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt begins in the middle of a modified hunk
+                    ExcerptRange {
+                        context: Point::new(5, 0)..Point::new(6, 5),
+                        primary: Default::default(),
+                    },
+                ],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [
+                    // excerpt ends at a deletion
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt starts at a deletion
+                    ExcerptRange {
+                        context: Point::new(2, 0)..Point::new(2, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt fully contains a deletion hunk
+                    ExcerptRange {
+                        context: Point::new(1, 0)..Point::new(2, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt fully contains an insertion hunk
+                    ExcerptRange {
+                        context: Point::new(4, 0)..Point::new(6, 5),
+                        primary: Default::default(),
+                    },
+                ],
+                cx,
+            );
+            multibuffer
+        });
+
+        let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
+
+        assert_eq!(
+            snapshot.text(),
+            "
+                1.zero
+                1.ONE
+                1.FIVE
+                1.six
+                2.zero
+                2.one
+                2.two
+                2.one
+                2.two
+                2.four
+                2.five
+                2.six"
+                .unindent()
+        );
+
+        let expected = [
+            (DiffHunkStatus::Modified, 1..2),
+            (DiffHunkStatus::Modified, 2..3),
+            //TODO: Define better when and where removed hunks show up at range extremities
+            (DiffHunkStatus::Removed, 6..6),
+            (DiffHunkStatus::Removed, 8..8),
+            (DiffHunkStatus::Added, 10..11),
+        ];
+
+        assert_eq!(
+            snapshot
+                .git_diff_hunks_in_range(0..12)
+                .map(|hunk| (hunk.status(), hunk.buffer_range))
+                .collect::<Vec<_>>(),
+            &expected,
+        );
+
+        assert_eq!(
+            snapshot
+                .git_diff_hunks_in_range_rev(0..12)
+                .map(|hunk| (hunk.status(), hunk.buffer_range))
+                .collect::<Vec<_>>(),
+            expected
+                .iter()
+                .rev()
+                .cloned()
+                .collect::<Vec<_>>()
+                .as_slice(),
+        );
+    }
+}

crates/editor/src/hover_popover.rs 🔗

@@ -9,13 +9,15 @@ use gpui::{
     actions,
     elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
     platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Element, ModelHandle, Task, ViewContext,
+    AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle,
+};
+use language::{
+    markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown,
 };
-use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
 use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
-use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText};
 use std::{ops::Range, sync::Arc, time::Duration};
 use util::TryFutureExt;
+use workspace::Workspace;
 
 pub const HOVER_DELAY_MILLIS: u64 = 350;
 pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@@ -105,12 +107,15 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
                     this.hover_state.diagnostic_popover = None;
                 })?;
 
+                let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
+                let blocks = vec![inlay_hover.tooltip];
+                let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
+
                 let hover_popover = InfoPopover {
                     project: project.clone(),
                     symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
-                    blocks: vec![inlay_hover.tooltip],
-                    language: None,
-                    rendered_content: None,
+                    blocks,
+                    parsed_content,
                 };
 
                 this.update(&mut cx, |this, cx| {
@@ -288,35 +293,38 @@ fn show_hover(
                     });
             })?;
 
-            // Construct new hover popover from hover request
-            let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
-                if hover_result.is_empty() {
-                    return None;
+            let hover_result = hover_request.await.ok().flatten();
+            let hover_popover = match hover_result {
+                Some(hover_result) if !hover_result.is_empty() => {
+                    // Create symbol range of anchors for highlighting and filtering of future requests.
+                    let range = if let Some(range) = hover_result.range {
+                        let start = snapshot
+                            .buffer_snapshot
+                            .anchor_in_excerpt(excerpt_id.clone(), range.start);
+                        let end = snapshot
+                            .buffer_snapshot
+                            .anchor_in_excerpt(excerpt_id.clone(), range.end);
+
+                        start..end
+                    } else {
+                        anchor..anchor
+                    };
+
+                    let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
+                    let blocks = hover_result.contents;
+                    let language = hover_result.language;
+                    let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
+
+                    Some(InfoPopover {
+                        project: project.clone(),
+                        symbol_range: RangeInEditor::Text(range),
+                        blocks,
+                        parsed_content,
+                    })
                 }
 
-                // Create symbol range of anchors for highlighting and filtering
-                // of future requests.
-                let range = if let Some(range) = hover_result.range {
-                    let start = snapshot
-                        .buffer_snapshot
-                        .anchor_in_excerpt(excerpt_id.clone(), range.start);
-                    let end = snapshot
-                        .buffer_snapshot
-                        .anchor_in_excerpt(excerpt_id.clone(), range.end);
-
-                    start..end
-                } else {
-                    anchor..anchor
-                };
-
-                Some(InfoPopover {
-                    project: project.clone(),
-                    symbol_range: RangeInEditor::Text(range),
-                    blocks: hover_result.contents,
-                    language: hover_result.language,
-                    rendered_content: None,
-                })
-            });
+                _ => None,
+            };
 
             this.update(&mut cx, |this, cx| {
                 if let Some(symbol_range) = hover_popover
@@ -345,44 +353,56 @@ fn show_hover(
     editor.hover_state.info_task = Some(task);
 }
 
-fn render_blocks(
+async fn parse_blocks(
     blocks: &[HoverBlock],
     language_registry: &Arc<LanguageRegistry>,
-    language: Option<&Arc<Language>>,
-) -> RichText {
-    let mut data = RichText {
-        text: Default::default(),
-        highlights: Default::default(),
-        region_ranges: Default::default(),
-        regions: Default::default(),
-    };
+    language: Option<Arc<Language>>,
+) -> markdown::ParsedMarkdown {
+    let mut text = String::new();
+    let mut highlights = Vec::new();
+    let mut region_ranges = Vec::new();
+    let mut regions = Vec::new();
 
     for block in blocks {
         match &block.kind {
             HoverBlockKind::PlainText => {
-                new_paragraph(&mut data.text, &mut Vec::new());
-                data.text.push_str(&block.text);
+                markdown::new_paragraph(&mut text, &mut Vec::new());
+                text.push_str(&block.text);
             }
+
             HoverBlockKind::Markdown => {
-                render_markdown_mut(&block.text, language_registry, language, &mut data)
+                markdown::parse_markdown_block(
+                    &block.text,
+                    language_registry,
+                    language.clone(),
+                    &mut text,
+                    &mut highlights,
+                    &mut region_ranges,
+                    &mut regions,
+                )
+                .await
             }
+
             HoverBlockKind::Code { language } => {
                 if let Some(language) = language_registry
                     .language_for_name(language)
                     .now_or_never()
                     .and_then(Result::ok)
                 {
-                    render_code(&mut data.text, &mut data.highlights, &block.text, &language);
+                    markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
                 } else {
-                    data.text.push_str(&block.text);
+                    text.push_str(&block.text);
                 }
             }
         }
     }
 
-    data.text = data.text.trim().to_string();
-
-    data
+    ParsedMarkdown {
+        text: text.trim().to_string(),
+        highlights,
+        region_ranges,
+        regions,
+    }
 }
 
 #[derive(Default)]
@@ -403,6 +423,7 @@ impl HoverState {
         snapshot: &EditorSnapshot,
         style: &EditorStyle,
         visible_rows: Range<u32>,
+        workspace: Option<WeakViewHandle<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
         // If there is a diagnostic, position the popovers based on that.
@@ -432,7 +453,7 @@ impl HoverState {
             elements.push(diagnostic_popover.render(style, cx));
         }
         if let Some(info_popover) = self.info_popover.as_mut() {
-            elements.push(info_popover.render(style, cx));
+            elements.push(info_popover.render(style, workspace, cx));
         }
 
         Some((point, elements))
@@ -444,32 +465,23 @@ pub struct InfoPopover {
     pub project: ModelHandle<Project>,
     symbol_range: RangeInEditor,
     pub blocks: Vec<HoverBlock>,
-    language: Option<Arc<Language>>,
-    rendered_content: Option<RichText>,
+    parsed_content: ParsedMarkdown,
 }
 
 impl InfoPopover {
     pub fn render(
         &mut self,
         style: &EditorStyle,
+        workspace: Option<WeakViewHandle<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> AnyElement<Editor> {
-        let rendered_content = self.rendered_content.get_or_insert_with(|| {
-            render_blocks(
-                &self.blocks,
-                self.project.read(cx).languages(),
-                self.language.as_ref(),
-            )
-        });
-
-        MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
-            let code_span_background_color = style.document_highlight_read_background;
+        MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
             Flex::column()
-                .scrollable::<HoverBlock>(1, None, cx)
-                .with_child(rendered_content.element(
-                    style.syntax.clone(),
-                    style.text.clone(),
-                    code_span_background_color,
+                .scrollable::<HoverBlock>(0, None, cx)
+                .with_child(crate::render_parsed_markdown::<HoverBlock>(
+                    &self.parsed_content,
+                    style,
+                    workspace,
                     cx,
                 ))
                 .contained()
@@ -572,7 +584,6 @@ mod tests {
     use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
     use lsp::LanguageServerId;
     use project::{HoverBlock, HoverBlockKind};
-    use rich_text::Highlight;
     use smol::stream::StreamExt;
     use unindent::Unindent;
     use util::test::marked_text_ranges;
@@ -793,7 +804,7 @@ mod tests {
                 }],
             );
 
-            let rendered = render_blocks(&blocks, &Default::default(), None);
+            let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
             assert_eq!(
                 rendered.text,
                 code_str.trim(),
@@ -900,7 +911,7 @@ mod tests {
                 // Links
                 Row {
                     blocks: vec![HoverBlock {
-                        text: "one [two](the-url) three".to_string(),
+                        text: "one [two](https://the-url) three".to_string(),
                         kind: HoverBlockKind::Markdown,
                     }],
                     expected_marked_text: "one «two» three".to_string(),
@@ -921,7 +932,7 @@ mod tests {
                                 - a
                                 - b
                             * two
-                                - [c](the-url)
+                                - [c](https://the-url)
                                 - d"
                         .unindent(),
                         kind: HoverBlockKind::Markdown,
@@ -985,7 +996,7 @@ mod tests {
                 expected_styles,
             } in &rows[0..]
             {
-                let rendered = render_blocks(&blocks, &Default::default(), None);
+                let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
 
                 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
                 let expected_highlights = ranges
@@ -1001,11 +1012,8 @@ mod tests {
                     .highlights
                     .iter()
                     .filter_map(|(range, highlight)| {
-                        let style = match highlight {
-                            Highlight::Id(id) => id.style(&style.syntax)?,
-                            Highlight::Highlight(style) => style.clone(),
-                        };
-                        Some((range.clone(), style))
+                        let highlight = highlight.to_highlight_style(&style.syntax)?;
+                        Some((range.clone(), highlight))
                     })
                     .collect();
 
@@ -1258,11 +1266,7 @@ mod tests {
                 "Popover range should match the new type label part"
             );
             assert_eq!(
-                popover
-                    .rendered_content
-                    .as_ref()
-                    .expect("should have label text for new type hint")
-                    .text,
+                popover.parsed_content.text,
                 format!("A tooltip for `{new_type_label}`"),
                 "Rendered text should not anyhow alter backticks"
             );
@@ -1316,11 +1320,7 @@ mod tests {
                 "Popover range should match the struct label part"
             );
             assert_eq!(
-                popover
-                    .rendered_content
-                    .as_ref()
-                    .expect("should have label text for struct hint")
-                    .text,
+                popover.parsed_content.text,
                 format!("A tooltip for {struct_label}"),
                 "Rendered markdown element should remove backticks from text"
             );

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -2138,7 +2138,7 @@ pub mod tests {
         });
     }
 
-    #[gpui::test]
+    #[gpui::test(iterations = 10)]
     async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
             settings.defaults.inlay_hints = Some(InlayHintSettings {
@@ -2400,11 +2400,13 @@ pub mod tests {
         ));
         cx.foreground().run_until_parked();
         editor.update(cx, |editor, cx| {
-            let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            ranges.sort_by_key(|r| r.start);
+
             assert_eq!(ranges.len(), 3,
                 "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
-            let visible_query_range = &ranges[0];
-            let above_query_range = &ranges[1];
+            let above_query_range = &ranges[0];
+            let visible_query_range = &ranges[1];
             let below_query_range = &ranges[2];
             assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
                 "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");

crates/editor/src/movement.rs 🔗

@@ -1,7 +1,8 @@
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
-use crate::{char_kind, CharKind, ToOffset, ToPoint};
+use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
+use gpui::{FontCache, TextLayoutCache};
 use language::Point;
-use std::ops::Range;
+use std::{ops::Range, sync::Arc};
 
 #[derive(Debug, PartialEq)]
 pub enum FindRange {
@@ -9,6 +10,14 @@ pub enum FindRange {
     MultiLine,
 }
 
+/// TextLayoutDetails encompasses everything we need to move vertically
+/// taking into account variable width characters.
+pub struct TextLayoutDetails {
+    pub font_cache: Arc<FontCache>,
+    pub text_layout_cache: Arc<TextLayoutCache>,
+    pub editor_style: EditorStyle,
+}
+
 pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     if point.column() > 0 {
         *point.column_mut() -= 1;
@@ -47,8 +56,16 @@ pub fn up(
     start: DisplayPoint,
     goal: SelectionGoal,
     preserve_column_at_start: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
-    up_by_rows(map, start, 1, goal, preserve_column_at_start)
+    up_by_rows(
+        map,
+        start,
+        1,
+        goal,
+        preserve_column_at_start,
+        text_layout_details,
+    )
 }
 
 pub fn down(
@@ -56,8 +73,16 @@ pub fn down(
     start: DisplayPoint,
     goal: SelectionGoal,
     preserve_column_at_end: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
-    down_by_rows(map, start, 1, goal, preserve_column_at_end)
+    down_by_rows(
+        map,
+        start,
+        1,
+        goal,
+        preserve_column_at_end,
+        text_layout_details,
+    )
 }
 
 pub fn up_by_rows(
@@ -66,11 +91,13 @@ pub fn up_by_rows(
     row_count: u32,
     goal: SelectionGoal,
     preserve_column_at_start: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
-    let mut goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
-        _ => map.column_to_chars(start.row(), start.column()),
+    let mut goal_x = match goal {
+        SelectionGoal::HorizontalPosition(x) => x,
+        SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+        SelectionGoal::HorizontalRange { end, .. } => end,
+        _ => map.x_for_point(start, text_layout_details),
     };
 
     let prev_row = start.row().saturating_sub(row_count);
@@ -79,19 +106,19 @@ pub fn up_by_rows(
         Bias::Left,
     );
     if point.row() < start.row() {
-        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
+        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_start {
         return (start, goal);
     } else {
         point = DisplayPoint::new(0, 0);
-        goal_column = 0;
+        goal_x = 0.0;
     }
 
     let mut clipped_point = map.clip_point(point, Bias::Left);
     if clipped_point.row() < point.row() {
         clipped_point = map.clip_point(point, Bias::Right);
     }
-    (clipped_point, SelectionGoal::Column(goal_column))
+    (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
 }
 
 pub fn down_by_rows(
@@ -100,29 +127,31 @@ pub fn down_by_rows(
     row_count: u32,
     goal: SelectionGoal,
     preserve_column_at_end: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
-    let mut goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
-        _ => map.column_to_chars(start.row(), start.column()),
+    let mut goal_x = match goal {
+        SelectionGoal::HorizontalPosition(x) => x,
+        SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+        SelectionGoal::HorizontalRange { end, .. } => end,
+        _ => map.x_for_point(start, text_layout_details),
     };
 
     let new_row = start.row() + row_count;
     let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
     if point.row() > start.row() {
-        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
+        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_end {
         return (start, goal);
     } else {
         point = map.max_point();
-        goal_column = map.column_to_chars(point.row(), point.column())
+        goal_x = map.x_for_point(point, text_layout_details)
     }
 
     let mut clipped_point = map.clip_point(point, Bias::Right);
     if clipped_point.row() > point.row() {
         clipped_point = map.clip_point(point, Bias::Left);
     }
-    (clipped_point, SelectionGoal::Column(goal_column))
+    (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
 }
 
 pub fn line_beginning(
@@ -340,6 +369,30 @@ pub fn find_boundary(
     map.clip_point(offset.to_display_point(map), Bias::Right)
 }
 
+pub fn chars_after(
+    map: &DisplaySnapshot,
+    mut offset: usize,
+) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
+    map.buffer_snapshot.chars_at(offset).map(move |ch| {
+        let before = offset;
+        offset = offset + ch.len_utf8();
+        (ch, before..offset)
+    })
+}
+
+pub fn chars_before(
+    map: &DisplaySnapshot,
+    mut offset: usize,
+) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
+    map.buffer_snapshot
+        .reversed_chars_at(offset)
+        .map(move |ch| {
+            let after = offset;
+            offset = offset - ch.len_utf8();
+            (ch, offset..after)
+        })
+}
+
 pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
     let raw_point = point.to_point(map);
     let scope = map.buffer_snapshot.language_scope_at(raw_point);
@@ -396,9 +449,11 @@ pub fn split_display_range_by_lines(
 mod tests {
     use super::*;
     use crate::{
-        display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
-        InlayId, MultiBuffer,
+        display_map::Inlay,
+        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+        Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
     };
+    use project::Project;
     use settings::SettingsStore;
     use util::post_inc;
 
@@ -676,7 +731,9 @@ mod tests {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
                 surrounding_word(&snapshot, display_points[1]),
-                display_points[0]..display_points[2]
+                display_points[0]..display_points[2],
+                "{}",
+                marked_text.to_string()
             );
         }
 
@@ -686,128 +743,178 @@ mod tests {
         assert("loremˇ ˇ  ˇipsum", cx);
         assert("lorem\nˇˇˇ\nipsum", cx);
         assert("lorem\nˇˇipsumˇ", cx);
-        assert("lorem,ˇˇ ˇipsum", cx);
+        assert("loremˇ,ˇˇ ipsum", cx);
         assert("ˇloremˇˇ, ipsum", cx);
     }
 
     #[gpui::test]
-    fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
-        init_test(cx);
-
-        let family_id = cx
-            .font_cache()
-            .load_family(&["Helvetica"], &Default::default())
-            .unwrap();
-        let font_id = cx
-            .font_cache()
-            .select_font(family_id, &Default::default())
-            .unwrap();
+    async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| {
+            init_test(cx);
+        });
 
-        let buffer =
-            cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
-        let multibuffer = cx.add_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(0);
-            multibuffer.push_excerpts(
-                buffer.clone(),
-                [
-                    ExcerptRange {
-                        context: Point::new(0, 0)..Point::new(1, 4),
-                        primary: None,
-                    },
-                    ExcerptRange {
-                        context: Point::new(2, 0)..Point::new(3, 2),
-                        primary: None,
-                    },
-                ],
-                cx,
+        let mut cx = EditorTestContext::new(cx).await;
+        let editor = cx.editor.clone();
+        let window = cx.window.clone();
+        cx.update_window(window, |cx| {
+            let text_layout_details =
+                editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
+
+            let family_id = cx
+                .font_cache()
+                .load_family(&["Helvetica"], &Default::default())
+                .unwrap();
+            let font_id = cx
+                .font_cache()
+                .select_font(family_id, &Default::default())
+                .unwrap();
+
+            let buffer =
+                cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
+            let multibuffer = cx.add_model(|cx| {
+                let mut multibuffer = MultiBuffer::new(0);
+                multibuffer.push_excerpts(
+                    buffer.clone(),
+                    [
+                        ExcerptRange {
+                            context: Point::new(0, 0)..Point::new(1, 4),
+                            primary: None,
+                        },
+                        ExcerptRange {
+                            context: Point::new(2, 0)..Point::new(3, 2),
+                            primary: None,
+                        },
+                    ],
+                    cx,
+                );
+                multibuffer
+            });
+            let display_map =
+                cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
+            let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
+            assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+
+            let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
+
+            // Can't move up into the first excerpt's header
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(2, 2),
+                    SelectionGoal::HorizontalPosition(col_2_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::HorizontalPosition(0.0)
+                ),
+            );
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::None,
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::HorizontalPosition(0.0)
+                ),
             );
-            multibuffer
-        });
-        let display_map =
-            cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
-        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
 
-        assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+            let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
 
-        // Can't move up into the first excerpt's header
-        assert_eq!(
-            up(
-                &snapshot,
-                DisplayPoint::new(2, 2),
-                SelectionGoal::Column(2),
-                false
-            ),
-            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
-        );
-        assert_eq!(
-            up(
-                &snapshot,
-                DisplayPoint::new(2, 0),
-                SelectionGoal::None,
-                false
-            ),
-            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
-        );
+            // Move up and down within first excerpt
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_4_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_4_x)
+                ),
+            );
 
-        // Move up and down within first excerpt
-        assert_eq!(
-            up(
-                &snapshot,
-                DisplayPoint::new(3, 4),
-                SelectionGoal::Column(4),
-                false
-            ),
-            (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
-        );
-        assert_eq!(
-            down(
-                &snapshot,
-                DisplayPoint::new(2, 3),
-                SelectionGoal::Column(4),
-                false
-            ),
-            (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
-        );
+            let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
 
-        // Move up and down across second excerpt's header
-        assert_eq!(
-            up(
-                &snapshot,
-                DisplayPoint::new(6, 5),
-                SelectionGoal::Column(5),
-                false
-            ),
-            (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
-        );
-        assert_eq!(
-            down(
-                &snapshot,
-                DisplayPoint::new(3, 4),
-                SelectionGoal::Column(5),
-                false
-            ),
-            (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
-        );
+            // Move up and down across second excerpt's header
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(6, 5),
+                    SelectionGoal::HorizontalPosition(col_5_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(6, 5),
+                    SelectionGoal::HorizontalPosition(col_5_x)
+                ),
+            );
 
-        // Can't move down off the end
-        assert_eq!(
-            down(
-                &snapshot,
-                DisplayPoint::new(7, 0),
-                SelectionGoal::Column(0),
-                false
-            ),
-            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
-        );
-        assert_eq!(
-            down(
-                &snapshot,
-                DisplayPoint::new(7, 2),
-                SelectionGoal::Column(2),
-                false
-            ),
-            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
-        );
+            let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
+
+            // Can't move down off the end
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(7, 0),
+                    SelectionGoal::HorizontalPosition(0.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x)
+                ),
+            );
+        });
     }
 
     fn init_test(cx: &mut gpui::AppContext) {
@@ -815,5 +922,6 @@ mod tests {
         theme::init((), cx);
         language::init(cx);
         crate::init(cx);
+        Project::init_settings(cx);
     }
 }

crates/editor/src/selections_collection.rs 🔗

@@ -1,6 +1,6 @@
 use std::{
     cell::Ref,
-    cmp, iter, mem,
+    iter, mem,
     ops::{Deref, DerefMut, Range, Sub},
     sync::Arc,
 };
@@ -13,6 +13,7 @@ use util::post_inc;
 
 use crate::{
     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+    movement::TextLayoutDetails,
     Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
 };
 
@@ -305,23 +306,29 @@ impl SelectionsCollection {
         &mut self,
         display_map: &DisplaySnapshot,
         row: u32,
-        columns: &Range<u32>,
+        positions: &Range<f32>,
         reversed: bool,
+        text_layout_details: &TextLayoutDetails,
     ) -> Option<Selection<Point>> {
-        let is_empty = columns.start == columns.end;
+        let is_empty = positions.start == positions.end;
         let line_len = display_map.line_len(row);
-        if columns.start < line_len || (is_empty && columns.start == line_len) {
-            let start = DisplayPoint::new(row, columns.start);
-            let end = DisplayPoint::new(row, cmp::min(columns.end, line_len));
+
+        let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+
+        let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
+        if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) {
+            let start = DisplayPoint::new(row, start_col);
+            let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+            let end = DisplayPoint::new(row, end_col);
 
             Some(Selection {
                 id: post_inc(&mut self.next_selection_id),
                 start: start.to_point(display_map),
                 end: end.to_point(display_map),
                 reversed,
-                goal: SelectionGoal::ColumnRange {
-                    start: columns.start,
-                    end: columns.end,
+                goal: SelectionGoal::HorizontalRange {
+                    start: positions.start,
+                    end: positions.end,
                 },
             })
         } else {

crates/editor/src/test.rs 🔗

@@ -8,6 +8,7 @@ use crate::{
 
 use gpui::{ModelHandle, ViewContext};
 
+use project::Project;
 use util::test::{marked_text_offsets, marked_text_ranges};
 
 #[cfg(test)]
@@ -63,9 +64,20 @@ pub fn assert_text_with_selections(
     assert_eq!(editor.selections.ranges(cx), text_ranges);
 }
 
+// RA thinks this is dead code even though it is used in a whole lot of tests
+#[allow(dead_code)]
+#[cfg(any(test, feature = "test-support"))]
 pub(crate) fn build_editor(
     buffer: ModelHandle<MultiBuffer>,
     cx: &mut ViewContext<Editor>,
 ) -> Editor {
     Editor::new(EditorMode::Full, buffer, None, None, cx)
 }
+
+pub(crate) fn build_editor_with_project(
+    project: ModelHandle<Project>,
+    buffer: ModelHandle<MultiBuffer>,
+    cx: &mut ViewContext<Editor>,
+) -> Editor {
+    Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
+}

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

@@ -6,18 +6,18 @@ use std::{
 
 use anyhow::Result;
 
+use crate::{Editor, ToPoint};
 use collections::HashSet;
 use futures::Future;
 use gpui::{json, ViewContext, ViewHandle};
 use indoc::indoc;
 use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
 use lsp::{notification, request};
+use multi_buffer::ToPointUtf16;
 use project::Project;
 use smol::stream::StreamExt;
 use workspace::{AppState, Workspace, WorkspaceHandle};
 
-use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint};
-
 use super::editor_test_context::EditorTestContext;
 
 pub struct EditorLspTestContext<'a> {

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

@@ -18,7 +18,7 @@ use util::{
     test::{generate_marked_text, marked_text_ranges},
 };
 
-use super::build_editor;
+use super::build_editor_with_project;
 
 pub struct EditorTestContext<'a> {
     pub cx: &'a mut gpui::TestAppContext,
@@ -29,13 +29,24 @@ pub struct EditorTestContext<'a> {
 impl<'a> EditorTestContext<'a> {
     pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
         let fs = FakeFs::new(cx.background());
-        let project = Project::test(fs, [], cx).await;
+        // fs.insert_file("/file", "".to_owned()).await;
+        fs.insert_tree(
+            "/root",
+            gpui::serde_json::json!({
+                "file": "",
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/root".as_ref()], cx).await;
         let buffer = project
-            .update(cx, |project, cx| project.create_buffer("", None, cx))
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/root/file", cx)
+            })
+            .await
             .unwrap();
         let window = cx.add_window(|cx| {
             cx.focus_self();
-            build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx)
+            build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx)
         });
         let editor = window.root(cx);
         Self {

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

@@ -2,7 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
 
 use crate::{
     json::{self, ToJson, Value},
-    AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, Vector2FExt, ViewContext,
+    AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt,
+    ViewContext,
 };
 use pathfinder_geometry::{
     rect::RectF,
@@ -10,10 +11,10 @@ use pathfinder_geometry::{
 };
 use serde_json::json;
 
-#[derive(Default)]
 struct ScrollState {
     scroll_to: Cell<Option<usize>>,
     scroll_position: Cell<f32>,
+    type_tag: TypeTag,
 }
 
 pub struct Flex<V> {
@@ -66,8 +67,14 @@ impl<V: 'static> Flex<V> {
     where
         Tag: 'static,
     {
-        let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
-        scroll_state.read(cx).scroll_to.set(scroll_to);
+        let scroll_state = cx.element_state::<Tag, Rc<ScrollState>>(
+            element_id,
+            Rc::new(ScrollState {
+                scroll_to: Cell::new(scroll_to),
+                scroll_position: Default::default(),
+                type_tag: TypeTag::new::<Tag>(),
+            }),
+        );
         self.scroll_state = Some((scroll_state, cx.handle().id()));
         self
     }
@@ -276,38 +283,44 @@ impl<V: 'static> Element<V> for Flex<V> {
         if let Some((scroll_state, id)) = &self.scroll_state {
             let scroll_state = scroll_state.read(cx).clone();
             cx.scene().push_mouse_region(
-                crate::MouseRegion::new::<Self>(*id, 0, bounds)
-                    .on_scroll({
-                        let axis = self.axis;
-                        move |e, _: &mut V, cx| {
-                            if remaining_space < 0. {
-                                let scroll_delta = e.delta.raw();
-
-                                let mut delta = match axis {
-                                    Axis::Horizontal => {
-                                        if scroll_delta.x().abs() >= scroll_delta.y().abs() {
-                                            scroll_delta.x()
-                                        } else {
-                                            scroll_delta.y()
-                                        }
+                crate::MouseRegion::from_handlers(
+                    scroll_state.type_tag,
+                    *id,
+                    0,
+                    bounds,
+                    Default::default(),
+                )
+                .on_scroll({
+                    let axis = self.axis;
+                    move |e, _: &mut V, cx| {
+                        if remaining_space < 0. {
+                            let scroll_delta = e.delta.raw();
+
+                            let mut delta = match axis {
+                                Axis::Horizontal => {
+                                    if scroll_delta.x().abs() >= scroll_delta.y().abs() {
+                                        scroll_delta.x()
+                                    } else {
+                                        scroll_delta.y()
                                     }
-                                    Axis::Vertical => scroll_delta.y(),
-                                };
-                                if !e.delta.precise() {
-                                    delta *= 20.;
                                 }
+                                Axis::Vertical => scroll_delta.y(),
+                            };
+                            if !e.delta.precise() {
+                                delta *= 20.;
+                            }
 
-                                scroll_state
-                                    .scroll_position
-                                    .set(scroll_state.scroll_position.get() - delta);
+                            scroll_state
+                                .scroll_position
+                                .set(scroll_state.scroll_position.get() - delta);
 
-                                cx.notify();
-                            } else {
-                                cx.propagate_event();
-                            }
+                            cx.notify();
+                        } else {
+                            cx.propagate_event();
                         }
-                    })
-                    .on_move(|_, _: &mut V, _| { /* Capture move events */ }),
+                    }
+                })
+                .on_move(|_, _: &mut V, _| { /* Capture move events */ }),
             )
         }
 

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

@@ -30,7 +30,7 @@ struct StateInner<V> {
     orientation: Orientation,
     overdraw: f32,
     #[allow(clippy::type_complexity)]
-    scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut V, &mut ViewContext<V>)>>,
+    scroll_handler: Option<Box<dyn FnMut(Range<usize>, usize, &mut V, &mut ViewContext<V>)>>,
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -378,6 +378,10 @@ impl<V: 'static> ListState<V> {
             .extend((0..element_count).map(|_| ListItem::Unrendered), &());
     }
 
+    pub fn item_count(&self) -> usize {
+        self.0.borrow().items.summary().count
+    }
+
     pub fn splice(&self, old_range: Range<usize>, count: usize) {
         let state = &mut *self.0.borrow_mut();
 
@@ -416,7 +420,7 @@ impl<V: 'static> ListState<V> {
 
     pub fn set_scroll_handler(
         &mut self,
-        handler: impl FnMut(Range<usize>, &mut V, &mut ViewContext<V>) + 'static,
+        handler: impl FnMut(Range<usize>, usize, &mut V, &mut ViewContext<V>) + 'static,
     ) {
         self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
     }
@@ -529,7 +533,12 @@ impl<V: 'static> StateInner<V> {
 
         if self.scroll_handler.is_some() {
             let visible_range = self.visible_range(height, scroll_top);
-            self.scroll_handler.as_mut().unwrap()(visible_range, view, cx);
+            self.scroll_handler.as_mut().unwrap()(
+                visible_range,
+                self.items.summary().count,
+                view,
+                cx,
+            );
         }
 
         cx.notify();

crates/gpui/src/text_layout.rs 🔗

@@ -266,6 +266,8 @@ impl Line {
         self.layout.len == 0
     }
 
+    /// index_for_x returns the character containing the given x coordinate.
+    /// (e.g. to handle a mouse-click)
     pub fn index_for_x(&self, x: f32) -> Option<usize> {
         if x >= self.layout.width {
             None
@@ -281,6 +283,28 @@ impl Line {
         }
     }
 
+    /// closest_index_for_x returns the character boundary closest to the given x coordinate
+    /// (e.g. to handle aligning up/down arrow keys)
+    pub fn closest_index_for_x(&self, x: f32) -> usize {
+        let mut prev_index = 0;
+        let mut prev_x = 0.0;
+
+        for run in self.layout.runs.iter() {
+            for glyph in run.glyphs.iter() {
+                if glyph.position.x() >= x {
+                    if glyph.position.x() - x < x - prev_x {
+                        return glyph.index;
+                    } else {
+                        return prev_index;
+                    }
+                }
+                prev_index = glyph.index;
+                prev_x = glyph.position.x();
+            }
+        }
+        prev_index
+    }
+
     pub fn paint(
         &self,
         origin: Vector2F,

crates/gpui2/src/action.rs 🔗

@@ -4,7 +4,7 @@ use collections::{HashMap, HashSet};
 use serde::Deserialize;
 use std::any::{type_name, Any};
 
-pub trait Action: Any + Send + Sync {
+pub trait Action: Any + Send {
     fn qualified_name() -> SharedString
     where
         Self: Sized;
@@ -19,7 +19,7 @@ pub trait Action: Any + Send + Sync {
 
 impl<A> Action for A
 where
-    A: for<'a> Deserialize<'a> + PartialEq + Any + Send + Sync + Clone + Default,
+    A: for<'a> Deserialize<'a> + PartialEq + Any + Send + Clone + Default,
 {
     fn qualified_name() -> SharedString {
         type_name::<A>().into()

crates/gpui2/src/app.rs 🔗

@@ -23,7 +23,7 @@ use crate::{
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
 use futures::{future::BoxFuture, Future};
-use parking_lot::{Mutex, RwLock};
+use parking_lot::Mutex;
 use slotmap::SlotMap;
 use std::{
     any::{type_name, Any, TypeId},
@@ -39,7 +39,10 @@ use util::http::{self, HttpClient};
 
 pub struct App(Arc<Mutex<AppContext>>);
 
+/// Represents an application before it is fully launched. Once your app is
+/// configured, you'll start the app with `App::run`.
 impl App {
+    /// Builds an app with the given asset source.
     pub fn production(asset_source: Arc<dyn AssetSource>) -> Self {
         Self(AppContext::new(
             current_platform(),
@@ -48,6 +51,8 @@ impl App {
         ))
     }
 
+    /// Start the application. The provided callback will be called once the
+    /// app is fully launched.
     pub fn run<F>(self, on_finish_launching: F)
     where
         F: 'static + FnOnce(&mut MainThread<AppContext>),
@@ -61,6 +66,8 @@ impl App {
         }));
     }
 
+    /// Register a handler to be invoked when the platform instructs the application
+    /// to open one or more URLs.
     pub fn on_open_urls<F>(&self, mut callback: F) -> &Self
     where
         F: 'static + FnMut(Vec<String>, &mut AppContext),
@@ -110,11 +117,10 @@ impl App {
 
 type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
 type FrameCallback = Box<dyn FnOnce(&mut WindowContext) + Send>;
-type Handler = Box<dyn FnMut(&mut AppContext) -> bool + Send + Sync + 'static>;
-type Listener = Box<dyn FnMut(&dyn Any, &mut AppContext) -> bool + Send + Sync + 'static>;
-type QuitHandler =
-    Box<dyn FnMut(&mut AppContext) -> BoxFuture<'static, ()> + Send + Sync + 'static>;
-type ReleaseListener = Box<dyn FnMut(&mut dyn Any, &mut AppContext) + Send + Sync + 'static>;
+type Handler = Box<dyn FnMut(&mut AppContext) -> bool + Send + 'static>;
+type Listener = Box<dyn FnMut(&dyn Any, &mut AppContext) -> bool + Send + 'static>;
+type QuitHandler = Box<dyn FnMut(&mut AppContext) -> BoxFuture<'static, ()> + Send + 'static>;
+type ReleaseListener = Box<dyn FnMut(&mut dyn Any, &mut AppContext) + Send + 'static>;
 
 pub struct AppContext {
     this: Weak<Mutex<AppContext>>,
@@ -131,12 +137,11 @@ pub struct AppContext {
     pub(crate) image_cache: ImageCache,
     pub(crate) text_style_stack: Vec<TextStyleRefinement>,
     pub(crate) globals_by_type: HashMap<TypeId, AnyBox>,
-    pub(crate) unit_entity: Handle<()>,
     pub(crate) entities: EntityMap,
     pub(crate) windows: SlotMap<WindowId, Option<Window>>,
-    pub(crate) keymap: Arc<RwLock<Keymap>>,
+    pub(crate) keymap: Arc<Mutex<Keymap>>,
     pub(crate) global_action_listeners:
-        HashMap<TypeId, Vec<Box<dyn Fn(&dyn Action, DispatchPhase, &mut Self) + Send + Sync>>>,
+        HashMap<TypeId, Vec<Box<dyn Fn(&dyn Action, DispatchPhase, &mut Self) + Send>>>,
     action_builders: HashMap<SharedString, ActionBuilder>,
     pending_effects: VecDeque<Effect>,
     pub(crate) pending_notifications: HashSet<EntityId>,
@@ -163,8 +168,8 @@ impl AppContext {
         );
 
         let text_system = Arc::new(TextSystem::new(platform.text_system()));
-        let mut entities = EntityMap::new();
-        let unit_entity = entities.insert(entities.reserve(), ());
+        let entities = EntityMap::new();
+
         let app_metadata = AppMetadata {
             os_name: platform.os_name(),
             os_version: platform.os_version().ok(),
@@ -186,10 +191,9 @@ impl AppContext {
                 image_cache: ImageCache::new(http_client),
                 text_style_stack: Vec::new(),
                 globals_by_type: HashMap::default(),
-                unit_entity,
                 entities,
                 windows: SlotMap::with_key(),
-                keymap: Arc::new(RwLock::new(Keymap::default())),
+                keymap: Arc::new(Mutex::new(Keymap::default())),
                 global_action_listeners: HashMap::default(),
                 action_builders: HashMap::default(),
                 pending_effects: VecDeque::new(),
@@ -207,6 +211,8 @@ impl AppContext {
         })
     }
 
+    /// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit`
+    /// will be given 100ms to complete before exiting.
     pub fn quit(&mut self) {
         let mut futures = Vec::new();
 
@@ -234,6 +240,8 @@ impl AppContext {
         self.app_metadata.clone()
     }
 
+    /// Schedules all windows in the application to be redrawn. This can be called
+    /// multiple times in an update cycle and still result in a single redraw.
     pub fn refresh(&mut self) {
         self.pending_effects.push_back(Effect::Refresh);
     }
@@ -313,6 +321,9 @@ impl AppContext {
         self.pending_effects.push_back(effect);
     }
 
+    /// Called at the end of AppContext::update to complete any side effects
+    /// such as notifying observers, emitting events, etc. Effects can themselves
+    /// cause effects, so we continue looping until all effects are processed.
     fn flush_effects(&mut self) {
         loop {
             self.release_dropped_entities();
@@ -363,6 +374,9 @@ impl AppContext {
         }
     }
 
+    /// Repeatedly called during `flush_effects` to release any entities whose
+    /// reference count has become zero. We invoke any release observers before dropping
+    /// each entity.
     fn release_dropped_entities(&mut self) {
         loop {
             let dropped = self.entities.take_dropped();
@@ -380,6 +394,9 @@ impl AppContext {
         }
     }
 
+    /// Repeatedly called during `flush_effects` to handle a focused handle being dropped.
+    /// For now, we simply blur the window if this happens, but we may want to support invoking
+    /// a window blur handler to restore focus to some logical element.
     fn release_dropped_focus_handles(&mut self) {
         for window_handle in self.windows() {
             self.update_window(window_handle, |cx| {
@@ -414,7 +431,7 @@ impl AppContext {
     fn apply_emit_effect(&mut self, emitter: EntityId, event: Box<dyn Any>) {
         self.event_listeners
             .clone()
-            .retain(&emitter, |handler| handler(&event, self));
+            .retain(&emitter, |handler| handler(event.as_ref(), self));
     }
 
     fn apply_focus_changed_effect(
@@ -462,10 +479,12 @@ impl AppContext {
             .retain(&type_id, |observer| observer(self));
     }
 
-    fn apply_defer_effect(&mut self, callback: Box<dyn FnOnce(&mut Self) + Send + Sync + 'static>) {
+    fn apply_defer_effect(&mut self, callback: Box<dyn FnOnce(&mut Self) + Send + 'static>) {
         callback(self);
     }
 
+    /// Creates an `AsyncAppContext`, which can be cloned and has a static lifetime
+    /// so it can be held across `await` points.
     pub fn to_async(&self) -> AsyncAppContext {
         AsyncAppContext {
             app: unsafe { mem::transmute(self.this.clone()) },
@@ -473,10 +492,14 @@ impl AppContext {
         }
     }
 
+    /// Obtains a reference to the executor, which can be used to spawn futures.
     pub fn executor(&self) -> &Executor {
         &self.executor
     }
 
+    /// Runs the given closure on the main thread, where interaction with the platform
+    /// is possible. The given closure will be invoked with a `MainThread<AppContext>`, which
+    /// has platform-specific methods that aren't present on `AppContext`.
     pub fn run_on_main<R>(
         &mut self,
         f: impl FnOnce(&mut MainThread<AppContext>) -> R + Send + 'static,
@@ -497,6 +520,11 @@ impl AppContext {
         }
     }
 
+    /// Spawns the future returned by the given function on the main thread, where interaction with
+    /// the platform is possible. The given closure will be invoked with a `MainThread<AsyncAppContext>`,
+    /// which has platform-specific methods that aren't present on `AsyncAppContext`. The future will be
+    /// polled exclusively on the main thread.
+    // todo!("I think we need somehow to prevent the MainThread<AsyncAppContext> from implementing Send")
     pub fn spawn_on_main<F, R>(
         &self,
         f: impl FnOnce(MainThread<AsyncAppContext>) -> F + Send + 'static,
@@ -509,6 +537,8 @@ impl AppContext {
         self.executor.spawn_on_main(move || f(MainThread(cx)))
     }
 
+    /// Spawns the future returned by the given function on the thread pool. The closure will be invoked
+    /// with AsyncAppContext, which allows the application state to be accessed across await points.
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static) -> Task<R>
     where
         Fut: Future<Output = R> + Send + 'static,
@@ -521,20 +551,25 @@ impl AppContext {
         })
     }
 
-    pub fn defer(&mut self, f: impl FnOnce(&mut AppContext) + 'static + Send + Sync) {
+    /// Schedules the given function to be run at the end of the current effect cycle, allowing entities
+    /// that are currently on the stack to be returned to the app.
+    pub fn defer(&mut self, f: impl FnOnce(&mut AppContext) + 'static + Send) {
         self.push_effect(Effect::Defer {
             callback: Box::new(f),
         });
     }
 
+    /// Accessor for the application's asset source, which is provided when constructing the `App`.
     pub fn asset_source(&self) -> &Arc<dyn AssetSource> {
         &self.asset_source
     }
 
+    /// Accessor for the text system.
     pub fn text_system(&self) -> &Arc<TextSystem> {
         &self.text_system
     }
 
+    /// The current text style. Which is composed of all the style refinements provided to `with_text_style`.
     pub fn text_style(&self) -> TextStyle {
         let mut style = TextStyle::default();
         for refinement in &self.text_style_stack {
@@ -543,10 +578,12 @@ impl AppContext {
         style
     }
 
+    /// Check whether a global of the given type has been assigned.
     pub fn has_global<G: 'static>(&self) -> bool {
         self.globals_by_type.contains_key(&TypeId::of::<G>())
     }
 
+    /// Access the global of the given type. Panics if a global for that type has not been assigned.
     pub fn global<G: 'static>(&self) -> &G {
         self.globals_by_type
             .get(&TypeId::of::<G>())
@@ -555,12 +592,14 @@ impl AppContext {
             .unwrap()
     }
 
+    /// Access the global of the given type if a value has been assigned.
     pub fn try_global<G: 'static>(&self) -> Option<&G> {
         self.globals_by_type
             .get(&TypeId::of::<G>())
             .map(|any_state| any_state.downcast_ref::<G>().unwrap())
     }
 
+    /// Access the global of the given type mutably. Panics if a global for that type has not been assigned.
     pub fn global_mut<G: 'static>(&mut self) -> &mut G {
         let global_type = TypeId::of::<G>();
         self.push_effect(Effect::NotifyGlobalObservers { global_type });
@@ -571,7 +610,9 @@ impl AppContext {
             .unwrap()
     }
 
-    pub fn default_global<G: 'static + Default + Sync + Send>(&mut self) -> &mut G {
+    /// Access the global of the given type mutably. A default value is assigned if a global of this type has not
+    /// yet been assigned.
+    pub fn default_global<G: 'static + Default + Send>(&mut self) -> &mut G {
         let global_type = TypeId::of::<G>();
         self.push_effect(Effect::NotifyGlobalObservers { global_type });
         self.globals_by_type
@@ -581,12 +622,15 @@ impl AppContext {
             .unwrap()
     }
 
-    pub fn set_global<G: Any + Send + Sync>(&mut self, global: G) {
+    /// Set the value of the global of the given type.
+    pub fn set_global<G: Any + Send>(&mut self, global: G) {
         let global_type = TypeId::of::<G>();
         self.push_effect(Effect::NotifyGlobalObservers { global_type });
         self.globals_by_type.insert(global_type, Box::new(global));
     }
 
+    /// Update the global of the given type with a closure. Unlike `global_mut`, this method provides
+    /// your closure with mutable access to the `AppContext` and the global simultaneously.
     pub fn update_global<G: 'static, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R {
         let mut global = self.lease_global::<G>();
         let result = f(&mut global, self);
@@ -594,9 +638,10 @@ impl AppContext {
         result
     }
 
+    /// Register a callback to be invoked when a global of the given type is updated.
     pub fn observe_global<G: 'static>(
         &mut self,
-        mut f: impl FnMut(&mut Self) + Send + Sync + 'static,
+        mut f: impl FnMut(&mut Self) + Send + 'static,
     ) -> Subscription {
         self.global_observers.insert(
             TypeId::of::<G>(),
@@ -607,6 +652,7 @@ impl AppContext {
         )
     }
 
+    /// Move the global of the given type to the stack.
     pub(crate) fn lease_global<G: 'static>(&mut self) -> GlobalLease<G> {
         GlobalLease::new(
             self.globals_by_type
@@ -616,6 +662,7 @@ impl AppContext {
         )
     }
 
+    /// Restore the global of the given type after it is moved to the stack.
     pub(crate) fn end_global_lease<G: 'static>(&mut self, lease: GlobalLease<G>) {
         let global_type = TypeId::of::<G>();
         self.push_effect(Effect::NotifyGlobalObservers { global_type });
@@ -625,7 +672,7 @@ impl AppContext {
     pub fn observe_release<E: 'static>(
         &mut self,
         handle: &Handle<E>,
-        mut on_release: impl FnMut(&mut E, &mut AppContext) + Send + Sync + 'static,
+        mut on_release: impl FnMut(&mut E, &mut AppContext) + Send + 'static,
     ) -> Subscription {
         self.release_listeners.insert(
             handle.entity_id,
@@ -644,15 +691,14 @@ impl AppContext {
         self.text_style_stack.pop();
     }
 
+    /// Register key bindings.
     pub fn bind_keys(&mut self, bindings: impl IntoIterator<Item = KeyBinding>) {
-        self.keymap.write().add_bindings(bindings);
+        self.keymap.lock().add_bindings(bindings);
         self.pending_effects.push_back(Effect::Refresh);
     }
 
-    pub fn on_action<A: Action>(
-        &mut self,
-        listener: impl Fn(&A, &mut Self) + Send + Sync + 'static,
-    ) {
+    /// Register a global listener for actions invoked via the keyboard.
+    pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + Send + 'static) {
         self.global_action_listeners
             .entry(TypeId::of::<A>())
             .or_default()
@@ -664,10 +710,12 @@ impl AppContext {
             }));
     }
 
+    /// Register an action type to allow it to be referenced in keymaps.
     pub fn register_action_type<A: Action>(&mut self) {
         self.action_builders.insert(A::qualified_name(), A::build);
     }
 
+    /// Construct an action based on its name and parameters.
     pub fn build_action(
         &mut self,
         name: &str,
@@ -680,18 +728,23 @@ impl AppContext {
         (build)(params)
     }
 
+    /// Halt propagation of a mouse event, keyboard event, or action. This prevents listeners
+    /// that have not yet been invoked from receiving the event.
     pub fn stop_propagation(&mut self) {
         self.propagate_event = false;
     }
 }
 
 impl Context for AppContext {
-    type EntityContext<'a, 'w, T> = ModelContext<'a, T>;
+    type EntityContext<'a, T> = ModelContext<'a, T>;
     type Result<T> = T;
 
-    fn entity<T: Any + Send + Sync>(
+    /// Build an entity that is owned by the application. The given function will be invoked with
+    /// a `ModelContext` and must return an object representing the entity. A `Handle` will be returned
+    /// which can be used to access the entity in a context.
+    fn entity<T: 'static + Send>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+        build_entity: impl FnOnce(&mut Self::EntityContext<'_, T>) -> T,
     ) -> Handle<T> {
         self.update(|cx| {
             let slot = cx.entities.reserve();
@@ -700,10 +753,12 @@ impl Context for AppContext {
         })
     }
 
+    /// Update the entity referenced by the given handle. The function is passed a mutable reference to the
+    /// entity along with a `ModelContext` for the entity.
     fn update_entity<T: 'static, R>(
         &mut self,
         handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, T>) -> R,
     ) -> R {
         self.update(|cx| {
             let mut entity = cx.entities.lease(handle);
@@ -725,30 +780,37 @@ where
         self.0.borrow().platform.borrow_on_main_thread()
     }
 
+    /// Instructs the platform to activate the application by bringing it to the foreground.
     pub fn activate(&self, ignoring_other_apps: bool) {
         self.platform().activate(ignoring_other_apps);
     }
 
+    /// Writes data to the platform clipboard.
     pub fn write_to_clipboard(&self, item: ClipboardItem) {
         self.platform().write_to_clipboard(item)
     }
 
+    /// Reads data from the platform clipboard.
     pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
         self.platform().read_from_clipboard()
     }
 
+    /// Writes credentials to the platform keychain.
     pub fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
         self.platform().write_credentials(url, username, password)
     }
 
+    /// Reads credentials from the platform keychain.
     pub fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
         self.platform().read_credentials(url)
     }
 
+    /// Deletes credentials from the platform keychain.
     pub fn delete_credentials(&self, url: &str) -> Result<()> {
         self.platform().delete_credentials(url)
     }
 
+    /// Directs the platform's default browser to open the given URL.
     pub fn open_url(&self, url: &str) {
         self.platform().open_url(url);
     }
@@ -779,6 +841,9 @@ impl MainThread<AppContext> {
         })
     }
 
+    /// Opens a new window with the given option and the root view returned by the given function.
+    /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
+    /// functionality.
     pub fn open_window<V: 'static>(
         &mut self,
         options: crate::WindowOptions,
@@ -795,7 +860,9 @@ impl MainThread<AppContext> {
         })
     }
 
-    pub fn update_global<G: 'static + Send + Sync, R>(
+    /// Update the global of the given type with a closure. Unlike `global_mut`, this method provides
+    /// your closure with mutable access to the `MainThread<AppContext>` and the global simultaneously.
+    pub fn update_global<G: 'static + Send, R>(
         &mut self,
         update: impl FnOnce(&mut G, &mut MainThread<AppContext>) -> R,
     ) -> R {
@@ -806,13 +873,14 @@ impl MainThread<AppContext> {
     }
 }
 
+/// These effects are processed at the end of each application update cycle.
 pub(crate) enum Effect {
     Notify {
         emitter: EntityId,
     },
     Emit {
         emitter: EntityId,
-        event: Box<dyn Any + Send + Sync + 'static>,
+        event: Box<dyn Any + Send + 'static>,
     },
     FocusChanged {
         window_handle: AnyWindowHandle,
@@ -823,10 +891,11 @@ pub(crate) enum Effect {
         global_type: TypeId,
     },
     Defer {
-        callback: Box<dyn FnOnce(&mut AppContext) + Send + Sync + 'static>,
+        callback: Box<dyn FnOnce(&mut AppContext) + Send + 'static>,
     },
 }
 
+/// Wraps a global variable value during `update_global` while the value has been moved to the stack.
 pub(crate) struct GlobalLease<G: 'static> {
     global: AnyBox,
     global_type: PhantomData<G>,
@@ -855,6 +924,8 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
     }
 }
 
+/// Contains state associated with an active drag operation, started by dragging an element
+/// within the window or by dragging into the app from the underlying platform.
 pub(crate) struct AnyDrag {
     pub drag_handle_view: Option<AnyView>,
     pub cursor_offset: Point<Pixels>,

crates/gpui2/src/app/async_context.rs 🔗

@@ -1,11 +1,11 @@
 use crate::{
     AnyWindowHandle, AppContext, Context, Executor, Handle, MainThread, ModelContext, Result, Task,
-    ViewContext, WindowContext,
+    WindowContext,
 };
 use anyhow::Context as _;
 use derive_more::{Deref, DerefMut};
 use parking_lot::Mutex;
-use std::{any::Any, future::Future, sync::Weak};
+use std::{future::Future, sync::Weak};
 
 #[derive(Clone)]
 pub struct AsyncAppContext {
@@ -14,15 +14,15 @@ pub struct AsyncAppContext {
 }
 
 impl Context for AsyncAppContext {
-    type EntityContext<'a, 'w, T> = ModelContext<'a, T>;
+    type EntityContext<'a, T> = ModelContext<'a, T>;
     type Result<T> = Result<T>;
 
     fn entity<T: 'static>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+        build_entity: impl FnOnce(&mut Self::EntityContext<'_, T>) -> T,
     ) -> Self::Result<Handle<T>>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let app = self.app.upgrade().context("app was released")?;
         let mut lock = app.lock(); // Need this to compile
@@ -32,7 +32,7 @@ impl Context for AsyncAppContext {
     fn update_entity<T: 'static, R>(
         &mut self,
         handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, T>) -> R,
     ) -> Self::Result<R> {
         let app = self.app.upgrade().context("app was released")?;
         let mut lock = app.lock(); // Need this to compile
@@ -221,15 +221,15 @@ impl AsyncWindowContext {
 }
 
 impl Context for AsyncWindowContext {
-    type EntityContext<'a, 'w, T> = ViewContext<'a, 'w, T>;
+    type EntityContext<'a, T> = ModelContext<'a, T>;
     type Result<T> = Result<T>;
 
     fn entity<T>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+        build_entity: impl FnOnce(&mut Self::EntityContext<'_, T>) -> T,
     ) -> Result<Handle<T>>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         self.app
             .update_window(self.window, |cx| cx.entity(build_entity))
@@ -238,7 +238,7 @@ impl Context for AsyncWindowContext {
     fn update_entity<T: 'static, R>(
         &mut self,
         handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, T>) -> R,
     ) -> Result<R> {
         self.app
             .update_window(self.window, |cx| cx.update_entity(handle, update))

crates/gpui2/src/app/entity_map.rs 🔗

@@ -4,7 +4,7 @@ use derive_more::{Deref, DerefMut};
 use parking_lot::{RwLock, RwLockUpgradableReadGuard};
 use slotmap::{SecondaryMap, SlotMap};
 use std::{
-    any::{type_name, Any, TypeId},
+    any::{type_name, TypeId},
     fmt::{self, Display},
     hash::{Hash, Hasher},
     marker::PhantomData,
@@ -59,7 +59,7 @@ impl EntityMap {
     /// Insert an entity into a slot obtained by calling `reserve`.
     pub fn insert<T>(&mut self, slot: Slot<T>, entity: T) -> Handle<T>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let handle = slot.0;
         self.entities.insert(handle.entity_id, Box::new(entity));
@@ -100,10 +100,15 @@ impl EntityMap {
     }
 
     pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> {
-        let dropped_entity_ids = mem::take(&mut self.ref_counts.write().dropped_entity_ids);
+        let mut ref_counts = self.ref_counts.write();
+        let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
+
         dropped_entity_ids
             .into_iter()
-            .map(|entity_id| (entity_id, self.entities.remove(entity_id).unwrap()))
+            .map(|entity_id| {
+                ref_counts.counts.remove(entity_id);
+                (entity_id, self.entities.remove(entity_id).unwrap())
+            })
             .collect()
     }
 }
@@ -212,7 +217,6 @@ impl Drop for AnyHandle {
             if prev_count == 1 {
                 // We were the last reference to this entity, so we can remove it.
                 let mut entity_map = RwLockUpgradableReadGuard::upgrade(entity_map);
-                entity_map.counts.remove(self.entity_id);
                 entity_map.dropped_entity_ids.push(self.entity_id);
             }
         }
@@ -280,7 +284,7 @@ impl<T: 'static> Handle<T> {
     pub fn update<C, R>(
         &self,
         cx: &mut C,
-        update: impl FnOnce(&mut T, &mut C::EntityContext<'_, '_, T>) -> R,
+        update: impl FnOnce(&mut T, &mut C::EntityContext<'_, T>) -> R,
     ) -> C::Result<R>
     where
         C: Context,
@@ -423,7 +427,7 @@ impl<T: 'static> WeakHandle<T> {
     pub fn update<C, R>(
         &self,
         cx: &mut C,
-        update: impl FnOnce(&mut T, &mut C::EntityContext<'_, '_, T>) -> R,
+        update: impl FnOnce(&mut T, &mut C::EntityContext<'_, T>) -> R,
     ) -> Result<R>
     where
         C: Context,

crates/gpui2/src/app/model_context.rs 🔗

@@ -43,10 +43,10 @@ impl<'a, T: 'static> ModelContext<'a, T> {
     pub fn observe<T2: 'static>(
         &mut self,
         handle: &Handle<T2>,
-        mut on_notify: impl FnMut(&mut T, Handle<T2>, &mut ModelContext<'_, T>) + Send + Sync + 'static,
+        mut on_notify: impl FnMut(&mut T, Handle<T2>, &mut ModelContext<'_, T>) + Send + 'static,
     ) -> Subscription
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let this = self.weak_handle();
         let handle = handle.downgrade();
@@ -68,18 +68,17 @@ impl<'a, T: 'static> ModelContext<'a, T> {
         handle: &Handle<E>,
         mut on_event: impl FnMut(&mut T, Handle<E>, &E::Event, &mut ModelContext<'_, T>)
             + Send
-            + Sync
             + 'static,
     ) -> Subscription
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let this = self.weak_handle();
         let handle = handle.downgrade();
         self.app.event_listeners.insert(
             handle.entity_id,
             Box::new(move |event, cx| {
-                let event = event.downcast_ref().expect("invalid event type");
+                let event: &E::Event = event.downcast_ref().expect("invalid event type");
                 if let Some((this, handle)) = this.upgrade().zip(handle.upgrade()) {
                     this.update(cx, |this, cx| on_event(this, handle, event, cx));
                     true
@@ -92,7 +91,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
 
     pub fn on_release(
         &mut self,
-        mut on_release: impl FnMut(&mut T, &mut AppContext) + Send + Sync + 'static,
+        mut on_release: impl FnMut(&mut T, &mut AppContext) + Send + 'static,
     ) -> Subscription
     where
         T: 'static,
@@ -109,10 +108,10 @@ impl<'a, T: 'static> ModelContext<'a, T> {
     pub fn observe_release<E: 'static>(
         &mut self,
         handle: &Handle<E>,
-        mut on_release: impl FnMut(&mut T, &mut E, &mut ModelContext<'_, T>) + Send + Sync + 'static,
+        mut on_release: impl FnMut(&mut T, &mut E, &mut ModelContext<'_, T>) + Send + 'static,
     ) -> Subscription
     where
-        T: Any + Send + Sync,
+        T: Any + Send,
     {
         let this = self.weak_handle();
         self.app.observe_release(handle, move |entity, cx| {
@@ -124,10 +123,10 @@ impl<'a, T: 'static> ModelContext<'a, T> {
 
     pub fn observe_global<G: 'static>(
         &mut self,
-        mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + Send + Sync + 'static,
+        mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + Send + 'static,
     ) -> Subscription
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let handle = self.weak_handle();
         self.global_observers.insert(
@@ -138,11 +137,11 @@ impl<'a, T: 'static> ModelContext<'a, T> {
 
     pub fn on_app_quit<Fut>(
         &mut self,
-        mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + Send + Sync + 'static,
+        mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + Send + 'static,
     ) -> Subscription
     where
         Fut: 'static + Future<Output = ()> + Send,
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let handle = self.weak_handle();
         self.app.quit_observers.insert(
@@ -173,7 +172,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
 
     pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
     where
-        G: 'static + Send + Sync,
+        G: 'static + Send,
     {
         let mut global = self.app.lease_global::<G>();
         let result = f(&mut global, self);
@@ -210,7 +209,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
 impl<'a, T> ModelContext<'a, T>
 where
     T: EventEmitter,
-    T::Event: Send + Sync,
+    T::Event: Send,
 {
     pub fn emit(&mut self, event: T::Event) {
         self.app.pending_effects.push_back(Effect::Emit {
@@ -221,15 +220,15 @@ where
 }
 
 impl<'a, T> Context for ModelContext<'a, T> {
-    type EntityContext<'b, 'c, U> = ModelContext<'b, U>;
+    type EntityContext<'b, U> = ModelContext<'b, U>;
     type Result<U> = U;
 
     fn entity<U>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, U>) -> U,
+        build_entity: impl FnOnce(&mut Self::EntityContext<'_, U>) -> U,
     ) -> Handle<U>
     where
-        U: 'static + Send + Sync,
+        U: 'static + Send,
     {
         self.app.entity(build_entity)
     }
@@ -237,7 +236,7 @@ impl<'a, T> Context for ModelContext<'a, T> {
     fn update_entity<U: 'static, R>(
         &mut self,
         handle: &Handle<U>,
-        update: impl FnOnce(&mut U, &mut Self::EntityContext<'_, '_, U>) -> R,
+        update: impl FnOnce(&mut U, &mut Self::EntityContext<'_, U>) -> R,
     ) -> R {
         self.app.update_entity(handle, update)
     }

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

@@ -3,7 +3,7 @@ use crate::{
     ModelContext, Result, Task, TestDispatcher, TestPlatform, WindowContext,
 };
 use parking_lot::Mutex;
-use std::{any::Any, future::Future, sync::Arc};
+use std::{future::Future, sync::Arc};
 
 #[derive(Clone)]
 pub struct TestAppContext {
@@ -12,15 +12,15 @@ pub struct TestAppContext {
 }
 
 impl Context for TestAppContext {
-    type EntityContext<'a, 'w, T> = ModelContext<'a, T>;
+    type EntityContext<'a, T> = ModelContext<'a, T>;
     type Result<T> = T;
 
     fn entity<T: 'static>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+        build_entity: impl FnOnce(&mut Self::EntityContext<'_, T>) -> T,
     ) -> Self::Result<Handle<T>>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let mut lock = self.app.lock();
         lock.entity(build_entity)
@@ -29,7 +29,7 @@ impl Context for TestAppContext {
     fn update_entity<T: 'static, R>(
         &mut self,
         handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, T>) -> R,
     ) -> Self::Result<R> {
         let mut lock = self.app.lock();
         lock.update_entity(handle, update)

crates/gpui2/src/element.rs 🔗

@@ -16,8 +16,6 @@ pub trait Element<V: 'static> {
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState;
-    // where
-    //     V: Any + Send + Sync;
 
     fn layout(
         &mut self,
@@ -25,8 +23,6 @@ pub trait Element<V: 'static> {
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) -> LayoutId;
-    // where
-    //     V: Any + Send + Sync;
 
     fn paint(
         &mut self,
@@ -35,9 +31,6 @@ pub trait Element<V: 'static> {
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     );
-
-    // where
-    //     Self::ViewState: Any + Send + Sync;
 }
 
 #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
@@ -104,8 +97,7 @@ impl<V, E: Element<V>> RenderedElement<V, E> {
 impl<V, E> ElementObject<V> for RenderedElement<V, E>
 where
     E: Element<V>,
-    // E::ViewState: Any + Send + Sync,
-    E::ElementState: Any + Send + Sync,
+    E::ElementState: 'static + Send,
 {
     fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
         let frame_state = if let Some(id) = self.element.id() {
@@ -178,18 +170,16 @@ where
     }
 }
 
-pub struct AnyElement<V>(Box<dyn ElementObject<V> + Send + Sync>);
+pub struct AnyElement<V>(Box<dyn ElementObject<V> + Send>);
 
 unsafe impl<V> Send for AnyElement<V> {}
-unsafe impl<V> Sync for AnyElement<V> {}
 
 impl<V> AnyElement<V> {
     pub fn new<E>(element: E) -> Self
     where
         V: 'static,
-        E: 'static + Send + Sync,
-        E: Element<V>,
-        E::ElementState: Any + Send + Sync,
+        E: 'static + Element<V> + Send,
+        E::ElementState: Any + Send,
     {
         AnyElement(Box::new(RenderedElement::new(element)))
     }
@@ -230,8 +220,8 @@ impl<V> Component<V> for AnyElement<V> {
 impl<V, E, F> Element<V> for Option<F>
 where
     V: 'static,
-    E: 'static + Component<V> + Send + Sync,
-    F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + Sync + 'static,
+    E: 'static + Component<V> + Send,
+    F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static,
 {
     type ElementState = AnyElement<V>;
 
@@ -274,8 +264,8 @@ where
 impl<V, E, F> Component<V> for Option<F>
 where
     V: 'static,
-    E: 'static + Component<V> + Send + Sync,
-    F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + Sync + 'static,
+    E: 'static + Component<V> + Send,
+    F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static,
 {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
@@ -285,8 +275,8 @@ where
 impl<V, E, F> Component<V> for F
 where
     V: 'static,
-    E: 'static + Component<V> + Send + Sync,
-    F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + Sync + 'static,
+    E: 'static + Component<V> + Send,
+    F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static,
 {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(Some(self))

crates/gpui2/src/executor.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{AppContext, PlatformDispatcher};
-use futures::{channel::mpsc, pin_mut};
+use futures::{channel::mpsc, pin_mut, FutureExt};
 use smol::prelude::*;
 use std::{
     fmt::Debug,
@@ -162,16 +162,16 @@ impl Executor {
         duration: Duration,
         future: impl Future<Output = R>,
     ) -> Result<R, impl Future<Output = R>> {
-        let mut future = Box::pin(future);
-        let timeout = {
-            let future = &mut future;
-            async {
-                let timer = async {
-                    self.timer(duration).await;
-                    Err(())
-                };
-                let future = async move { Ok(future.await) };
-                timer.race(future).await
+        let mut future = Box::pin(future.fuse());
+        if duration.is_zero() {
+            return Err(future);
+        }
+
+        let mut timer = self.timer(duration).fuse();
+        let timeout = async {
+            futures::select_biased! {
+                value = future => Ok(value),
+                _ = timer => Err(()),
             }
         };
         match self.block(timeout) {
@@ -216,7 +216,7 @@ impl Executor {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
-        self.dispatcher.as_test().unwrap().simulate_random_delay()
+        self.spawn(self.dispatcher.as_test().unwrap().simulate_random_delay())
     }
 
     #[cfg(any(test, feature = "test-support"))]

crates/gpui2/src/focusable.rs 🔗

@@ -4,12 +4,11 @@ use crate::{
 };
 use refineable::Refineable;
 use smallvec::SmallVec;
-use std::sync::Arc;
 
 pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
 
 pub type FocusListener<V> =
-    Arc<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + Send + Sync + 'static>;
+    Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + Send + 'static>;
 
 pub trait Focusable<V: 'static>: Element<V> {
     fn focus_listeners(&mut self) -> &mut FocusListeners<V>;
@@ -43,13 +42,13 @@ pub trait Focusable<V: 'static>: Element<V> {
 
     fn on_focus(
         mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.focus_listeners()
-            .push(Arc::new(move |view, focus_handle, event, cx| {
+            .push(Box::new(move |view, focus_handle, event, cx| {
                 if event.focused.as_ref() == Some(focus_handle) {
                     listener(view, event, cx)
                 }
@@ -59,13 +58,13 @@ pub trait Focusable<V: 'static>: Element<V> {
 
     fn on_blur(
         mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.focus_listeners()
-            .push(Arc::new(move |view, focus_handle, event, cx| {
+            .push(Box::new(move |view, focus_handle, event, cx| {
                 if event.blurred.as_ref() == Some(focus_handle) {
                     listener(view, event, cx)
                 }
@@ -75,13 +74,13 @@ pub trait Focusable<V: 'static>: Element<V> {
 
     fn on_focus_in(
         mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.focus_listeners()
-            .push(Arc::new(move |view, focus_handle, event, cx| {
+            .push(Box::new(move |view, focus_handle, event, cx| {
                 let descendant_blurred = event
                     .blurred
                     .as_ref()
@@ -100,13 +99,13 @@ pub trait Focusable<V: 'static>: Element<V> {
 
     fn on_focus_out(
         mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.focus_listeners()
-            .push(Arc::new(move |view, focus_handle, event, cx| {
+            .push(Box::new(move |view, focus_handle, event, cx| {
                 let descendant_blurred = event
                     .blurred
                     .as_ref()
@@ -123,7 +122,7 @@ pub trait Focusable<V: 'static>: Element<V> {
     }
 }
 
-pub trait ElementFocus<V: 'static>: 'static + Send + Sync {
+pub trait ElementFocus<V: 'static>: 'static + Send {
     fn as_focusable(&self) -> Option<&FocusEnabled<V>>;
     fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>>;
 
@@ -138,7 +137,7 @@ pub trait ElementFocus<V: 'static>: 'static + Send + Sync {
                 .focus_handle
                 .get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle()))
                 .clone();
-            for listener in focusable.focus_listeners.iter().cloned() {
+            for listener in focusable.focus_listeners.drain(..) {
                 let focus_handle = focus_handle.clone();
                 cx.on_focus_changed(move |view, event, cx| {
                     listener(view, &focus_handle, event, cx)

crates/gpui2/src/gpui2.rs 🔗

@@ -67,23 +67,42 @@ use std::{
 };
 use taffy::TaffyLayoutEngine;
 
-type AnyBox = Box<dyn Any + Send + Sync>;
+type AnyBox = Box<dyn Any + Send>;
 
 pub trait Context {
-    type EntityContext<'a, 'w, T>;
+    type EntityContext<'a, T>;
     type Result<T>;
 
     fn entity<T>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+        build_entity: impl FnOnce(&mut Self::EntityContext<'_, T>) -> T,
     ) -> Self::Result<Handle<T>>
     where
-        T: 'static + Send + Sync;
+        T: 'static + Send;
 
     fn update_entity<T: 'static, R>(
         &mut self,
         handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, T>) -> R,
+    ) -> Self::Result<R>;
+}
+
+pub trait VisualContext: Context {
+    type ViewContext<'a, 'w, V>;
+
+    fn build_view<E, V>(
+        &mut self,
+        build_entity: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V,
+        render: impl Fn(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static,
+    ) -> Self::Result<View<V>>
+    where
+        E: Component<V>,
+        V: 'static + Send;
+
+    fn update_view<V: 'static, R>(
+        &mut self,
+        view: &View<V>,
+        update: impl FnOnce(&mut V, &mut Self::ViewContext<'_, '_, V>) -> R,
     ) -> Self::Result<R>;
 }
 
@@ -111,21 +130,21 @@ impl<T> DerefMut for MainThread<T> {
 }
 
 impl<C: Context> Context for MainThread<C> {
-    type EntityContext<'a, 'w, T> = MainThread<C::EntityContext<'a, 'w, T>>;
+    type EntityContext<'a, T> = MainThread<C::EntityContext<'a, T>>;
     type Result<T> = C::Result<T>;
 
     fn entity<T>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+        build_entity: impl FnOnce(&mut Self::EntityContext<'_, T>) -> T,
     ) -> Self::Result<Handle<T>>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         self.0.entity(|cx| {
             let cx = unsafe {
                 mem::transmute::<
-                    &mut C::EntityContext<'_, '_, T>,
-                    &mut MainThread<C::EntityContext<'_, '_, T>>,
+                    &mut C::EntityContext<'_, T>,
+                    &mut MainThread<C::EntityContext<'_, T>>,
                 >(cx)
             };
             build_entity(cx)
@@ -135,13 +154,13 @@ impl<C: Context> Context for MainThread<C> {
     fn update_entity<T: 'static, R>(
         &mut self,
         handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, T>) -> R,
     ) -> Self::Result<R> {
         self.0.update_entity(handle, |entity, cx| {
             let cx = unsafe {
                 mem::transmute::<
-                    &mut C::EntityContext<'_, '_, T>,
-                    &mut MainThread<C::EntityContext<'_, '_, T>>,
+                    &mut C::EntityContext<'_, T>,
+                    &mut MainThread<C::EntityContext<'_, T>>,
                 >(cx)
             };
             update(entity, cx)
@@ -149,12 +168,55 @@ impl<C: Context> Context for MainThread<C> {
     }
 }
 
+impl<C: VisualContext> VisualContext for MainThread<C> {
+    type ViewContext<'a, 'w, V> = MainThread<C::ViewContext<'a, 'w, V>>;
+
+    fn build_view<E, V>(
+        &mut self,
+        build_entity: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V,
+        render: impl Fn(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static,
+    ) -> Self::Result<View<V>>
+    where
+        E: Component<V>,
+        V: 'static + Send,
+    {
+        self.0.build_view(
+            |cx| {
+                let cx = unsafe {
+                    mem::transmute::<
+                        &mut C::ViewContext<'_, '_, V>,
+                        &mut MainThread<C::ViewContext<'_, '_, V>>,
+                    >(cx)
+                };
+                build_entity(cx)
+            },
+            render,
+        )
+    }
+
+    fn update_view<V: 'static, R>(
+        &mut self,
+        view: &View<V>,
+        update: impl FnOnce(&mut V, &mut Self::ViewContext<'_, '_, V>) -> R,
+    ) -> Self::Result<R> {
+        self.0.update_view(view, |view_state, cx| {
+            let cx = unsafe {
+                mem::transmute::<
+                    &mut C::ViewContext<'_, '_, V>,
+                    &mut MainThread<C::ViewContext<'_, '_, V>>,
+                >(cx)
+            };
+            update(view_state, cx)
+        })
+    }
+}
+
 pub trait BorrowAppContext {
     fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R
     where
         F: FnOnce(&mut Self) -> R;
 
-    fn set_global<T: Send + Sync + 'static>(&mut self, global: T);
+    fn set_global<T: Send + 'static>(&mut self, global: T);
 }
 
 impl<C> BorrowAppContext for C
@@ -171,7 +233,7 @@ where
         result
     }
 
-    fn set_global<G: 'static + Send + Sync>(&mut self, global: G) {
+    fn set_global<G: 'static + Send>(&mut self, global: G) {
         self.borrow_mut().set_global(global)
     }
 }

crates/gpui2/src/interactive.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{
-    point, px, view, Action, AnyBox, AnyDrag, AppContext, BorrowWindow, Bounds, Component,
+    point, px, Action, AnyBox, AnyDrag, AppContext, BorrowWindow, Bounds, Component,
     DispatchContext, DispatchPhase, Element, ElementId, FocusHandle, KeyMatch, Keystroke,
-    Modifiers, Overflow, Pixels, Point, SharedString, Size, Style, StyleRefinement, ViewContext,
+    Modifiers, Overflow, Pixels, Point, SharedString, Size, Style, StyleRefinement, View,
+    ViewContext,
 };
 use collections::HashMap;
 use derive_more::{Deref, DerefMut};
@@ -12,6 +13,7 @@ use std::{
     any::{Any, TypeId},
     fmt::Debug,
     marker::PhantomData,
+    mem,
     ops::Deref,
     path::PathBuf,
     sync::Arc,
@@ -48,14 +50,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     fn on_mouse_down(
         mut self,
         button: MouseButton,
-        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction()
             .mouse_down_listeners
-            .push(Arc::new(move |view, event, bounds, phase, cx| {
+            .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
                     && event.button == button
                     && bounds.contains_point(&event.position)
@@ -69,14 +71,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     fn on_mouse_up(
         mut self,
         button: MouseButton,
-        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction()
             .mouse_up_listeners
-            .push(Arc::new(move |view, event, bounds, phase, cx| {
+            .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
                     && event.button == button
                     && bounds.contains_point(&event.position)
@@ -90,14 +92,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     fn on_mouse_down_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction()
             .mouse_down_listeners
-            .push(Arc::new(move |view, event, bounds, phase, cx| {
+            .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Capture
                     && event.button == button
                     && !bounds.contains_point(&event.position)
@@ -111,14 +113,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     fn on_mouse_up_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction()
             .mouse_up_listeners
-            .push(Arc::new(move |view, event, bounds, phase, cx| {
+            .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Capture
                     && event.button == button
                     && !bounds.contains_point(&event.position)
@@ -131,14 +133,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
 
     fn on_mouse_move(
         mut self,
-        handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction()
             .mouse_move_listeners
-            .push(Arc::new(move |view, event, bounds, phase, cx| {
+            .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
                     handler(view, event, cx);
                 }
@@ -148,14 +150,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
 
     fn on_scroll_wheel(
         mut self,
-        handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction()
             .scroll_wheel_listeners
-            .push(Arc::new(move |view, event, bounds, phase, cx| {
+            .push(Box::new(move |view, event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
                     handler(view, event, cx);
                 }
@@ -176,14 +178,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
 
     fn on_action<A: 'static>(
         mut self,
-        listener: impl Fn(&mut V, &A, DispatchPhase, &mut ViewContext<V>) + Send + Sync + 'static,
+        listener: impl Fn(&mut V, &A, DispatchPhase, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction().key_listeners.push((
             TypeId::of::<A>(),
-            Arc::new(move |view, event, _, phase, cx| {
+            Box::new(move |view, event, _, phase, cx| {
                 let event = event.downcast_ref().unwrap();
                 listener(view, event, phase, cx);
                 None
@@ -194,17 +196,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
 
     fn on_key_down(
         mut self,
-        listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>)
-            + Send
-            + Sync
-            + 'static,
+        listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction().key_listeners.push((
             TypeId::of::<KeyDownEvent>(),
-            Arc::new(move |view, event, _, phase, cx| {
+            Box::new(move |view, event, _, phase, cx| {
                 let event = event.downcast_ref().unwrap();
                 listener(view, event, phase, cx);
                 None
@@ -215,17 +214,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
 
     fn on_key_up(
         mut self,
-        listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>)
-            + Send
-            + Sync
-            + 'static,
+        listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction().key_listeners.push((
             TypeId::of::<KeyUpEvent>(),
-            Arc::new(move |view, event, _, phase, cx| {
+            Box::new(move |view, event, _, phase, cx| {
                 let event = event.downcast_ref().unwrap();
                 listener(view, event, phase, cx);
                 None
@@ -264,14 +260,14 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
 
     fn on_drop<S: 'static>(
         mut self,
-        listener: impl Fn(&mut V, S, &mut ViewContext<V>) + Send + Sync + 'static,
+        listener: impl Fn(&mut V, S, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateless_interaction().drop_listeners.push((
             TypeId::of::<S>(),
-            Arc::new(move |view, drag_state, cx| {
+            Box::new(move |view, drag_state, cx| {
                 listener(view, *drag_state.downcast().unwrap(), cx);
             }),
         ));
@@ -307,26 +303,26 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
 
     fn on_click(
         mut self,
-        listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
         self.stateful_interaction()
             .click_listeners
-            .push(Arc::new(move |view, event, cx| listener(view, event, cx)));
+            .push(Box::new(move |view, event, cx| listener(view, event, cx)));
         self
     }
 
     fn on_drag<S, R, E>(
         mut self,
-        listener: impl Fn(&mut V, &mut ViewContext<V>) -> Drag<S, R, V, E> + Send + Sync + 'static,
+        listener: impl Fn(&mut V, &mut ViewContext<V>) -> Drag<S, R, V, E> + Send + 'static,
     ) -> Self
     where
         Self: Sized,
-        S: Any + Send + Sync,
+        S: Any + Send,
         R: Fn(&mut V, &mut ViewContext<V>) -> E,
-        R: 'static + Send + Sync,
+        R: 'static + Send,
         E: Component<V>,
     {
         debug_assert!(
@@ -334,11 +330,10 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
             "calling on_drag more than once on the same element is not supported"
         );
         self.stateful_interaction().drag_listener =
-            Some(Arc::new(move |view_state, cursor_offset, cx| {
+            Some(Box::new(move |view_state, cursor_offset, cx| {
                 let drag = listener(view_state, cx);
-                let view_handle = cx.handle().upgrade().unwrap();
                 let drag_handle_view = Some(
-                    view(view_handle, move |view_state, cx| {
+                    View::for_handle(cx.handle().upgrade().unwrap(), move |view_state, cx| {
                         (drag.render_drag_handle)(view_state, cx)
                     })
                     .into_any(),
@@ -354,7 +349,7 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
     }
 }
 
-pub trait ElementInteraction<V: 'static>: 'static + Send + Sync {
+pub trait ElementInteraction<V: 'static>: 'static + Send {
     fn as_stateless(&self) -> &StatelessInteraction<V>;
     fn as_stateless_mut(&mut self) -> &mut StatelessInteraction<V>;
     fn as_stateful(&self) -> Option<&StatefulInteraction<V>>;
@@ -369,7 +364,7 @@ pub trait ElementInteraction<V: 'static>: 'static + Send + Sync {
             cx.with_element_id(stateful.id.clone(), |global_id, cx| {
                 stateful.key_listeners.push((
                     TypeId::of::<KeyDownEvent>(),
-                    Arc::new(move |_, key_down, context, phase, cx| {
+                    Box::new(move |_, key_down, context, phase, cx| {
                         if phase == DispatchPhase::Bubble {
                             let key_down = key_down.downcast_ref::<KeyDownEvent>().unwrap();
                             if let KeyMatch::Some(action) =
@@ -387,9 +382,9 @@ pub trait ElementInteraction<V: 'static>: 'static + Send + Sync {
                 result
             })
         } else {
-            let stateless = self.as_stateless();
+            let stateless = self.as_stateless_mut();
             cx.with_key_dispatch_context(stateless.dispatch_context.clone(), |cx| {
-                cx.with_key_listeners(&stateless.key_listeners, f)
+                cx.with_key_listeners(mem::take(&mut stateless.key_listeners), f)
             })
         }
     }
@@ -455,26 +450,26 @@ pub trait ElementInteraction<V: 'static>: 'static + Send + Sync {
         element_state: &mut InteractiveElementState,
         cx: &mut ViewContext<V>,
     ) {
-        let stateless = self.as_stateless();
-        for listener in stateless.mouse_down_listeners.iter().cloned() {
+        let stateless = self.as_stateless_mut();
+        for listener in stateless.mouse_down_listeners.drain(..) {
             cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| {
                 listener(state, event, &bounds, phase, cx);
             })
         }
 
-        for listener in stateless.mouse_up_listeners.iter().cloned() {
+        for listener in stateless.mouse_up_listeners.drain(..) {
             cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| {
                 listener(state, event, &bounds, phase, cx);
             })
         }
 
-        for listener in stateless.mouse_move_listeners.iter().cloned() {
+        for listener in stateless.mouse_move_listeners.drain(..) {
             cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| {
                 listener(state, event, &bounds, phase, cx);
             })
         }
 
-        for listener in stateless.scroll_wheel_listeners.iter().cloned() {
+        for listener in stateless.scroll_wheel_listeners.drain(..) {
             cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| {
                 listener(state, event, &bounds, phase, cx);
             })
@@ -510,7 +505,7 @@ pub trait ElementInteraction<V: 'static>: 'static + Send + Sync {
         }
 
         if cx.active_drag.is_some() {
-            let drop_listeners = stateless.drop_listeners.clone();
+            let drop_listeners = mem::take(&mut stateless.drop_listeners);
             cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
                 if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
                     if let Some(drag_state_type) =
@@ -532,9 +527,9 @@ pub trait ElementInteraction<V: 'static>: 'static + Send + Sync {
             });
         }
 
-        if let Some(stateful) = self.as_stateful() {
-            let click_listeners = stateful.click_listeners.clone();
-            let drag_listener = stateful.drag_listener.clone();
+        if let Some(stateful) = self.as_stateful_mut() {
+            let click_listeners = mem::take(&mut stateful.click_listeners);
+            let drag_listener = mem::take(&mut stateful.drag_listener);
 
             if !click_listeners.is_empty() || drag_listener.is_some() {
                 let pending_mouse_down = element_state.pending_mouse_down.clone();
@@ -690,7 +685,7 @@ impl<V> From<ElementId> for StatefulInteraction<V> {
     }
 }
 
-type DropListener<V> = dyn Fn(&mut V, AnyBox, &mut ViewContext<V>) + 'static + Send + Sync;
+type DropListener<V> = dyn Fn(&mut V, AnyBox, &mut ViewContext<V>) + 'static + Send;
 
 pub struct StatelessInteraction<V> {
     pub dispatch_context: DispatchContext,
@@ -703,7 +698,7 @@ pub struct StatelessInteraction<V> {
     pub group_hover_style: Option<GroupStyle>,
     drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
     group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
-    drop_listeners: SmallVec<[(TypeId, Arc<DropListener<V>>); 2]>,
+    drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
 }
 
 impl<V> StatelessInteraction<V> {
@@ -1082,40 +1077,35 @@ pub struct FocusEvent {
     pub focused: Option<FocusHandle>,
 }
 
-pub type MouseDownListener<V> = Arc<
+pub type MouseDownListener<V> = Box<
     dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
         + Send
-        + Sync
         + 'static,
 >;
-pub type MouseUpListener<V> = Arc<
+pub type MouseUpListener<V> = Box<
     dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
         + Send
-        + Sync
         + 'static,
 >;
 
-pub type MouseMoveListener<V> = Arc<
+pub type MouseMoveListener<V> = Box<
     dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
         + Send
-        + Sync
         + 'static,
 >;
 
-pub type ScrollWheelListener<V> = Arc<
+pub type ScrollWheelListener<V> = Box<
     dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
         + Send
-        + Sync
         + 'static,
 >;
 
-pub type ClickListener<V> =
-    Arc<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + Send + Sync + 'static>;
+pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + Send + 'static>;
 
 pub(crate) type DragListener<V> =
-    Arc<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + Send + Sync + 'static>;
+    Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + Send + 'static>;
 
-pub type KeyListener<V> = Arc<
+pub type KeyListener<V> = Box<
     dyn Fn(
             &mut V,
             &dyn Any,
@@ -1124,6 +1114,5 @@ pub type KeyListener<V> = Arc<
             &mut ViewContext<V>,
         ) -> Option<Box<dyn Action>>
         + Send
-        + Sync
         + 'static,
 >;

crates/gpui2/src/keymap/matcher.rs 🔗

@@ -1,17 +1,17 @@
 use crate::{Action, DispatchContext, Keymap, KeymapVersion, Keystroke};
-use parking_lot::RwLock;
+use parking_lot::Mutex;
 use smallvec::SmallVec;
 use std::sync::Arc;
 
 pub struct KeyMatcher {
     pending_keystrokes: Vec<Keystroke>,
-    keymap: Arc<RwLock<Keymap>>,
+    keymap: Arc<Mutex<Keymap>>,
     keymap_version: KeymapVersion,
 }
 
 impl KeyMatcher {
-    pub fn new(keymap: Arc<RwLock<Keymap>>) -> Self {
-        let keymap_version = keymap.read().version();
+    pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
+        let keymap_version = keymap.lock().version();
         Self {
             pending_keystrokes: Vec::new(),
             keymap_version,
@@ -21,7 +21,7 @@ impl KeyMatcher {
 
     // todo!("replace with a function that calls an FnMut for every binding matching the action")
     // pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator<Item = &Binding> {
-    //     self.keymap.read().bindings_for_action(action_id)
+    //     self.keymap.lock().bindings_for_action(action_id)
     // }
 
     pub fn clear_pending(&mut self) {
@@ -46,7 +46,7 @@ impl KeyMatcher {
         keystroke: &Keystroke,
         context_stack: &[&DispatchContext],
     ) -> KeyMatch {
-        let keymap = self.keymap.read();
+        let keymap = self.keymap.lock();
         // Clear pending keystrokes if the keymap has changed since the last matched keystroke.
         if keymap.version() != self.keymap_version {
             self.keymap_version = keymap.version();
@@ -89,7 +89,7 @@ impl KeyMatcher {
         contexts: &[&DispatchContext],
     ) -> Option<SmallVec<[Keystroke; 2]>> {
         self.keymap
-            .read()
+            .lock()
             .bindings()
             .iter()
             .rev()

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

@@ -185,8 +185,7 @@ impl MacPlatform {
         }))
     }
 
-    unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> {
-        let pasteboard = self.0.lock().pasteboard;
+    unsafe fn read_from_pasteboard(&self, pasteboard: *mut Object, kind: id) -> Option<&[u8]> {
         let data = pasteboard.dataForType(kind);
         if data == nil {
             None
@@ -787,14 +786,16 @@ impl Platform for MacPlatform {
     fn read_from_clipboard(&self) -> Option<ClipboardItem> {
         let state = self.0.lock();
         unsafe {
-            if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) {
+            if let Some(text_bytes) =
+                self.read_from_pasteboard(state.pasteboard, NSPasteboardTypeString)
+            {
                 let text = String::from_utf8_lossy(text_bytes).to_string();
                 let hash_bytes = self
-                    .read_from_pasteboard(state.text_hash_pasteboard_type)
+                    .read_from_pasteboard(state.pasteboard, state.text_hash_pasteboard_type)
                     .and_then(|bytes| bytes.try_into().ok())
                     .map(u64::from_be_bytes);
                 let metadata_bytes = self
-                    .read_from_pasteboard(state.metadata_pasteboard_type)
+                    .read_from_pasteboard(state.pasteboard, state.metadata_pasteboard_type)
                     .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok());
 
                 if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) {

crates/gpui2/src/scene.rs 🔗

@@ -775,55 +775,55 @@ impl Bounds<ScaledPixels> {
     }
 }
 
-#[cfg(test)]
-mod tests {
-    use crate::{point, size};
-
-    use super::*;
-    use smallvec::smallvec;
-
-    #[test]
-    fn test_scene() {
-        let mut scene = SceneBuilder::new();
-        assert_eq!(scene.layers_by_order.len(), 0);
-
-        scene.insert(&smallvec![1].into(), quad());
-        scene.insert(&smallvec![2].into(), shadow());
-        scene.insert(&smallvec![3].into(), quad());
-
-        let mut batches_count = 0;
-        for _ in scene.build().batches() {
-            batches_count += 1;
-        }
-        assert_eq!(batches_count, 3);
-    }
-
-    fn quad() -> Quad {
-        Quad {
-            order: 0,
-            bounds: Bounds {
-                origin: point(ScaledPixels(0.), ScaledPixels(0.)),
-                size: size(ScaledPixels(100.), ScaledPixels(100.)),
-            },
-            content_mask: Default::default(),
-            background: Default::default(),
-            border_color: Default::default(),
-            corner_radii: Default::default(),
-            border_widths: Default::default(),
-        }
-    }
-
-    fn shadow() -> Shadow {
-        Shadow {
-            order: Default::default(),
-            bounds: Bounds {
-                origin: point(ScaledPixels(0.), ScaledPixels(0.)),
-                size: size(ScaledPixels(100.), ScaledPixels(100.)),
-            },
-            corner_radii: Default::default(),
-            content_mask: Default::default(),
-            color: Default::default(),
-            blur_radius: Default::default(),
-        }
-    }
-}
+// #[cfg(test)]
+// mod tests {
+//     use crate::{point, size};
+
+//     use super::*;
+//     use smallvec::smallvec;
+
+//     #[test]
+//     fn test_scene() {
+//         let mut scene = SceneBuilder::new();
+//         assert_eq!(scene.layers_by_order.len(), 0);
+
+//         scene.insert(&smallvec![1].into(), quad());
+//         scene.insert(&smallvec![2].into(), shadow());
+//         scene.insert(&smallvec![3].into(), quad());
+
+//         let mut batches_count = 0;
+//         for _ in scene.build().batches() {
+//             batches_count += 1;
+//         }
+//         assert_eq!(batches_count, 3);
+//     }
+
+//     fn quad() -> Quad {
+//         Quad {
+//             order: 0,
+//             bounds: Bounds {
+//                 origin: point(ScaledPixels(0.), ScaledPixels(0.)),
+//                 size: size(ScaledPixels(100.), ScaledPixels(100.)),
+//             },
+//             content_mask: Default::default(),
+//             background: Default::default(),
+//             border_color: Default::default(),
+//             corner_radii: Default::default(),
+//             border_widths: Default::default(),
+//         }
+//     }
+
+//     fn shadow() -> Shadow {
+//         Shadow {
+//             order: Default::default(),
+//             bounds: Bounds {
+//                 origin: point(ScaledPixels(0.), ScaledPixels(0.)),
+//                 size: size(ScaledPixels(100.), ScaledPixels(100.)),
+//             },
+//             corner_radii: Default::default(),
+//             content_mask: Default::default(),
+//             color: Default::default(),
+//             blur_radius: Default::default(),
+//         }
+//     }
+// }

crates/gpui2/src/style.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
     Corners, CornersRefinement, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures,
-    FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rems, Result,
+    FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rems, Result, Rgba,
     SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext,
 };
 use refineable::{Cascade, Refineable};
@@ -417,3 +417,12 @@ impl From<Hsla> for HighlightStyle {
         }
     }
 }
+
+impl From<Rgba> for HighlightStyle {
+    fn from(color: Rgba) -> Self {
+        Self {
+            color: Some(color.into()),
+            ..Default::default()
+        }
+    }
+}

crates/gpui2/src/subscription.rs 🔗

@@ -21,8 +21,8 @@ struct SubscriberSetState<EmitterKey, Callback> {
 
 impl<EmitterKey, Callback> SubscriberSet<EmitterKey, Callback>
 where
-    EmitterKey: 'static + Send + Sync + Ord + Clone + Debug,
-    Callback: 'static + Send + Sync,
+    EmitterKey: 'static + Send + Ord + Clone + Debug,
+    Callback: 'static + Send,
 {
     pub fn new() -> Self {
         Self(Arc::new(Mutex::new(SubscriberSetState {
@@ -96,7 +96,7 @@ where
 
 #[must_use]
 pub struct Subscription {
-    unsubscribe: Option<Box<dyn FnOnce() + Send + Sync + 'static>>,
+    unsubscribe: Option<Box<dyn FnOnce() + Send + 'static>>,
 }
 
 impl Subscription {

crates/gpui2/src/taffy.rs 🔗

@@ -179,7 +179,7 @@ struct Measureable<F>(F);
 
 impl<F> taffy::tree::Measurable for Measureable<F>
 where
-    F: Send + Sync + Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels>,
+    F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync,
 {
     fn measure(
         &self,

crates/gpui2/src/test.rs 🔗

@@ -8,7 +8,7 @@ use std::{
 pub fn run_test(
     mut num_iterations: u64,
     max_retries: usize,
-    test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher)),
+    test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)),
     on_fail_fn: Option<fn()>,
     _fn_name: String, // todo!("re-enable fn_name")
 ) {
@@ -28,7 +28,7 @@ pub fn run_test(
             }
             let result = panic::catch_unwind(|| {
                 let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed));
-                test_fn(dispatcher);
+                test_fn(dispatcher, seed);
             });
 
             match result {

crates/gpui2/src/view.rs 🔗

@@ -1,24 +1,63 @@
-use parking_lot::Mutex;
-
 use crate::{
-    AnyBox, AnyElement, AnyHandle, BorrowWindow, Bounds, Component, Element, ElementId, Handle,
-    LayoutId, Pixels, ViewContext, WindowContext,
+    AnyBox, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId,
+    EntityId, Handle, LayoutId, Pixels, Size, ViewContext, VisualContext, WeakHandle,
+    WindowContext,
+};
+use anyhow::{Context, Result};
+use parking_lot::Mutex;
+use std::{
+    marker::PhantomData,
+    sync::{Arc, Weak},
 };
-use std::{marker::PhantomData, sync::Arc};
 
 pub struct View<V> {
-    state: Handle<V>,
-    render: Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyElement<V> + Send + Sync + 'static>,
+    pub(crate) state: Handle<V>,
+    render: Arc<Mutex<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyElement<V> + Send + 'static>>,
+}
+
+impl<V: 'static> View<V> {
+    pub fn for_handle<E>(
+        state: Handle<V>,
+        render: impl Fn(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static,
+    ) -> View<V>
+    where
+        E: Component<V>,
+    {
+        View {
+            state,
+            render: Arc::new(Mutex::new(
+                move |state: &mut V, cx: &mut ViewContext<'_, '_, V>| render(state, cx).render(),
+            )),
+        }
+    }
 }
 
 impl<V: 'static> View<V> {
     pub fn into_any(self) -> AnyView {
-        AnyView {
-            view: Arc::new(Mutex::new(self)),
+        AnyView(Arc::new(self))
+    }
+
+    pub fn downgrade(&self) -> WeakView<V> {
+        WeakView {
+            state: self.state.downgrade(),
+            render: Arc::downgrade(&self.render),
         }
     }
 }
 
+impl<V: 'static> View<V> {
+    pub fn update<C, R>(
+        &self,
+        cx: &mut C,
+        f: impl FnOnce(&mut V, &mut C::ViewContext<'_, '_, V>) -> R,
+    ) -> C::Result<R>
+    where
+        C: VisualContext,
+    {
+        cx.update_view(self, f)
+    }
+}
+
 impl<V> Clone for View<V> {
     fn clone(&self) -> Self {
         Self {
@@ -28,19 +67,6 @@ impl<V> Clone for View<V> {
     }
 }
 
-pub fn view<V, E>(
-    state: Handle<V>,
-    render: impl Fn(&mut V, &mut ViewContext<V>) -> E + Send + Sync + 'static,
-) -> View<V>
-where
-    E: Component<V>,
-{
-    View {
-        state,
-        render: Arc::new(move |state, cx| render(state, cx).render()),
-    }
-}
-
 impl<V: 'static, ParentViewState: 'static> Component<ParentViewState> for View<V> {
     fn render(self) -> AnyElement<ParentViewState> {
         AnyElement::new(EraseViewState {
@@ -63,8 +89,8 @@ impl<V: 'static> Element<()> for View<V> {
         _: Option<Self::ElementState>,
         cx: &mut ViewContext<()>,
     ) -> Self::ElementState {
-        self.state.update(cx, |state, cx| {
-            let mut any_element = (self.render)(state, cx);
+        self.update(cx, |state, cx| {
+            let mut any_element = (self.render.lock())(state, cx);
             any_element.initialize(state, cx);
             any_element
         })
@@ -76,7 +102,7 @@ impl<V: 'static> Element<()> for View<V> {
         element: &mut Self::ElementState,
         cx: &mut ViewContext<()>,
     ) -> LayoutId {
-        self.state.update(cx, |state, cx| element.layout(state, cx))
+        self.update(cx, |state, cx| element.layout(state, cx))
     }
 
     fn paint(
@@ -86,7 +112,38 @@ impl<V: 'static> Element<()> for View<V> {
         element: &mut Self::ElementState,
         cx: &mut ViewContext<()>,
     ) {
-        self.state.update(cx, |state, cx| element.paint(state, cx))
+        self.update(cx, |state, cx| element.paint(state, cx))
+    }
+}
+
+pub struct WeakView<V> {
+    pub(crate) state: WeakHandle<V>,
+    render: Weak<Mutex<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyElement<V> + Send + 'static>>,
+}
+
+impl<V: 'static> WeakView<V> {
+    pub fn upgrade(&self) -> Option<View<V>> {
+        let state = self.state.upgrade()?;
+        let render = self.render.upgrade()?;
+        Some(View { state, render })
+    }
+
+    pub fn update<R>(
+        &self,
+        cx: &mut WindowContext,
+        f: impl FnOnce(&mut V, &mut ViewContext<V>) -> R,
+    ) -> Result<R> {
+        let view = self.upgrade().context("error upgrading view")?;
+        Ok(view.update(cx, f))
+    }
+}
+
+impl<V> Clone for WeakView<V> {
+    fn clone(&self) -> Self {
+        Self {
+            state: self.state.clone(),
+            render: self.render.clone(),
+        }
     }
 }
 
@@ -96,7 +153,6 @@ struct EraseViewState<V, ParentV> {
 }
 
 unsafe impl<V, ParentV> Send for EraseViewState<V, ParentV> {}
-unsafe impl<V, ParentV> Sync for EraseViewState<V, ParentV> {}
 
 impl<V: 'static, ParentV: 'static> Component<ParentV> for EraseViewState<V, ParentV> {
     fn render(self) -> AnyElement<ParentV> {
@@ -141,39 +197,39 @@ impl<V: 'static, ParentV: 'static> Element<ParentV> for EraseViewState<V, Parent
 }
 
 trait ViewObject: Send + Sync {
-    fn entity_handle(&self) -> &AnyHandle;
-    fn initialize(&mut self, cx: &mut WindowContext) -> AnyBox;
-    fn layout(&mut self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId;
-    fn paint(&mut self, bounds: Bounds<Pixels>, element: &mut AnyBox, cx: &mut WindowContext);
+    fn entity_id(&self) -> EntityId;
+    fn initialize(&self, cx: &mut WindowContext) -> AnyBox;
+    fn layout(&self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId;
+    fn paint(&self, bounds: Bounds<Pixels>, element: &mut AnyBox, cx: &mut WindowContext);
 }
 
 impl<V: 'static> ViewObject for View<V> {
-    fn entity_handle(&self) -> &AnyHandle {
-        &self.state
+    fn entity_id(&self) -> EntityId {
+        self.state.entity_id()
     }
 
-    fn initialize(&mut self, cx: &mut WindowContext) -> AnyBox {
-        cx.with_element_id(self.state.entity_id, |_global_id, cx| {
-            self.state.update(cx, |state, cx| {
-                let mut any_element = Box::new((self.render)(state, cx));
+    fn initialize(&self, cx: &mut WindowContext) -> AnyBox {
+        cx.with_element_id(self.entity_id(), |_global_id, cx| {
+            self.update(cx, |state, cx| {
+                let mut any_element = Box::new((self.render.lock())(state, cx));
                 any_element.initialize(state, cx);
                 any_element as AnyBox
             })
         })
     }
 
-    fn layout(&mut self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId {
-        cx.with_element_id(self.state.entity_id, |_global_id, cx| {
-            self.state.update(cx, |state, cx| {
+    fn layout(&self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId {
+        cx.with_element_id(self.entity_id(), |_global_id, cx| {
+            self.update(cx, |state, cx| {
                 let element = element.downcast_mut::<AnyElement<V>>().unwrap();
                 element.layout(state, cx)
             })
         })
     }
 
-    fn paint(&mut self, _: Bounds<Pixels>, element: &mut AnyBox, cx: &mut WindowContext) {
-        cx.with_element_id(self.state.entity_id, |_global_id, cx| {
-            self.state.update(cx, |state, cx| {
+    fn paint(&self, _: Bounds<Pixels>, element: &mut AnyBox, cx: &mut WindowContext) {
+        cx.with_element_id(self.entity_id(), |_global_id, cx| {
+            self.update(cx, |state, cx| {
                 let element = element.downcast_mut::<AnyElement<V>>().unwrap();
                 element.paint(state, cx);
             });
@@ -181,13 +237,18 @@ impl<V: 'static> ViewObject for View<V> {
     }
 }
 
-pub struct AnyView {
-    view: Arc<Mutex<dyn ViewObject>>,
-}
+#[derive(Clone)]
+pub struct AnyView(Arc<dyn ViewObject>);
 
 impl AnyView {
-    pub fn entity_handle(&self) -> AnyHandle {
-        self.view.lock().entity_handle().clone()
+    pub(crate) fn draw(&self, available_space: Size<AvailableSpace>, cx: &mut WindowContext) {
+        let mut rendered_element = self.0.initialize(cx);
+        let layout_id = self.0.layout(&mut rendered_element, cx);
+        cx.window
+            .layout_engine
+            .compute_layout(layout_id, available_space);
+        let bounds = cx.window.layout_engine.layout_bounds(layout_id);
+        self.0.paint(bounds, &mut rendered_element, cx);
     }
 }
 
@@ -203,8 +264,8 @@ impl<ParentV: 'static> Component<ParentV> for AnyView {
 impl Element<()> for AnyView {
     type ElementState = AnyBox;
 
-    fn id(&self) -> Option<ElementId> {
-        Some(ElementId::View(self.view.lock().entity_handle().entity_id))
+    fn id(&self) -> Option<crate::ElementId> {
+        Some(ElementId::View(self.0.entity_id()))
     }
 
     fn initialize(
@@ -213,7 +274,7 @@ impl Element<()> for AnyView {
         _: Option<Self::ElementState>,
         cx: &mut ViewContext<()>,
     ) -> Self::ElementState {
-        self.view.lock().initialize(cx)
+        self.0.initialize(cx)
     }
 
     fn layout(
@@ -222,7 +283,7 @@ impl Element<()> for AnyView {
         element: &mut Self::ElementState,
         cx: &mut ViewContext<()>,
     ) -> LayoutId {
-        self.view.lock().layout(element, cx)
+        self.0.layout(element, cx)
     }
 
     fn paint(
@@ -232,7 +293,7 @@ impl Element<()> for AnyView {
         element: &mut AnyBox,
         cx: &mut ViewContext<()>,
     ) {
-        self.view.lock().paint(bounds, element, cx)
+        self.0.paint(bounds, element, cx)
     }
 }
 
@@ -242,7 +303,6 @@ struct EraseAnyViewState<ParentViewState> {
 }
 
 unsafe impl<ParentV> Send for EraseAnyViewState<ParentV> {}
-unsafe impl<ParentV> Sync for EraseAnyViewState<ParentV> {}
 
 impl<ParentV: 'static> Component<ParentV> for EraseAnyViewState<ParentV> {
     fn render(self) -> AnyElement<ParentV> {
@@ -263,7 +323,7 @@ impl<ParentV: 'static> Element<ParentV> for EraseAnyViewState<ParentV> {
         _: Option<Self::ElementState>,
         cx: &mut ViewContext<ParentV>,
     ) -> Self::ElementState {
-        self.view.view.lock().initialize(cx)
+        self.view.0.initialize(cx)
     }
 
     fn layout(
@@ -272,7 +332,7 @@ impl<ParentV: 'static> Element<ParentV> for EraseAnyViewState<ParentV> {
         element: &mut Self::ElementState,
         cx: &mut ViewContext<ParentV>,
     ) -> LayoutId {
-        self.view.view.lock().layout(element, cx)
+        self.view.0.layout(element, cx)
     }
 
     fn paint(
@@ -282,14 +342,6 @@ impl<ParentV: 'static> Element<ParentV> for EraseAnyViewState<ParentV> {
         element: &mut Self::ElementState,
         cx: &mut ViewContext<ParentV>,
     ) {
-        self.view.view.lock().paint(bounds, element, cx)
-    }
-}
-
-impl Clone for AnyView {
-    fn clone(&self) -> Self {
-        Self {
-            view: self.view.clone(),
-        }
+        self.view.0.paint(bounds, element, cx)
     }
 }

crates/gpui2/src/window.rs 🔗

@@ -1,13 +1,13 @@
 use crate::{
-    px, size, Action, AnyBox, AnyDrag, AnyHandle, AnyView, AppContext, AsyncWindowContext,
-    AvailableSpace, Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId,
-    Edges, Effect, Element, EntityId, EventEmitter, ExternalPaths, FileDropEvent, FocusEvent,
-    FontId, GlobalElementId, GlyphId, Handle, Hsla, ImageData, InputEvent, IsZero, KeyListener,
-    KeyMatch, KeyMatcher, Keystroke, LayoutId, MainThread, MainThreadOnly, Modifiers,
-    MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
-    PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams,
-    RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
-    Style, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, WeakHandle,
+    px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
+    Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect,
+    EntityId, EventEmitter, ExternalPaths, FileDropEvent, FocusEvent, FontId, GlobalElementId,
+    GlyphId, Handle, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher,
+    Keystroke, LayoutId, MainThread, MainThreadOnly, ModelContext, Modifiers, MonochromeSprite,
+    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas,
+    PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderImageParams,
+    RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, Subscription,
+    TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakHandle, WeakView,
     WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::Result;
@@ -30,24 +30,30 @@ use std::{
 };
 use util::ResultExt;
 
+/// A global stacking order, which is created by stacking successive z-index values.
+/// Each z-index will always be interpreted in the context of its parent z-index.
 #[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default)]
-pub struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
+pub(crate) struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
 
+/// Represents the two different phases when dispatching events.
 #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
 pub enum DispatchPhase {
-    /// After the capture phase comes the bubble phase, in which event handlers are
-    /// invoked front to back. This is the phase you'll usually want to use for event handlers.
+    /// After the capture phase comes the bubble phase, in which mouse event listeners are
+    /// invoked front to back and keyboard event listeners are invoked from the focused element
+    /// to the root of the element tree. This is the phase you'll most commonly want to use when
+    /// registering event listeners.
     #[default]
     Bubble,
-    /// During the initial capture phase, event handlers are invoked back to front. This phase
+    /// During the initial capture phase, mouse event listeners are invoked back to front, and keyboard
+    /// listeners are invoked from the root of the tree downward toward the focused element. This phase
     /// is used for special purposes such as clearing the "pressed" state for click events. If
     /// you stop event propagation during this phase, you need to know what you're doing. Handlers
     /// outside of the immediate region may rely on detecting non-local events during this phase.
     Capture,
 }
 
-type AnyListener = Arc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + Send + Sync + 'static>;
-type AnyKeyListener = Arc<
+type AnyListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + Send + 'static>;
+type AnyKeyListener = Box<
     dyn Fn(
             &dyn Any,
             &[&DispatchContext],
@@ -55,13 +61,13 @@ type AnyKeyListener = Arc<
             &mut WindowContext,
         ) -> Option<Box<dyn Action>>
         + Send
-        + Sync
         + 'static,
 >;
-type AnyFocusListener = Arc<dyn Fn(&FocusEvent, &mut WindowContext) + Send + Sync + 'static>;
+type AnyFocusListener = Box<dyn Fn(&FocusEvent, &mut WindowContext) + Send + 'static>;
 
 slotmap::new_key_type! { pub struct FocusId; }
 
+/// A handle which can be used to track and manipulate the focused element in a window.
 pub struct FocusHandle {
     pub(crate) id: FocusId,
     handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
@@ -93,20 +99,26 @@ impl FocusHandle {
         }
     }
 
+    /// Obtains whether the element associated with this handle is currently focused.
     pub fn is_focused(&self, cx: &WindowContext) -> bool {
         cx.window.focus == Some(self.id)
     }
 
+    /// Obtains whether the element associated with this handle contains the focused
+    /// element or is itself focused.
     pub fn contains_focused(&self, cx: &WindowContext) -> bool {
         cx.focused()
             .map_or(false, |focused| self.contains(&focused, cx))
     }
 
+    /// Obtains whether the element associated with this handle is contained within the
+    /// focused element or is itself focused.
     pub fn within_focused(&self, cx: &WindowContext) -> bool {
         let focused = cx.focused();
         focused.map_or(false, |focused| focused.contains(self, cx))
     }
 
+    /// Obtains whether this handle contains the given handle in the most recently rendered frame.
     pub(crate) fn contains(&self, other: &Self, cx: &WindowContext) -> bool {
         let mut ancestor = Some(other.id);
         while let Some(ancestor_id) = ancestor {
@@ -144,6 +156,7 @@ impl Drop for FocusHandle {
     }
 }
 
+// Holds the state for a specific window.
 pub struct Window {
     pub(crate) handle: AnyWindowHandle,
     platform_window: MainThreadOnly<Box<dyn PlatformWindow>>,
@@ -151,7 +164,7 @@ pub struct Window {
     sprite_atlas: Arc<dyn PlatformAtlas>,
     rem_size: Pixels,
     content_size: Size<Pixels>,
-    layout_engine: TaffyLayoutEngine,
+    pub(crate) layout_engine: TaffyLayoutEngine,
     pub(crate) root_view: Option<AnyView>,
     pub(crate) element_id_stack: GlobalElementId,
     prev_frame_element_states: HashMap<GlobalElementId, AnyBox>,
@@ -254,6 +267,9 @@ impl Window {
     }
 }
 
+/// When constructing the element tree, we maintain a stack of key dispatch frames until we
+/// find the focused element. We interleave key listeners with dispatch contexts so we can use the
+/// contexts when matching key events against the keymap.
 enum KeyDispatchStackFrame {
     Listener {
         event_type: TypeId,
@@ -262,6 +278,9 @@ enum KeyDispatchStackFrame {
     Context(DispatchContext),
 }
 
+/// Indicates which region of the window is visible. Content falling outside of this mask will not be
+/// rendered. Currently, only rectangular content masks are supported, but we give the mask its own type
+/// to leave room to support more complex shapes in the future.
 #[derive(Clone, Debug, Default, PartialEq, Eq)]
 #[repr(C)]
 pub struct ContentMask<P: Clone + Default + Debug> {
@@ -269,20 +288,25 @@ pub struct ContentMask<P: Clone + Default + Debug> {
 }
 
 impl ContentMask<Pixels> {
+    /// Scale the content mask's pixel units by the given scaling factor.
     pub fn scale(&self, factor: f32) -> ContentMask<ScaledPixels> {
         ContentMask {
             bounds: self.bounds.scale(factor),
         }
     }
 
+    /// Intersect the content mask with the given content mask.
     pub fn intersect(&self, other: &Self) -> Self {
         let bounds = self.bounds.intersect(&other.bounds);
         ContentMask { bounds }
     }
 }
 
+/// Provides access to application state in the context of a single window. Derefs
+/// to an `AppContext`, so you can also pass a `WindowContext` to any method that takes
+/// an `AppContext` and call any `AppContext` methods.
 pub struct WindowContext<'a, 'w> {
-    app: Reference<'a, AppContext>,
+    pub(crate) app: Reference<'a, AppContext>,
     pub(crate) window: Reference<'w, Window>,
 }
 
@@ -301,28 +325,30 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         }
     }
 
+    /// Obtain a handle to the window that belongs to this context.
     pub fn window_handle(&self) -> AnyWindowHandle {
         self.window.handle
     }
 
-    pub fn root_view(&self) -> Option<AnyHandle> {
-        Some(self.window.root_view.as_ref()?.entity_handle())
-    }
-
+    /// Mark the window as dirty, scheduling it to be redrawn on the next frame.
     pub fn notify(&mut self) {
         self.window.dirty = true;
     }
 
+    /// Obtain a new `FocusHandle`, which allows you to track and manipulate the keyboard focus
+    /// for elements rendered within this window.
     pub fn focus_handle(&mut self) -> FocusHandle {
         FocusHandle::new(&self.window.focus_handles)
     }
 
+    /// Obtain the currently focused `FocusHandle`. If no elements are focused, returns `None`.
     pub fn focused(&self) -> Option<FocusHandle> {
         self.window
             .focus
             .and_then(|id| FocusHandle::for_id(id, &self.window.focus_handles))
     }
 
+    /// Move focus to the element associated with the given `FocusHandle`.
     pub fn focus(&mut self, handle: &FocusHandle) {
         if self.window.last_blur.is_none() {
             self.window.last_blur = Some(self.window.focus);
@@ -336,6 +362,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         self.notify();
     }
 
+    /// Remove focus from all elements within this context's window.
     pub fn blur(&mut self) {
         if self.window.last_blur.is_none() {
             self.window.last_blur = Some(self.window.focus);
@@ -349,6 +376,9 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         self.notify();
     }
 
+    /// Schedule the given closure to be run on the main thread. It will be invoked with
+    /// a `MainThread<WindowContext>`, which provides access to platform-specific functionality
+    /// of the window.
     pub fn run_on_main<R>(
         &mut self,
         f: impl FnOnce(&mut MainThread<WindowContext<'_, '_>>) -> R + Send + 'static,
@@ -366,10 +396,13 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         }
     }
 
+    /// Create an `AsyncWindowContext`, which has a static lifetime and can be held across
+    /// await points in async code.
     pub fn to_async(&self) -> AsyncWindowContext {
         AsyncWindowContext::new(self.app.to_async(), self.window.handle)
     }
 
+    /// Schedule the given closure to be run directly after the current frame is rendered.
     pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + Send + 'static) {
         let f = Box::new(f);
         let display_id = self.window.display_id;
@@ -413,6 +446,9 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         .detach();
     }
 
+    /// Spawn the future returned by the given closure on the application thread pool.
+    /// The closure is provided a handle to the current window and an `AsyncWindowContext` for
+    /// use within your future.
     pub fn spawn<Fut, R>(
         &mut self,
         f: impl FnOnce(AnyWindowHandle, AsyncWindowContext) -> Fut + Send + 'static,
@@ -429,6 +465,8 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         })
     }
 
+    /// Update the global of the given type. The given closure is given simultaneous mutable
+    /// access both to the global and the context.
     pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
     where
         G: 'static,
@@ -439,6 +477,9 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         result
     }
 
+    /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
+    /// layout is being requested, along with the layout ids of any children. This method is called during
+    /// calls to the `Element::layout` trait method and enables any element to participate in layout.
     pub fn request_layout(
         &mut self,
         style: &Style,
@@ -453,6 +494,12 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             .request_layout(style, rem_size, &self.app.layout_id_buffer)
     }
 
+    /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children,
+    /// this variant takes a function that is invoked during layout so you can use arbitrary logic to
+    /// determine the element's size. One place this is used internally is when measuring text.
+    ///
+    /// The given closure is invoked at layout time with the known dimensions and available space and
+    /// returns a `Size`.
     pub fn request_measured_layout<
         F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync + 'static,
     >(
@@ -466,6 +513,9 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             .request_measured_layout(style, rem_size, measure)
     }
 
+    /// Obtain the bounds computed for the given LayoutId relative to the window. This method should not
+    /// be invoked until the paint phase begins, and will usually be invoked by GPUI itself automatically
+    /// in order to pass your element its `Bounds` automatically.
     pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds<Pixels> {
         let mut bounds = self
             .window
@@ -476,14 +526,20 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         bounds
     }
 
+    /// The scale factor of the display associated with the window. For example, it could
+    /// return 2.0 for a "retina" display, indicating that each logical pixel should actually
+    /// be rendered as two pixels on screen.
     pub fn scale_factor(&self) -> f32 {
         self.window.scale_factor
     }
 
+    /// The size of an em for the base font of the application. Adjusting this value allows the
+    /// UI to scale, just like zooming a web page.
     pub fn rem_size(&self) -> Pixels {
         self.window.rem_size
     }
 
+    /// The line height associated with the current text style.
     pub fn line_height(&self) -> Pixels {
         let rem_size = self.rem_size();
         let text_style = self.text_style();
@@ -492,17 +548,26 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             .to_pixels(text_style.font_size.into(), rem_size)
     }
 
+    /// Call to prevent the default action of an event. Currently only used to prevent
+    /// parent elements from becoming focused on mouse down.
     pub fn prevent_default(&mut self) {
         self.window.default_prevented = true;
     }
 
+    /// Obtain whether default has been prevented for the event currently being dispatched.
     pub fn default_prevented(&self) -> bool {
         self.window.default_prevented
     }
 
+    /// Register a mouse event listener on the window for the current frame. The type of event
+    /// is determined by the first parameter of the given listener. When the next frame is rendered
+    /// the listener will be cleared.
+    ///
+    /// This is a fairly low-level method, so prefer using event handlers on elements unless you have
+    /// a specific need to register a global listener.
     pub fn on_mouse_event<Event: 'static>(
         &mut self,
-        handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + Send + Sync + 'static,
+        handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + Send + 'static,
     ) {
         let order = self.window.z_index_stack.clone();
         self.window
@@ -511,23 +576,27 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             .or_default()
             .push((
                 order,
-                Arc::new(move |event: &dyn Any, phase, cx| {
+                Box::new(move |event: &dyn Any, phase, cx| {
                     handler(event.downcast_ref().unwrap(), phase, cx)
                 }),
             ))
     }
 
+    /// The position of the mouse relative to the window.
     pub fn mouse_position(&self) -> Point<Pixels> {
         self.window.mouse_position
     }
 
-    pub fn stack<R>(&mut self, order: u32, f: impl FnOnce(&mut Self) -> R) -> R {
-        self.window.z_index_stack.push(order);
+    /// Called during painting to invoke the given closure in a new stacking context. The given
+    /// z-index is interpreted relative to the previous call to `stack`.
+    pub fn stack<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
+        self.window.z_index_stack.push(z_index);
         let result = f(self);
         self.window.z_index_stack.pop();
         result
     }
 
+    /// Paint one or more drop shadows into the scene for the current frame at the current z-index.
     pub fn paint_shadows(
         &mut self,
         bounds: Bounds<Pixels>,
@@ -555,6 +624,8 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         }
     }
 
+    /// Paint one or more quads into the scene for the current frame at the current stacking context.
+    /// Quads are colored rectangular regions with an optional background, border, and corner radius.
     pub fn paint_quad(
         &mut self,
         bounds: Bounds<Pixels>,
@@ -581,6 +652,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         );
     }
 
+    /// Paint the given `Path` into the scene for the current frame at the current z-index.
     pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Hsla>) {
         let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
@@ -592,6 +664,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             .insert(&window.z_index_stack, path.scale(scale_factor));
     }
 
+    /// Paint an underline into the scene for the current frame at the current z-index.
     pub fn paint_underline(
         &mut self,
         origin: Point<Pixels>,
@@ -624,6 +697,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         Ok(())
     }
 
+    /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index.
     pub fn paint_glyph(
         &mut self,
         origin: Point<Pixels>,
@@ -676,6 +750,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         Ok(())
     }
 
+    /// Paint an emoji glyph into the scene for the current frame at the current z-index.
     pub fn paint_emoji(
         &mut self,
         origin: Point<Pixels>,
@@ -726,6 +801,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         Ok(())
     }
 
+    /// Paint a monochrome SVG into the scene for the current frame at the current stacking context.
     pub fn paint_svg(
         &mut self,
         bounds: Bounds<Pixels>,
@@ -766,6 +842,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         Ok(())
     }
 
+    /// Paint an image into the scene for the current frame at the current z-index.
     pub fn paint_image(
         &mut self,
         bounds: Bounds<Pixels>,
@@ -801,61 +878,42 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         Ok(())
     }
 
+    /// Draw pixels to the display for this window based on the contents of its scene.
     pub(crate) fn draw(&mut self) {
-        let unit_entity = self.unit_entity.clone();
-        self.update_entity(&unit_entity, |view, cx| {
-            cx.start_frame();
+        let root_view = self.window.root_view.take().unwrap();
 
-            let mut root_view = cx.window.root_view.take().unwrap();
+        self.start_frame();
 
-            cx.stack(0, |cx| {
-                let available_space = cx.window.content_size.map(Into::into);
-                draw_any_view(&mut root_view, available_space, cx);
-            });
-
-            if let Some(mut active_drag) = cx.active_drag.take() {
-                cx.stack(1, |cx| {
-                    let offset = cx.mouse_position() - active_drag.cursor_offset;
-                    cx.with_element_offset(Some(offset), |cx| {
-                        let available_space =
-                            size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-                        if let Some(drag_handle_view) = &mut active_drag.drag_handle_view {
-                            draw_any_view(drag_handle_view, available_space, cx);
-                        }
-                        cx.active_drag = Some(active_drag);
-                    });
-                });
-            }
-
-            cx.window.root_view = Some(root_view);
-            let scene = cx.window.scene_builder.build();
-
-            cx.run_on_main(view, |_, cx| {
-                cx.window
-                    .platform_window
-                    .borrow_on_main_thread()
-                    .draw(scene);
-                cx.window.dirty = false;
-            })
-            .detach();
+        self.stack(0, |cx| {
+            let available_space = cx.window.content_size.map(Into::into);
+            root_view.draw(available_space, cx);
         });
 
-        fn draw_any_view(
-            view: &mut AnyView,
-            available_space: Size<AvailableSpace>,
-            cx: &mut ViewContext<()>,
-        ) {
-            cx.with_optional_element_state(view.id(), |element_state, cx| {
-                let mut element_state = view.initialize(&mut (), element_state, cx);
-                let layout_id = view.layout(&mut (), &mut element_state, cx);
-                cx.window
-                    .layout_engine
-                    .compute_layout(layout_id, available_space);
-                let bounds = cx.window.layout_engine.layout_bounds(layout_id);
-                view.paint(bounds, &mut (), &mut element_state, cx);
-                ((), element_state)
+        if let Some(mut active_drag) = self.app.active_drag.take() {
+            self.stack(1, |cx| {
+                let offset = cx.mouse_position() - active_drag.cursor_offset;
+                cx.with_element_offset(Some(offset), |cx| {
+                    let available_space =
+                        size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+                    if let Some(drag_handle_view) = &mut active_drag.drag_handle_view {
+                        drag_handle_view.draw(available_space, cx);
+                    }
+                    cx.active_drag = Some(active_drag);
+                });
             });
         }
+
+        self.window.root_view = Some(root_view);
+        let scene = self.window.scene_builder.build();
+
+        self.run_on_main(|cx| {
+            cx.window
+                .platform_window
+                .borrow_on_main_thread()
+                .draw(scene);
+            cx.window.dirty = false;
+        })
+        .detach();
     }
 
     fn start_frame(&mut self) {
@@ -893,12 +951,17 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         window.freeze_key_dispatch_stack = false;
     }
 
+    /// Dispatch a mouse or keyboard event on the window.
     fn dispatch_event(&mut self, event: InputEvent) -> bool {
         let event = match event {
+            // Track the mouse position with our own state, since accessing the platform
+            // API for the mouse position can only occur on the main thread.
             InputEvent::MouseMove(mouse_move) => {
                 self.window.mouse_position = mouse_move.position;
                 InputEvent::MouseMove(mouse_move)
             }
+            // Translate dragging and dropping of external files from the operating system
+            // to internal drag and drop events.
             InputEvent::FileDrop(file_drop) => match file_drop {
                 FileDropEvent::Entered { position, files } => {
                     self.window.mouse_position = position;
@@ -1059,6 +1122,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         true
     }
 
+    /// Attempt to map a keystroke to an action based on the keymap.
     pub fn match_keystroke(
         &mut self,
         element_id: &GlobalElementId,
@@ -1081,9 +1145,11 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         key_match
     }
 
+    /// Register the given handler to be invoked whenever the global of the given type
+    /// is updated.
     pub fn observe_global<G: 'static>(
         &mut self,
-        f: impl Fn(&mut WindowContext<'_, '_>) + Send + Sync + 'static,
+        f: impl Fn(&mut WindowContext<'_, '_>) + Send + 'static,
     ) -> Subscription {
         let window_handle = self.window.handle;
         self.global_observers.insert(
@@ -1182,40 +1248,74 @@ impl<'a, 'w> MainThread<WindowContext<'a, 'w>> {
 }
 
 impl Context for WindowContext<'_, '_> {
-    type EntityContext<'a, 'w, T> = ViewContext<'a, 'w, T>;
+    type EntityContext<'a, T> = ModelContext<'a, T>;
     type Result<T> = T;
 
     fn entity<T>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+        build_entity: impl FnOnce(&mut Self::EntityContext<'_, T>) -> T,
     ) -> Handle<T>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let slot = self.app.entities.reserve();
-        let entity = build_entity(&mut ViewContext::mutable(
-            &mut *self.app,
-            &mut self.window,
-            slot.downgrade(),
-        ));
+        let entity = build_entity(&mut ModelContext::mutable(&mut *self.app, slot.downgrade()));
         self.entities.insert(slot, entity)
     }
 
     fn update_entity<T: 'static, R>(
         &mut self,
         handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, T>) -> R,
     ) -> R {
         let mut entity = self.entities.lease(handle);
         let result = update(
             &mut *entity,
-            &mut ViewContext::mutable(&mut *self.app, &mut *self.window, handle.downgrade()),
+            &mut ModelContext::mutable(&mut *self.app, handle.downgrade()),
         );
         self.entities.end_lease(entity);
         result
     }
 }
 
+impl VisualContext for WindowContext<'_, '_> {
+    type ViewContext<'a, 'w, V> = ViewContext<'a, 'w, V>;
+
+    /// Builds a new view in the current window. The first argument is a function that builds
+    /// an entity representing the view's state. It is invoked with a `ViewContext` that provides
+    /// entity-specific access to the window and application state during construction. The second
+    /// argument is a render function that returns a component based on the view's state.
+    fn build_view<E, V>(
+        &mut self,
+        build_view_state: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V,
+        render: impl Fn(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static,
+    ) -> Self::Result<View<V>>
+    where
+        E: crate::Component<V>,
+        V: 'static + Send,
+    {
+        let slot = self.app.entities.reserve();
+        let view = View::for_handle(slot.clone(), render);
+        let mut cx = ViewContext::mutable(&mut *self.app, &mut *self.window, view.downgrade());
+        let entity = build_view_state(&mut cx);
+        self.entities.insert(slot, entity);
+        view
+    }
+
+    /// Update the given view. Prefer calling `View::update` instead, which calls this method.
+    fn update_view<T: 'static, R>(
+        &mut self,
+        view: &View<T>,
+        update: impl FnOnce(&mut T, &mut Self::ViewContext<'_, '_, T>) -> R,
+    ) -> Self::Result<R> {
+        let mut lease = self.app.entities.lease(&view.state);
+        let mut cx = ViewContext::mutable(&mut *self.app, &mut *self.window, view.downgrade());
+        let result = update(&mut *lease, &mut cx);
+        cx.app.entities.end_lease(lease);
+        result
+    }
+}
+
 impl<'a, 'w> std::ops::Deref for WindowContext<'a, 'w> {
     type Target = AppContext;
 
@@ -1255,6 +1355,10 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         self.borrow_mut()
     }
 
+    /// Pushes the given element id onto the global stack and invokes the given closure
+    /// with a `GlobalElementId`, which disambiguates the given id in the context of its ancestor
+    /// ids. Because elements are discarded and recreated on each frame, the `GlobalElementId` is
+    /// used to associate state with identified elements across separate frames.
     fn with_element_id<R>(
         &mut self,
         id: impl Into<ElementId>,
@@ -1281,6 +1385,8 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         result
     }
 
+    /// Invoke the given function with the given content mask after intersecting it
+    /// with the current mask.
     fn with_content_mask<R>(
         &mut self,
         mask: ContentMask<Pixels>,
@@ -1293,6 +1399,8 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         result
     }
 
+    /// Update the global element offset based on the given offset. This is used to implement
+    /// scrolling and position drag handles.
     fn with_element_offset<R>(
         &mut self,
         offset: Option<Point<Pixels>>,
@@ -1309,6 +1417,7 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         result
     }
 
+    /// Obtain the current element offset.
     fn element_offset(&self) -> Point<Pixels> {
         self.window()
             .element_offset_stack
@@ -1317,13 +1426,17 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
             .unwrap_or_default()
     }
 
+    /// Update or intialize state for an element with the given id that lives across multiple
+    /// frames. If an element with this id existed in the previous frame, its state will be passed
+    /// to the given closure. The state returned by the closure will be stored so it can be referenced
+    /// when drawing the next frame.
     fn with_element_state<S, R>(
         &mut self,
         id: ElementId,
         f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
     ) -> R
     where
-        S: Any + Send + Sync,
+        S: 'static + Send,
     {
         self.with_element_id(id, |global_id, cx| {
             if let Some(any) = cx
@@ -1353,13 +1466,15 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         })
     }
 
+    /// Like `with_element_state`, but for situations where the element_id is optional. If the
+    /// id is `None`, no state will be retrieved or stored.
     fn with_optional_element_state<S, R>(
         &mut self,
         element_id: Option<ElementId>,
         f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
     ) -> R
     where
-        S: Any + Send + Sync,
+        S: 'static + Send,
     {
         if let Some(element_id) = element_id {
             self.with_element_state(element_id, f)
@@ -1368,6 +1483,7 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         }
     }
 
+    /// Obtain the current content mask.
     fn content_mask(&self) -> ContentMask<Pixels> {
         self.window()
             .content_mask_stack
@@ -1381,6 +1497,8 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
             })
     }
 
+    /// The size of an em for the base font of the application. Adjusting this value allows the
+    /// UI to scale, just like zooming a web page.
     fn rem_size(&self) -> Pixels {
         self.window().rem_size
     }
@@ -1402,7 +1520,7 @@ impl<T> BorrowWindow for T where T: BorrowMut<AppContext> + BorrowMut<Window> {}
 
 pub struct ViewContext<'a, 'w, V> {
     window_cx: WindowContext<'a, 'w>,
-    view_state: WeakHandle<V>,
+    view: WeakView<V>,
 }
 
 impl<V> Borrow<AppContext> for ViewContext<'_, '_, V> {
@@ -1430,15 +1548,23 @@ impl<V> BorrowMut<Window> for ViewContext<'_, '_, V> {
 }
 
 impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
-    fn mutable(app: &'a mut AppContext, window: &'w mut Window, view_state: WeakHandle<V>) -> Self {
+    pub(crate) fn mutable(
+        app: &'a mut AppContext,
+        window: &'w mut Window,
+        view: WeakView<V>,
+    ) -> Self {
         Self {
             window_cx: WindowContext::mutable(app, window),
-            view_state,
+            view,
         }
     }
 
+    pub fn view(&self) -> WeakView<V> {
+        self.view.clone()
+    }
+
     pub fn handle(&self) -> WeakHandle<V> {
-        self.view_state.clone()
+        self.view.state.clone()
     }
 
     pub fn stack<R>(&mut self, order: u32, f: impl FnOnce(&mut Self) -> R) -> R {
@@ -1450,27 +1576,22 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
 
     pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext<V>) + Send + 'static)
     where
-        V: Any + Send + Sync,
+        V: Any + Send,
     {
-        let entity = self.handle();
-        self.window_cx.on_next_frame(move |cx| {
-            entity.update(cx, f).ok();
-        });
+        let view = self.view().upgrade().unwrap();
+        self.window_cx.on_next_frame(move |cx| view.update(cx, f));
     }
 
     pub fn observe<E>(
         &mut self,
         handle: &Handle<E>,
-        mut on_notify: impl FnMut(&mut V, Handle<E>, &mut ViewContext<'_, '_, V>)
-            + Send
-            + Sync
-            + 'static,
+        mut on_notify: impl FnMut(&mut V, Handle<E>, &mut ViewContext<'_, '_, V>) + Send + 'static,
     ) -> Subscription
     where
         E: 'static,
-        V: Any + Send + Sync,
+        V: Any + Send,
     {
-        let this = self.handle();
+        let view = self.view();
         let handle = handle.downgrade();
         let window_handle = self.window.handle;
         self.app.observers.insert(
@@ -1478,7 +1599,7 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
             Box::new(move |cx| {
                 cx.update_window(window_handle, |cx| {
                     if let Some(handle) = handle.upgrade() {
-                        this.update(cx, |this, cx| on_notify(this, handle, cx))
+                        view.update(cx, |this, cx| on_notify(this, handle, cx))
                             .is_ok()
                     } else {
                         false
@@ -1494,10 +1615,9 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
         handle: &Handle<E>,
         mut on_event: impl FnMut(&mut V, Handle<E>, &E::Event, &mut ViewContext<'_, '_, V>)
             + Send
-            + Sync
             + 'static,
     ) -> Subscription {
-        let this = self.handle();
+        let view = self.view();
         let handle = handle.downgrade();
         let window_handle = self.window.handle;
         self.app.event_listeners.insert(
@@ -1506,7 +1626,7 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
                 cx.update_window(window_handle, |cx| {
                     if let Some(handle) = handle.upgrade() {
                         let event = event.downcast_ref().expect("invalid event type");
-                        this.update(cx, |this, cx| on_event(this, handle, event, cx))
+                        view.update(cx, |this, cx| on_event(this, handle, event, cx))
                             .is_ok()
                     } else {
                         false
@@ -1519,11 +1639,11 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
 
     pub fn on_release(
         &mut self,
-        mut on_release: impl FnMut(&mut V, &mut WindowContext) + Send + Sync + 'static,
+        mut on_release: impl FnMut(&mut V, &mut WindowContext) + Send + 'static,
     ) -> Subscription {
         let window_handle = self.window.handle;
         self.app.release_listeners.insert(
-            self.view_state.entity_id,
+            self.view.state.entity_id,
             Box::new(move |this, cx| {
                 let this = this.downcast_mut().expect("invalid entity type");
                 // todo!("are we okay with silently swallowing the error?")
@@ -1535,30 +1655,37 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
     pub fn observe_release<T: 'static>(
         &mut self,
         handle: &Handle<T>,
-        mut on_release: impl FnMut(&mut V, &mut T, &mut ViewContext<'_, '_, V>) + Send + Sync + 'static,
-    ) -> Subscription {
-        let this = self.handle();
+        mut on_release: impl FnMut(&mut V, &mut T, &mut ViewContext<'_, '_, V>) + Send + 'static,
+    ) -> Subscription
+    where
+        V: Any + Send,
+    {
+        let view = self.view();
         let window_handle = self.window.handle;
-        self.app.observe_release(handle, move |entity, cx| {
-            let _ = cx.update_window(window_handle, |cx| {
-                this.update(cx, |this, cx| on_release(this, entity, cx))
-            });
-        })
+        self.app.release_listeners.insert(
+            handle.entity_id,
+            Box::new(move |entity, cx| {
+                let entity = entity.downcast_mut().expect("invalid entity type");
+                let _ = cx.update_window(window_handle, |cx| {
+                    view.update(cx, |this, cx| on_release(this, entity, cx))
+                });
+            }),
+        )
     }
 
     pub fn notify(&mut self) {
         self.window_cx.notify();
         self.window_cx.app.push_effect(Effect::Notify {
-            emitter: self.view_state.entity_id,
+            emitter: self.view.state.entity_id,
         });
     }
 
     pub fn on_focus_changed(
         &mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + Sync + 'static,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
     ) {
-        let handle = self.handle();
-        self.window.focus_listeners.push(Arc::new(move |event, cx| {
+        let handle = self.view();
+        self.window.focus_listeners.push(Box::new(move |event, cx| {
             handle
                 .update(cx, |view, cx| listener(view, event, cx))
                 .log_err();
@@ -1567,13 +1694,14 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
 
     pub fn with_key_listeners<R>(
         &mut self,
-        key_listeners: &[(TypeId, KeyListener<V>)],
+        key_listeners: impl IntoIterator<Item = (TypeId, KeyListener<V>)>,
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
+        let old_stack_len = self.window.key_dispatch_stack.len();
         if !self.window.freeze_key_dispatch_stack {
-            for (event_type, listener) in key_listeners.iter().cloned() {
-                let handle = self.handle();
-                let listener = Arc::new(
+            for (event_type, listener) in key_listeners {
+                let handle = self.view();
+                let listener = Box::new(
                     move |event: &dyn Any,
                           context_stack: &[&DispatchContext],
                           phase: DispatchPhase,
@@ -1598,8 +1726,7 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
         let result = f(self);
 
         if !self.window.freeze_key_dispatch_stack {
-            let prev_len = self.window.key_dispatch_stack.len() - key_listeners.len();
-            self.window.key_dispatch_stack.truncate(prev_len);
+            self.window.key_dispatch_stack.truncate(old_stack_len);
         }
 
         result
@@ -1663,29 +1790,29 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
             let cx = unsafe { mem::transmute::<&mut Self, &mut MainThread<Self>>(self) };
             Task::ready(Ok(f(view, cx)))
         } else {
-            let handle = self.handle().upgrade().unwrap();
-            self.window_cx.run_on_main(move |cx| handle.update(cx, f))
+            let view = self.view().upgrade().unwrap();
+            self.window_cx.run_on_main(move |cx| view.update(cx, f))
         }
     }
 
     pub fn spawn<Fut, R>(
         &mut self,
-        f: impl FnOnce(WeakHandle<V>, AsyncWindowContext) -> Fut + Send + 'static,
+        f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut + Send + 'static,
     ) -> Task<R>
     where
         R: Send + 'static,
         Fut: Future<Output = R> + Send + 'static,
     {
-        let handle = self.handle();
+        let view = self.view();
         self.window_cx.spawn(move |_, cx| {
-            let result = f(handle, cx);
+            let result = f(view, cx);
             async move { result.await }
         })
     }
 
     pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
     where
-        G: 'static + Send + Sync,
+        G: 'static + Send,
     {
         let mut global = self.app.lease_global::<G>();
         let result = f(&mut global, self);
@@ -1695,10 +1822,10 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
 
     pub fn observe_global<G: 'static>(
         &mut self,
-        f: impl Fn(&mut V, &mut ViewContext<'_, '_, V>) + Send + Sync + 'static,
+        f: impl Fn(&mut V, &mut ViewContext<'_, '_, V>) + Send + 'static,
     ) -> Subscription {
         let window_handle = self.window.handle;
-        let handle = self.handle();
+        let handle = self.view();
         self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| {
@@ -1712,9 +1839,9 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
 
     pub fn on_mouse_event<Event: 'static>(
         &mut self,
-        handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + Send + Sync + 'static,
+        handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + Send + 'static,
     ) {
-        let handle = self.handle().upgrade().unwrap();
+        let handle = self.view().upgrade().unwrap();
         self.window_cx.on_mouse_event(move |event, phase, cx| {
             handle.update(cx, |view, cx| {
                 handler(view, event, phase, cx);
@@ -1726,10 +1853,10 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
 impl<'a, 'w, V> ViewContext<'a, 'w, V>
 where
     V: EventEmitter,
-    V::Event: Any + Send + Sync,
+    V::Event: Any + Send,
 {
     pub fn emit(&mut self, event: V::Event) {
-        let emitter = self.view_state.entity_id;
+        let emitter = self.view.state.entity_id;
         self.app.push_effect(Effect::Emit {
             emitter,
             event: Box::new(event),
@@ -1738,15 +1865,15 @@ where
 }
 
 impl<'a, 'w, V> Context for ViewContext<'a, 'w, V> {
-    type EntityContext<'b, 'c, U> = ViewContext<'b, 'c, U>;
+    type EntityContext<'b, U> = ModelContext<'b, U>;
     type Result<U> = U;
 
     fn entity<T>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+        build_entity: impl FnOnce(&mut Self::EntityContext<'_, T>) -> T,
     ) -> Handle<T>
     where
-        T: 'static + Send + Sync,
+        T: 'static + Send,
     {
         self.window_cx.entity(build_entity)
     }

crates/gpui2_macros/src/test.rs 🔗

@@ -86,7 +86,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                     let last_segment = ty.path.segments.last();
                     match last_segment.map(|s| s.ident.to_string()).as_deref() {
                         Some("StdRng") => {
-                            inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
+                            inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
                             continue;
                         }
                         Some("Executor") => {
@@ -133,7 +133,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                 gpui2::run_test(
                     #num_iterations as u64,
                     #max_retries,
-                    &mut |dispatcher| {
+                    &mut |dispatcher, _seed| {
                         let executor = gpui2::Executor::new(std::sync::Arc::new(dispatcher.clone()));
                         #cx_vars
                         executor.block(#inner_fn_name(#inner_fn_args));
@@ -156,7 +156,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                     let last_segment = ty.path.segments.last();
 
                     if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() {
-                        inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
+                        inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
                         continue;
                     }
                 } else if let Type::Reference(ty) = &*arg.ty {
@@ -212,7 +212,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                 gpui2::run_test(
                     #num_iterations as u64,
                     #max_retries,
-                    &mut |dispatcher| {
+                    &mut |dispatcher, _seed| {
                         #cx_vars
                         #inner_fn_name(#inner_fn_args);
                         #cx_teardowns

crates/journal2/Cargo.toml 🔗

@@ -0,0 +1,27 @@
+[package]
+name = "journal2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/journal2.rs"
+doctest = false
+
+[dependencies]
+editor = { path = "../editor" }
+gpui2 = { path = "../gpui2" }
+util = { path = "../util" }
+workspace = { path = "../workspace" }
+settings2 = { path = "../settings2" }
+
+anyhow.workspace = true
+chrono = "0.4"
+dirs = "4.0"
+serde.workspace = true
+schemars.workspace = true
+log.workspace = true
+shellexpand = "2.1.0"
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }

crates/journal2/src/journal2.rs 🔗

@@ -0,0 +1,176 @@
+use anyhow::Result;
+use chrono::{Datelike, Local, NaiveTime, Timelike};
+use gpui2::AppContext;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings2::Settings;
+use std::{
+    fs::OpenOptions,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use workspace::AppState;
+// use zed::AppState;
+
+// todo!();
+// actions!(journal, [NewJournalEntry]);
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+pub struct JournalSettings {
+    pub path: Option<String>,
+    pub hour_format: Option<HourFormat>,
+}
+
+impl Default for JournalSettings {
+    fn default() -> Self {
+        Self {
+            path: Some("~".into()),
+            hour_format: Some(Default::default()),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum HourFormat {
+    #[default]
+    Hour12,
+    Hour24,
+}
+
+impl settings2::Settings for JournalSettings {
+    const KEY: Option<&'static str> = Some("journal");
+
+    type FileContent = Self;
+
+    fn load(
+        defaults: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut AppContext,
+    ) -> Result<Self> {
+        Self::load_via_json_merge(defaults, user_values)
+    }
+}
+
+pub fn init(_: Arc<AppState>, cx: &mut AppContext) {
+    JournalSettings::register(cx);
+
+    // todo!()
+    // cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx));
+}
+
+pub fn new_journal_entry(_: Arc<AppState>, cx: &mut AppContext) {
+    let settings = JournalSettings::get_global(cx);
+    let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) {
+        Some(journal_dir) => journal_dir,
+        None => {
+            log::error!("Can't determine journal directory");
+            return;
+        }
+    };
+
+    let now = Local::now();
+    let month_dir = journal_dir
+        .join(format!("{:02}", now.year()))
+        .join(format!("{:02}", now.month()));
+    let entry_path = month_dir.join(format!("{:02}.md", now.day()));
+    let now = now.time();
+    let _entry_heading = heading_entry(now, &settings.hour_format);
+
+    let _create_entry = cx.executor().spawn(async move {
+        std::fs::create_dir_all(month_dir)?;
+        OpenOptions::new()
+            .create(true)
+            .write(true)
+            .open(&entry_path)?;
+        Ok::<_, std::io::Error>((journal_dir, entry_path))
+    });
+
+    // todo!("workspace")
+    // cx.spawn(|cx| async move {
+    //     let (journal_dir, entry_path) = create_entry.await?;
+    //     let (workspace, _) =
+    //         cx.update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))?;
+
+    //     let opened = workspace
+    //         .update(&mut cx, |workspace, cx| {
+    //             workspace.open_paths(vec![entry_path], true, cx)
+    //         })?
+    //         .await;
+
+    //     if let Some(Some(Ok(item))) = opened.first() {
+    //         if let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) {
+    //             editor.update(&mut cx, |editor, cx| {
+    //                 let len = editor.buffer().read(cx).len(cx);
+    //                 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+    //                     s.select_ranges([len..len])
+    //                 });
+    //                 if len > 0 {
+    //                     editor.insert("\n\n", cx);
+    //                 }
+    //                 editor.insert(&entry_heading, cx);
+    //                 editor.insert("\n\n", cx);
+    //             })?;
+    //         }
+    //     }
+
+    //     anyhow::Ok(())
+    // })
+    // .detach_and_log_err(cx);
+}
+
+fn journal_dir(path: &str) -> Option<PathBuf> {
+    let expanded_journal_dir = shellexpand::full(path) //TODO handle this better
+        .ok()
+        .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"));
+
+    return expanded_journal_dir;
+}
+
+fn heading_entry(now: NaiveTime, hour_format: &Option<HourFormat>) -> String {
+    match hour_format {
+        Some(HourFormat::Hour24) => {
+            let hour = now.hour();
+            format!("# {}:{:02}", hour, now.minute())
+        }
+        _ => {
+            let (pm, hour) = now.hour12();
+            let am_or_pm = if pm { "PM" } else { "AM" };
+            format!("# {}:{:02} {}", hour, now.minute(), am_or_pm)
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    mod heading_entry_tests {
+        use super::super::*;
+
+        #[test]
+        fn test_heading_entry_defaults_to_hour_12() {
+            let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
+            let actual_heading_entry = heading_entry(naive_time, &None);
+            let expected_heading_entry = "# 3:00 PM";
+
+            assert_eq!(actual_heading_entry, expected_heading_entry);
+        }
+
+        #[test]
+        fn test_heading_entry_is_hour_12() {
+            let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
+            let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour12));
+            let expected_heading_entry = "# 3:00 PM";
+
+            assert_eq!(actual_heading_entry, expected_heading_entry);
+        }
+
+        #[test]
+        fn test_heading_entry_is_hour_24() {
+            let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
+            let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour24));
+            let expected_heading_entry = "# 15:00";
+
+            assert_eq!(actual_heading_entry, expected_heading_entry);
+        }
+    }
+}

crates/language/Cargo.toml 🔗

@@ -45,6 +45,7 @@ lazy_static.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
+pulldown-cmark = { version = "0.9.2", default-features = false }
 regex.workspace = true
 schemars.workspace = true
 serde.workspace = true

crates/language/src/buffer.rs 🔗

@@ -1,11 +1,13 @@
 pub use crate::{
     diagnostic_set::DiagnosticSet,
     highlight_map::{HighlightId, HighlightMap},
+    markdown::ParsedMarkdown,
     proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
 };
 use crate::{
     diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
     language_settings::{language_settings, LanguageSettings},
+    markdown::parse_markdown,
     outline::OutlineItem,
     syntax_map::{
         SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
@@ -143,11 +145,51 @@ pub struct Diagnostic {
     pub is_unnecessary: bool,
 }
 
+pub async fn prepare_completion_documentation(
+    documentation: &lsp::Documentation,
+    language_registry: &Arc<LanguageRegistry>,
+    language: Option<Arc<Language>>,
+) -> Documentation {
+    match documentation {
+        lsp::Documentation::String(text) => {
+            if text.lines().count() <= 1 {
+                Documentation::SingleLine(text.clone())
+            } else {
+                Documentation::MultiLinePlainText(text.clone())
+            }
+        }
+
+        lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
+            lsp::MarkupKind::PlainText => {
+                if value.lines().count() <= 1 {
+                    Documentation::SingleLine(value.clone())
+                } else {
+                    Documentation::MultiLinePlainText(value.clone())
+                }
+            }
+
+            lsp::MarkupKind::Markdown => {
+                let parsed = parse_markdown(value, language_registry, language).await;
+                Documentation::MultiLineMarkdown(parsed)
+            }
+        },
+    }
+}
+
+#[derive(Clone, Debug)]
+pub enum Documentation {
+    Undocumented,
+    SingleLine(String),
+    MultiLinePlainText(String),
+    MultiLineMarkdown(ParsedMarkdown),
+}
+
 #[derive(Clone, Debug)]
 pub struct Completion {
     pub old_range: Range<Anchor>,
     pub new_text: String,
     pub label: CodeLabel,
+    pub documentation: Option<Documentation>,
     pub server_id: LanguageServerId,
     pub lsp_completion: lsp::CompletionItem,
 }
@@ -159,7 +201,7 @@ pub struct CodeAction {
     pub lsp_action: lsp::CodeAction,
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum Operation {
     Buffer(text::Operation),
 
@@ -182,7 +224,7 @@ pub enum Operation {
     },
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum Event {
     Operation(Operation),
     Edited,
@@ -331,8 +373,8 @@ pub(crate) struct DiagnosticEndpoint {
 
 #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
 pub enum CharKind {
-    Punctuation,
     Whitespace,
+    Punctuation,
     Word,
 }
 
@@ -1406,82 +1448,95 @@ impl Buffer {
             return None;
         }
 
-        self.start_transaction();
-        self.pending_autoindent.take();
-        let autoindent_request = autoindent_mode
-            .and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode)));
-
-        let edit_operation = self.text.edit(edits.iter().cloned());
-        let edit_id = edit_operation.timestamp();
+        // Non-generic part hoisted out to reduce LLVM IR size.
+        fn tail(
+            this: &mut Buffer,
+            edits: Vec<(Range<usize>, Arc<str>)>,
+            autoindent_mode: Option<AutoindentMode>,
+            cx: &mut ModelContext<Buffer>,
+        ) -> Option<clock::Lamport> {
+            this.start_transaction();
+            this.pending_autoindent.take();
+            let autoindent_request = autoindent_mode
+                .and_then(|mode| this.language.as_ref().map(|_| (this.snapshot(), mode)));
+
+            let edit_operation = this.text.edit(edits.iter().cloned());
+            let edit_id = edit_operation.timestamp();
+
+            if let Some((before_edit, mode)) = autoindent_request {
+                let mut delta = 0isize;
+                let entries = edits
+                    .into_iter()
+                    .enumerate()
+                    .zip(&edit_operation.as_edit().unwrap().new_text)
+                    .map(|((ix, (range, _)), new_text)| {
+                        let new_text_length = new_text.len();
+                        let old_start = range.start.to_point(&before_edit);
+                        let new_start = (delta + range.start as isize) as usize;
+                        delta +=
+                            new_text_length as isize - (range.end as isize - range.start as isize);
+
+                        let mut range_of_insertion_to_indent = 0..new_text_length;
+                        let mut first_line_is_new = false;
+                        let mut original_indent_column = None;
+
+                        // When inserting an entire line at the beginning of an existing line,
+                        // treat the insertion as new.
+                        if new_text.contains('\n')
+                            && old_start.column
+                                <= before_edit.indent_size_for_line(old_start.row).len
+                        {
+                            first_line_is_new = true;
+                        }
 
-        if let Some((before_edit, mode)) = autoindent_request {
-            let mut delta = 0isize;
-            let entries = edits
-                .into_iter()
-                .enumerate()
-                .zip(&edit_operation.as_edit().unwrap().new_text)
-                .map(|((ix, (range, _)), new_text)| {
-                    let new_text_length = new_text.len();
-                    let old_start = range.start.to_point(&before_edit);
-                    let new_start = (delta + range.start as isize) as usize;
-                    delta += new_text_length as isize - (range.end as isize - range.start as isize);
-
-                    let mut range_of_insertion_to_indent = 0..new_text_length;
-                    let mut first_line_is_new = false;
-                    let mut original_indent_column = None;
-
-                    // When inserting an entire line at the beginning of an existing line,
-                    // treat the insertion as new.
-                    if new_text.contains('\n')
-                        && old_start.column <= before_edit.indent_size_for_line(old_start.row).len
-                    {
-                        first_line_is_new = true;
-                    }
+                        // When inserting text starting with a newline, avoid auto-indenting the
+                        // previous line.
+                        if new_text.starts_with('\n') {
+                            range_of_insertion_to_indent.start += 1;
+                            first_line_is_new = true;
+                        }
 
-                    // When inserting text starting with a newline, avoid auto-indenting the
-                    // previous line.
-                    if new_text.starts_with('\n') {
-                        range_of_insertion_to_indent.start += 1;
-                        first_line_is_new = true;
-                    }
+                        // Avoid auto-indenting after the insertion.
+                        if let AutoindentMode::Block {
+                            original_indent_columns,
+                        } = &mode
+                        {
+                            original_indent_column = Some(
+                                original_indent_columns.get(ix).copied().unwrap_or_else(|| {
+                                    indent_size_for_text(
+                                        new_text[range_of_insertion_to_indent.clone()].chars(),
+                                    )
+                                    .len
+                                }),
+                            );
+                            if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
+                                range_of_insertion_to_indent.end -= 1;
+                            }
+                        }
 
-                    // Avoid auto-indenting after the insertion.
-                    if let AutoindentMode::Block {
-                        original_indent_columns,
-                    } = &mode
-                    {
-                        original_indent_column =
-                            Some(original_indent_columns.get(ix).copied().unwrap_or_else(|| {
-                                indent_size_for_text(
-                                    new_text[range_of_insertion_to_indent.clone()].chars(),
-                                )
-                                .len
-                            }));
-                        if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
-                            range_of_insertion_to_indent.end -= 1;
+                        AutoindentRequestEntry {
+                            first_line_is_new,
+                            original_indent_column,
+                            indent_size: before_edit.language_indent_size_at(range.start, cx),
+                            range: this
+                                .anchor_before(new_start + range_of_insertion_to_indent.start)
+                                ..this.anchor_after(new_start + range_of_insertion_to_indent.end),
                         }
-                    }
+                    })
+                    .collect();
 
-                    AutoindentRequestEntry {
-                        first_line_is_new,
-                        original_indent_column,
-                        indent_size: before_edit.language_indent_size_at(range.start, cx),
-                        range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
-                            ..self.anchor_after(new_start + range_of_insertion_to_indent.end),
-                    }
-                })
-                .collect();
+                this.autoindent_requests.push(Arc::new(AutoindentRequest {
+                    before_edit,
+                    entries,
+                    is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
+                }));
+            }
 
-            self.autoindent_requests.push(Arc::new(AutoindentRequest {
-                before_edit,
-                entries,
-                is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
-            }));
+            this.end_transaction(cx);
+            this.send_operation(Operation::Buffer(edit_operation), cx);
+            Some(edit_id)
         }
-
-        self.end_transaction(cx);
-        self.send_operation(Operation::Buffer(edit_operation), cx);
-        Some(edit_id)
+        tail(self, edits, autoindent_mode, cx)
     }
 
     fn did_edit(

crates/language/src/language.rs 🔗

@@ -2,6 +2,7 @@ mod buffer;
 mod diagnostic_set;
 mod highlight_map;
 pub mod language_settings;
+pub mod markdown;
 mod outline;
 pub mod proto;
 mod syntax_map;
@@ -37,7 +38,7 @@ use std::{
     path::{Path, PathBuf},
     str,
     sync::{
-        atomic::{AtomicUsize, Ordering::SeqCst},
+        atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
 };
@@ -110,18 +111,17 @@ pub struct LanguageServerName(pub Arc<str>);
 pub struct CachedLspAdapter {
     pub name: LanguageServerName,
     pub short_name: &'static str,
-    pub initialization_options: Option<Value>,
     pub disk_based_diagnostic_sources: Vec<String>,
     pub disk_based_diagnostics_progress_token: Option<String>,
     pub language_ids: HashMap<String, String>,
     pub adapter: Arc<dyn LspAdapter>,
+    pub reinstall_attempt_count: AtomicU64,
 }
 
 impl CachedLspAdapter {
     pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
         let name = adapter.name().await;
         let short_name = adapter.short_name();
-        let initialization_options = adapter.initialization_options().await;
         let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
         let disk_based_diagnostics_progress_token =
             adapter.disk_based_diagnostics_progress_token().await;
@@ -130,11 +130,11 @@ impl CachedLspAdapter {
         Arc::new(CachedLspAdapter {
             name,
             short_name,
-            initialization_options,
             disk_based_diagnostic_sources,
             disk_based_diagnostics_progress_token,
             language_ids,
             adapter,
+            reinstall_attempt_count: AtomicU64::new(0),
         })
     }
 
@@ -228,8 +228,8 @@ impl CachedLspAdapter {
         self.adapter.label_for_symbol(name, kind, language).await
     }
 
-    pub fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        self.adapter.enabled_formatters()
+    pub fn prettier_plugins(&self) -> &[&'static str] {
+        self.adapter.prettier_plugins()
     }
 }
 
@@ -338,31 +338,8 @@ pub trait LspAdapter: 'static + Send + Sync {
         Default::default()
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        Vec::new()
-    }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum BundledFormatter {
-    Prettier {
-        // See https://prettier.io/docs/en/options.html#parser for a list of valid values.
-        // Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used.
-        // There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins.
-        //
-        // But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed.
-        // For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict.
-        parser_name: Option<&'static str>,
-        plugin_names: Vec<&'static str>,
-    },
-}
-
-impl BundledFormatter {
-    pub fn prettier(parser_name: &'static str) -> Self {
-        Self::Prettier {
-            parser_name: Some(parser_name),
-            plugin_names: Vec::new(),
-        }
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &[]
     }
 }
 
@@ -400,6 +377,8 @@ pub struct LanguageConfig {
     pub overrides: HashMap<String, LanguageConfigOverride>,
     #[serde(default)]
     pub word_characters: HashSet<char>,
+    #[serde(default)]
+    pub prettier_parser_name: Option<String>,
 }
 
 #[derive(Debug, Default)]
@@ -473,6 +452,7 @@ impl Default for LanguageConfig {
             overrides: Default::default(),
             collapsed_placeholder: Default::default(),
             word_characters: Default::default(),
+            prettier_parser_name: None,
         }
     }
 }
@@ -498,7 +478,7 @@ pub struct FakeLspAdapter {
     pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
     pub disk_based_diagnostics_progress_token: Option<String>,
     pub disk_based_diagnostics_sources: Vec<String>,
-    pub enabled_formatters: Vec<BundledFormatter>,
+    pub prettier_plugins: Vec<&'static str>,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -667,7 +647,7 @@ struct LanguageRegistryState {
 
 pub struct PendingLanguageServer {
     pub server_id: LanguageServerId,
-    pub task: Task<Result<Option<lsp::LanguageServer>>>,
+    pub task: Task<Result<lsp::LanguageServer>>,
     pub container_dir: Option<Arc<Path>>,
 }
 
@@ -906,6 +886,7 @@ impl LanguageRegistry {
 
     pub fn create_pending_language_server(
         self: &Arc<Self>,
+        stderr_capture: Arc<Mutex<Option<String>>>,
         language: Arc<Language>,
         adapter: Arc<CachedLspAdapter>,
         root_path: Arc<Path>,
@@ -945,7 +926,7 @@ impl LanguageRegistry {
                     })
                     .detach();
 
-                Ok(Some(server))
+                Ok(server)
             });
 
             return Some(PendingLanguageServer {
@@ -993,24 +974,23 @@ impl LanguageRegistry {
                     .clone();
                 drop(lock);
 
-                let binary = match entry.clone().await.log_err() {
-                    Some(binary) => binary,
-                    None => return Ok(None),
+                let binary = match entry.clone().await {
+                    Ok(binary) => binary,
+                    Err(err) => anyhow::bail!("{err}"),
                 };
 
                 if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
-                    if task.await.log_err().is_none() {
-                        return Ok(None);
-                    }
+                    task.await?;
                 }
 
-                Ok(Some(lsp::LanguageServer::new(
+                lsp::LanguageServer::new(
+                    stderr_capture,
                     server_id,
                     binary,
                     &root_path,
                     adapter.code_action_kinds(),
                     cx,
-                )?))
+                )
             })
         };
 
@@ -1599,6 +1579,10 @@ impl Language {
             override_id: None,
         }
     }
+
+    pub fn prettier_parser_name(&self) -> Option<&str> {
+        self.config.prettier_parser_name.as_deref()
+    }
 }
 
 impl LanguageScope {
@@ -1761,7 +1745,7 @@ impl Default for FakeLspAdapter {
             disk_based_diagnostics_progress_token: None,
             initialization_options: None,
             disk_based_diagnostics_sources: Vec::new(),
-            enabled_formatters: Vec::new(),
+            prettier_plugins: Vec::new(),
         }
     }
 }
@@ -1819,8 +1803,8 @@ impl LspAdapter for Arc<FakeLspAdapter> {
         self.initialization_options.clone()
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        self.enabled_formatters.clone()
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &self.prettier_plugins
     }
 }
 

crates/language/src/markdown.rs 🔗

@@ -0,0 +1,301 @@
+use std::sync::Arc;
+use std::{ops::Range, path::PathBuf};
+
+use crate::{HighlightId, Language, LanguageRegistry};
+use gpui::fonts::{self, HighlightStyle, Weight};
+use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
+
+#[derive(Debug, Clone)]
+pub struct ParsedMarkdown {
+    pub text: String,
+    pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
+    pub region_ranges: Vec<Range<usize>>,
+    pub regions: Vec<ParsedRegion>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MarkdownHighlight {
+    Style(MarkdownHighlightStyle),
+    Code(HighlightId),
+}
+
+impl MarkdownHighlight {
+    pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
+        match self {
+            MarkdownHighlight::Style(style) => {
+                let mut highlight = HighlightStyle::default();
+
+                if style.italic {
+                    highlight.italic = Some(true);
+                }
+
+                if style.underline {
+                    highlight.underline = Some(fonts::Underline {
+                        thickness: 1.0.into(),
+                        ..Default::default()
+                    });
+                }
+
+                if style.weight != fonts::Weight::default() {
+                    highlight.weight = Some(style.weight);
+                }
+
+                Some(highlight)
+            }
+
+            MarkdownHighlight::Code(id) => id.style(theme),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct MarkdownHighlightStyle {
+    pub italic: bool,
+    pub underline: bool,
+    pub weight: Weight,
+}
+
+#[derive(Debug, Clone)]
+pub struct ParsedRegion {
+    pub code: bool,
+    pub link: Option<Link>,
+}
+
+#[derive(Debug, Clone)]
+pub enum Link {
+    Web { url: String },
+    Path { path: PathBuf },
+}
+
+impl Link {
+    fn identify(text: String) -> Option<Link> {
+        if text.starts_with("http") {
+            return Some(Link::Web { url: text });
+        }
+
+        let path = PathBuf::from(text);
+        if path.is_absolute() {
+            return Some(Link::Path { path });
+        }
+
+        None
+    }
+}
+
+pub async fn parse_markdown(
+    markdown: &str,
+    language_registry: &Arc<LanguageRegistry>,
+    language: Option<Arc<Language>>,
+) -> ParsedMarkdown {
+    let mut text = String::new();
+    let mut highlights = Vec::new();
+    let mut region_ranges = Vec::new();
+    let mut regions = Vec::new();
+
+    parse_markdown_block(
+        markdown,
+        language_registry,
+        language,
+        &mut text,
+        &mut highlights,
+        &mut region_ranges,
+        &mut regions,
+    )
+    .await;
+
+    ParsedMarkdown {
+        text,
+        highlights,
+        region_ranges,
+        regions,
+    }
+}
+
+pub async fn parse_markdown_block(
+    markdown: &str,
+    language_registry: &Arc<LanguageRegistry>,
+    language: Option<Arc<Language>>,
+    text: &mut String,
+    highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
+    region_ranges: &mut Vec<Range<usize>>,
+    regions: &mut Vec<ParsedRegion>,
+) {
+    let mut bold_depth = 0;
+    let mut italic_depth = 0;
+    let mut link_url = None;
+    let mut current_language = None;
+    let mut list_stack = Vec::new();
+
+    for event in Parser::new_ext(&markdown, Options::all()) {
+        let prev_len = text.len();
+        match event {
+            Event::Text(t) => {
+                if let Some(language) = &current_language {
+                    highlight_code(text, highlights, t.as_ref(), language);
+                } else {
+                    text.push_str(t.as_ref());
+
+                    let mut style = MarkdownHighlightStyle::default();
+
+                    if bold_depth > 0 {
+                        style.weight = Weight::BOLD;
+                    }
+
+                    if italic_depth > 0 {
+                        style.italic = true;
+                    }
+
+                    if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) {
+                        region_ranges.push(prev_len..text.len());
+                        regions.push(ParsedRegion {
+                            code: false,
+                            link: Some(link),
+                        });
+                        style.underline = true;
+                    }
+
+                    if style != MarkdownHighlightStyle::default() {
+                        let mut new_highlight = true;
+                        if let Some((last_range, MarkdownHighlight::Style(last_style))) =
+                            highlights.last_mut()
+                        {
+                            if last_range.end == prev_len && last_style == &style {
+                                last_range.end = text.len();
+                                new_highlight = false;
+                            }
+                        }
+                        if new_highlight {
+                            let range = prev_len..text.len();
+                            highlights.push((range, MarkdownHighlight::Style(style)));
+                        }
+                    }
+                }
+            }
+
+            Event::Code(t) => {
+                text.push_str(t.as_ref());
+                region_ranges.push(prev_len..text.len());
+
+                let link = link_url.clone().and_then(|u| Link::identify(u));
+                if link.is_some() {
+                    highlights.push((
+                        prev_len..text.len(),
+                        MarkdownHighlight::Style(MarkdownHighlightStyle {
+                            underline: true,
+                            ..Default::default()
+                        }),
+                    ));
+                }
+                regions.push(ParsedRegion { code: true, link });
+            }
+
+            Event::Start(tag) => match tag {
+                Tag::Paragraph => new_paragraph(text, &mut list_stack),
+
+                Tag::Heading(_, _, _) => {
+                    new_paragraph(text, &mut list_stack);
+                    bold_depth += 1;
+                }
+
+                Tag::CodeBlock(kind) => {
+                    new_paragraph(text, &mut list_stack);
+                    current_language = if let CodeBlockKind::Fenced(language) = kind {
+                        language_registry
+                            .language_for_name(language.as_ref())
+                            .await
+                            .ok()
+                    } else {
+                        language.clone()
+                    }
+                }
+
+                Tag::Emphasis => italic_depth += 1,
+
+                Tag::Strong => bold_depth += 1,
+
+                Tag::Link(_, url, _) => link_url = Some(url.to_string()),
+
+                Tag::List(number) => {
+                    list_stack.push((number, false));
+                }
+
+                Tag::Item => {
+                    let len = list_stack.len();
+                    if let Some((list_number, has_content)) = list_stack.last_mut() {
+                        *has_content = false;
+                        if !text.is_empty() && !text.ends_with('\n') {
+                            text.push('\n');
+                        }
+                        for _ in 0..len - 1 {
+                            text.push_str("  ");
+                        }
+                        if let Some(number) = list_number {
+                            text.push_str(&format!("{}. ", number));
+                            *number += 1;
+                            *has_content = false;
+                        } else {
+                            text.push_str("- ");
+                        }
+                    }
+                }
+
+                _ => {}
+            },
+
+            Event::End(tag) => match tag {
+                Tag::Heading(_, _, _) => bold_depth -= 1,
+                Tag::CodeBlock(_) => current_language = None,
+                Tag::Emphasis => italic_depth -= 1,
+                Tag::Strong => bold_depth -= 1,
+                Tag::Link(_, _, _) => link_url = None,
+                Tag::List(_) => drop(list_stack.pop()),
+                _ => {}
+            },
+
+            Event::HardBreak => text.push('\n'),
+
+            Event::SoftBreak => text.push(' '),
+
+            _ => {}
+        }
+    }
+}
+
+pub fn highlight_code(
+    text: &mut String,
+    highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
+    content: &str,
+    language: &Arc<Language>,
+) {
+    let prev_len = text.len();
+    text.push_str(content);
+    for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
+        let highlight = MarkdownHighlight::Code(highlight_id);
+        highlights.push((prev_len + range.start..prev_len + range.end, highlight));
+    }
+}
+
+pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
+    let mut is_subsequent_paragraph_of_list = false;
+    if let Some((_, has_content)) = list_stack.last_mut() {
+        if *has_content {
+            is_subsequent_paragraph_of_list = true;
+        } else {
+            *has_content = true;
+            return;
+        }
+    }
+
+    if !text.is_empty() {
+        if !text.ends_with('\n') {
+            text.push('\n');
+        }
+        text.push('\n');
+    }
+    for _ in 0..list_stack.len().saturating_sub(1) {
+        text.push_str("  ");
+    }
+    if is_subsequent_paragraph_of_list {
+        text.push_str("  ");
+    }
+}

crates/language/src/proto.rs 🔗

@@ -482,6 +482,7 @@ pub async fn deserialize_completion(
                 lsp_completion.filter_text.as_deref(),
             )
         }),
+        documentation: None,
         server_id: LanguageServerId(completion.server_id as usize),
         lsp_completion,
     })

crates/language2/Cargo.toml 🔗

@@ -17,7 +17,7 @@ test-support = [
     "text/test-support",
     "tree-sitter-rust",
     "tree-sitter-typescript",
-    "settings/test-support",
+    "settings2/test-support",
     "util/test-support",
 ]
 
@@ -65,7 +65,7 @@ collections = { path = "../collections", features = ["test-support"] }
 gpui2 = { path = "../gpui2", features = ["test-support"] }
 lsp2 = { path = "../lsp2", features = ["test-support"] }
 text = { path = "../text", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
+settings2 = { path = "../settings2", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 ctor.workspace = true
 env_logger.workspace = true

crates/language2/src/buffer.rs 🔗

@@ -159,7 +159,7 @@ pub struct CodeAction {
     pub lsp_action: lsp2::CodeAction,
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum Operation {
     Buffer(text::Operation),
 
@@ -182,7 +182,7 @@ pub enum Operation {
     },
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum Event {
     Operation(Operation),
     Edited,
@@ -331,8 +331,8 @@ pub(crate) struct DiagnosticEndpoint {
 
 #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
 pub enum CharKind {
-    Punctuation,
     Whitespace,
+    Punctuation,
     Word,
 }
 

crates/language2/src/buffer_tests.rs 🔗

@@ -1,229 +1,220 @@
+use super::*;
+use crate::language_settings::{
+    AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent,
+};
 use crate::Buffer;
+use clock::ReplicaId;
+use collections::BTreeMap;
+use gpui2::{AppContext, Handle};
 use gpui2::{Context, TestAppContext};
+use indoc::indoc;
+use proto::deserialize_operation;
+use rand::prelude::*;
+use regex::RegexBuilder;
+use settings2::SettingsStore;
+use std::{
+    env,
+    ops::Range,
+    time::{Duration, Instant},
+};
+use text::network::Network;
+use text::LineEnding;
 use text::{Point, ToPoint};
+use unindent::Unindent as _;
+use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
+
+lazy_static! {
+    static ref TRAILING_WHITESPACE_REGEX: Regex = RegexBuilder::new("[ \t]+$")
+        .multi_line(true)
+        .build()
+        .unwrap();
+}
+
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}
+
+#[gpui2::test]
+fn test_line_endings(cx: &mut gpui2::AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), "one\r\ntwo\rthree")
+            .with_language(Arc::new(rust_lang()), cx);
+        assert_eq!(buffer.text(), "one\ntwo\nthree");
+        assert_eq!(buffer.line_ending(), LineEnding::Windows);
+
+        buffer.check_invariants();
+        buffer.edit(
+            [(buffer.len()..buffer.len(), "\r\nfour")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        buffer.edit([(0..0, "zero\r\n")], None, cx);
+        assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour");
+        assert_eq!(buffer.line_ending(), LineEnding::Windows);
+        buffer.check_invariants();
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_select_language() {
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(Arc::new(Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    )));
+    registry.add(Arc::new(Language::new(
+        LanguageConfig {
+            name: "Make".into(),
+            path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    )));
+
+    // matching file extension
+    assert_eq!(
+        registry
+            .language_for_file("zed/lib.rs", None)
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
+        Some("Rust".into())
+    );
+    assert_eq!(
+        registry
+            .language_for_file("zed/lib.mk", None)
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
+        Some("Make".into())
+    );
+
+    // matching filename
+    assert_eq!(
+        registry
+            .language_for_file("zed/Makefile", None)
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
+        Some("Make".into())
+    );
+
+    // matching suffix that is not the full file extension or filename
+    assert_eq!(
+        registry
+            .language_for_file("zed/cars", None)
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
+        None
+    );
+    assert_eq!(
+        registry
+            .language_for_file("zed/a.cars", None)
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
+        None
+    );
+    assert_eq!(
+        registry
+            .language_for_file("zed/sumk", None)
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
+        None
+    );
+}
+
+#[gpui2::test]
+fn test_edit_events(cx: &mut gpui2::AppContext) {
+    let mut now = Instant::now();
+    let buffer_1_events = Arc::new(Mutex::new(Vec::new()));
+    let buffer_2_events = Arc::new(Mutex::new(Vec::new()));
+
+    let buffer1 = cx.entity(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcdef"));
+    let buffer2 = cx.entity(|cx| Buffer::new(1, cx.entity_id().as_u64(), "abcdef"));
+    let buffer1_ops = Arc::new(Mutex::new(Vec::new()));
+    buffer1.update(cx, {
+        let buffer1_ops = buffer1_ops.clone();
+        |buffer, cx| {
+            let buffer_1_events = buffer_1_events.clone();
+            cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() {
+                Event::Operation(op) => buffer1_ops.lock().push(op),
+                event => buffer_1_events.lock().push(event),
+            })
+            .detach();
+            let buffer_2_events = buffer_2_events.clone();
+            cx.subscribe(&buffer2, move |_, _, event, _| {
+                buffer_2_events.lock().push(event.clone())
+            })
+            .detach();
+
+            // An edit emits an edited event, followed by a dirty changed event,
+            // since the buffer was previously in a clean state.
+            buffer.edit([(2..4, "XYZ")], None, cx);
+
+            // An empty transaction does not emit any events.
+            buffer.start_transaction();
+            buffer.end_transaction(cx);
+
+            // A transaction containing two edits emits one edited event.
+            now += Duration::from_secs(1);
+            buffer.start_transaction_at(now);
+            buffer.edit([(5..5, "u")], None, cx);
+            buffer.edit([(6..6, "w")], None, cx);
+            buffer.end_transaction_at(now, cx);
+
+            // Undoing a transaction emits one edited event.
+            buffer.undo(cx);
+        }
+    });
+
+    // Incorporating a set of remote ops emits a single edited event,
+    // followed by a dirty changed event.
+    buffer2.update(cx, |buffer, cx| {
+        buffer.apply_ops(buffer1_ops.lock().drain(..), cx).unwrap();
+    });
+    assert_eq!(
+        mem::take(&mut *buffer_1_events.lock()),
+        vec![
+            Event::Edited,
+            Event::DirtyChanged,
+            Event::Edited,
+            Event::Edited,
+        ]
+    );
+    assert_eq!(
+        mem::take(&mut *buffer_2_events.lock()),
+        vec![Event::Edited, Event::DirtyChanged]
+    );
+
+    buffer1.update(cx, |buffer, cx| {
+        // Undoing the first transaction emits edited event, followed by a
+        // dirty changed event, since the buffer is again in a clean state.
+        buffer.undo(cx);
+    });
+    // Incorporating the remote ops again emits a single edited event,
+    // followed by a dirty changed event.
+    buffer2.update(cx, |buffer, cx| {
+        buffer.apply_ops(buffer1_ops.lock().drain(..), cx).unwrap();
+    });
+    assert_eq!(
+        mem::take(&mut *buffer_1_events.lock()),
+        vec![Event::Edited, Event::DirtyChanged,]
+    );
+    assert_eq!(
+        mem::take(&mut *buffer_2_events.lock()),
+        vec![Event::Edited, Event::DirtyChanged]
+    );
+}
 
-// use crate::language_settings::{
-//     AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent,
-// };
-
-// use super::*;
-// use clock::ReplicaId;
-// use collections::BTreeMap;
-// use gpui2::{AppContext, Handle};
-// use indoc::indoc;
-// use proto::deserialize_operation;
-// use rand::prelude::*;
-// use regex::RegexBuilder;
-// use settings::SettingsStore;
-// use std::{
-//     cell::RefCell,
-//     env,
-//     ops::Range,
-//     rc::Rc,
-//     time::{Duration, Instant},
-// };
-// use text::network::Network;
-// use text::LineEnding;
-// use unindent::Unindent as _;
-// use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
-
-// lazy_static! {
-//     static ref TRAILING_WHITESPACE_REGEX: Regex = RegexBuilder::new("[ \t]+$")
-//         .multi_line(true)
-//         .build()
-//         .unwrap();
-// }
-
-// #[cfg(test)]
-// #[ctor::ctor]
-// fn init_logger() {
-//     if std::env::var("RUST_LOG").is_ok() {
-//         env_logger::init();
-//     }
-// }
-
-// #[gpui::test]
-// fn test_line_endings(cx: &mut gpui::AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let mut buffer = Buffer::new(0, cx.model_id() as u64, "one\r\ntwo\rthree")
-//             .with_language(Arc::new(rust_lang()), cx);
-//         assert_eq!(buffer.text(), "one\ntwo\nthree");
-//         assert_eq!(buffer.line_ending(), LineEnding::Windows);
-
-//         buffer.check_invariants();
-//         buffer.edit(
-//             [(buffer.len()..buffer.len(), "\r\nfour")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         buffer.edit([(0..0, "zero\r\n")], None, cx);
-//         assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour");
-//         assert_eq!(buffer.line_ending(), LineEnding::Windows);
-//         buffer.check_invariants();
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_select_language() {
-//     let registry = Arc::new(LanguageRegistry::test());
-//     registry.add(Arc::new(Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     )));
-//     registry.add(Arc::new(Language::new(
-//         LanguageConfig {
-//             name: "Make".into(),
-//             path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     )));
-
-//     // matching file extension
-//     assert_eq!(
-//         registry
-//             .language_for_file("zed/lib.rs", None)
-//             .now_or_never()
-//             .and_then(|l| Some(l.ok()?.name())),
-//         Some("Rust".into())
-//     );
-//     assert_eq!(
-//         registry
-//             .language_for_file("zed/lib.mk", None)
-//             .now_or_never()
-//             .and_then(|l| Some(l.ok()?.name())),
-//         Some("Make".into())
-//     );
-
-//     // matching filename
-//     assert_eq!(
-//         registry
-//             .language_for_file("zed/Makefile", None)
-//             .now_or_never()
-//             .and_then(|l| Some(l.ok()?.name())),
-//         Some("Make".into())
-//     );
-
-//     // matching suffix that is not the full file extension or filename
-//     assert_eq!(
-//         registry
-//             .language_for_file("zed/cars", None)
-//             .now_or_never()
-//             .and_then(|l| Some(l.ok()?.name())),
-//         None
-//     );
-//     assert_eq!(
-//         registry
-//             .language_for_file("zed/a.cars", None)
-//             .now_or_never()
-//             .and_then(|l| Some(l.ok()?.name())),
-//         None
-//     );
-//     assert_eq!(
-//         registry
-//             .language_for_file("zed/sumk", None)
-//             .now_or_never()
-//             .and_then(|l| Some(l.ok()?.name())),
-//         None
-//     );
-// }
-
-// #[gpui::test]
-// fn test_edit_events(cx: &mut gpui::AppContext) {
-//     let mut now = Instant::now();
-//     let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
-//     let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
-
-//     let buffer1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcdef"));
-//     let buffer2 = cx.add_model(|cx| Buffer::new(1, cx.model_id() as u64, "abcdef"));
-//     let buffer1_ops = Rc::new(RefCell::new(Vec::new()));
-//     buffer1.update(cx, {
-//         let buffer1_ops = buffer1_ops.clone();
-//         |buffer, cx| {
-//             let buffer_1_events = buffer_1_events.clone();
-//             cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() {
-//                 Event::Operation(op) => buffer1_ops.borrow_mut().push(op),
-//                 event => buffer_1_events.borrow_mut().push(event),
-//             })
-//             .detach();
-//             let buffer_2_events = buffer_2_events.clone();
-//             cx.subscribe(&buffer2, move |_, _, event, _| {
-//                 buffer_2_events.borrow_mut().push(event.clone())
-//             })
-//             .detach();
-
-//             // An edit emits an edited event, followed by a dirty changed event,
-//             // since the buffer was previously in a clean state.
-//             buffer.edit([(2..4, "XYZ")], None, cx);
-
-//             // An empty transaction does not emit any events.
-//             buffer.start_transaction();
-//             buffer.end_transaction(cx);
-
-//             // A transaction containing two edits emits one edited event.
-//             now += Duration::from_secs(1);
-//             buffer.start_transaction_at(now);
-//             buffer.edit([(5..5, "u")], None, cx);
-//             buffer.edit([(6..6, "w")], None, cx);
-//             buffer.end_transaction_at(now, cx);
-
-//             // Undoing a transaction emits one edited event.
-//             buffer.undo(cx);
-//         }
-//     });
-
-//     // Incorporating a set of remote ops emits a single edited event,
-//     // followed by a dirty changed event.
-//     buffer2.update(cx, |buffer, cx| {
-//         buffer
-//             .apply_ops(buffer1_ops.borrow_mut().drain(..), cx)
-//             .unwrap();
-//     });
-//     assert_eq!(
-//         mem::take(&mut *buffer_1_events.borrow_mut()),
-//         vec![
-//             Event::Edited,
-//             Event::DirtyChanged,
-//             Event::Edited,
-//             Event::Edited,
-//         ]
-//     );
-//     assert_eq!(
-//         mem::take(&mut *buffer_2_events.borrow_mut()),
-//         vec![Event::Edited, Event::DirtyChanged]
-//     );
-
-//     buffer1.update(cx, |buffer, cx| {
-//         // Undoing the first transaction emits edited event, followed by a
-//         // dirty changed event, since the buffer is again in a clean state.
-//         buffer.undo(cx);
-//     });
-//     // Incorporating the remote ops again emits a single edited event,
-//     // followed by a dirty changed event.
-//     buffer2.update(cx, |buffer, cx| {
-//         buffer
-//             .apply_ops(buffer1_ops.borrow_mut().drain(..), cx)
-//             .unwrap();
-//     });
-//     assert_eq!(
-//         mem::take(&mut *buffer_1_events.borrow_mut()),
-//         vec![Event::Edited, Event::DirtyChanged,]
-//     );
-//     assert_eq!(
-//         mem::take(&mut *buffer_2_events.borrow_mut()),
-//         vec![Event::Edited, Event::DirtyChanged]
-//     );
-// }
-
-// #[gpui::test] todo!()
 #[gpui2::test]
 async fn test_apply_diff(cx: &mut TestAppContext) {
     let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n";
@@ -247,2201 +238,2208 @@ async fn test_apply_diff(cx: &mut TestAppContext) {
     });
 }
 
-// #[gpui::test(iterations = 10)]
-// async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
-//     let text = [
-//         "zero",     //
-//         "one  ",    // 2 trailing spaces
-//         "two",      //
-//         "three   ", // 3 trailing spaces
-//         "four",     //
-//         "five    ", // 4 trailing spaces
-//     ]
-//     .join("\n");
-
-//     let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text));
-
-//     // Spawn a task to format the buffer's whitespace.
-//     // Pause so that the foratting task starts running.
-//     let format = buffer.read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx));
-//     smol::future::yield_now().await;
-
-//     // Edit the buffer while the normalization task is running.
-//     let version_before_edit = buffer.read_with(cx, |buffer, _| buffer.version());
-//     buffer.update(cx, |buffer, cx| {
-//         buffer.edit(
-//             [
-//                 (Point::new(0, 1)..Point::new(0, 1), "EE"),
-//                 (Point::new(3, 5)..Point::new(3, 5), "EEE"),
-//             ],
-//             None,
-//             cx,
-//         );
-//     });
-
-//     let format_diff = format.await;
-//     buffer.update(cx, |buffer, cx| {
-//         let version_before_format = format_diff.base_version.clone();
-//         buffer.apply_diff(format_diff, cx);
-
-//         // The outcome depends on the order of concurrent taks.
-//         //
-//         // If the edit occurred while searching for trailing whitespace ranges,
-//         // then the trailing whitespace region touched by the edit is left intact.
-//         if version_before_format == version_before_edit {
-//             assert_eq!(
-//                 buffer.text(),
-//                 [
-//                     "zEEero",      //
-//                     "one",         //
-//                     "two",         //
-//                     "threeEEE   ", //
-//                     "four",        //
-//                     "five",        //
-//                 ]
-//                 .join("\n")
-//             );
-//         }
-//         // Otherwise, all trailing whitespace is removed.
-//         else {
-//             assert_eq!(
-//                 buffer.text(),
-//                 [
-//                     "zEEero",   //
-//                     "one",      //
-//                     "two",      //
-//                     "threeEEE", //
-//                     "four",     //
-//                     "five",     //
-//                 ]
-//                 .join("\n")
-//             );
-//         }
-//     });
-// }
-
-// #[gpui::test]
-// async fn test_reparse(cx: &mut gpui::TestAppContext) {
-//     let text = "fn a() {}";
-//     let buffer = cx.add_model(|cx| {
-//         Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx)
-//     });
-
-//     // Wait for the initial text to parse
-//     buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await;
-//     assert_eq!(
-//         get_tree_sexp(&buffer, cx),
-//         concat!(
-//             "(source_file (function_item name: (identifier) ",
-//             "parameters: (parameters) ",
-//             "body: (block)))"
-//         )
-//     );
-
-//     buffer.update(cx, |buffer, _| {
-//         buffer.set_sync_parse_timeout(Duration::ZERO)
-//     });
-
-//     // Perform some edits (add parameter and variable reference)
-//     // Parsing doesn't begin until the transaction is complete
-//     buffer.update(cx, |buf, cx| {
-//         buf.start_transaction();
-
-//         let offset = buf.text().find(')').unwrap();
-//         buf.edit([(offset..offset, "b: C")], None, cx);
-//         assert!(!buf.is_parsing());
-
-//         let offset = buf.text().find('}').unwrap();
-//         buf.edit([(offset..offset, " d; ")], None, cx);
-//         assert!(!buf.is_parsing());
-
-//         buf.end_transaction(cx);
-//         assert_eq!(buf.text(), "fn a(b: C) { d; }");
-//         assert!(buf.is_parsing());
-//     });
-//     buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await;
-//     assert_eq!(
-//         get_tree_sexp(&buffer, cx),
-//         concat!(
-//             "(source_file (function_item name: (identifier) ",
-//             "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
-//             "body: (block (expression_statement (identifier)))))"
-//         )
-//     );
-
-//     // Perform a series of edits without waiting for the current parse to complete:
-//     // * turn identifier into a field expression
-//     // * turn field expression into a method call
-//     // * add a turbofish to the method call
-//     buffer.update(cx, |buf, cx| {
-//         let offset = buf.text().find(';').unwrap();
-//         buf.edit([(offset..offset, ".e")], None, cx);
-//         assert_eq!(buf.text(), "fn a(b: C) { d.e; }");
-//         assert!(buf.is_parsing());
-//     });
-//     buffer.update(cx, |buf, cx| {
-//         let offset = buf.text().find(';').unwrap();
-//         buf.edit([(offset..offset, "(f)")], None, cx);
-//         assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }");
-//         assert!(buf.is_parsing());
-//     });
-//     buffer.update(cx, |buf, cx| {
-//         let offset = buf.text().find("(f)").unwrap();
-//         buf.edit([(offset..offset, "::<G>")], None, cx);
-//         assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
-//         assert!(buf.is_parsing());
-//     });
-//     buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await;
-//     assert_eq!(
-//         get_tree_sexp(&buffer, cx),
-//         concat!(
-//             "(source_file (function_item name: (identifier) ",
-//             "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
-//             "body: (block (expression_statement (call_expression ",
-//             "function: (generic_function ",
-//             "function: (field_expression value: (identifier) field: (field_identifier)) ",
-//             "type_arguments: (type_arguments (type_identifier))) ",
-//             "arguments: (arguments (identifier)))))))",
-//         )
-//     );
-
-//     buffer.update(cx, |buf, cx| {
-//         buf.undo(cx);
-//         buf.undo(cx);
-//         buf.undo(cx);
-//         buf.undo(cx);
-//         assert_eq!(buf.text(), "fn a() {}");
-//         assert!(buf.is_parsing());
-//     });
-//     buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await;
-//     assert_eq!(
-//         get_tree_sexp(&buffer, cx),
-//         concat!(
-//             "(source_file (function_item name: (identifier) ",
-//             "parameters: (parameters) ",
-//             "body: (block)))"
-//         )
-//     );
-
-//     buffer.update(cx, |buf, cx| {
-//         buf.redo(cx);
-//         buf.redo(cx);
-//         buf.redo(cx);
-//         buf.redo(cx);
-//         assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
-//         assert!(buf.is_parsing());
-//     });
-//     buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await;
-//     assert_eq!(
-//         get_tree_sexp(&buffer, cx),
-//         concat!(
-//             "(source_file (function_item name: (identifier) ",
-//             "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
-//             "body: (block (expression_statement (call_expression ",
-//             "function: (generic_function ",
-//             "function: (field_expression value: (identifier) field: (field_identifier)) ",
-//             "type_arguments: (type_arguments (type_identifier))) ",
-//             "arguments: (arguments (identifier)))))))",
-//         )
-//     );
-// }
-
-// #[gpui::test]
-// async fn test_resetting_language(cx: &mut gpui::TestAppContext) {
-//     let buffer = cx.add_model(|cx| {
-//         let mut buffer =
-//             Buffer::new(0, cx.model_id() as u64, "{}").with_language(Arc::new(rust_lang()), cx);
-//         buffer.set_sync_parse_timeout(Duration::ZERO);
-//         buffer
-//     });
-
-//     // Wait for the initial text to parse
-//     buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await;
-//     assert_eq!(
-//         get_tree_sexp(&buffer, cx),
-//         "(source_file (expression_statement (block)))"
-//     );
-
-//     buffer.update(cx, |buffer, cx| {
-//         buffer.set_language(Some(Arc::new(json_lang())), cx)
-//     });
-//     buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await;
-//     assert_eq!(get_tree_sexp(&buffer, cx), "(document (object))");
-// }
-
-// #[gpui::test]
-// async fn test_outline(cx: &mut gpui::TestAppContext) {
-//     let text = r#"
-//         struct Person {
-//             name: String,
-//             age: usize,
-//         }
-
-//         mod module {
-//             enum LoginState {
-//                 LoggedOut,
-//                 LoggingOn,
-//                 LoggedIn {
-//                     person: Person,
-//                     time: Instant,
-//                 }
-//             }
-//         }
-
-//         impl Eq for Person {}
-
-//         impl Drop for Person {
-//             fn drop(&mut self) {
-//                 println!("bye");
-//             }
-//         }
-//     "#
-//     .unindent();
-
-//     let buffer = cx.add_model(|cx| {
-//         Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx)
-//     });
-//     let outline = buffer
-//         .read_with(cx, |buffer, _| buffer.snapshot().outline(None))
-//         .unwrap();
-
-//     assert_eq!(
-//         outline
-//             .items
-//             .iter()
-//             .map(|item| (item.text.as_str(), item.depth))
-//             .collect::<Vec<_>>(),
-//         &[
-//             ("struct Person", 0),
-//             ("name", 1),
-//             ("age", 1),
-//             ("mod module", 0),
-//             ("enum LoginState", 1),
-//             ("LoggedOut", 2),
-//             ("LoggingOn", 2),
-//             ("LoggedIn", 2),
-//             ("person", 3),
-//             ("time", 3),
-//             ("impl Eq for Person", 0),
-//             ("impl Drop for Person", 0),
-//             ("fn drop", 1),
-//         ]
-//     );
-
-//     // Without space, we only match on names
-//     assert_eq!(
-//         search(&outline, "oon", cx).await,
-//         &[
-//             ("mod module", vec![]),                    // included as the parent of a match
-//             ("enum LoginState", vec![]),               // included as the parent of a match
-//             ("LoggingOn", vec![1, 7, 8]),              // matches
-//             ("impl Drop for Person", vec![7, 18, 19]), // matches in two disjoint names
-//         ]
-//     );
-
-//     assert_eq!(
-//         search(&outline, "dp p", cx).await,
-//         &[
-//             ("impl Drop for Person", vec![5, 8, 9, 14]),
-//             ("fn drop", vec![]),
-//         ]
-//     );
-//     assert_eq!(
-//         search(&outline, "dpn", cx).await,
-//         &[("impl Drop for Person", vec![5, 14, 19])]
-//     );
-//     assert_eq!(
-//         search(&outline, "impl ", cx).await,
-//         &[
-//             ("impl Eq for Person", vec![0, 1, 2, 3, 4]),
-//             ("impl Drop for Person", vec![0, 1, 2, 3, 4]),
-//             ("fn drop", vec![]),
-//         ]
-//     );
-
-//     async fn search<'a>(
-//         outline: &'a Outline<Anchor>,
-//         query: &'a str,
-//         cx: &'a gpui::TestAppContext,
-//     ) -> Vec<(&'a str, Vec<usize>)> {
-//         let matches = cx
-//             .read(|cx| outline.search(query, cx.background().clone()))
-//             .await;
-//         matches
-//             .into_iter()
-//             .map(|mat| (outline.items[mat.candidate_id].text.as_str(), mat.positions))
-//             .collect::<Vec<_>>()
-//     }
-// }
-
-// #[gpui::test]
-// async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
-//     let text = r#"
-//         impl A for B<
-//             C
-//         > {
-//         };
-//     "#
-//     .unindent();
-
-//     let buffer = cx.add_model(|cx| {
-//         Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx)
-//     });
-//     let outline = buffer
-//         .read_with(cx, |buffer, _| buffer.snapshot().outline(None))
-//         .unwrap();
-
-//     assert_eq!(
-//         outline
-//             .items
-//             .iter()
-//             .map(|item| (item.text.as_str(), item.depth))
-//             .collect::<Vec<_>>(),
-//         &[("impl A for B<", 0)]
-//     );
-// }
-
-// #[gpui::test]
-// async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
-//     let language = javascript_lang()
-//         .with_outline_query(
-//             r#"
-//             (function_declaration
-//                 "function" @context
-//                 name: (_) @name
-//                 parameters: (formal_parameters
-//                     "(" @context.extra
-//                     ")" @context.extra)) @item
-//             "#,
-//         )
-//         .unwrap();
-
-//     let text = r#"
-//         function a() {}
-//         function b(c) {}
-//     "#
-//     .unindent();
-
-//     let buffer = cx.add_model(|cx| {
-//         Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx)
-//     });
-//     let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
-
-//     // extra context nodes are included in the outline.
-//     let outline = snapshot.outline(None).unwrap();
-//     assert_eq!(
-//         outline
-//             .items
-//             .iter()
-//             .map(|item| (item.text.as_str(), item.depth))
-//             .collect::<Vec<_>>(),
-//         &[("function a()", 0), ("function b( )", 0),]
-//     );
-
-//     // extra context nodes do not appear in breadcrumbs.
-//     let symbols = snapshot.symbols_containing(3, None).unwrap();
-//     assert_eq!(
-//         symbols
-//             .iter()
-//             .map(|item| (item.text.as_str(), item.depth))
-//             .collect::<Vec<_>>(),
-//         &[("function a", 0)]
-//     );
-// }
-
-// #[gpui::test]
-// async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
-//     let text = r#"
-//         impl Person {
-//             fn one() {
-//                 1
-//             }
-
-//             fn two() {
-//                 2
-//             }fn three() {
-//                 3
-//             }
-//         }
-//     "#
-//     .unindent();
-
-//     let buffer = cx.add_model(|cx| {
-//         Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx)
-//     });
-//     let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
-
-//     // point is at the start of an item
-//     assert_eq!(
-//         symbols_containing(Point::new(1, 4), &snapshot),
-//         vec![
-//             (
-//                 "impl Person".to_string(),
-//                 Point::new(0, 0)..Point::new(10, 1)
-//             ),
-//             ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
-//         ]
-//     );
-
-//     // point is in the middle of an item
-//     assert_eq!(
-//         symbols_containing(Point::new(2, 8), &snapshot),
-//         vec![
-//             (
-//                 "impl Person".to_string(),
-//                 Point::new(0, 0)..Point::new(10, 1)
-//             ),
-//             ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
-//         ]
-//     );
-
-//     // point is at the end of an item
-//     assert_eq!(
-//         symbols_containing(Point::new(3, 5), &snapshot),
-//         vec![
-//             (
-//                 "impl Person".to_string(),
-//                 Point::new(0, 0)..Point::new(10, 1)
-//             ),
-//             ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
-//         ]
-//     );
-
-//     // point is in between two adjacent items
-//     assert_eq!(
-//         symbols_containing(Point::new(7, 5), &snapshot),
-//         vec![
-//             (
-//                 "impl Person".to_string(),
-//                 Point::new(0, 0)..Point::new(10, 1)
-//             ),
-//             ("fn two".to_string(), Point::new(5, 4)..Point::new(7, 5))
-//         ]
-//     );
-
-//     fn symbols_containing(
-//         position: Point,
-//         snapshot: &BufferSnapshot,
-//     ) -> Vec<(String, Range<Point>)> {
-//         snapshot
-//             .symbols_containing(position, None)
-//             .unwrap()
-//             .into_iter()
-//             .map(|item| {
-//                 (
-//                     item.text,
-//                     item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot),
-//                 )
-//             })
-//             .collect()
-//     }
-// }
-
-// #[gpui::test]
-// fn test_enclosing_bracket_ranges(cx: &mut AppContext) {
-//     let mut assert = |selection_text, range_markers| {
-//         assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
-//     };
-
-//     assert(
-//         indoc! {"
-//             mod x {
-//                 moˇd y {
-
-//                 }
-//             }
-//             let foo = 1;"},
-//         vec![indoc! {"
-//             mod x «{»
-//                 mod y {
-
-//                 }
-//             «}»
-//             let foo = 1;"}],
-//     );
-
-//     assert(
-//         indoc! {"
-//             mod x {
-//                 mod y ˇ{
-
-//                 }
-//             }
-//             let foo = 1;"},
-//         vec![
-//             indoc! {"
-//                 mod x «{»
-//                     mod y {
-
-//                     }
-//                 «}»
-//                 let foo = 1;"},
-//             indoc! {"
-//                 mod x {
-//                     mod y «{»
-
-//                     «}»
-//                 }
-//                 let foo = 1;"},
-//         ],
-//     );
-
-//     assert(
-//         indoc! {"
-//             mod x {
-//                 mod y {
-
-//                 }ˇ
-//             }
-//             let foo = 1;"},
-//         vec![
-//             indoc! {"
-//                 mod x «{»
-//                     mod y {
-
-//                     }
-//                 «}»
-//                 let foo = 1;"},
-//             indoc! {"
-//                 mod x {
-//                     mod y «{»
-
-//                     «}»
-//                 }
-//                 let foo = 1;"},
-//         ],
-//     );
-
-//     assert(
-//         indoc! {"
-//             mod x {
-//                 mod y {
-
-//                 }
-//             ˇ}
-//             let foo = 1;"},
-//         vec![indoc! {"
-//             mod x «{»
-//                 mod y {
-
-//                 }
-//             «}»
-//             let foo = 1;"}],
-//     );
-
-//     assert(
-//         indoc! {"
-//             mod x {
-//                 mod y {
-
-//                 }
-//             }
-//             let fˇoo = 1;"},
-//         vec![],
-//     );
-
-//     // Regression test: avoid crash when querying at the end of the buffer.
-//     assert(
-//         indoc! {"
-//             mod x {
-//                 mod y {
-
-//                 }
-//             }
-//             let foo = 1;ˇ"},
-//         vec![],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: &mut AppContext) {
-//     let mut assert = |selection_text, bracket_pair_texts| {
-//         assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx)
-//     };
-
-//     assert(
-//         indoc! {"
-//         for (const a in b)ˇ {
-//             // a comment that's longer than the for-loop header
-//         }"},
-//         vec![indoc! {"
-//         for «(»const a in b«)» {
-//             // a comment that's longer than the for-loop header
-//         }"}],
-//     );
-
-//     // Regression test: even though the parent node of the parentheses (the for loop) does
-//     // intersect the given range, the parentheses themselves do not contain the range, so
-//     // they should not be returned. Only the curly braces contain the range.
-//     assert(
-//         indoc! {"
-//         for (const a in b) {ˇ
-//             // a comment that's longer than the for-loop header
-//         }"},
-//         vec![indoc! {"
-//         for (const a in b) «{»
-//             // a comment that's longer than the for-loop header
-//         «}»"}],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
-//     cx.add_model(|cx| {
-//         let text = "fn a() { b(|c| {}) }";
-//         let buffer =
-//             Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
-//         let snapshot = buffer.snapshot();
-
-//         assert_eq!(
-//             snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")),
-//             Some(range_of(text, "|"))
-//         );
-//         assert_eq!(
-//             snapshot.range_for_syntax_ancestor(range_of(text, "|")),
-//             Some(range_of(text, "|c|"))
-//         );
-//         assert_eq!(
-//             snapshot.range_for_syntax_ancestor(range_of(text, "|c|")),
-//             Some(range_of(text, "|c| {}"))
-//         );
-//         assert_eq!(
-//             snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")),
-//             Some(range_of(text, "(|c| {})"))
-//         );
-
-//         buffer
-//     });
-
-//     fn empty_range_at(text: &str, part: &str) -> Range<usize> {
-//         let start = text.find(part).unwrap();
-//         start..start
-//     }
-
-//     fn range_of(text: &str, part: &str) -> Range<usize> {
-//         let start = text.find(part).unwrap();
-//         start..start + part.len()
-//     }
-// }
-
-// #[gpui::test]
-// fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let text = "fn a() {}";
-//         let mut buffer =
-//             Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
-
-//         buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
-//         assert_eq!(buffer.text(), "fn a() {\n    \n}");
-
-//         buffer.edit(
-//             [(Point::new(1, 4)..Point::new(1, 4), "b()\n")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(buffer.text(), "fn a() {\n    b()\n    \n}");
-
-//         // Create a field expression on a new line, causing that line
-//         // to be indented.
-//         buffer.edit(
-//             [(Point::new(2, 4)..Point::new(2, 4), ".c")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(buffer.text(), "fn a() {\n    b()\n        .c\n}");
-
-//         // Remove the dot so that the line is no longer a field expression,
-//         // causing the line to be outdented.
-//         buffer.edit(
-//             [(Point::new(2, 8)..Point::new(2, 9), "")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(buffer.text(), "fn a() {\n    b()\n    c\n}");
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
-//     init_settings(cx, |settings| {
-//         settings.defaults.hard_tabs = Some(true);
-//     });
-
-//     cx.add_model(|cx| {
-//         let text = "fn a() {}";
-//         let mut buffer =
-//             Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
-
-//         buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
-//         assert_eq!(buffer.text(), "fn a() {\n\t\n}");
-
-//         buffer.edit(
-//             [(Point::new(1, 1)..Point::new(1, 1), "b()\n")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}");
-
-//         // Create a field expression on a new line, causing that line
-//         // to be indented.
-//         buffer.edit(
-//             [(Point::new(2, 1)..Point::new(2, 1), ".c")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}");
-
-//         // Remove the dot so that the line is no longer a field expression,
-//         // causing the line to be outdented.
-//         buffer.edit(
-//             [(Point::new(2, 2)..Point::new(2, 3), "")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}");
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let mut buffer = Buffer::new(
-//             0,
-//             cx.model_id() as u64,
-//             "
-//             fn a() {
-//             c;
-//             d;
-//             }
-//             "
-//             .unindent(),
-//         )
-//         .with_language(Arc::new(rust_lang()), cx);
-
-//         // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
-//         // their indentation is not adjusted.
-//         buffer.edit_via_marked_text(
-//             &"
-//             fn a() {
-//             c«()»;
-//             d«()»;
-//             }
-//             "
-//             .unindent(),
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//             fn a() {
-//             c();
-//             d();
-//             }
-//             "
-//             .unindent()
-//         );
-
-//         // When appending new content after these lines, the indentation is based on the
-//         // preceding lines' actual indentation.
-//         buffer.edit_via_marked_text(
-//             &"
-//             fn a() {
-//             c«
-//             .f
-//             .g()»;
-//             d«
-//             .f
-//             .g()»;
-//             }
-//             "
-//             .unindent(),
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//             fn a() {
-//             c
-//                 .f
-//                 .g();
-//             d
-//                 .f
-//                 .g();
-//             }
-//             "
-//             .unindent()
-//         );
-//         buffer
-//     });
-
-//     cx.add_model(|cx| {
-//         let mut buffer = Buffer::new(
-//             0,
-//             cx.model_id() as u64,
-//             "
-//             fn a() {
-//                 b();
-//                 |
-//             "
-//             .replace("|", "") // marker to preserve trailing whitespace
-//             .unindent(),
-//         )
-//         .with_language(Arc::new(rust_lang()), cx);
-
-//         // Insert a closing brace. It is outdented.
-//         buffer.edit_via_marked_text(
-//             &"
-//             fn a() {
-//                 b();
-//                 «}»
-//             "
-//             .unindent(),
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//             fn a() {
-//                 b();
-//             }
-//             "
-//             .unindent()
-//         );
-
-//         // Manually edit the leading whitespace. The edit is preserved.
-//         buffer.edit_via_marked_text(
-//             &"
-//             fn a() {
-//                 b();
-//             «    »}
-//             "
-//             .unindent(),
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//             fn a() {
-//                 b();
-//                 }
-//             "
-//             .unindent()
-//         );
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let mut buffer = Buffer::new(
-//             0,
-//             cx.model_id() as u64,
-//             "
-//             fn a() {
-//                 i
-//             }
-//             "
-//             .unindent(),
-//         )
-//         .with_language(Arc::new(rust_lang()), cx);
-
-//         // Regression test: line does not get outdented due to syntax error
-//         buffer.edit_via_marked_text(
-//             &"
-//             fn a() {
-//                 i«f let Some(x) = y»
-//             }
-//             "
-//             .unindent(),
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//             fn a() {
-//                 if let Some(x) = y
-//             }
-//             "
-//             .unindent()
-//         );
-
-//         buffer.edit_via_marked_text(
-//             &"
-//             fn a() {
-//                 if let Some(x) = y« {»
-//             }
-//             "
-//             .unindent(),
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//             fn a() {
-//                 if let Some(x) = y {
-//             }
-//             "
-//             .unindent()
-//         );
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let mut buffer = Buffer::new(
-//             0,
-//             cx.model_id() as u64,
-//             "
-//             fn a() {}
-//             "
-//             .unindent(),
-//         )
-//         .with_language(Arc::new(rust_lang()), cx);
-
-//         buffer.edit_via_marked_text(
-//             &"
-//             fn a(«
-//             b») {}
-//             "
-//             .unindent(),
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//             fn a(
-//                 b) {}
-//             "
-//             .unindent()
-//         );
-
-//         // The indentation suggestion changed because `@end` node (a close paren)
-//         // is now at the beginning of the line.
-//         buffer.edit_via_marked_text(
-//             &"
-//             fn a(
-//                 ˇ) {}
-//             "
-//             .unindent(),
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//                 fn a(
-//                 ) {}
-//             "
-//             .unindent()
-//         );
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let text = "a\nb";
-//         let mut buffer =
-//             Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
-//         buffer.edit(
-//             [(0..1, "\n"), (2..3, "\n")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(buffer.text(), "\n\n\n");
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let text = "
-//             const a: usize = 1;
-//             fn b() {
-//                 if c {
-//                     let d = 2;
-//                 }
-//             }
-//         "
-//         .unindent();
-
-//         let mut buffer =
-//             Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
-//         buffer.edit(
-//             [(Point::new(3, 0)..Point::new(3, 0), "e(\n    f()\n);\n")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//                 const a: usize = 1;
-//                 fn b() {
-//                     if c {
-//                         e(
-//                             f()
-//                         );
-//                         let d = 2;
-//                     }
-//                 }
-//             "
-//             .unindent()
-//         );
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_block_mode(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let text = r#"
-//             fn a() {
-//                 b();
-//             }
-//         "#
-//         .unindent();
-//         let mut buffer =
-//             Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
-
-//         // When this text was copied, both of the quotation marks were at the same
-//         // indent level, but the indentation of the first line was not included in
-//         // the copied text. This information is retained in the
-//         // 'original_indent_columns' vector.
-//         let original_indent_columns = vec![4];
-//         let inserted_text = r#"
-//             "
-//                   c
-//                     d
-//                       e
-//                 "
-//         "#
-//         .unindent();
-
-//         // Insert the block at column zero. The entire block is indented
-//         // so that the first line matches the previous line's indentation.
-//         buffer.edit(
-//             [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
-//             Some(AutoindentMode::Block {
-//                 original_indent_columns: original_indent_columns.clone(),
-//             }),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             r#"
-//             fn a() {
-//                 b();
-//                 "
-//                   c
-//                     d
-//                       e
-//                 "
-//             }
-//             "#
-//             .unindent()
-//         );
-
-//         // Grouping is disabled in tests, so we need 2 undos
-//         buffer.undo(cx); // Undo the auto-indent
-//         buffer.undo(cx); // Undo the original edit
-
-//         // Insert the block at a deeper indent level. The entire block is outdented.
-//         buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "        ")], None, cx);
-//         buffer.edit(
-//             [(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
-//             Some(AutoindentMode::Block {
-//                 original_indent_columns: original_indent_columns.clone(),
-//             }),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             r#"
-//             fn a() {
-//                 b();
-//                 "
-//                   c
-//                     d
-//                       e
-//                 "
-//             }
-//             "#
-//             .unindent()
-//         );
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let text = r#"
-//             fn a() {
-//                 if b() {
-
-//                 }
-//             }
-//         "#
-//         .unindent();
-//         let mut buffer =
-//             Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx);
-
-//         // The original indent columns are not known, so this text is
-//         // auto-indented in a block as if the first line was copied in
-//         // its entirety.
-//         let original_indent_columns = Vec::new();
-//         let inserted_text = "    c\n        .d()\n        .e();";
-
-//         // Insert the block at column zero. The entire block is indented
-//         // so that the first line matches the previous line's indentation.
-//         buffer.edit(
-//             [(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
-//             Some(AutoindentMode::Block {
-//                 original_indent_columns: original_indent_columns.clone(),
-//             }),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             r#"
-//             fn a() {
-//                 if b() {
-//                     c
-//                         .d()
-//                         .e();
-//                 }
-//             }
-//             "#
-//             .unindent()
-//         );
-
-//         // Grouping is disabled in tests, so we need 2 undos
-//         buffer.undo(cx); // Undo the auto-indent
-//         buffer.undo(cx); // Undo the original edit
-
-//         // Insert the block at a deeper indent level. The entire block is outdented.
-//         buffer.edit(
-//             [(Point::new(2, 0)..Point::new(2, 0), " ".repeat(12))],
-//             None,
-//             cx,
-//         );
-//         buffer.edit(
-//             [(Point::new(2, 12)..Point::new(2, 12), inserted_text)],
-//             Some(AutoindentMode::Block {
-//                 original_indent_columns: Vec::new(),
-//             }),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             r#"
-//             fn a() {
-//                 if b() {
-//                     c
-//                         .d()
-//                         .e();
-//                 }
-//             }
-//             "#
-//             .unindent()
-//         );
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let text = "
-//             * one
-//                 - a
-//                 - b
-//             * two
-//         "
-//         .unindent();
-
-//         let mut buffer = Buffer::new(0, cx.model_id() as u64, text).with_language(
-//             Arc::new(Language::new(
-//                 LanguageConfig {
-//                     name: "Markdown".into(),
-//                     auto_indent_using_last_non_empty_line: false,
-//                     ..Default::default()
-//                 },
-//                 Some(tree_sitter_json::language()),
-//             )),
-//             cx,
-//         );
-//         buffer.edit(
-//             [(Point::new(3, 0)..Point::new(3, 0), "\n")],
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//             * one
-//                 - a
-//                 - b
-
-//             * two
-//             "
-//             .unindent()
-//         );
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
-//     init_settings(cx, |settings| {
-//         settings.languages.extend([
-//             (
-//                 "HTML".into(),
-//                 LanguageSettingsContent {
-//                     tab_size: Some(2.try_into().unwrap()),
-//                     ..Default::default()
-//                 },
-//             ),
-//             (
-//                 "JavaScript".into(),
-//                 LanguageSettingsContent {
-//                     tab_size: Some(8.try_into().unwrap()),
-//                     ..Default::default()
-//                 },
-//             ),
-//         ])
-//     });
-
-//     let html_language = Arc::new(html_lang());
-
-//     let javascript_language = Arc::new(javascript_lang());
-
-//     let language_registry = Arc::new(LanguageRegistry::test());
-//     language_registry.add(html_language.clone());
-//     language_registry.add(javascript_language.clone());
-
-//     cx.add_model(|cx| {
-//         let (text, ranges) = marked_text_ranges(
-//             &"
-//                 <div>ˇ
-//                 </div>
-//                 <script>
-//                     init({ˇ
-//                     })
-//                 </script>
-//                 <span>ˇ
-//                 </span>
-//             "
-//             .unindent(),
-//             false,
-//         );
-
-//         let mut buffer = Buffer::new(0, cx.model_id() as u64, text);
-//         buffer.set_language_registry(language_registry);
-//         buffer.set_language(Some(html_language), cx);
-//         buffer.edit(
-//             ranges.into_iter().map(|range| (range, "\na")),
-//             Some(AutoindentMode::EachLine),
-//             cx,
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             "
-//                 <div>
-//                   a
-//                 </div>
-//                 <script>
-//                     init({
-//                             a
-//                     })
-//                 </script>
-//                 <span>
-//                   a
-//                 </span>
-//             "
-//             .unindent()
-//         );
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
-//     init_settings(cx, |settings| {
-//         settings.defaults.tab_size = Some(2.try_into().unwrap());
-//     });
-
-//     cx.add_model(|cx| {
-//         let mut buffer =
-//             Buffer::new(0, cx.model_id() as u64, "").with_language(Arc::new(ruby_lang()), cx);
-
-//         let text = r#"
-//             class C
-//             def a(b, c)
-//             puts b
-//             puts c
-//             rescue
-//             puts "errored"
-//             exit 1
-//             end
-//             end
-//         "#
-//         .unindent();
-
-//         buffer.edit([(0..0, text)], Some(AutoindentMode::EachLine), cx);
-
-//         assert_eq!(
-//             buffer.text(),
-//             r#"
-//                 class C
-//                   def a(b, c)
-//                     puts b
-//                     puts c
-//                   rescue
-//                     puts "errored"
-//                     exit 1
-//                   end
-//                 end
-//             "#
-//             .unindent()
-//         );
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let language = Language::new(
-//             LanguageConfig {
-//                 name: "JavaScript".into(),
-//                 line_comment: Some("// ".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()],
-//                     ],
-//                 },
-//                 overrides: [(
-//                     "element".into(),
-//                     LanguageConfigOverride {
-//                         line_comment: Override::Remove { remove: true },
-//                         block_comment: Override::Set(("{/*".into(), "*/}".into())),
-//                         ..Default::default()
-//                     },
-//                 )]
-//                 .into_iter()
-//                 .collect(),
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_typescript::language_tsx()),
-//         )
-//         .with_override_query(
-//             r#"
-//                 (jsx_element) @element
-//                 (string) @string
-//             "#,
-//         )
-//         .unwrap();
-
-//         let text = r#"a["b"] = <C d="e"></C>;"#;
-
-//         let buffer =
-//             Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx);
-//         let snapshot = buffer.snapshot();
-
-//         let config = snapshot.language_scope_at(0).unwrap();
-//         assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
-//         // Both bracket pairs are enabled
-//         assert_eq!(
-//             config.brackets().map(|e| e.1).collect::<Vec<_>>(),
-//             &[true, true]
-//         );
-
-//         let string_config = snapshot.language_scope_at(3).unwrap();
-//         assert_eq!(string_config.line_comment_prefix().unwrap().as_ref(), "// ");
-//         // Second bracket pair is disabled
-//         assert_eq!(
-//             string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
-//             &[true, false]
-//         );
-
-//         let element_config = snapshot.language_scope_at(10).unwrap();
-//         assert_eq!(element_config.line_comment_prefix(), None);
-//         assert_eq!(
-//             element_config.block_comment_delimiters(),
-//             Some((&"{/*".into(), &"*/}".into()))
-//         );
-//         // Both bracket pairs are enabled
-//         assert_eq!(
-//             element_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
-//             &[true, true]
-//         );
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_language_scope_at_with_rust(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let language = Language::new(
-//             LanguageConfig {
-//                 name: "Rust".into(),
-//                 brackets: BracketPairConfig {
-//                     pairs: vec![
-//                         BracketPair {
-//                             start: "{".into(),
-//                             end: "}".into(),
-//                             close: true,
-//                             newline: false,
-//                         },
-//                         BracketPair {
-//                             start: "'".into(),
-//                             end: "'".into(),
-//                             close: true,
-//                             newline: false,
-//                         },
-//                     ],
-//                     disabled_scopes_by_bracket_ix: vec![
-//                         Vec::new(), //
-//                         vec!["string".into()],
-//                     ],
-//                 },
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_rust::language()),
-//         )
-//         .with_override_query(
-//             r#"
-//                 (string_literal) @string
-//             "#,
-//         )
-//         .unwrap();
-
-//         let text = r#"
-//             const S: &'static str = "hello";
-//         "#
-//         .unindent();
-
-//         let buffer = Buffer::new(0, cx.model_id() as u64, text.clone())
-//             .with_language(Arc::new(language), cx);
-//         let snapshot = buffer.snapshot();
-
-//         // By default, all brackets are enabled
-//         let config = snapshot.language_scope_at(0).unwrap();
-//         assert_eq!(
-//             config.brackets().map(|e| e.1).collect::<Vec<_>>(),
-//             &[true, true]
-//         );
-
-//         // Within a string, the quotation brackets are disabled.
-//         let string_config = snapshot
-//             .language_scope_at(text.find("ello").unwrap())
-//             .unwrap();
-//         assert_eq!(
-//             string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
-//             &[true, false]
-//         );
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
-//     init_settings(cx, |_| {});
-
-//     cx.add_model(|cx| {
-//         let text = r#"
-//             <ol>
-//             <% people.each do |person| %>
-//                 <li>
-//                     <%= person.name %>
-//                 </li>
-//             <% end %>
-//             </ol>
-//         "#
-//         .unindent();
-
-//         let language_registry = Arc::new(LanguageRegistry::test());
-//         language_registry.add(Arc::new(ruby_lang()));
-//         language_registry.add(Arc::new(html_lang()));
-//         language_registry.add(Arc::new(erb_lang()));
-
-//         let mut buffer = Buffer::new(0, cx.model_id() as u64, text);
-//         buffer.set_language_registry(language_registry.clone());
-//         buffer.set_language(
-//             language_registry
-//                 .language_for_name("ERB")
-//                 .now_or_never()
-//                 .unwrap()
-//                 .ok(),
-//             cx,
-//         );
-
-//         let snapshot = buffer.snapshot();
-//         let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap();
-//         assert_eq!(html_config.line_comment_prefix(), None);
-//         assert_eq!(
-//             html_config.block_comment_delimiters(),
-//             Some((&"<!--".into(), &"-->".into()))
-//         );
-
-//         let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap();
-//         assert_eq!(ruby_config.line_comment_prefix().unwrap().as_ref(), "# ");
-//         assert_eq!(ruby_config.block_comment_delimiters(), None);
-
-//         buffer
-//     });
-// }
-
-// #[gpui::test]
-// fn test_serialization(cx: &mut gpui::AppContext) {
-//     let mut now = Instant::now();
-
-//     let buffer1 = cx.add_model(|cx| {
-//         let mut buffer = Buffer::new(0, cx.model_id() as u64, "abc");
-//         buffer.edit([(3..3, "D")], None, cx);
-
-//         now += Duration::from_secs(1);
-//         buffer.start_transaction_at(now);
-//         buffer.edit([(4..4, "E")], None, cx);
-//         buffer.end_transaction_at(now, cx);
-//         assert_eq!(buffer.text(), "abcDE");
-
-//         buffer.undo(cx);
-//         assert_eq!(buffer.text(), "abcD");
-
-//         buffer.edit([(4..4, "F")], None, cx);
-//         assert_eq!(buffer.text(), "abcDF");
-//         buffer
-//     });
-//     assert_eq!(buffer1.read(cx).text(), "abcDF");
-
-//     let state = buffer1.read(cx).to_proto();
-//     let ops = cx
-//         .background()
-//         .block(buffer1.read(cx).serialize_ops(None, cx));
-//     let buffer2 = cx.add_model(|cx| {
-//         let mut buffer = Buffer::from_proto(1, state, None).unwrap();
-//         buffer
-//             .apply_ops(
-//                 ops.into_iter()
-//                     .map(|op| proto::deserialize_operation(op).unwrap()),
-//                 cx,
-//             )
-//             .unwrap();
-//         buffer
-//     });
-//     assert_eq!(buffer2.read(cx).text(), "abcDF");
-// }
-
-// #[gpui::test(iterations = 100)]
-// fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
-//     let min_peers = env::var("MIN_PEERS")
-//         .map(|i| i.parse().expect("invalid `MIN_PEERS` variable"))
-//         .unwrap_or(1);
-//     let max_peers = env::var("MAX_PEERS")
-//         .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
-//         .unwrap_or(5);
-//     let operations = env::var("OPERATIONS")
-//         .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-//         .unwrap_or(10);
-
-//     let base_text_len = rng.gen_range(0..10);
-//     let base_text = RandomCharIter::new(&mut rng)
-//         .take(base_text_len)
-//         .collect::<String>();
-//     let mut replica_ids = Vec::new();
-//     let mut buffers = Vec::new();
-//     let network = Rc::new(RefCell::new(Network::new(rng.clone())));
-//     let base_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text.as_str()));
-
-//     for i in 0..rng.gen_range(min_peers..=max_peers) {
-//         let buffer = cx.add_model(|cx| {
-//             let state = base_buffer.read(cx).to_proto();
-//             let ops = cx
-//                 .background()
-//                 .block(base_buffer.read(cx).serialize_ops(None, cx));
-//             let mut buffer = Buffer::from_proto(i as ReplicaId, state, None).unwrap();
-//             buffer
-//                 .apply_ops(
-//                     ops.into_iter()
-//                         .map(|op| proto::deserialize_operation(op).unwrap()),
-//                     cx,
-//                 )
-//                 .unwrap();
-//             buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
-//             let network = network.clone();
-//             cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
-//                 if let Event::Operation(op) = event {
-//                     network
-//                         .borrow_mut()
-//                         .broadcast(buffer.replica_id(), vec![proto::serialize_operation(op)]);
-//                 }
-//             })
-//             .detach();
-//             buffer
-//         });
-//         buffers.push(buffer);
-//         replica_ids.push(i as ReplicaId);
-//         network.borrow_mut().add_peer(i as ReplicaId);
-//         log::info!("Adding initial peer with replica id {}", i);
-//     }
-
-//     log::info!("initial text: {:?}", base_text);
-
-//     let mut now = Instant::now();
-//     let mut mutation_count = operations;
-//     let mut next_diagnostic_id = 0;
-//     let mut active_selections = BTreeMap::default();
-//     loop {
-//         let replica_index = rng.gen_range(0..replica_ids.len());
-//         let replica_id = replica_ids[replica_index];
-//         let buffer = &mut buffers[replica_index];
-//         let mut new_buffer = None;
-//         match rng.gen_range(0..100) {
-//             0..=29 if mutation_count != 0 => {
-//                 buffer.update(cx, |buffer, cx| {
-//                     buffer.start_transaction_at(now);
-//                     buffer.randomly_edit(&mut rng, 5, cx);
-//                     buffer.end_transaction_at(now, cx);
-//                     log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text());
-//                 });
-//                 mutation_count -= 1;
-//             }
-//             30..=39 if mutation_count != 0 => {
-//                 buffer.update(cx, |buffer, cx| {
-//                     if rng.gen_bool(0.2) {
-//                         log::info!("peer {} clearing active selections", replica_id);
-//                         active_selections.remove(&replica_id);
-//                         buffer.remove_active_selections(cx);
-//                     } else {
-//                         let mut selections = Vec::new();
-//                         for id in 0..rng.gen_range(1..=5) {
-//                             let range = buffer.random_byte_range(0, &mut rng);
-//                             selections.push(Selection {
-//                                 id,
-//                                 start: buffer.anchor_before(range.start),
-//                                 end: buffer.anchor_before(range.end),
-//                                 reversed: false,
-//                                 goal: SelectionGoal::None,
-//                             });
-//                         }
-//                         let selections: Arc<[Selection<Anchor>]> = selections.into();
-//                         log::info!(
-//                             "peer {} setting active selections: {:?}",
-//                             replica_id,
-//                             selections
-//                         );
-//                         active_selections.insert(replica_id, selections.clone());
-//                         buffer.set_active_selections(selections, false, Default::default(), cx);
-//                     }
-//                 });
-//                 mutation_count -= 1;
-//             }
-//             40..=49 if mutation_count != 0 && replica_id == 0 => {
-//                 let entry_count = rng.gen_range(1..=5);
-//                 buffer.update(cx, |buffer, cx| {
-//                     let diagnostics = DiagnosticSet::new(
-//                         (0..entry_count).map(|_| {
-//                             let range = buffer.random_byte_range(0, &mut rng);
-//                             let range = range.to_point_utf16(buffer);
-//                             let range = range.start..range.end;
-//                             DiagnosticEntry {
-//                                 range,
-//                                 diagnostic: Diagnostic {
-//                                     message: post_inc(&mut next_diagnostic_id).to_string(),
-//                                     ..Default::default()
-//                                 },
-//                             }
-//                         }),
-//                         buffer,
-//                     );
-//                     log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics);
-//                     buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
-//                 });
-//                 mutation_count -= 1;
-//             }
-//             50..=59 if replica_ids.len() < max_peers => {
-//                 let old_buffer_state = buffer.read(cx).to_proto();
-//                 let old_buffer_ops = cx
-//                     .background()
-//                     .block(buffer.read(cx).serialize_ops(None, cx));
-//                 let new_replica_id = (0..=replica_ids.len() as ReplicaId)
-//                     .filter(|replica_id| *replica_id != buffer.read(cx).replica_id())
-//                     .choose(&mut rng)
-//                     .unwrap();
-//                 log::info!(
-//                     "Adding new replica {} (replicating from {})",
-//                     new_replica_id,
-//                     replica_id
-//                 );
-//                 new_buffer = Some(cx.add_model(|cx| {
-//                     let mut new_buffer =
-//                         Buffer::from_proto(new_replica_id, old_buffer_state, None).unwrap();
-//                     new_buffer
-//                         .apply_ops(
-//                             old_buffer_ops
-//                                 .into_iter()
-//                                 .map(|op| deserialize_operation(op).unwrap()),
-//                             cx,
-//                         )
-//                         .unwrap();
-//                     log::info!(
-//                         "New replica {} text: {:?}",
-//                         new_buffer.replica_id(),
-//                         new_buffer.text()
-//                     );
-//                     new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
-//                     let network = network.clone();
-//                     cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
-//                         if let Event::Operation(op) = event {
-//                             network.borrow_mut().broadcast(
-//                                 buffer.replica_id(),
-//                                 vec![proto::serialize_operation(op)],
-//                             );
-//                         }
-//                     })
-//                     .detach();
-//                     new_buffer
-//                 }));
-//                 network.borrow_mut().replicate(replica_id, new_replica_id);
-
-//                 if new_replica_id as usize == replica_ids.len() {
-//                     replica_ids.push(new_replica_id);
-//                 } else {
-//                     let new_buffer = new_buffer.take().unwrap();
-//                     while network.borrow().has_unreceived(new_replica_id) {
-//                         let ops = network
-//                             .borrow_mut()
-//                             .receive(new_replica_id)
-//                             .into_iter()
-//                             .map(|op| proto::deserialize_operation(op).unwrap());
-//                         if ops.len() > 0 {
-//                             log::info!(
-//                                 "peer {} (version: {:?}) applying {} ops from the network. {:?}",
-//                                 new_replica_id,
-//                                 buffer.read(cx).version(),
-//                                 ops.len(),
-//                                 ops
-//                             );
-//                             new_buffer.update(cx, |new_buffer, cx| {
-//                                 new_buffer.apply_ops(ops, cx).unwrap();
-//                             });
-//                         }
-//                     }
-//                     buffers[new_replica_id as usize] = new_buffer;
-//                 }
-//             }
-//             60..=69 if mutation_count != 0 => {
-//                 buffer.update(cx, |buffer, cx| {
-//                     buffer.randomly_undo_redo(&mut rng, cx);
-//                     log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text());
-//                 });
-//                 mutation_count -= 1;
-//             }
-//             _ if network.borrow().has_unreceived(replica_id) => {
-//                 let ops = network
-//                     .borrow_mut()
-//                     .receive(replica_id)
-//                     .into_iter()
-//                     .map(|op| proto::deserialize_operation(op).unwrap());
-//                 if ops.len() > 0 {
-//                     log::info!(
-//                         "peer {} (version: {:?}) applying {} ops from the network. {:?}",
-//                         replica_id,
-//                         buffer.read(cx).version(),
-//                         ops.len(),
-//                         ops
-//                     );
-//                     buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx).unwrap());
-//                 }
-//             }
-//             _ => {}
-//         }
-
-//         now += Duration::from_millis(rng.gen_range(0..=200));
-//         buffers.extend(new_buffer);
-
-//         for buffer in &buffers {
-//             buffer.read(cx).check_invariants();
-//         }
-
-//         if mutation_count == 0 && network.borrow().is_idle() {
-//             break;
-//         }
-//     }
-
-//     let first_buffer = buffers[0].read(cx).snapshot();
-//     for buffer in &buffers[1..] {
-//         let buffer = buffer.read(cx).snapshot();
-//         assert_eq!(
-//             buffer.version(),
-//             first_buffer.version(),
-//             "Replica {} version != Replica 0 version",
-//             buffer.replica_id()
-//         );
-//         assert_eq!(
-//             buffer.text(),
-//             first_buffer.text(),
-//             "Replica {} text != Replica 0 text",
-//             buffer.replica_id()
-//         );
-//         assert_eq!(
-//             buffer
-//                 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
-//                 .collect::<Vec<_>>(),
-//             first_buffer
-//                 .diagnostics_in_range::<_, usize>(0..first_buffer.len(), false)
-//                 .collect::<Vec<_>>(),
-//             "Replica {} diagnostics != Replica 0 diagnostics",
-//             buffer.replica_id()
-//         );
-//     }
-
-//     for buffer in &buffers {
-//         let buffer = buffer.read(cx).snapshot();
-//         let actual_remote_selections = buffer
-//             .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
-//             .map(|(replica_id, _, _, selections)| (replica_id, selections.collect::<Vec<_>>()))
-//             .collect::<Vec<_>>();
-//         let expected_remote_selections = active_selections
-//             .iter()
-//             .filter(|(replica_id, _)| **replica_id != buffer.replica_id())
-//             .map(|(replica_id, selections)| (*replica_id, selections.iter().collect::<Vec<_>>()))
-//             .collect::<Vec<_>>();
-//         assert_eq!(
-//             actual_remote_selections,
-//             expected_remote_selections,
-//             "Replica {} remote selections != expected selections",
-//             buffer.replica_id()
-//         );
-//     }
-// }
-
-// #[test]
-// fn test_contiguous_ranges() {
-//     assert_eq!(
-//         contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
-//         &[1..4, 5..7, 9..13]
-//     );
-
-//     // Respects the `max_len` parameter
-//     assert_eq!(
-//         contiguous_ranges(
-//             [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
-//             3
-//         )
-//         .collect::<Vec<_>>(),
-//         &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
-//     );
-// }
-
-// #[gpui::test(iterations = 500)]
-// fn test_trailing_whitespace_ranges(mut rng: StdRng) {
-//     // Generate a random multi-line string containing
-//     // some lines with trailing whitespace.
-//     let mut text = String::new();
-//     for _ in 0..rng.gen_range(0..16) {
-//         for _ in 0..rng.gen_range(0..36) {
-//             text.push(match rng.gen_range(0..10) {
-//                 0..=1 => ' ',
-//                 3 => '\t',
-//                 _ => rng.gen_range('a'..'z'),
-//             });
-//         }
-//         text.push('\n');
-//     }
-
-//     match rng.gen_range(0..10) {
-//         // sometimes remove the last newline
-//         0..=1 => drop(text.pop()), //
-
-//         // sometimes add extra newlines
-//         2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))),
-//         _ => {}
-//     }
-
-//     let rope = Rope::from(text.as_str());
-//     let actual_ranges = trailing_whitespace_ranges(&rope);
-//     let expected_ranges = TRAILING_WHITESPACE_REGEX
-//         .find_iter(&text)
-//         .map(|m| m.range())
-//         .collect::<Vec<_>>();
-//     assert_eq!(
-//         actual_ranges,
-//         expected_ranges,
-//         "wrong ranges for text lines:\n{:?}",
-//         text.split("\n").collect::<Vec<_>>()
-//     );
-// }
-
-// fn ruby_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "Ruby".into(),
-//             path_suffixes: vec!["rb".to_string()],
-//             line_comment: Some("# ".into()),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_ruby::language()),
-//     )
-//     .with_indents_query(
-//         r#"
-//             (class "end" @end) @indent
-//             (method "end" @end) @indent
-//             (rescue) @outdent
-//             (then) @indent
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn html_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "HTML".into(),
-//             block_comment: Some(("<!--".into(), "-->".into())),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_html::language()),
-//     )
-//     .with_indents_query(
-//         "
-//         (element
-//           (start_tag) @start
-//           (end_tag)? @end) @indent
-//         ",
-//     )
-//     .unwrap()
-//     .with_injection_query(
-//         r#"
-//         (script_element
-//             (raw_text) @content
-//             (#set! "language" "javascript"))
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn erb_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "ERB".into(),
-//             path_suffixes: vec!["erb".to_string()],
-//             block_comment: Some(("<%#".into(), "%>".into())),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_embedded_template::language()),
-//     )
-//     .with_injection_query(
-//         r#"
-//             (
-//                 (code) @content
-//                 (#set! "language" "ruby")
-//                 (#set! "combined")
-//             )
-
-//             (
-//                 (content) @content
-//                 (#set! "language" "html")
-//                 (#set! "combined")
-//             )
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn rust_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     )
-//     .with_indents_query(
-//         r#"
-//         (call_expression) @indent
-//         (field_expression) @indent
-//         (_ "(" ")" @end) @indent
-//         (_ "{" "}" @end) @indent
-//         "#,
-//     )
-//     .unwrap()
-//     .with_brackets_query(
-//         r#"
-//         ("{" @open "}" @close)
-//         "#,
-//     )
-//     .unwrap()
-//     .with_outline_query(
-//         r#"
-//         (struct_item
-//             "struct" @context
-//             name: (_) @name) @item
-//         (enum_item
-//             "enum" @context
-//             name: (_) @name) @item
-//         (enum_variant
-//             name: (_) @name) @item
-//         (field_declaration
-//             name: (_) @name) @item
-//         (impl_item
-//             "impl" @context
-//             trait: (_)? @name
-//             "for"? @context
-//             type: (_) @name) @item
-//         (function_item
-//             "fn" @context
-//             name: (_) @name) @item
-//         (mod_item
-//             "mod" @context
-//             name: (_) @name) @item
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn json_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "Json".into(),
-//             path_suffixes: vec!["js".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_json::language()),
-//     )
-// }
-
-// fn javascript_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "JavaScript".into(),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_typescript::language_tsx()),
-//     )
-//     .with_brackets_query(
-//         r#"
-//         ("{" @open "}" @close)
-//         ("(" @open ")" @close)
-//         "#,
-//     )
-//     .unwrap()
-//     .with_indents_query(
-//         r#"
-//         (object "}" @end) @indent
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
-//     buffer.read_with(cx, |buffer, _| {
-//         let snapshot = buffer.snapshot();
-//         let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
-//         layers[0].node().to_sexp()
-//     })
-// }
-
-// // Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers`
-// fn assert_bracket_pairs(
-//     selection_text: &'static str,
-//     bracket_pair_texts: Vec<&'static str>,
-//     language: Language,
-//     cx: &mut AppContext,
-// ) {
-//     let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
-//     let buffer = cx.add_model(|cx| {
-//         Buffer::new(0, cx.model_id() as u64, expected_text.clone())
-//             .with_language(Arc::new(language), cx)
-//     });
-//     let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot());
-
-//     let selection_range = selection_ranges[0].clone();
-
-//     let bracket_pairs = bracket_pair_texts
-//         .into_iter()
-//         .map(|pair_text| {
-//             let (bracket_text, ranges) = marked_text_ranges(pair_text, false);
-//             assert_eq!(bracket_text, expected_text);
-//             (ranges[0].clone(), ranges[1].clone())
-//         })
-//         .collect::<Vec<_>>();
-
-//     assert_set_eq!(
-//         buffer.bracket_ranges(selection_range).collect::<Vec<_>>(),
-//         bracket_pairs
-//     );
-// }
-
-// fn init_settings(cx: &mut AppContext, f: fn(&mut AllLanguageSettingsContent)) {
-//     cx.set_global(SettingsStore::test(cx));
-//     crate::init(cx);
-//     cx.update_global::<SettingsStore, _, _>(|settings, cx| {
-//         settings.update_user_settings::<AllLanguageSettings>(cx, f);
-//     });
-// }
+#[gpui2::test(iterations = 10)]
+async fn test_normalize_whitespace(cx: &mut gpui2::TestAppContext) {
+    let text = [
+        "zero",     //
+        "one  ",    // 2 trailing spaces
+        "two",      //
+        "three   ", // 3 trailing spaces
+        "four",     //
+        "five    ", // 4 trailing spaces
+    ]
+    .join("\n");
+
+    let buffer = cx.entity(|cx| Buffer::new(0, cx.entity_id().as_u64(), text));
+
+    // Spawn a task to format the buffer's whitespace.
+    // Pause so that the foratting task starts running.
+    let format = buffer.update(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx));
+    smol::future::yield_now().await;
+
+    // Edit the buffer while the normalization task is running.
+    let version_before_edit = buffer.update(cx, |buffer, _| buffer.version());
+    buffer.update(cx, |buffer, cx| {
+        buffer.edit(
+            [
+                (Point::new(0, 1)..Point::new(0, 1), "EE"),
+                (Point::new(3, 5)..Point::new(3, 5), "EEE"),
+            ],
+            None,
+            cx,
+        );
+    });
+
+    let format_diff = format.await;
+    buffer.update(cx, |buffer, cx| {
+        let version_before_format = format_diff.base_version.clone();
+        buffer.apply_diff(format_diff, cx);
+
+        // The outcome depends on the order of concurrent taks.
+        //
+        // If the edit occurred while searching for trailing whitespace ranges,
+        // then the trailing whitespace region touched by the edit is left intact.
+        if version_before_format == version_before_edit {
+            assert_eq!(
+                buffer.text(),
+                [
+                    "zEEero",      //
+                    "one",         //
+                    "two",         //
+                    "threeEEE   ", //
+                    "four",        //
+                    "five",        //
+                ]
+                .join("\n")
+            );
+        }
+        // Otherwise, all trailing whitespace is removed.
+        else {
+            assert_eq!(
+                buffer.text(),
+                [
+                    "zEEero",   //
+                    "one",      //
+                    "two",      //
+                    "threeEEE", //
+                    "four",     //
+                    "five",     //
+                ]
+                .join("\n")
+            );
+        }
+    });
+}
+
+#[gpui2::test]
+async fn test_reparse(cx: &mut gpui2::TestAppContext) {
+    let text = "fn a() {}";
+    let buffer = cx.entity(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx)
+    });
+
+    // Wait for the initial text to parse
+    cx.executor().run_until_parked();
+    assert!(!buffer.update(cx, |buffer, _| buffer.is_parsing()));
+    assert_eq!(
+        get_tree_sexp(&buffer, cx),
+        concat!(
+            "(source_file (function_item name: (identifier) ",
+            "parameters: (parameters) ",
+            "body: (block)))"
+        )
+    );
+
+    buffer.update(cx, |buffer, _| {
+        buffer.set_sync_parse_timeout(Duration::ZERO)
+    });
+
+    // Perform some edits (add parameter and variable reference)
+    // Parsing doesn't begin until the transaction is complete
+    buffer.update(cx, |buf, cx| {
+        buf.start_transaction();
+
+        let offset = buf.text().find(')').unwrap();
+        buf.edit([(offset..offset, "b: C")], None, cx);
+        assert!(!buf.is_parsing());
+
+        let offset = buf.text().find('}').unwrap();
+        buf.edit([(offset..offset, " d; ")], None, cx);
+        assert!(!buf.is_parsing());
+
+        buf.end_transaction(cx);
+        assert_eq!(buf.text(), "fn a(b: C) { d; }");
+        assert!(buf.is_parsing());
+    });
+    cx.executor().run_until_parked();
+    assert!(!buffer.update(cx, |buffer, _| buffer.is_parsing()));
+    assert_eq!(
+        get_tree_sexp(&buffer, cx),
+        concat!(
+            "(source_file (function_item name: (identifier) ",
+            "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
+            "body: (block (expression_statement (identifier)))))"
+        )
+    );
+
+    // Perform a series of edits without waiting for the current parse to complete:
+    // * turn identifier into a field expression
+    // * turn field expression into a method call
+    // * add a turbofish to the method call
+    buffer.update(cx, |buf, cx| {
+        let offset = buf.text().find(';').unwrap();
+        buf.edit([(offset..offset, ".e")], None, cx);
+        assert_eq!(buf.text(), "fn a(b: C) { d.e; }");
+        assert!(buf.is_parsing());
+    });
+    buffer.update(cx, |buf, cx| {
+        let offset = buf.text().find(';').unwrap();
+        buf.edit([(offset..offset, "(f)")], None, cx);
+        assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }");
+        assert!(buf.is_parsing());
+    });
+    buffer.update(cx, |buf, cx| {
+        let offset = buf.text().find("(f)").unwrap();
+        buf.edit([(offset..offset, "::<G>")], None, cx);
+        assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
+        assert!(buf.is_parsing());
+    });
+    cx.executor().run_until_parked();
+    assert_eq!(
+        get_tree_sexp(&buffer, cx),
+        concat!(
+            "(source_file (function_item name: (identifier) ",
+            "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
+            "body: (block (expression_statement (call_expression ",
+            "function: (generic_function ",
+            "function: (field_expression value: (identifier) field: (field_identifier)) ",
+            "type_arguments: (type_arguments (type_identifier))) ",
+            "arguments: (arguments (identifier)))))))",
+        )
+    );
+
+    buffer.update(cx, |buf, cx| {
+        buf.undo(cx);
+        buf.undo(cx);
+        buf.undo(cx);
+        buf.undo(cx);
+        assert_eq!(buf.text(), "fn a() {}");
+        assert!(buf.is_parsing());
+    });
+
+    cx.executor().run_until_parked();
+    assert_eq!(
+        get_tree_sexp(&buffer, cx),
+        concat!(
+            "(source_file (function_item name: (identifier) ",
+            "parameters: (parameters) ",
+            "body: (block)))"
+        )
+    );
+
+    buffer.update(cx, |buf, cx| {
+        buf.redo(cx);
+        buf.redo(cx);
+        buf.redo(cx);
+        buf.redo(cx);
+        assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
+        assert!(buf.is_parsing());
+    });
+    cx.executor().run_until_parked();
+    assert_eq!(
+        get_tree_sexp(&buffer, cx),
+        concat!(
+            "(source_file (function_item name: (identifier) ",
+            "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
+            "body: (block (expression_statement (call_expression ",
+            "function: (generic_function ",
+            "function: (field_expression value: (identifier) field: (field_identifier)) ",
+            "type_arguments: (type_arguments (type_identifier))) ",
+            "arguments: (arguments (identifier)))))))",
+        )
+    );
+}
+
+#[gpui2::test]
+async fn test_resetting_language(cx: &mut gpui2::TestAppContext) {
+    let buffer = cx.entity(|cx| {
+        let mut buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), "{}").with_language(Arc::new(rust_lang()), cx);
+        buffer.set_sync_parse_timeout(Duration::ZERO);
+        buffer
+    });
+
+    // Wait for the initial text to parse
+    cx.executor().run_until_parked();
+    assert_eq!(
+        get_tree_sexp(&buffer, cx),
+        "(source_file (expression_statement (block)))"
+    );
+
+    buffer.update(cx, |buffer, cx| {
+        buffer.set_language(Some(Arc::new(json_lang())), cx)
+    });
+    cx.executor().run_until_parked();
+    assert_eq!(get_tree_sexp(&buffer, cx), "(document (object))");
+}
+
+#[gpui2::test]
+async fn test_outline(cx: &mut gpui2::TestAppContext) {
+    let text = r#"
+        struct Person {
+            name: String,
+            age: usize,
+        }
+
+        mod module {
+            enum LoginState {
+                LoggedOut,
+                LoggingOn,
+                LoggedIn {
+                    person: Person,
+                    time: Instant,
+                }
+            }
+        }
+
+        impl Eq for Person {}
+
+        impl Drop for Person {
+            fn drop(&mut self) {
+                println!("bye");
+            }
+        }
+    "#
+    .unindent();
+
+    let buffer = cx.entity(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx)
+    });
+    let outline = buffer
+        .update(cx, |buffer, _| buffer.snapshot().outline(None))
+        .unwrap();
+
+    assert_eq!(
+        outline
+            .items
+            .iter()
+            .map(|item| (item.text.as_str(), item.depth))
+            .collect::<Vec<_>>(),
+        &[
+            ("struct Person", 0),
+            ("name", 1),
+            ("age", 1),
+            ("mod module", 0),
+            ("enum LoginState", 1),
+            ("LoggedOut", 2),
+            ("LoggingOn", 2),
+            ("LoggedIn", 2),
+            ("person", 3),
+            ("time", 3),
+            ("impl Eq for Person", 0),
+            ("impl Drop for Person", 0),
+            ("fn drop", 1),
+        ]
+    );
+
+    // Without space, we only match on names
+    assert_eq!(
+        search(&outline, "oon", cx).await,
+        &[
+            ("mod module", vec![]),                    // included as the parent of a match
+            ("enum LoginState", vec![]),               // included as the parent of a match
+            ("LoggingOn", vec![1, 7, 8]),              // matches
+            ("impl Drop for Person", vec![7, 18, 19]), // matches in two disjoint names
+        ]
+    );
+
+    assert_eq!(
+        search(&outline, "dp p", cx).await,
+        &[
+            ("impl Drop for Person", vec![5, 8, 9, 14]),
+            ("fn drop", vec![]),
+        ]
+    );
+    assert_eq!(
+        search(&outline, "dpn", cx).await,
+        &[("impl Drop for Person", vec![5, 14, 19])]
+    );
+    assert_eq!(
+        search(&outline, "impl ", cx).await,
+        &[
+            ("impl Eq for Person", vec![0, 1, 2, 3, 4]),
+            ("impl Drop for Person", vec![0, 1, 2, 3, 4]),
+            ("fn drop", vec![]),
+        ]
+    );
+
+    async fn search<'a>(
+        outline: &'a Outline<Anchor>,
+        query: &'a str,
+        cx: &'a gpui2::TestAppContext,
+    ) -> Vec<(&'a str, Vec<usize>)> {
+        let matches = cx
+            .update(|cx| outline.search(query, cx.executor().clone()))
+            .await;
+        matches
+            .into_iter()
+            .map(|mat| (outline.items[mat.candidate_id].text.as_str(), mat.positions))
+            .collect::<Vec<_>>()
+    }
+}
+
+#[gpui2::test]
+async fn test_outline_nodes_with_newlines(cx: &mut gpui2::TestAppContext) {
+    let text = r#"
+        impl A for B<
+            C
+        > {
+        };
+    "#
+    .unindent();
+
+    let buffer = cx.entity(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx)
+    });
+    let outline = buffer
+        .update(cx, |buffer, _| buffer.snapshot().outline(None))
+        .unwrap();
+
+    assert_eq!(
+        outline
+            .items
+            .iter()
+            .map(|item| (item.text.as_str(), item.depth))
+            .collect::<Vec<_>>(),
+        &[("impl A for B<", 0)]
+    );
+}
+
+#[gpui2::test]
+async fn test_outline_with_extra_context(cx: &mut gpui2::TestAppContext) {
+    let language = javascript_lang()
+        .with_outline_query(
+            r#"
+            (function_declaration
+                "function" @context
+                name: (_) @name
+                parameters: (formal_parameters
+                    "(" @context.extra
+                    ")" @context.extra)) @item
+            "#,
+        )
+        .unwrap();
+
+    let text = r#"
+        function a() {}
+        function b(c) {}
+    "#
+    .unindent();
+
+    let buffer = cx.entity(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(language), cx)
+    });
+    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
+
+    // extra context nodes are included in the outline.
+    let outline = snapshot.outline(None).unwrap();
+    assert_eq!(
+        outline
+            .items
+            .iter()
+            .map(|item| (item.text.as_str(), item.depth))
+            .collect::<Vec<_>>(),
+        &[("function a()", 0), ("function b( )", 0),]
+    );
+
+    // extra context nodes do not appear in breadcrumbs.
+    let symbols = snapshot.symbols_containing(3, None).unwrap();
+    assert_eq!(
+        symbols
+            .iter()
+            .map(|item| (item.text.as_str(), item.depth))
+            .collect::<Vec<_>>(),
+        &[("function a", 0)]
+    );
+}
+
+#[gpui2::test]
+async fn test_symbols_containing(cx: &mut gpui2::TestAppContext) {
+    let text = r#"
+        impl Person {
+            fn one() {
+                1
+            }
+
+            fn two() {
+                2
+            }fn three() {
+                3
+            }
+        }
+    "#
+    .unindent();
+
+    let buffer = cx.entity(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx)
+    });
+    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
+
+    // point is at the start of an item
+    assert_eq!(
+        symbols_containing(Point::new(1, 4), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
+        ]
+    );
+
+    // point is in the middle of an item
+    assert_eq!(
+        symbols_containing(Point::new(2, 8), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
+        ]
+    );
+
+    // point is at the end of an item
+    assert_eq!(
+        symbols_containing(Point::new(3, 5), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
+        ]
+    );
+
+    // point is in between two adjacent items
+    assert_eq!(
+        symbols_containing(Point::new(7, 5), &snapshot),
+        vec![
+            (
+                "impl Person".to_string(),
+                Point::new(0, 0)..Point::new(10, 1)
+            ),
+            ("fn two".to_string(), Point::new(5, 4)..Point::new(7, 5))
+        ]
+    );
+
+    fn symbols_containing(
+        position: Point,
+        snapshot: &BufferSnapshot,
+    ) -> Vec<(String, Range<Point>)> {
+        snapshot
+            .symbols_containing(position, None)
+            .unwrap()
+            .into_iter()
+            .map(|item| {
+                (
+                    item.text,
+                    item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot),
+                )
+            })
+            .collect()
+    }
+}
+
+#[gpui2::test]
+fn test_enclosing_bracket_ranges(cx: &mut AppContext) {
+    let mut assert = |selection_text, range_markers| {
+        assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
+    };
+
+    assert(
+        indoc! {"
+            mod x {
+                moˇd y {
+
+                }
+            }
+            let foo = 1;"},
+        vec![indoc! {"
+            mod x «{»
+                mod y {
+
+                }
+            «}»
+            let foo = 1;"}],
+    );
+
+    assert(
+        indoc! {"
+            mod x {
+                mod y ˇ{
+
+                }
+            }
+            let foo = 1;"},
+        vec![
+            indoc! {"
+                mod x «{»
+                    mod y {
+
+                    }
+                «}»
+                let foo = 1;"},
+            indoc! {"
+                mod x {
+                    mod y «{»
+
+                    «}»
+                }
+                let foo = 1;"},
+        ],
+    );
+
+    assert(
+        indoc! {"
+            mod x {
+                mod y {
+
+                }ˇ
+            }
+            let foo = 1;"},
+        vec![
+            indoc! {"
+                mod x «{»
+                    mod y {
+
+                    }
+                «}»
+                let foo = 1;"},
+            indoc! {"
+                mod x {
+                    mod y «{»
+
+                    «}»
+                }
+                let foo = 1;"},
+        ],
+    );
+
+    assert(
+        indoc! {"
+            mod x {
+                mod y {
+
+                }
+            ˇ}
+            let foo = 1;"},
+        vec![indoc! {"
+            mod x «{»
+                mod y {
+
+                }
+            «}»
+            let foo = 1;"}],
+    );
+
+    assert(
+        indoc! {"
+            mod x {
+                mod y {
+
+                }
+            }
+            let fˇoo = 1;"},
+        vec![],
+    );
+
+    // Regression test: avoid crash when querying at the end of the buffer.
+    assert(
+        indoc! {"
+            mod x {
+                mod y {
+
+                }
+            }
+            let foo = 1;ˇ"},
+        vec![],
+    );
+}
+
+#[gpui2::test]
+fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: &mut AppContext) {
+    let mut assert = |selection_text, bracket_pair_texts| {
+        assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx)
+    };
+
+    assert(
+        indoc! {"
+        for (const a in b)ˇ {
+            // a comment that's longer than the for-loop header
+        }"},
+        vec![indoc! {"
+        for «(»const a in b«)» {
+            // a comment that's longer than the for-loop header
+        }"}],
+    );
+
+    // Regression test: even though the parent node of the parentheses (the for loop) does
+    // intersect the given range, the parentheses themselves do not contain the range, so
+    // they should not be returned. Only the curly braces contain the range.
+    assert(
+        indoc! {"
+        for (const a in b) {ˇ
+            // a comment that's longer than the for-loop header
+        }"},
+        vec![indoc! {"
+        for (const a in b) «{»
+            // a comment that's longer than the for-loop header
+        «}»"}],
+    );
+}
+
+#[gpui2::test]
+fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
+    cx.entity(|cx| {
+        let text = "fn a() { b(|c| {}) }";
+        let buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
+        let snapshot = buffer.snapshot();
+
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")),
+            Some(range_of(text, "|"))
+        );
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(range_of(text, "|")),
+            Some(range_of(text, "|c|"))
+        );
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(range_of(text, "|c|")),
+            Some(range_of(text, "|c| {}"))
+        );
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")),
+            Some(range_of(text, "(|c| {})"))
+        );
+
+        buffer
+    });
+
+    fn empty_range_at(text: &str, part: &str) -> Range<usize> {
+        let start = text.find(part).unwrap();
+        start..start
+    }
+
+    fn range_of(text: &str, part: &str) -> Range<usize> {
+        let start = text.find(part).unwrap();
+        start..start + part.len()
+    }
+}
+
+#[gpui2::test]
+fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let text = "fn a() {}";
+        let mut buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
+
+        buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
+        assert_eq!(buffer.text(), "fn a() {\n    \n}");
+
+        buffer.edit(
+            [(Point::new(1, 4)..Point::new(1, 4), "b()\n")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(buffer.text(), "fn a() {\n    b()\n    \n}");
+
+        // Create a field expression on a new line, causing that line
+        // to be indented.
+        buffer.edit(
+            [(Point::new(2, 4)..Point::new(2, 4), ".c")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(buffer.text(), "fn a() {\n    b()\n        .c\n}");
+
+        // Remove the dot so that the line is no longer a field expression,
+        // causing the line to be outdented.
+        buffer.edit(
+            [(Point::new(2, 8)..Point::new(2, 9), "")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(buffer.text(), "fn a() {\n    b()\n    c\n}");
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
+    init_settings(cx, |settings| {
+        settings.defaults.hard_tabs = Some(true);
+    });
+
+    cx.entity(|cx| {
+        let text = "fn a() {}";
+        let mut buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
+
+        buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
+        assert_eq!(buffer.text(), "fn a() {\n\t\n}");
+
+        buffer.edit(
+            [(Point::new(1, 1)..Point::new(1, 1), "b()\n")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}");
+
+        // Create a field expression on a new line, causing that line
+        // to be indented.
+        buffer.edit(
+            [(Point::new(2, 1)..Point::new(2, 1), ".c")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}");
+
+        // Remove the dot so that the line is no longer a field expression,
+        // causing the line to be outdented.
+        buffer.edit(
+            [(Point::new(2, 2)..Point::new(2, 3), "")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}");
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let entity_id = cx.entity_id();
+        let mut buffer = Buffer::new(
+            0,
+            entity_id.as_u64(),
+            "
+            fn a() {
+            c;
+            d;
+            }
+            "
+            .unindent(),
+        )
+        .with_language(Arc::new(rust_lang()), cx);
+
+        // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
+        // their indentation is not adjusted.
+        buffer.edit_via_marked_text(
+            &"
+            fn a() {
+            c«()»;
+            d«()»;
+            }
+            "
+            .unindent(),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+            fn a() {
+            c();
+            d();
+            }
+            "
+            .unindent()
+        );
+
+        // When appending new content after these lines, the indentation is based on the
+        // preceding lines' actual indentation.
+        buffer.edit_via_marked_text(
+            &"
+            fn a() {
+            c«
+            .f
+            .g()»;
+            d«
+            .f
+            .g()»;
+            }
+            "
+            .unindent(),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+
+        assert_eq!(
+            buffer.text(),
+            "
+            fn a() {
+            c
+                .f
+                .g();
+            d
+                .f
+                .g();
+            }
+            "
+            .unindent()
+        );
+        buffer
+    });
+
+    cx.entity(|cx| {
+        eprintln!("second buffer: {:?}", cx.entity_id());
+
+        let mut buffer = Buffer::new(
+            0,
+            cx.entity_id().as_u64(),
+            "
+            fn a() {
+                b();
+                |
+            "
+            .replace("|", "") // marker to preserve trailing whitespace
+            .unindent(),
+        )
+        .with_language(Arc::new(rust_lang()), cx);
+
+        // Insert a closing brace. It is outdented.
+        buffer.edit_via_marked_text(
+            &"
+            fn a() {
+                b();
+                «}»
+            "
+            .unindent(),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+            fn a() {
+                b();
+            }
+            "
+            .unindent()
+        );
+
+        // Manually edit the leading whitespace. The edit is preserved.
+        buffer.edit_via_marked_text(
+            &"
+            fn a() {
+                b();
+            «    »}
+            "
+            .unindent(),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+            fn a() {
+                b();
+                }
+            "
+            .unindent()
+        );
+        buffer
+    });
+
+    eprintln!("DONE");
+}
+
+#[gpui2::test]
+fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let mut buffer = Buffer::new(
+            0,
+            cx.entity_id().as_u64(),
+            "
+            fn a() {
+                i
+            }
+            "
+            .unindent(),
+        )
+        .with_language(Arc::new(rust_lang()), cx);
+
+        // Regression test: line does not get outdented due to syntax error
+        buffer.edit_via_marked_text(
+            &"
+            fn a() {
+                i«f let Some(x) = y»
+            }
+            "
+            .unindent(),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+            fn a() {
+                if let Some(x) = y
+            }
+            "
+            .unindent()
+        );
+
+        buffer.edit_via_marked_text(
+            &"
+            fn a() {
+                if let Some(x) = y« {»
+            }
+            "
+            .unindent(),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+            fn a() {
+                if let Some(x) = y {
+            }
+            "
+            .unindent()
+        );
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let mut buffer = Buffer::new(
+            0,
+            cx.entity_id().as_u64(),
+            "
+            fn a() {}
+            "
+            .unindent(),
+        )
+        .with_language(Arc::new(rust_lang()), cx);
+
+        buffer.edit_via_marked_text(
+            &"
+            fn a(«
+            b») {}
+            "
+            .unindent(),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+            fn a(
+                b) {}
+            "
+            .unindent()
+        );
+
+        // The indentation suggestion changed because `@end` node (a close paren)
+        // is now at the beginning of the line.
+        buffer.edit_via_marked_text(
+            &"
+            fn a(
+                ˇ) {}
+            "
+            .unindent(),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+                fn a(
+                ) {}
+            "
+            .unindent()
+        );
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let text = "a\nb";
+        let mut buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
+        buffer.edit(
+            [(0..1, "\n"), (2..3, "\n")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(buffer.text(), "\n\n\n");
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let text = "
+            const a: usize = 1;
+            fn b() {
+                if c {
+                    let d = 2;
+                }
+            }
+        "
+        .unindent();
+
+        let mut buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
+        buffer.edit(
+            [(Point::new(3, 0)..Point::new(3, 0), "e(\n    f()\n);\n")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+                const a: usize = 1;
+                fn b() {
+                    if c {
+                        e(
+                            f()
+                        );
+                        let d = 2;
+                    }
+                }
+            "
+            .unindent()
+        );
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_block_mode(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let text = r#"
+            fn a() {
+                b();
+            }
+        "#
+        .unindent();
+        let mut buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
+
+        // When this text was copied, both of the quotation marks were at the same
+        // indent level, but the indentation of the first line was not included in
+        // the copied text. This information is retained in the
+        // 'original_indent_columns' vector.
+        let original_indent_columns = vec![4];
+        let inserted_text = r#"
+            "
+                  c
+                    d
+                      e
+                "
+        "#
+        .unindent();
+
+        // Insert the block at column zero. The entire block is indented
+        // so that the first line matches the previous line's indentation.
+        buffer.edit(
+            [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
+            Some(AutoindentMode::Block {
+                original_indent_columns: original_indent_columns.clone(),
+            }),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            r#"
+            fn a() {
+                b();
+                "
+                  c
+                    d
+                      e
+                "
+            }
+            "#
+            .unindent()
+        );
+
+        // Grouping is disabled in tests, so we need 2 undos
+        buffer.undo(cx); // Undo the auto-indent
+        buffer.undo(cx); // Undo the original edit
+
+        // Insert the block at a deeper indent level. The entire block is outdented.
+        buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "        ")], None, cx);
+        buffer.edit(
+            [(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
+            Some(AutoindentMode::Block {
+                original_indent_columns: original_indent_columns.clone(),
+            }),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            r#"
+            fn a() {
+                b();
+                "
+                  c
+                    d
+                      e
+                "
+            }
+            "#
+            .unindent()
+        );
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let text = r#"
+            fn a() {
+                if b() {
+
+                }
+            }
+        "#
+        .unindent();
+        let mut buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
+
+        // The original indent columns are not known, so this text is
+        // auto-indented in a block as if the first line was copied in
+        // its entirety.
+        let original_indent_columns = Vec::new();
+        let inserted_text = "    c\n        .d()\n        .e();";
+
+        // Insert the block at column zero. The entire block is indented
+        // so that the first line matches the previous line's indentation.
+        buffer.edit(
+            [(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
+            Some(AutoindentMode::Block {
+                original_indent_columns: original_indent_columns.clone(),
+            }),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            r#"
+            fn a() {
+                if b() {
+                    c
+                        .d()
+                        .e();
+                }
+            }
+            "#
+            .unindent()
+        );
+
+        // Grouping is disabled in tests, so we need 2 undos
+        buffer.undo(cx); // Undo the auto-indent
+        buffer.undo(cx); // Undo the original edit
+
+        // Insert the block at a deeper indent level. The entire block is outdented.
+        buffer.edit(
+            [(Point::new(2, 0)..Point::new(2, 0), " ".repeat(12))],
+            None,
+            cx,
+        );
+        buffer.edit(
+            [(Point::new(2, 12)..Point::new(2, 12), inserted_text)],
+            Some(AutoindentMode::Block {
+                original_indent_columns: Vec::new(),
+            }),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            r#"
+            fn a() {
+                if b() {
+                    c
+                        .d()
+                        .e();
+                }
+            }
+            "#
+            .unindent()
+        );
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let text = "
+            * one
+                - a
+                - b
+            * two
+        "
+        .unindent();
+
+        let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), text).with_language(
+            Arc::new(Language::new(
+                LanguageConfig {
+                    name: "Markdown".into(),
+                    auto_indent_using_last_non_empty_line: false,
+                    ..Default::default()
+                },
+                Some(tree_sitter_json::language()),
+            )),
+            cx,
+        );
+        buffer.edit(
+            [(Point::new(3, 0)..Point::new(3, 0), "\n")],
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+            * one
+                - a
+                - b
+
+            * two
+            "
+            .unindent()
+        );
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
+    init_settings(cx, |settings| {
+        settings.languages.extend([
+            (
+                "HTML".into(),
+                LanguageSettingsContent {
+                    tab_size: Some(2.try_into().unwrap()),
+                    ..Default::default()
+                },
+            ),
+            (
+                "JavaScript".into(),
+                LanguageSettingsContent {
+                    tab_size: Some(8.try_into().unwrap()),
+                    ..Default::default()
+                },
+            ),
+        ])
+    });
+
+    let html_language = Arc::new(html_lang());
+
+    let javascript_language = Arc::new(javascript_lang());
+
+    let language_registry = Arc::new(LanguageRegistry::test());
+    language_registry.add(html_language.clone());
+    language_registry.add(javascript_language.clone());
+
+    cx.entity(|cx| {
+        let (text, ranges) = marked_text_ranges(
+            &"
+                <div>ˇ
+                </div>
+                <script>
+                    init({ˇ
+                    })
+                </script>
+                <span>ˇ
+                </span>
+            "
+            .unindent(),
+            false,
+        );
+
+        let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), text);
+        buffer.set_language_registry(language_registry);
+        buffer.set_language(Some(html_language), cx);
+        buffer.edit(
+            ranges.into_iter().map(|range| (range, "\na")),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+                <div>
+                  a
+                </div>
+                <script>
+                    init({
+                            a
+                    })
+                </script>
+                <span>
+                  a
+                </span>
+            "
+            .unindent()
+        );
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
+    init_settings(cx, |settings| {
+        settings.defaults.tab_size = Some(2.try_into().unwrap());
+    });
+
+    cx.entity(|cx| {
+        let mut buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), "").with_language(Arc::new(ruby_lang()), cx);
+
+        let text = r#"
+            class C
+            def a(b, c)
+            puts b
+            puts c
+            rescue
+            puts "errored"
+            exit 1
+            end
+            end
+        "#
+        .unindent();
+
+        buffer.edit([(0..0, text)], Some(AutoindentMode::EachLine), cx);
+
+        assert_eq!(
+            buffer.text(),
+            r#"
+                class C
+                  def a(b, c)
+                    puts b
+                    puts c
+                  rescue
+                    puts "errored"
+                    exit 1
+                  end
+                end
+            "#
+            .unindent()
+        );
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let language = Language::new(
+            LanguageConfig {
+                name: "JavaScript".into(),
+                line_comment: Some("// ".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()],
+                    ],
+                },
+                overrides: [(
+                    "element".into(),
+                    LanguageConfigOverride {
+                        line_comment: Override::Remove { remove: true },
+                        block_comment: Override::Set(("{/*".into(), "*/}".into())),
+                        ..Default::default()
+                    },
+                )]
+                .into_iter()
+                .collect(),
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::language_tsx()),
+        )
+        .with_override_query(
+            r#"
+                (jsx_element) @element
+                (string) @string
+            "#,
+        )
+        .unwrap();
+
+        let text = r#"a["b"] = <C d="e"></C>;"#;
+
+        let buffer =
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(language), cx);
+        let snapshot = buffer.snapshot();
+
+        let config = snapshot.language_scope_at(0).unwrap();
+        assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
+        // Both bracket pairs are enabled
+        assert_eq!(
+            config.brackets().map(|e| e.1).collect::<Vec<_>>(),
+            &[true, true]
+        );
+
+        let string_config = snapshot.language_scope_at(3).unwrap();
+        assert_eq!(string_config.line_comment_prefix().unwrap().as_ref(), "// ");
+        // Second bracket pair is disabled
+        assert_eq!(
+            string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
+            &[true, false]
+        );
+
+        let element_config = snapshot.language_scope_at(10).unwrap();
+        assert_eq!(element_config.line_comment_prefix(), None);
+        assert_eq!(
+            element_config.block_comment_delimiters(),
+            Some((&"{/*".into(), &"*/}".into()))
+        );
+        // Both bracket pairs are enabled
+        assert_eq!(
+            element_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
+            &[true, true]
+        );
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_language_scope_at_with_rust(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".into(),
+                            end: "}".into(),
+                            close: true,
+                            newline: false,
+                        },
+                        BracketPair {
+                            start: "'".into(),
+                            end: "'".into(),
+                            close: true,
+                            newline: false,
+                        },
+                    ],
+                    disabled_scopes_by_bracket_ix: vec![
+                        Vec::new(), //
+                        vec!["string".into()],
+                    ],
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_override_query(
+            r#"
+                (string_literal) @string
+            "#,
+        )
+        .unwrap();
+
+        let text = r#"
+            const S: &'static str = "hello";
+        "#
+        .unindent();
+
+        let buffer = Buffer::new(0, cx.entity_id().as_u64(), text.clone())
+            .with_language(Arc::new(language), cx);
+        let snapshot = buffer.snapshot();
+
+        // By default, all brackets are enabled
+        let config = snapshot.language_scope_at(0).unwrap();
+        assert_eq!(
+            config.brackets().map(|e| e.1).collect::<Vec<_>>(),
+            &[true, true]
+        );
+
+        // Within a string, the quotation brackets are disabled.
+        let string_config = snapshot
+            .language_scope_at(text.find("ello").unwrap())
+            .unwrap();
+        assert_eq!(
+            string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
+            &[true, false]
+        );
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
+    init_settings(cx, |_| {});
+
+    cx.entity(|cx| {
+        let text = r#"
+            <ol>
+            <% people.each do |person| %>
+                <li>
+                    <%= person.name %>
+                </li>
+            <% end %>
+            </ol>
+        "#
+        .unindent();
+
+        let language_registry = Arc::new(LanguageRegistry::test());
+        language_registry.add(Arc::new(ruby_lang()));
+        language_registry.add(Arc::new(html_lang()));
+        language_registry.add(Arc::new(erb_lang()));
+
+        let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), text);
+        buffer.set_language_registry(language_registry.clone());
+        buffer.set_language(
+            language_registry
+                .language_for_name("ERB")
+                .now_or_never()
+                .unwrap()
+                .ok(),
+            cx,
+        );
+
+        let snapshot = buffer.snapshot();
+        let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap();
+        assert_eq!(html_config.line_comment_prefix(), None);
+        assert_eq!(
+            html_config.block_comment_delimiters(),
+            Some((&"<!--".into(), &"-->".into()))
+        );
+
+        let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap();
+        assert_eq!(ruby_config.line_comment_prefix().unwrap().as_ref(), "# ");
+        assert_eq!(ruby_config.block_comment_delimiters(), None);
+
+        buffer
+    });
+}
+
+#[gpui2::test]
+fn test_serialization(cx: &mut gpui2::AppContext) {
+    let mut now = Instant::now();
+
+    let buffer1 = cx.entity(|cx| {
+        let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), "abc");
+        buffer.edit([(3..3, "D")], None, cx);
+
+        now += Duration::from_secs(1);
+        buffer.start_transaction_at(now);
+        buffer.edit([(4..4, "E")], None, cx);
+        buffer.end_transaction_at(now, cx);
+        assert_eq!(buffer.text(), "abcDE");
+
+        buffer.undo(cx);
+        assert_eq!(buffer.text(), "abcD");
+
+        buffer.edit([(4..4, "F")], None, cx);
+        assert_eq!(buffer.text(), "abcDF");
+        buffer
+    });
+    assert_eq!(buffer1.read(cx).text(), "abcDF");
+
+    let state = buffer1.read(cx).to_proto();
+    let ops = cx
+        .executor()
+        .block(buffer1.read(cx).serialize_ops(None, cx));
+    let buffer2 = cx.entity(|cx| {
+        let mut buffer = Buffer::from_proto(1, state, None).unwrap();
+        buffer
+            .apply_ops(
+                ops.into_iter()
+                    .map(|op| proto::deserialize_operation(op).unwrap()),
+                cx,
+            )
+            .unwrap();
+        buffer
+    });
+    assert_eq!(buffer2.read(cx).text(), "abcDF");
+}
+
+#[gpui2::test(iterations = 100)]
+fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
+    let min_peers = env::var("MIN_PEERS")
+        .map(|i| i.parse().expect("invalid `MIN_PEERS` variable"))
+        .unwrap_or(1);
+    let max_peers = env::var("MAX_PEERS")
+        .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
+        .unwrap_or(5);
+    let operations = env::var("OPERATIONS")
+        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+        .unwrap_or(10);
+
+    let base_text_len = rng.gen_range(0..10);
+    let base_text = RandomCharIter::new(&mut rng)
+        .take(base_text_len)
+        .collect::<String>();
+    let mut replica_ids = Vec::new();
+    let mut buffers = Vec::new();
+    let network = Arc::new(Mutex::new(Network::new(rng.clone())));
+    let base_buffer = cx.entity(|cx| Buffer::new(0, cx.entity_id().as_u64(), base_text.as_str()));
+
+    for i in 0..rng.gen_range(min_peers..=max_peers) {
+        let buffer = cx.entity(|cx| {
+            let state = base_buffer.read(cx).to_proto();
+            let ops = cx
+                .executor()
+                .block(base_buffer.read(cx).serialize_ops(None, cx));
+            let mut buffer = Buffer::from_proto(i as ReplicaId, state, None).unwrap();
+            buffer
+                .apply_ops(
+                    ops.into_iter()
+                        .map(|op| proto::deserialize_operation(op).unwrap()),
+                    cx,
+                )
+                .unwrap();
+            buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
+            let network = network.clone();
+            cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
+                if let Event::Operation(op) = event {
+                    network
+                        .lock()
+                        .broadcast(buffer.replica_id(), vec![proto::serialize_operation(op)]);
+                }
+            })
+            .detach();
+            buffer
+        });
+        buffers.push(buffer);
+        replica_ids.push(i as ReplicaId);
+        network.lock().add_peer(i as ReplicaId);
+        log::info!("Adding initial peer with replica id {}", i);
+    }
+
+    log::info!("initial text: {:?}", base_text);
+
+    let mut now = Instant::now();
+    let mut mutation_count = operations;
+    let mut next_diagnostic_id = 0;
+    let mut active_selections = BTreeMap::default();
+    loop {
+        let replica_index = rng.gen_range(0..replica_ids.len());
+        let replica_id = replica_ids[replica_index];
+        let buffer = &mut buffers[replica_index];
+        let mut new_buffer = None;
+        match rng.gen_range(0..100) {
+            0..=29 if mutation_count != 0 => {
+                buffer.update(cx, |buffer, cx| {
+                    buffer.start_transaction_at(now);
+                    buffer.randomly_edit(&mut rng, 5, cx);
+                    buffer.end_transaction_at(now, cx);
+                    log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text());
+                });
+                mutation_count -= 1;
+            }
+            30..=39 if mutation_count != 0 => {
+                buffer.update(cx, |buffer, cx| {
+                    if rng.gen_bool(0.2) {
+                        log::info!("peer {} clearing active selections", replica_id);
+                        active_selections.remove(&replica_id);
+                        buffer.remove_active_selections(cx);
+                    } else {
+                        let mut selections = Vec::new();
+                        for id in 0..rng.gen_range(1..=5) {
+                            let range = buffer.random_byte_range(0, &mut rng);
+                            selections.push(Selection {
+                                id,
+                                start: buffer.anchor_before(range.start),
+                                end: buffer.anchor_before(range.end),
+                                reversed: false,
+                                goal: SelectionGoal::None,
+                            });
+                        }
+                        let selections: Arc<[Selection<Anchor>]> = selections.into();
+                        log::info!(
+                            "peer {} setting active selections: {:?}",
+                            replica_id,
+                            selections
+                        );
+                        active_selections.insert(replica_id, selections.clone());
+                        buffer.set_active_selections(selections, false, Default::default(), cx);
+                    }
+                });
+                mutation_count -= 1;
+            }
+            40..=49 if mutation_count != 0 && replica_id == 0 => {
+                let entry_count = rng.gen_range(1..=5);
+                buffer.update(cx, |buffer, cx| {
+                    let diagnostics = DiagnosticSet::new(
+                        (0..entry_count).map(|_| {
+                            let range = buffer.random_byte_range(0, &mut rng);
+                            let range = range.to_point_utf16(buffer);
+                            let range = range.start..range.end;
+                            DiagnosticEntry {
+                                range,
+                                diagnostic: Diagnostic {
+                                    message: post_inc(&mut next_diagnostic_id).to_string(),
+                                    ..Default::default()
+                                },
+                            }
+                        }),
+                        buffer,
+                    );
+                    log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics);
+                    buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
+                });
+                mutation_count -= 1;
+            }
+            50..=59 if replica_ids.len() < max_peers => {
+                let old_buffer_state = buffer.read(cx).to_proto();
+                let old_buffer_ops = cx.executor().block(buffer.read(cx).serialize_ops(None, cx));
+                let new_replica_id = (0..=replica_ids.len() as ReplicaId)
+                    .filter(|replica_id| *replica_id != buffer.read(cx).replica_id())
+                    .choose(&mut rng)
+                    .unwrap();
+                log::info!(
+                    "Adding new replica {} (replicating from {})",
+                    new_replica_id,
+                    replica_id
+                );
+                new_buffer = Some(cx.entity(|cx| {
+                    let mut new_buffer =
+                        Buffer::from_proto(new_replica_id, old_buffer_state, None).unwrap();
+                    new_buffer
+                        .apply_ops(
+                            old_buffer_ops
+                                .into_iter()
+                                .map(|op| deserialize_operation(op).unwrap()),
+                            cx,
+                        )
+                        .unwrap();
+                    log::info!(
+                        "New replica {} text: {:?}",
+                        new_buffer.replica_id(),
+                        new_buffer.text()
+                    );
+                    new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
+                    let network = network.clone();
+                    cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
+                        if let Event::Operation(op) = event {
+                            network.lock().broadcast(
+                                buffer.replica_id(),
+                                vec![proto::serialize_operation(op)],
+                            );
+                        }
+                    })
+                    .detach();
+                    new_buffer
+                }));
+                network.lock().replicate(replica_id, new_replica_id);
+
+                if new_replica_id as usize == replica_ids.len() {
+                    replica_ids.push(new_replica_id);
+                } else {
+                    let new_buffer = new_buffer.take().unwrap();
+                    while network.lock().has_unreceived(new_replica_id) {
+                        let ops = network
+                            .lock()
+                            .receive(new_replica_id)
+                            .into_iter()
+                            .map(|op| proto::deserialize_operation(op).unwrap());
+                        if ops.len() > 0 {
+                            log::info!(
+                                "peer {} (version: {:?}) applying {} ops from the network. {:?}",
+                                new_replica_id,
+                                buffer.read(cx).version(),
+                                ops.len(),
+                                ops
+                            );
+                            new_buffer.update(cx, |new_buffer, cx| {
+                                new_buffer.apply_ops(ops, cx).unwrap();
+                            });
+                        }
+                    }
+                    buffers[new_replica_id as usize] = new_buffer;
+                }
+            }
+            60..=69 if mutation_count != 0 => {
+                buffer.update(cx, |buffer, cx| {
+                    buffer.randomly_undo_redo(&mut rng, cx);
+                    log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text());
+                });
+                mutation_count -= 1;
+            }
+            _ if network.lock().has_unreceived(replica_id) => {
+                let ops = network
+                    .lock()
+                    .receive(replica_id)
+                    .into_iter()
+                    .map(|op| proto::deserialize_operation(op).unwrap());
+                if ops.len() > 0 {
+                    log::info!(
+                        "peer {} (version: {:?}) applying {} ops from the network. {:?}",
+                        replica_id,
+                        buffer.read(cx).version(),
+                        ops.len(),
+                        ops
+                    );
+                    buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx).unwrap());
+                }
+            }
+            _ => {}
+        }
+
+        now += Duration::from_millis(rng.gen_range(0..=200));
+        buffers.extend(new_buffer);
+
+        for buffer in &buffers {
+            buffer.read(cx).check_invariants();
+        }
+
+        if mutation_count == 0 && network.lock().is_idle() {
+            break;
+        }
+    }
+
+    let first_buffer = buffers[0].read(cx).snapshot();
+    for buffer in &buffers[1..] {
+        let buffer = buffer.read(cx).snapshot();
+        assert_eq!(
+            buffer.version(),
+            first_buffer.version(),
+            "Replica {} version != Replica 0 version",
+            buffer.replica_id()
+        );
+        assert_eq!(
+            buffer.text(),
+            first_buffer.text(),
+            "Replica {} text != Replica 0 text",
+            buffer.replica_id()
+        );
+        assert_eq!(
+            buffer
+                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
+                .collect::<Vec<_>>(),
+            first_buffer
+                .diagnostics_in_range::<_, usize>(0..first_buffer.len(), false)
+                .collect::<Vec<_>>(),
+            "Replica {} diagnostics != Replica 0 diagnostics",
+            buffer.replica_id()
+        );
+    }
+
+    for buffer in &buffers {
+        let buffer = buffer.read(cx).snapshot();
+        let actual_remote_selections = buffer
+            .remote_selections_in_range(Anchor::MIN..Anchor::MAX)
+            .map(|(replica_id, _, _, selections)| (replica_id, selections.collect::<Vec<_>>()))
+            .collect::<Vec<_>>();
+        let expected_remote_selections = active_selections
+            .iter()
+            .filter(|(replica_id, _)| **replica_id != buffer.replica_id())
+            .map(|(replica_id, selections)| (*replica_id, selections.iter().collect::<Vec<_>>()))
+            .collect::<Vec<_>>();
+        assert_eq!(
+            actual_remote_selections,
+            expected_remote_selections,
+            "Replica {} remote selections != expected selections",
+            buffer.replica_id()
+        );
+    }
+}
+
+#[test]
+fn test_contiguous_ranges() {
+    assert_eq!(
+        contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
+        &[1..4, 5..7, 9..13]
+    );
+
+    // Respects the `max_len` parameter
+    assert_eq!(
+        contiguous_ranges(
+            [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
+            3
+        )
+        .collect::<Vec<_>>(),
+        &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
+    );
+}
+
+#[gpui2::test(iterations = 500)]
+fn test_trailing_whitespace_ranges(mut rng: StdRng) {
+    // Generate a random multi-line string containing
+    // some lines with trailing whitespace.
+    let mut text = String::new();
+    for _ in 0..rng.gen_range(0..16) {
+        for _ in 0..rng.gen_range(0..36) {
+            text.push(match rng.gen_range(0..10) {
+                0..=1 => ' ',
+                3 => '\t',
+                _ => rng.gen_range('a'..'z'),
+            });
+        }
+        text.push('\n');
+    }
+
+    match rng.gen_range(0..10) {
+        // sometimes remove the last newline
+        0..=1 => drop(text.pop()), //
+
+        // sometimes add extra newlines
+        2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))),
+        _ => {}
+    }
+
+    let rope = Rope::from(text.as_str());
+    let actual_ranges = trailing_whitespace_ranges(&rope);
+    let expected_ranges = TRAILING_WHITESPACE_REGEX
+        .find_iter(&text)
+        .map(|m| m.range())
+        .collect::<Vec<_>>();
+    assert_eq!(
+        actual_ranges,
+        expected_ranges,
+        "wrong ranges for text lines:\n{:?}",
+        text.split("\n").collect::<Vec<_>>()
+    );
+}
+
+fn ruby_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Ruby".into(),
+            path_suffixes: vec!["rb".to_string()],
+            line_comment: Some("# ".into()),
+            ..Default::default()
+        },
+        Some(tree_sitter_ruby::language()),
+    )
+    .with_indents_query(
+        r#"
+            (class "end" @end) @indent
+            (method "end" @end) @indent
+            (rescue) @outdent
+            (then) @indent
+        "#,
+    )
+    .unwrap()
+}
+
+fn html_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "HTML".into(),
+            block_comment: Some(("<!--".into(), "-->".into())),
+            ..Default::default()
+        },
+        Some(tree_sitter_html::language()),
+    )
+    .with_indents_query(
+        "
+        (element
+          (start_tag) @start
+          (end_tag)? @end) @indent
+        ",
+    )
+    .unwrap()
+    .with_injection_query(
+        r#"
+        (script_element
+            (raw_text) @content
+            (#set! "language" "javascript"))
+        "#,
+    )
+    .unwrap()
+}
+
+fn erb_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "ERB".into(),
+            path_suffixes: vec!["erb".to_string()],
+            block_comment: Some(("<%#".into(), "%>".into())),
+            ..Default::default()
+        },
+        Some(tree_sitter_embedded_template::language()),
+    )
+    .with_injection_query(
+        r#"
+            (
+                (code) @content
+                (#set! "language" "ruby")
+                (#set! "combined")
+            )
+
+            (
+                (content) @content
+                (#set! "language" "html")
+                (#set! "combined")
+            )
+        "#,
+    )
+    .unwrap()
+}
+
+fn rust_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    )
+    .with_indents_query(
+        r#"
+        (call_expression) @indent
+        (field_expression) @indent
+        (_ "(" ")" @end) @indent
+        (_ "{" "}" @end) @indent
+        "#,
+    )
+    .unwrap()
+    .with_brackets_query(
+        r#"
+        ("{" @open "}" @close)
+        "#,
+    )
+    .unwrap()
+    .with_outline_query(
+        r#"
+        (struct_item
+            "struct" @context
+            name: (_) @name) @item
+        (enum_item
+            "enum" @context
+            name: (_) @name) @item
+        (enum_variant
+            name: (_) @name) @item
+        (field_declaration
+            name: (_) @name) @item
+        (impl_item
+            "impl" @context
+            trait: (_)? @name
+            "for"? @context
+            type: (_) @name) @item
+        (function_item
+            "fn" @context
+            name: (_) @name) @item
+        (mod_item
+            "mod" @context
+            name: (_) @name) @item
+        "#,
+    )
+    .unwrap()
+}
+
+fn json_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Json".into(),
+            path_suffixes: vec!["js".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_json::language()),
+    )
+}
+
+fn javascript_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "JavaScript".into(),
+            ..Default::default()
+        },
+        Some(tree_sitter_typescript::language_tsx()),
+    )
+    .with_brackets_query(
+        r#"
+        ("{" @open "}" @close)
+        ("(" @open ")" @close)
+        "#,
+    )
+    .unwrap()
+    .with_indents_query(
+        r#"
+        (object "}" @end) @indent
+        "#,
+    )
+    .unwrap()
+}
+
+fn get_tree_sexp(buffer: &Handle<Buffer>, cx: &mut gpui2::TestAppContext) -> String {
+    buffer.update(cx, |buffer, _| {
+        let snapshot = buffer.snapshot();
+        let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
+        layers[0].node().to_sexp()
+    })
+}
+
+// Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers`
+fn assert_bracket_pairs(
+    selection_text: &'static str,
+    bracket_pair_texts: Vec<&'static str>,
+    language: Language,
+    cx: &mut AppContext,
+) {
+    let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
+    let buffer = cx.entity(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), expected_text.clone())
+            .with_language(Arc::new(language), cx)
+    });
+    let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot());
+
+    let selection_range = selection_ranges[0].clone();
+
+    let bracket_pairs = bracket_pair_texts
+        .into_iter()
+        .map(|pair_text| {
+            let (bracket_text, ranges) = marked_text_ranges(pair_text, false);
+            assert_eq!(bracket_text, expected_text);
+            (ranges[0].clone(), ranges[1].clone())
+        })
+        .collect::<Vec<_>>();
+
+    assert_set_eq!(
+        buffer.bracket_ranges(selection_range).collect::<Vec<_>>(),
+        bracket_pairs
+    );
+}
+
+fn init_settings(cx: &mut AppContext, f: fn(&mut AllLanguageSettingsContent)) {
+    let settings_store = SettingsStore::test(cx);
+    cx.set_global(settings_store);
+    crate::init(cx);
+    cx.update_global::<SettingsStore, _>(|settings, cx| {
+        settings.update_user_settings::<AllLanguageSettings>(cx, f);
+    });
+}

crates/language2/src/highlight_map.rs 🔗

@@ -76,36 +76,36 @@ impl Default for HighlightId {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use gpui2::color::Color;
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui2::rgba;
 
-//     #[test]
-//     fn test_highlight_map() {
-//         let theme = SyntaxTheme::new(
-//             [
-//                 ("function", Color::from_u32(0x100000ff)),
-//                 ("function.method", Color::from_u32(0x200000ff)),
-//                 ("function.async", Color::from_u32(0x300000ff)),
-//                 ("variable.builtin.self.rust", Color::from_u32(0x400000ff)),
-//                 ("variable.builtin", Color::from_u32(0x500000ff)),
-//                 ("variable", Color::from_u32(0x600000ff)),
-//             ]
-//             .iter()
-//             .map(|(name, color)| (name.to_string(), (*color).into()))
-//             .collect(),
-//         );
+    #[test]
+    fn test_highlight_map() {
+        let theme = SyntaxTheme {
+            highlights: [
+                ("function", rgba(0x100000ff)),
+                ("function.method", rgba(0x200000ff)),
+                ("function.async", rgba(0x300000ff)),
+                ("variable.builtin.self.rust", rgba(0x400000ff)),
+                ("variable.builtin", rgba(0x500000ff)),
+                ("variable", rgba(0x600000ff)),
+            ]
+            .iter()
+            .map(|(name, color)| (name.to_string(), (*color).into()))
+            .collect(),
+        };
 
-//         let capture_names = &[
-//             "function.special".to_string(),
-//             "function.async.rust".to_string(),
-//             "variable.builtin.self".to_string(),
-//         ];
+        let capture_names = &[
+            "function.special".to_string(),
+            "function.async.rust".to_string(),
+            "variable.builtin.self".to_string(),
+        ];
 
-//         let map = HighlightMap::new(capture_names, &theme);
-//         assert_eq!(map.get(0).name(&theme), Some("function"));
-//         assert_eq!(map.get(1).name(&theme), Some("function.async"));
-//         assert_eq!(map.get(2).name(&theme), Some("variable.builtin"));
-//     }
-// }
+        let map = HighlightMap::new(capture_names, &theme);
+        assert_eq!(map.get(0).name(&theme), Some("function"));
+        assert_eq!(map.get(1).name(&theme), Some("function.async"));
+        assert_eq!(map.get(2).name(&theme), Some("variable.builtin"));
+    }
+}

crates/language2/src/language2.rs 🔗

@@ -37,7 +37,7 @@ use std::{
     path::{Path, PathBuf},
     str,
     sync::{
-        atomic::{AtomicUsize, Ordering::SeqCst},
+        atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
 };
@@ -115,6 +115,7 @@ pub struct CachedLspAdapter {
     pub disk_based_diagnostics_progress_token: Option<String>,
     pub language_ids: HashMap<String, String>,
     pub adapter: Arc<dyn LspAdapter>,
+    pub reinstall_attempt_count: AtomicU64,
 }
 
 impl CachedLspAdapter {
@@ -135,6 +136,7 @@ impl CachedLspAdapter {
             disk_based_diagnostics_progress_token,
             language_ids,
             adapter,
+            reinstall_attempt_count: AtomicU64::new(0),
         })
     }
 

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

@@ -1,1323 +1,1323 @@
-// use super::*;
-// use crate::LanguageConfig;
-// use rand::rngs::StdRng;
-// use std::{env, ops::Range, sync::Arc};
-// use text::Buffer;
-// use tree_sitter::Node;
-// use unindent::Unindent as _;
-// use util::test::marked_text_ranges;
-
-// #[test]
-// fn test_splice_included_ranges() {
-//     let ranges = vec![ts_range(20..30), ts_range(50..60), ts_range(80..90)];
-
-//     let (new_ranges, change) = splice_included_ranges(
-//         ranges.clone(),
-//         &[54..56, 58..68],
-//         &[ts_range(50..54), ts_range(59..67)],
-//     );
-//     assert_eq!(
-//         new_ranges,
-//         &[
-//             ts_range(20..30),
-//             ts_range(50..54),
-//             ts_range(59..67),
-//             ts_range(80..90),
-//         ]
-//     );
-//     assert_eq!(change, 1..3);
-
-//     let (new_ranges, change) = splice_included_ranges(ranges.clone(), &[70..71, 91..100], &[]);
-//     assert_eq!(
-//         new_ranges,
-//         &[ts_range(20..30), ts_range(50..60), ts_range(80..90)]
-//     );
-//     assert_eq!(change, 2..3);
-
-//     let (new_ranges, change) =
-//         splice_included_ranges(ranges.clone(), &[], &[ts_range(0..2), ts_range(70..75)]);
-//     assert_eq!(
-//         new_ranges,
-//         &[
-//             ts_range(0..2),
-//             ts_range(20..30),
-//             ts_range(50..60),
-//             ts_range(70..75),
-//             ts_range(80..90)
-//         ]
-//     );
-//     assert_eq!(change, 0..4);
-
-//     let (new_ranges, change) =
-//         splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]);
-//     assert_eq!(new_ranges, &[ts_range(25..55), ts_range(80..90)]);
-//     assert_eq!(change, 0..1);
-
-//     // does not create overlapping ranges
-//     let (new_ranges, change) =
-//         splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]);
-//     assert_eq!(
-//         new_ranges,
-//         &[ts_range(20..32), ts_range(50..60), ts_range(80..90)]
-//     );
-//     assert_eq!(change, 0..1);
-
-//     fn ts_range(range: Range<usize>) -> tree_sitter::Range {
-//         tree_sitter::Range {
-//             start_byte: range.start,
-//             start_point: tree_sitter::Point {
-//                 row: 0,
-//                 column: range.start,
-//             },
-//             end_byte: range.end,
-//             end_point: tree_sitter::Point {
-//                 row: 0,
-//                 column: range.end,
-//             },
-//         }
-//     }
-// }
-
-// #[gpui::test]
-// fn test_syntax_map_layers_for_range() {
-//     let registry = Arc::new(LanguageRegistry::test());
-//     let language = Arc::new(rust_lang());
-//     registry.add(language.clone());
-
-//     let mut buffer = Buffer::new(
-//         0,
-//         0,
-//         r#"
-//             fn a() {
-//                 assert_eq!(
-//                     b(vec![C {}]),
-//                     vec![d.e],
-//                 );
-//                 println!("{}", f(|_| true));
-//             }
-//         "#
-//         .unindent(),
-//     );
-
-//     let mut syntax_map = SyntaxMap::new();
-//     syntax_map.set_language_registry(registry.clone());
-//     syntax_map.reparse(language.clone(), &buffer);
-
-//     assert_layers_for_range(
-//         &syntax_map,
-//         &buffer,
-//         Point::new(2, 0)..Point::new(2, 0),
-//         &[
-//             "...(function_item ... (block (expression_statement (macro_invocation...",
-//             "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...",
-//         ],
-//     );
-//     assert_layers_for_range(
-//         &syntax_map,
-//         &buffer,
-//         Point::new(2, 14)..Point::new(2, 16),
-//         &[
-//             "...(function_item ...",
-//             "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...",
-//             "...(array_expression (struct_expression ...",
-//         ],
-//     );
-//     assert_layers_for_range(
-//         &syntax_map,
-//         &buffer,
-//         Point::new(3, 14)..Point::new(3, 16),
-//         &[
-//             "...(function_item ...",
-//             "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...",
-//             "...(array_expression (field_expression ...",
-//         ],
-//     );
-//     assert_layers_for_range(
-//         &syntax_map,
-//         &buffer,
-//         Point::new(5, 12)..Point::new(5, 16),
-//         &[
-//             "...(function_item ...",
-//             "...(call_expression ... (arguments (closure_expression ...",
-//         ],
-//     );
-
-//     // Replace a vec! macro invocation with a plain slice, removing a syntactic layer.
-//     let macro_name_range = range_for_text(&buffer, "vec!");
-//     buffer.edit([(macro_name_range, "&")]);
-//     syntax_map.interpolate(&buffer);
-//     syntax_map.reparse(language.clone(), &buffer);
-
-//     assert_layers_for_range(
-//             &syntax_map,
-//             &buffer,
-//             Point::new(2, 14)..Point::new(2, 16),
-//             &[
-//                 "...(function_item ...",
-//                 "...(tuple_expression (call_expression ... arguments: (arguments (reference_expression value: (array_expression...",
-//             ],
-//         );
-
-//     // Put the vec! macro back, adding back the syntactic layer.
-//     buffer.undo();
-//     syntax_map.interpolate(&buffer);
-//     syntax_map.reparse(language.clone(), &buffer);
-
-//     assert_layers_for_range(
-//         &syntax_map,
-//         &buffer,
-//         Point::new(2, 14)..Point::new(2, 16),
-//         &[
-//             "...(function_item ...",
-//             "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...",
-//             "...(array_expression (struct_expression ...",
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_dynamic_language_injection() {
-//     let registry = Arc::new(LanguageRegistry::test());
-//     let markdown = Arc::new(markdown_lang());
-//     registry.add(markdown.clone());
-//     registry.add(Arc::new(rust_lang()));
-//     registry.add(Arc::new(ruby_lang()));
-
-//     let mut buffer = Buffer::new(
-//         0,
-//         0,
-//         r#"
-//             This is a code block:
-
-//             ```rs
-//             fn foo() {}
-//             ```
-//         "#
-//         .unindent(),
-//     );
-
-//     let mut syntax_map = SyntaxMap::new();
-//     syntax_map.set_language_registry(registry.clone());
-//     syntax_map.reparse(markdown.clone(), &buffer);
-//     assert_layers_for_range(
-//             &syntax_map,
-//             &buffer,
-//             Point::new(3, 0)..Point::new(3, 0),
-//             &[
-//                 "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...",
-//                 "...(function_item name: (identifier) parameters: (parameters) body: (block)...",
-//             ],
-//         );
-
-//     // Replace Rust with Ruby in code block.
-//     let macro_name_range = range_for_text(&buffer, "rs");
-//     buffer.edit([(macro_name_range, "ruby")]);
-//     syntax_map.interpolate(&buffer);
-//     syntax_map.reparse(markdown.clone(), &buffer);
-//     assert_layers_for_range(
-//             &syntax_map,
-//             &buffer,
-//             Point::new(3, 0)..Point::new(3, 0),
-//             &[
-//                 "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...",
-//                 "...(call method: (identifier) arguments: (argument_list (call method: (identifier) arguments: (argument_list) block: (block)...",
-//             ],
-//         );
-
-//     // Replace Ruby with a language that hasn't been loaded yet.
-//     let macro_name_range = range_for_text(&buffer, "ruby");
-//     buffer.edit([(macro_name_range, "html")]);
-//     syntax_map.interpolate(&buffer);
-//     syntax_map.reparse(markdown.clone(), &buffer);
-//     assert_layers_for_range(
-//             &syntax_map,
-//             &buffer,
-//             Point::new(3, 0)..Point::new(3, 0),
-//             &[
-//                 "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter..."
-//             ],
-//         );
-//     assert!(syntax_map.contains_unknown_injections());
-
-//     registry.add(Arc::new(html_lang()));
-//     syntax_map.reparse(markdown.clone(), &buffer);
-//     assert_layers_for_range(
-//             &syntax_map,
-//             &buffer,
-//             Point::new(3, 0)..Point::new(3, 0),
-//             &[
-//                 "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...",
-//                 "(fragment (text))",
-//             ],
-//         );
-//     assert!(!syntax_map.contains_unknown_injections());
-// }
-
-// #[gpui::test]
-// fn test_typing_multiple_new_injections() {
-//     let (buffer, syntax_map) = test_edit_sequence(
-//         "Rust",
-//         &[
-//             "fn a() { dbg }",
-//             "fn a() { dbg«!» }",
-//             "fn a() { dbg!«()» }",
-//             "fn a() { dbg!(«b») }",
-//             "fn a() { dbg!(b«.») }",
-//             "fn a() { dbg!(b.«c») }",
-//             "fn a() { dbg!(b.c«()») }",
-//             "fn a() { dbg!(b.c(«vec»)) }",
-//             "fn a() { dbg!(b.c(vec«!»)) }",
-//             "fn a() { dbg!(b.c(vec!«[]»)) }",
-//             "fn a() { dbg!(b.c(vec![«d»])) }",
-//             "fn a() { dbg!(b.c(vec![d«.»])) }",
-//             "fn a() { dbg!(b.c(vec![d.«e»])) }",
-//         ],
-//     );
-
-//     assert_capture_ranges(
-//         &syntax_map,
-//         &buffer,
-//         &["field"],
-//         "fn a() { dbg!(b.«c»(vec![d.«e»])) }",
-//     );
-// }
-
-// #[gpui::test]
-// fn test_pasting_new_injection_line_between_others() {
-//     let (buffer, syntax_map) = test_edit_sequence(
-//         "Rust",
-//         &[
-//             "
-//                 fn a() {
-//                     b!(B {});
-//                     c!(C {});
-//                     d!(D {});
-//                     e!(E {});
-//                     f!(F {});
-//                     g!(G {});
-//                 }
-//             ",
-//             "
-//                 fn a() {
-//                     b!(B {});
-//                     c!(C {});
-//                     d!(D {});
-//                 «    h!(H {});
-//                 »    e!(E {});
-//                     f!(F {});
-//                     g!(G {});
-//                 }
-//             ",
-//         ],
-//     );
-
-//     assert_capture_ranges(
-//         &syntax_map,
-//         &buffer,
-//         &["struct"],
-//         "
-//         fn a() {
-//             b!(«B {}»);
-//             c!(«C {}»);
-//             d!(«D {}»);
-//             h!(«H {}»);
-//             e!(«E {}»);
-//             f!(«F {}»);
-//             g!(«G {}»);
-//         }
-//         ",
-//     );
-// }
-
-// #[gpui::test]
-// fn test_joining_injections_with_child_injections() {
-//     let (buffer, syntax_map) = test_edit_sequence(
-//         "Rust",
-//         &[
-//             "
-//                 fn a() {
-//                     b!(
-//                         c![one.two.three],
-//                         d![four.five.six],
-//                     );
-//                     e!(
-//                         f![seven.eight],
-//                     );
-//                 }
-//             ",
-//             "
-//                 fn a() {
-//                     b!(
-//                         c![one.two.three],
-//                         d![four.five.six],
-//                     ˇ    f![seven.eight],
-//                     );
-//                 }
-//             ",
-//         ],
-//     );
-
-//     assert_capture_ranges(
-//         &syntax_map,
-//         &buffer,
-//         &["field"],
-//         "
-//         fn a() {
-//             b!(
-//                 c![one.«two».«three»],
-//                 d![four.«five».«six»],
-//                 f![seven.«eight»],
-//             );
-//         }
-//         ",
-//     );
-// }
-
-// #[gpui::test]
-// fn test_editing_edges_of_injection() {
-//     test_edit_sequence(
-//         "Rust",
-//         &[
-//             "
-//                 fn a() {
-//                     b!(c!())
-//                 }
-//             ",
-//             "
-//                 fn a() {
-//                     «d»!(c!())
-//                 }
-//             ",
-//             "
-//                 fn a() {
-//                     «e»d!(c!())
-//                 }
-//             ",
-//             "
-//                 fn a() {
-//                     ed!«[»c!()«]»
-//                 }
-//             ",
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_edits_preceding_and_intersecting_injection() {
-//     test_edit_sequence(
-//         "Rust",
-//         &[
-//             //
-//             "const aaaaaaaaaaaa: B = c!(d(e.f));",
-//             "const aˇa: B = c!(d(eˇ));",
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_non_local_changes_create_injections() {
-//     test_edit_sequence(
-//         "Rust",
-//         &[
-//             "
-//                 // a! {
-//                     static B: C = d;
-//                 // }
-//             ",
-//             "
-//                 ˇa! {
-//                     static B: C = d;
-//                 ˇ}
-//             ",
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_creating_many_injections_in_one_edit() {
-//     test_edit_sequence(
-//         "Rust",
-//         &[
-//             "
-//                 fn a() {
-//                     one(Two::three(3));
-//                     four(Five::six(6));
-//                     seven(Eight::nine(9));
-//                 }
-//             ",
-//             "
-//                 fn a() {
-//                     one«!»(Two::three(3));
-//                     four«!»(Five::six(6));
-//                     seven«!»(Eight::nine(9));
-//                 }
-//             ",
-//             "
-//                 fn a() {
-//                     one!(Two::three«!»(3));
-//                     four!(Five::six«!»(6));
-//                     seven!(Eight::nine«!»(9));
-//                 }
-//             ",
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_editing_across_injection_boundary() {
-//     test_edit_sequence(
-//         "Rust",
-//         &[
-//             "
-//                 fn one() {
-//                     two();
-//                     three!(
-//                         three.four,
-//                         five.six,
-//                     );
-//                 }
-//             ",
-//             "
-//                 fn one() {
-//                     two();
-//                     th«irty_five![»
-//                         three.four,
-//                         five.six,
-//                     «   seven.eight,
-//                     ];»
-//                 }
-//             ",
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_removing_injection_by_replacing_across_boundary() {
-//     test_edit_sequence(
-//         "Rust",
-//         &[
-//             "
-//                 fn one() {
-//                     two!(
-//                         three.four,
-//                     );
-//                 }
-//             ",
-//             "
-//                 fn one() {
-//                     t«en
-//                         .eleven(
-//                         twelve,
-//                     »
-//                         three.four,
-//                     );
-//                 }
-//             ",
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_combined_injections_simple() {
-//     let (buffer, syntax_map) = test_edit_sequence(
-//         "ERB",
-//         &[
-//             "
-//                 <body>
-//                     <% if @one %>
-//                         <div class=one>
-//                     <% else %>
-//                         <div class=two>
-//                     <% end %>
-//                     </div>
-//                 </body>
-//             ",
-//             "
-//                 <body>
-//                     <% if @one %>
-//                         <div class=one>
-//                     ˇ else ˇ
-//                         <div class=two>
-//                     <% end %>
-//                     </div>
-//                 </body>
-//             ",
-//             "
-//                 <body>
-//                     <% if @one «;» end %>
-//                     </div>
-//                 </body>
-//             ",
-//         ],
-//     );
-
-//     assert_capture_ranges(
-//         &syntax_map,
-//         &buffer,
-//         &["tag", "ivar"],
-//         "
-//             <«body»>
-//                 <% if «@one» ; end %>
-//                 </«div»>
-//             </«body»>
-//         ",
-//     );
-// }
-
-// #[gpui::test]
-// fn test_combined_injections_empty_ranges() {
-//     test_edit_sequence(
-//         "ERB",
-//         &[
-//             "
-//                 <% if @one %>
-//                 <% else %>
-//                 <% end %>
-//             ",
-//             "
-//                 <% if @one %>
-//                 ˇ<% end %>
-//             ",
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_combined_injections_edit_edges_of_ranges() {
-//     let (buffer, syntax_map) = test_edit_sequence(
-//         "ERB",
-//         &[
-//             "
-//                 <%= one @two %>
-//                 <%= three @four %>
-//             ",
-//             "
-//                 <%= one @two %ˇ
-//                 <%= three @four %>
-//             ",
-//             "
-//                 <%= one @two %«>»
-//                 <%= three @four %>
-//             ",
-//         ],
-//     );
-
-//     assert_capture_ranges(
-//         &syntax_map,
-//         &buffer,
-//         &["tag", "ivar"],
-//         "
-//             <%= one «@two» %>
-//             <%= three «@four» %>
-//         ",
-//     );
-// }
-
-// #[gpui::test]
-// fn test_combined_injections_splitting_some_injections() {
-//     let (_buffer, _syntax_map) = test_edit_sequence(
-//         "ERB",
-//         &[
-//             r#"
-//                 <%A if b(:c) %>
-//                 d
-//                 <% end %>
-//                 eee
-//                 <% f %>
-//             "#,
-//             r#"
-//                 <%« AAAAAAA %>
-//                 hhhhhhh
-//                 <%=» if b(:c) %>
-//                 d
-//                 <% end %>
-//                 eee
-//                 <% f %>
-//             "#,
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_combined_injections_editing_after_last_injection() {
-//     test_edit_sequence(
-//         "ERB",
-//         &[
-//             r#"
-//                 <% foo %>
-//                 <div></div>
-//                 <% bar %>
-//             "#,
-//             r#"
-//                 <% foo %>
-//                 <div></div>
-//                 <% bar %>«
-//                 more text»
-//             "#,
-//         ],
-//     );
-// }
-
-// #[gpui::test]
-// fn test_combined_injections_inside_injections() {
-//     let (buffer, syntax_map) = test_edit_sequence(
-//         "Markdown",
-//         &[
-//             r#"
-//                 here is
-//                 some
-//                 ERB code:
-
-//                 ```erb
-//                 <ul>
-//                 <% people.each do |person| %>
-//                     <li><%= person.name %></li>
-//                     <li><%= person.age %></li>
-//                 <% end %>
-//                 </ul>
-//                 ```
-//             "#,
-//             r#"
-//                 here is
-//                 some
-//                 ERB code:
-
-//                 ```erb
-//                 <ul>
-//                 <% people«2».each do |person| %>
-//                     <li><%= person.name %></li>
-//                     <li><%= person.age %></li>
-//                 <% end %>
-//                 </ul>
-//                 ```
-//             "#,
-//             // Inserting a comment character inside one code directive
-//             // does not cause the other code directive to become a comment,
-//             // because newlines are included in between each injection range.
-//             r#"
-//                 here is
-//                 some
-//                 ERB code:
-
-//                 ```erb
-//                 <ul>
-//                 <% people2.each do |person| %>
-//                     <li><%= «# »person.name %></li>
-//                     <li><%= person.age %></li>
-//                 <% end %>
-//                 </ul>
-//                 ```
-//             "#,
-//         ],
-//     );
-
-//     // Check that the code directive below the ruby comment is
-//     // not parsed as a comment.
-//     assert_capture_ranges(
-//         &syntax_map,
-//         &buffer,
-//         &["method"],
-//         "
-//             here is
-//             some
-//             ERB code:
-
-//             ```erb
-//             <ul>
-//             <% people2.«each» do |person| %>
-//                 <li><%= # person.name %></li>
-//                 <li><%= person.«age» %></li>
-//             <% end %>
-//             </ul>
-//             ```
-//         ",
-//     );
-// }
-
-// #[gpui::test]
-// fn test_empty_combined_injections_inside_injections() {
-//     let (buffer, syntax_map) = test_edit_sequence(
-//         "Markdown",
-//         &[r#"
-//             ```erb
-//             hello
-//             ```
-
-//             goodbye
-//         "#],
-//     );
-
-//     assert_layers_for_range(
-//         &syntax_map,
-//         &buffer,
-//         Point::new(0, 0)..Point::new(5, 0),
-//         &[
-//             "...(paragraph)...",
-//             "(template...",
-//             "(fragment...",
-//             // The ruby syntax tree should be empty, since there are
-//             // no interpolations in the ERB template.
-//             "(program)",
-//         ],
-//     );
-// }
-
-// #[gpui::test(iterations = 50)]
-// fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
-//     let text = r#"
-//         fn test_something() {
-//             let vec = vec![5, 1, 3, 8];
-//             assert_eq!(
-//                 vec
-//                     .into_iter()
-//                     .map(|i| i * 2)
-//                     .collect::<Vec<usize>>(),
-//                 vec![
-//                     5 * 2, 1 * 2, 3 * 2, 8 * 2
-//                 ],
-//             );
-//         }
-//     "#
-//     .unindent()
-//     .repeat(2);
-
-//     let registry = Arc::new(LanguageRegistry::test());
-//     let language = Arc::new(rust_lang());
-//     registry.add(language.clone());
-
-//     test_random_edits(text, registry, language, rng);
-// }
-
-// #[gpui::test(iterations = 50)]
-// fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
-//     let text = r#"
-//         <div id="main">
-//         <% if one?(:two) %>
-//             <p class="three" four>
-//             <%= yield :five %>
-//             </p>
-//         <% elsif Six.seven(8) %>
-//             <p id="three" four>
-//             <%= yield :five %>
-//             </p>
-//         <% else %>
-//             <span>Ok</span>
-//         <% end %>
-//         </div>
-//     "#
-//     .unindent()
-//     .repeat(5);
-
-//     let registry = Arc::new(LanguageRegistry::test());
-//     let language = Arc::new(erb_lang());
-//     registry.add(language.clone());
-//     registry.add(Arc::new(ruby_lang()));
-//     registry.add(Arc::new(html_lang()));
-
-//     test_random_edits(text, registry, language, rng);
-// }
-
-// #[gpui::test(iterations = 50)]
-// fn test_random_syntax_map_edits_with_heex(rng: StdRng) {
-//     let text = r#"
-//         defmodule TheModule do
-//             def the_method(assigns) do
-//                 ~H"""
-//                 <%= if @empty do %>
-//                     <div class="h-4"></div>
-//                 <% else %>
-//                     <div class="max-w-2xl w-full animate-pulse">
-//                     <div class="flex-1 space-y-4">
-//                         <div class={[@bg_class, "h-4 rounded-lg w-3/4"]}></div>
-//                         <div class={[@bg_class, "h-4 rounded-lg"]}></div>
-//                         <div class={[@bg_class, "h-4 rounded-lg w-5/6"]}></div>
-//                     </div>
-//                     </div>
-//                 <% end %>
-//                 """
-//             end
-//         end
-//     "#
-//     .unindent()
-//     .repeat(3);
-
-//     let registry = Arc::new(LanguageRegistry::test());
-//     let language = Arc::new(elixir_lang());
-//     registry.add(language.clone());
-//     registry.add(Arc::new(heex_lang()));
-//     registry.add(Arc::new(html_lang()));
-
-//     test_random_edits(text, registry, language, rng);
-// }
-
-// fn test_random_edits(
-//     text: String,
-//     registry: Arc<LanguageRegistry>,
-//     language: Arc<Language>,
-//     mut rng: StdRng,
-// ) {
-//     let operations = env::var("OPERATIONS")
-//         .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-//         .unwrap_or(10);
-
-//     let mut buffer = Buffer::new(0, 0, text);
-
-//     let mut syntax_map = SyntaxMap::new();
-//     syntax_map.set_language_registry(registry.clone());
-//     syntax_map.reparse(language.clone(), &buffer);
-
-//     let mut reference_syntax_map = SyntaxMap::new();
-//     reference_syntax_map.set_language_registry(registry.clone());
-
-//     log::info!("initial text:\n{}", buffer.text());
-
-//     for _ in 0..operations {
-//         let prev_buffer = buffer.snapshot();
-//         let prev_syntax_map = syntax_map.snapshot();
-
-//         buffer.randomly_edit(&mut rng, 3);
-//         log::info!("text:\n{}", buffer.text());
-
-//         syntax_map.interpolate(&buffer);
-//         check_interpolation(&prev_syntax_map, &syntax_map, &prev_buffer, &buffer);
-
-//         syntax_map.reparse(language.clone(), &buffer);
-
-//         reference_syntax_map.clear();
-//         reference_syntax_map.reparse(language.clone(), &buffer);
-//     }
-
-//     for i in 0..operations {
-//         let i = operations - i - 1;
-//         buffer.undo();
-//         log::info!("undoing operation {}", i);
-//         log::info!("text:\n{}", buffer.text());
-
-//         syntax_map.interpolate(&buffer);
-//         syntax_map.reparse(language.clone(), &buffer);
-
-//         reference_syntax_map.clear();
-//         reference_syntax_map.reparse(language.clone(), &buffer);
-//         assert_eq!(
-//             syntax_map.layers(&buffer).len(),
-//             reference_syntax_map.layers(&buffer).len(),
-//             "wrong number of layers after undoing edit {i}"
-//         );
-//     }
-
-//     let layers = syntax_map.layers(&buffer);
-//     let reference_layers = reference_syntax_map.layers(&buffer);
-//     for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) {
-//         assert_eq!(
-//             edited_layer.node().to_sexp(),
-//             reference_layer.node().to_sexp()
-//         );
-//         assert_eq!(edited_layer.node().range(), reference_layer.node().range());
-//     }
-// }
-
-// fn check_interpolation(
-//     old_syntax_map: &SyntaxSnapshot,
-//     new_syntax_map: &SyntaxSnapshot,
-//     old_buffer: &BufferSnapshot,
-//     new_buffer: &BufferSnapshot,
-// ) {
-//     let edits = new_buffer
-//         .edits_since::<usize>(&old_buffer.version())
-//         .collect::<Vec<_>>();
-
-//     for (old_layer, new_layer) in old_syntax_map
-//         .layers
-//         .iter()
-//         .zip(new_syntax_map.layers.iter())
-//     {
-//         assert_eq!(old_layer.range, new_layer.range);
-//         let Some(old_tree) = old_layer.content.tree() else {
-//             continue;
-//         };
-//         let Some(new_tree) = new_layer.content.tree() else {
-//             continue;
-//         };
-//         let old_start_byte = old_layer.range.start.to_offset(old_buffer);
-//         let new_start_byte = new_layer.range.start.to_offset(new_buffer);
-//         let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point();
-//         let new_start_point = new_layer.range.start.to_point(new_buffer).to_ts_point();
-//         let old_node = old_tree.root_node_with_offset(old_start_byte, old_start_point);
-//         let new_node = new_tree.root_node_with_offset(new_start_byte, new_start_point);
-//         check_node_edits(
-//             old_layer.depth,
-//             &old_layer.range,
-//             old_node,
-//             new_node,
-//             old_buffer,
-//             new_buffer,
-//             &edits,
-//         );
-//     }
-
-//     fn check_node_edits(
-//         depth: usize,
-//         range: &Range<Anchor>,
-//         old_node: Node,
-//         new_node: Node,
-//         old_buffer: &BufferSnapshot,
-//         new_buffer: &BufferSnapshot,
-//         edits: &[text::Edit<usize>],
-//     ) {
-//         assert_eq!(old_node.kind(), new_node.kind());
-
-//         let old_range = old_node.byte_range();
-//         let new_range = new_node.byte_range();
-
-//         let is_edited = edits
-//             .iter()
-//             .any(|edit| edit.new.start < new_range.end && edit.new.end > new_range.start);
-//         if is_edited {
-//             assert!(
-//                 new_node.has_changes(),
-//                 concat!(
-//                     "failed to mark node as edited.\n",
-//                     "layer depth: {}, old layer range: {:?}, new layer range: {:?},\n",
-//                     "node kind: {}, old node range: {:?}, new node range: {:?}",
-//                 ),
-//                 depth,
-//                 range.to_offset(old_buffer),
-//                 range.to_offset(new_buffer),
-//                 new_node.kind(),
-//                 old_range,
-//                 new_range,
-//             );
-//         }
-
-//         if !new_node.has_changes() {
-//             assert_eq!(
-//                 old_buffer
-//                     .text_for_range(old_range.clone())
-//                     .collect::<String>(),
-//                 new_buffer
-//                     .text_for_range(new_range.clone())
-//                     .collect::<String>(),
-//                 concat!(
-//                     "mismatched text for node\n",
-//                     "layer depth: {}, old layer range: {:?}, new layer range: {:?},\n",
-//                     "node kind: {}, old node range:{:?}, new node range:{:?}",
-//                 ),
-//                 depth,
-//                 range.to_offset(old_buffer),
-//                 range.to_offset(new_buffer),
-//                 new_node.kind(),
-//                 old_range,
-//                 new_range,
-//             );
-//         }
-
-//         for i in 0..new_node.child_count() {
-//             check_node_edits(
-//                 depth,
-//                 range,
-//                 old_node.child(i).unwrap(),
-//                 new_node.child(i).unwrap(),
-//                 old_buffer,
-//                 new_buffer,
-//                 edits,
-//             )
-//         }
-//     }
-// }
-
-// fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap) {
-//     let registry = Arc::new(LanguageRegistry::test());
-//     registry.add(Arc::new(elixir_lang()));
-//     registry.add(Arc::new(heex_lang()));
-//     registry.add(Arc::new(rust_lang()));
-//     registry.add(Arc::new(ruby_lang()));
-//     registry.add(Arc::new(html_lang()));
-//     registry.add(Arc::new(erb_lang()));
-//     registry.add(Arc::new(markdown_lang()));
-
-//     let language = registry
-//         .language_for_name(language_name)
-//         .now_or_never()
-//         .unwrap()
-//         .unwrap();
-//     let mut buffer = Buffer::new(0, 0, Default::default());
-
-//     let mut mutated_syntax_map = SyntaxMap::new();
-//     mutated_syntax_map.set_language_registry(registry.clone());
-//     mutated_syntax_map.reparse(language.clone(), &buffer);
-
-//     for (i, marked_string) in steps.into_iter().enumerate() {
-//         let marked_string = marked_string.unindent();
-//         log::info!("incremental parse {i}: {marked_string:?}");
-//         buffer.edit_via_marked_text(&marked_string);
-
-//         // Reparse the syntax map
-//         mutated_syntax_map.interpolate(&buffer);
-//         mutated_syntax_map.reparse(language.clone(), &buffer);
-
-//         // Create a second syntax map from scratch
-//         log::info!("fresh parse {i}: {marked_string:?}");
-//         let mut reference_syntax_map = SyntaxMap::new();
-//         reference_syntax_map.set_language_registry(registry.clone());
-//         reference_syntax_map.reparse(language.clone(), &buffer);
-
-//         // Compare the mutated syntax map to the new syntax map
-//         let mutated_layers = mutated_syntax_map.layers(&buffer);
-//         let reference_layers = reference_syntax_map.layers(&buffer);
-//         assert_eq!(
-//             mutated_layers.len(),
-//             reference_layers.len(),
-//             "wrong number of layers at step {i}"
-//         );
-//         for (edited_layer, reference_layer) in
-//             mutated_layers.into_iter().zip(reference_layers.into_iter())
-//         {
-//             assert_eq!(
-//                 edited_layer.node().to_sexp(),
-//                 reference_layer.node().to_sexp(),
-//                 "different layer at step {i}"
-//             );
-//             assert_eq!(
-//                 edited_layer.node().range(),
-//                 reference_layer.node().range(),
-//                 "different layer at step {i}"
-//             );
-//         }
-//     }
-
-//     (buffer, mutated_syntax_map)
-// }
-
-// fn html_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "HTML".into(),
-//             path_suffixes: vec!["html".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_html::language()),
-//     )
-//     .with_highlights_query(
-//         r#"
-//             (tag_name) @tag
-//             (erroneous_end_tag_name) @tag
-//             (attribute_name) @property
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn ruby_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "Ruby".into(),
-//             path_suffixes: vec!["rb".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_ruby::language()),
-//     )
-//     .with_highlights_query(
-//         r#"
-//             ["if" "do" "else" "end"] @keyword
-//             (instance_variable) @ivar
-//             (call method: (identifier) @method)
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn erb_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "ERB".into(),
-//             path_suffixes: vec!["erb".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_embedded_template::language()),
-//     )
-//     .with_highlights_query(
-//         r#"
-//             ["<%" "%>"] @keyword
-//         "#,
-//     )
-//     .unwrap()
-//     .with_injection_query(
-//         r#"
-//             (
-//                 (code) @content
-//                 (#set! "language" "ruby")
-//                 (#set! "combined")
-//             )
-
-//             (
-//                 (content) @content
-//                 (#set! "language" "html")
-//                 (#set! "combined")
-//             )
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn rust_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     )
-//     .with_highlights_query(
-//         r#"
-//             (field_identifier) @field
-//             (struct_expression) @struct
-//         "#,
-//     )
-//     .unwrap()
-//     .with_injection_query(
-//         r#"
-//             (macro_invocation
-//                 (token_tree) @content
-//                 (#set! "language" "rust"))
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn markdown_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "Markdown".into(),
-//             path_suffixes: vec!["md".into()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_markdown::language()),
-//     )
-//     .with_injection_query(
-//         r#"
-//             (fenced_code_block
-//                 (info_string
-//                     (language) @language)
-//                 (code_fence_content) @content)
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn elixir_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "Elixir".into(),
-//             path_suffixes: vec!["ex".into()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_elixir::language()),
-//     )
-//     .with_highlights_query(
-//         r#"
-
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn heex_lang() -> Language {
-//     Language::new(
-//         LanguageConfig {
-//             name: "HEEx".into(),
-//             path_suffixes: vec!["heex".into()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_heex::language()),
-//     )
-//     .with_injection_query(
-//         r#"
-//         (
-//           (directive
-//             [
-//               (partial_expression_value)
-//               (expression_value)
-//               (ending_expression_value)
-//             ] @content)
-//           (#set! language "elixir")
-//           (#set! combined)
-//         )
-
-//         ((expression (expression_value) @content)
-//          (#set! language "elixir"))
-//         "#,
-//     )
-//     .unwrap()
-// }
-
-// fn range_for_text(buffer: &Buffer, text: &str) -> Range<usize> {
-//     let start = buffer.as_rope().to_string().find(text).unwrap();
-//     start..start + text.len()
-// }
-
-// #[track_caller]
-// fn assert_layers_for_range(
-//     syntax_map: &SyntaxMap,
-//     buffer: &BufferSnapshot,
-//     range: Range<Point>,
-//     expected_layers: &[&str],
-// ) {
-//     let layers = syntax_map
-//         .layers_for_range(range, &buffer)
-//         .collect::<Vec<_>>();
-//     assert_eq!(
-//         layers.len(),
-//         expected_layers.len(),
-//         "wrong number of layers"
-//     );
-//     for (i, (layer, expected_s_exp)) in layers.iter().zip(expected_layers.iter()).enumerate() {
-//         let actual_s_exp = layer.node().to_sexp();
-//         assert!(
-//             string_contains_sequence(
-//                 &actual_s_exp,
-//                 &expected_s_exp.split("...").collect::<Vec<_>>()
-//             ),
-//             "layer {i}:\n\nexpected: {expected_s_exp}\nactual:   {actual_s_exp}",
-//         );
-//     }
-// }
-
-// fn assert_capture_ranges(
-//     syntax_map: &SyntaxMap,
-//     buffer: &BufferSnapshot,
-//     highlight_query_capture_names: &[&str],
-//     marked_string: &str,
-// ) {
-//     let mut actual_ranges = Vec::<Range<usize>>::new();
-//     let captures = syntax_map.captures(0..buffer.len(), buffer, |grammar| {
-//         grammar.highlights_query.as_ref()
-//     });
-//     let queries = captures
-//         .grammars()
-//         .iter()
-//         .map(|grammar| grammar.highlights_query.as_ref().unwrap())
-//         .collect::<Vec<_>>();
-//     for capture in captures {
-//         let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
-//         if highlight_query_capture_names.contains(&name.as_str()) {
-//             actual_ranges.push(capture.node.byte_range());
-//         }
-//     }
-
-//     let (text, expected_ranges) = marked_text_ranges(&marked_string.unindent(), false);
-//     assert_eq!(text, buffer.text());
-//     assert_eq!(actual_ranges, expected_ranges);
-// }
-
-// pub fn string_contains_sequence(text: &str, parts: &[&str]) -> bool {
-//     let mut last_part_end = 0;
-//     for part in parts {
-//         if let Some(start_ix) = text[last_part_end..].find(part) {
-//             last_part_end = start_ix + part.len();
-//         } else {
-//             return false;
-//         }
-//     }
-//     true
-// }
+use super::*;
+use crate::LanguageConfig;
+use rand::rngs::StdRng;
+use std::{env, ops::Range, sync::Arc};
+use text::Buffer;
+use tree_sitter::Node;
+use unindent::Unindent as _;
+use util::test::marked_text_ranges;
+
+#[test]
+fn test_splice_included_ranges() {
+    let ranges = vec![ts_range(20..30), ts_range(50..60), ts_range(80..90)];
+
+    let (new_ranges, change) = splice_included_ranges(
+        ranges.clone(),
+        &[54..56, 58..68],
+        &[ts_range(50..54), ts_range(59..67)],
+    );
+    assert_eq!(
+        new_ranges,
+        &[
+            ts_range(20..30),
+            ts_range(50..54),
+            ts_range(59..67),
+            ts_range(80..90),
+        ]
+    );
+    assert_eq!(change, 1..3);
+
+    let (new_ranges, change) = splice_included_ranges(ranges.clone(), &[70..71, 91..100], &[]);
+    assert_eq!(
+        new_ranges,
+        &[ts_range(20..30), ts_range(50..60), ts_range(80..90)]
+    );
+    assert_eq!(change, 2..3);
+
+    let (new_ranges, change) =
+        splice_included_ranges(ranges.clone(), &[], &[ts_range(0..2), ts_range(70..75)]);
+    assert_eq!(
+        new_ranges,
+        &[
+            ts_range(0..2),
+            ts_range(20..30),
+            ts_range(50..60),
+            ts_range(70..75),
+            ts_range(80..90)
+        ]
+    );
+    assert_eq!(change, 0..4);
+
+    let (new_ranges, change) =
+        splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]);
+    assert_eq!(new_ranges, &[ts_range(25..55), ts_range(80..90)]);
+    assert_eq!(change, 0..1);
+
+    // does not create overlapping ranges
+    let (new_ranges, change) =
+        splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]);
+    assert_eq!(
+        new_ranges,
+        &[ts_range(20..32), ts_range(50..60), ts_range(80..90)]
+    );
+    assert_eq!(change, 0..1);
+
+    fn ts_range(range: Range<usize>) -> tree_sitter::Range {
+        tree_sitter::Range {
+            start_byte: range.start,
+            start_point: tree_sitter::Point {
+                row: 0,
+                column: range.start,
+            },
+            end_byte: range.end,
+            end_point: tree_sitter::Point {
+                row: 0,
+                column: range.end,
+            },
+        }
+    }
+}
+
+#[gpui2::test]
+fn test_syntax_map_layers_for_range() {
+    let registry = Arc::new(LanguageRegistry::test());
+    let language = Arc::new(rust_lang());
+    registry.add(language.clone());
+
+    let mut buffer = Buffer::new(
+        0,
+        0,
+        r#"
+            fn a() {
+                assert_eq!(
+                    b(vec![C {}]),
+                    vec![d.e],
+                );
+                println!("{}", f(|_| true));
+            }
+        "#
+        .unindent(),
+    );
+
+    let mut syntax_map = SyntaxMap::new();
+    syntax_map.set_language_registry(registry.clone());
+    syntax_map.reparse(language.clone(), &buffer);
+
+    assert_layers_for_range(
+        &syntax_map,
+        &buffer,
+        Point::new(2, 0)..Point::new(2, 0),
+        &[
+            "...(function_item ... (block (expression_statement (macro_invocation...",
+            "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...",
+        ],
+    );
+    assert_layers_for_range(
+        &syntax_map,
+        &buffer,
+        Point::new(2, 14)..Point::new(2, 16),
+        &[
+            "...(function_item ...",
+            "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...",
+            "...(array_expression (struct_expression ...",
+        ],
+    );
+    assert_layers_for_range(
+        &syntax_map,
+        &buffer,
+        Point::new(3, 14)..Point::new(3, 16),
+        &[
+            "...(function_item ...",
+            "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...",
+            "...(array_expression (field_expression ...",
+        ],
+    );
+    assert_layers_for_range(
+        &syntax_map,
+        &buffer,
+        Point::new(5, 12)..Point::new(5, 16),
+        &[
+            "...(function_item ...",
+            "...(call_expression ... (arguments (closure_expression ...",
+        ],
+    );
+
+    // Replace a vec! macro invocation with a plain slice, removing a syntactic layer.
+    let macro_name_range = range_for_text(&buffer, "vec!");
+    buffer.edit([(macro_name_range, "&")]);
+    syntax_map.interpolate(&buffer);
+    syntax_map.reparse(language.clone(), &buffer);
+
+    assert_layers_for_range(
+            &syntax_map,
+            &buffer,
+            Point::new(2, 14)..Point::new(2, 16),
+            &[
+                "...(function_item ...",
+                "...(tuple_expression (call_expression ... arguments: (arguments (reference_expression value: (array_expression...",
+            ],
+        );
+
+    // Put the vec! macro back, adding back the syntactic layer.
+    buffer.undo();
+    syntax_map.interpolate(&buffer);
+    syntax_map.reparse(language.clone(), &buffer);
+
+    assert_layers_for_range(
+        &syntax_map,
+        &buffer,
+        Point::new(2, 14)..Point::new(2, 16),
+        &[
+            "...(function_item ...",
+            "...(tuple_expression (call_expression ... arguments: (arguments (macro_invocation...",
+            "...(array_expression (struct_expression ...",
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_dynamic_language_injection() {
+    let registry = Arc::new(LanguageRegistry::test());
+    let markdown = Arc::new(markdown_lang());
+    registry.add(markdown.clone());
+    registry.add(Arc::new(rust_lang()));
+    registry.add(Arc::new(ruby_lang()));
+
+    let mut buffer = Buffer::new(
+        0,
+        0,
+        r#"
+            This is a code block:
+
+            ```rs
+            fn foo() {}
+            ```
+        "#
+        .unindent(),
+    );
+
+    let mut syntax_map = SyntaxMap::new();
+    syntax_map.set_language_registry(registry.clone());
+    syntax_map.reparse(markdown.clone(), &buffer);
+    assert_layers_for_range(
+            &syntax_map,
+            &buffer,
+            Point::new(3, 0)..Point::new(3, 0),
+            &[
+                "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...",
+                "...(function_item name: (identifier) parameters: (parameters) body: (block)...",
+            ],
+        );
+
+    // Replace Rust with Ruby in code block.
+    let macro_name_range = range_for_text(&buffer, "rs");
+    buffer.edit([(macro_name_range, "ruby")]);
+    syntax_map.interpolate(&buffer);
+    syntax_map.reparse(markdown.clone(), &buffer);
+    assert_layers_for_range(
+            &syntax_map,
+            &buffer,
+            Point::new(3, 0)..Point::new(3, 0),
+            &[
+                "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...",
+                "...(call method: (identifier) arguments: (argument_list (call method: (identifier) arguments: (argument_list) block: (block)...",
+            ],
+        );
+
+    // Replace Ruby with a language that hasn't been loaded yet.
+    let macro_name_range = range_for_text(&buffer, "ruby");
+    buffer.edit([(macro_name_range, "html")]);
+    syntax_map.interpolate(&buffer);
+    syntax_map.reparse(markdown.clone(), &buffer);
+    assert_layers_for_range(
+            &syntax_map,
+            &buffer,
+            Point::new(3, 0)..Point::new(3, 0),
+            &[
+                "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter..."
+            ],
+        );
+    assert!(syntax_map.contains_unknown_injections());
+
+    registry.add(Arc::new(html_lang()));
+    syntax_map.reparse(markdown.clone(), &buffer);
+    assert_layers_for_range(
+            &syntax_map,
+            &buffer,
+            Point::new(3, 0)..Point::new(3, 0),
+            &[
+                "...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...",
+                "(fragment (text))",
+            ],
+        );
+    assert!(!syntax_map.contains_unknown_injections());
+}
+
+#[gpui2::test]
+fn test_typing_multiple_new_injections() {
+    let (buffer, syntax_map) = test_edit_sequence(
+        "Rust",
+        &[
+            "fn a() { dbg }",
+            "fn a() { dbg«!» }",
+            "fn a() { dbg!«()» }",
+            "fn a() { dbg!(«b») }",
+            "fn a() { dbg!(b«.») }",
+            "fn a() { dbg!(b.«c») }",
+            "fn a() { dbg!(b.c«()») }",
+            "fn a() { dbg!(b.c(«vec»)) }",
+            "fn a() { dbg!(b.c(vec«!»)) }",
+            "fn a() { dbg!(b.c(vec!«[]»)) }",
+            "fn a() { dbg!(b.c(vec![«d»])) }",
+            "fn a() { dbg!(b.c(vec![d«.»])) }",
+            "fn a() { dbg!(b.c(vec![d.«e»])) }",
+        ],
+    );
+
+    assert_capture_ranges(
+        &syntax_map,
+        &buffer,
+        &["field"],
+        "fn a() { dbg!(b.«c»(vec![d.«e»])) }",
+    );
+}
+
+#[gpui2::test]
+fn test_pasting_new_injection_line_between_others() {
+    let (buffer, syntax_map) = test_edit_sequence(
+        "Rust",
+        &[
+            "
+                fn a() {
+                    b!(B {});
+                    c!(C {});
+                    d!(D {});
+                    e!(E {});
+                    f!(F {});
+                    g!(G {});
+                }
+            ",
+            "
+                fn a() {
+                    b!(B {});
+                    c!(C {});
+                    d!(D {});
+                «    h!(H {});
+                »    e!(E {});
+                    f!(F {});
+                    g!(G {});
+                }
+            ",
+        ],
+    );
+
+    assert_capture_ranges(
+        &syntax_map,
+        &buffer,
+        &["struct"],
+        "
+        fn a() {
+            b!(«B {}»);
+            c!(«C {}»);
+            d!(«D {}»);
+            h!(«H {}»);
+            e!(«E {}»);
+            f!(«F {}»);
+            g!(«G {}»);
+        }
+        ",
+    );
+}
+
+#[gpui2::test]
+fn test_joining_injections_with_child_injections() {
+    let (buffer, syntax_map) = test_edit_sequence(
+        "Rust",
+        &[
+            "
+                fn a() {
+                    b!(
+                        c![one.two.three],
+                        d![four.five.six],
+                    );
+                    e!(
+                        f![seven.eight],
+                    );
+                }
+            ",
+            "
+                fn a() {
+                    b!(
+                        c![one.two.three],
+                        d![four.five.six],
+                    ˇ    f![seven.eight],
+                    );
+                }
+            ",
+        ],
+    );
+
+    assert_capture_ranges(
+        &syntax_map,
+        &buffer,
+        &["field"],
+        "
+        fn a() {
+            b!(
+                c![one.«two».«three»],
+                d![four.«five».«six»],
+                f![seven.«eight»],
+            );
+        }
+        ",
+    );
+}
+
+#[gpui2::test]
+fn test_editing_edges_of_injection() {
+    test_edit_sequence(
+        "Rust",
+        &[
+            "
+                fn a() {
+                    b!(c!())
+                }
+            ",
+            "
+                fn a() {
+                    «d»!(c!())
+                }
+            ",
+            "
+                fn a() {
+                    «e»d!(c!())
+                }
+            ",
+            "
+                fn a() {
+                    ed!«[»c!()«]»
+                }
+            ",
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_edits_preceding_and_intersecting_injection() {
+    test_edit_sequence(
+        "Rust",
+        &[
+            //
+            "const aaaaaaaaaaaa: B = c!(d(e.f));",
+            "const aˇa: B = c!(d(eˇ));",
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_non_local_changes_create_injections() {
+    test_edit_sequence(
+        "Rust",
+        &[
+            "
+                // a! {
+                    static B: C = d;
+                // }
+            ",
+            "
+                ˇa! {
+                    static B: C = d;
+                ˇ}
+            ",
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_creating_many_injections_in_one_edit() {
+    test_edit_sequence(
+        "Rust",
+        &[
+            "
+                fn a() {
+                    one(Two::three(3));
+                    four(Five::six(6));
+                    seven(Eight::nine(9));
+                }
+            ",
+            "
+                fn a() {
+                    one«!»(Two::three(3));
+                    four«!»(Five::six(6));
+                    seven«!»(Eight::nine(9));
+                }
+            ",
+            "
+                fn a() {
+                    one!(Two::three«!»(3));
+                    four!(Five::six«!»(6));
+                    seven!(Eight::nine«!»(9));
+                }
+            ",
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_editing_across_injection_boundary() {
+    test_edit_sequence(
+        "Rust",
+        &[
+            "
+                fn one() {
+                    two();
+                    three!(
+                        three.four,
+                        five.six,
+                    );
+                }
+            ",
+            "
+                fn one() {
+                    two();
+                    th«irty_five![»
+                        three.four,
+                        five.six,
+                    «   seven.eight,
+                    ];»
+                }
+            ",
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_removing_injection_by_replacing_across_boundary() {
+    test_edit_sequence(
+        "Rust",
+        &[
+            "
+                fn one() {
+                    two!(
+                        three.four,
+                    );
+                }
+            ",
+            "
+                fn one() {
+                    t«en
+                        .eleven(
+                        twelve,
+                    »
+                        three.four,
+                    );
+                }
+            ",
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_combined_injections_simple() {
+    let (buffer, syntax_map) = test_edit_sequence(
+        "ERB",
+        &[
+            "
+                <body>
+                    <% if @one %>
+                        <div class=one>
+                    <% else %>
+                        <div class=two>
+                    <% end %>
+                    </div>
+                </body>
+            ",
+            "
+                <body>
+                    <% if @one %>
+                        <div class=one>
+                    ˇ else ˇ
+                        <div class=two>
+                    <% end %>
+                    </div>
+                </body>
+            ",
+            "
+                <body>
+                    <% if @one «;» end %>
+                    </div>
+                </body>
+            ",
+        ],
+    );
+
+    assert_capture_ranges(
+        &syntax_map,
+        &buffer,
+        &["tag", "ivar"],
+        "
+            <«body»>
+                <% if «@one» ; end %>
+                </«div»>
+            </«body»>
+        ",
+    );
+}
+
+#[gpui2::test]
+fn test_combined_injections_empty_ranges() {
+    test_edit_sequence(
+        "ERB",
+        &[
+            "
+                <% if @one %>
+                <% else %>
+                <% end %>
+            ",
+            "
+                <% if @one %>
+                ˇ<% end %>
+            ",
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_combined_injections_edit_edges_of_ranges() {
+    let (buffer, syntax_map) = test_edit_sequence(
+        "ERB",
+        &[
+            "
+                <%= one @two %>
+                <%= three @four %>
+            ",
+            "
+                <%= one @two %ˇ
+                <%= three @four %>
+            ",
+            "
+                <%= one @two %«>»
+                <%= three @four %>
+            ",
+        ],
+    );
+
+    assert_capture_ranges(
+        &syntax_map,
+        &buffer,
+        &["tag", "ivar"],
+        "
+            <%= one «@two» %>
+            <%= three «@four» %>
+        ",
+    );
+}
+
+#[gpui2::test]
+fn test_combined_injections_splitting_some_injections() {
+    let (_buffer, _syntax_map) = test_edit_sequence(
+        "ERB",
+        &[
+            r#"
+                <%A if b(:c) %>
+                d
+                <% end %>
+                eee
+                <% f %>
+            "#,
+            r#"
+                <%« AAAAAAA %>
+                hhhhhhh
+                <%=» if b(:c) %>
+                d
+                <% end %>
+                eee
+                <% f %>
+            "#,
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_combined_injections_editing_after_last_injection() {
+    test_edit_sequence(
+        "ERB",
+        &[
+            r#"
+                <% foo %>
+                <div></div>
+                <% bar %>
+            "#,
+            r#"
+                <% foo %>
+                <div></div>
+                <% bar %>«
+                more text»
+            "#,
+        ],
+    );
+}
+
+#[gpui2::test]
+fn test_combined_injections_inside_injections() {
+    let (buffer, syntax_map) = test_edit_sequence(
+        "Markdown",
+        &[
+            r#"
+                here is
+                some
+                ERB code:
+
+                ```erb
+                <ul>
+                <% people.each do |person| %>
+                    <li><%= person.name %></li>
+                    <li><%= person.age %></li>
+                <% end %>
+                </ul>
+                ```
+            "#,
+            r#"
+                here is
+                some
+                ERB code:
+
+                ```erb
+                <ul>
+                <% people«2».each do |person| %>
+                    <li><%= person.name %></li>
+                    <li><%= person.age %></li>
+                <% end %>
+                </ul>
+                ```
+            "#,
+            // Inserting a comment character inside one code directive
+            // does not cause the other code directive to become a comment,
+            // because newlines are included in between each injection range.
+            r#"
+                here is
+                some
+                ERB code:
+
+                ```erb
+                <ul>
+                <% people2.each do |person| %>
+                    <li><%= «# »person.name %></li>
+                    <li><%= person.age %></li>
+                <% end %>
+                </ul>
+                ```
+            "#,
+        ],
+    );
+
+    // Check that the code directive below the ruby comment is
+    // not parsed as a comment.
+    assert_capture_ranges(
+        &syntax_map,
+        &buffer,
+        &["method"],
+        "
+            here is
+            some
+            ERB code:
+
+            ```erb
+            <ul>
+            <% people2.«each» do |person| %>
+                <li><%= # person.name %></li>
+                <li><%= person.«age» %></li>
+            <% end %>
+            </ul>
+            ```
+        ",
+    );
+}
+
+#[gpui2::test]
+fn test_empty_combined_injections_inside_injections() {
+    let (buffer, syntax_map) = test_edit_sequence(
+        "Markdown",
+        &[r#"
+            ```erb
+            hello
+            ```
+
+            goodbye
+        "#],
+    );
+
+    assert_layers_for_range(
+        &syntax_map,
+        &buffer,
+        Point::new(0, 0)..Point::new(5, 0),
+        &[
+            "...(paragraph)...",
+            "(template...",
+            "(fragment...",
+            // The ruby syntax tree should be empty, since there are
+            // no interpolations in the ERB template.
+            "(program)",
+        ],
+    );
+}
+
+#[gpui2::test(iterations = 50)]
+fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
+    let text = r#"
+        fn test_something() {
+            let vec = vec![5, 1, 3, 8];
+            assert_eq!(
+                vec
+                    .into_iter()
+                    .map(|i| i * 2)
+                    .collect::<Vec<usize>>(),
+                vec![
+                    5 * 2, 1 * 2, 3 * 2, 8 * 2
+                ],
+            );
+        }
+    "#
+    .unindent()
+    .repeat(2);
+
+    let registry = Arc::new(LanguageRegistry::test());
+    let language = Arc::new(rust_lang());
+    registry.add(language.clone());
+
+    test_random_edits(text, registry, language, rng);
+}
+
+#[gpui2::test(iterations = 50)]
+fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
+    let text = r#"
+        <div id="main">
+        <% if one?(:two) %>
+            <p class="three" four>
+            <%= yield :five %>
+            </p>
+        <% elsif Six.seven(8) %>
+            <p id="three" four>
+            <%= yield :five %>
+            </p>
+        <% else %>
+            <span>Ok</span>
+        <% end %>
+        </div>
+    "#
+    .unindent()
+    .repeat(5);
+
+    let registry = Arc::new(LanguageRegistry::test());
+    let language = Arc::new(erb_lang());
+    registry.add(language.clone());
+    registry.add(Arc::new(ruby_lang()));
+    registry.add(Arc::new(html_lang()));
+
+    test_random_edits(text, registry, language, rng);
+}
+
+#[gpui2::test(iterations = 50)]
+fn test_random_syntax_map_edits_with_heex(rng: StdRng) {
+    let text = r#"
+        defmodule TheModule do
+            def the_method(assigns) do
+                ~H"""
+                <%= if @empty do %>
+                    <div class="h-4"></div>
+                <% else %>
+                    <div class="max-w-2xl w-full animate-pulse">
+                    <div class="flex-1 space-y-4">
+                        <div class={[@bg_class, "h-4 rounded-lg w-3/4"]}></div>
+                        <div class={[@bg_class, "h-4 rounded-lg"]}></div>
+                        <div class={[@bg_class, "h-4 rounded-lg w-5/6"]}></div>
+                    </div>
+                    </div>
+                <% end %>
+                """
+            end
+        end
+    "#
+    .unindent()
+    .repeat(3);
+
+    let registry = Arc::new(LanguageRegistry::test());
+    let language = Arc::new(elixir_lang());
+    registry.add(language.clone());
+    registry.add(Arc::new(heex_lang()));
+    registry.add(Arc::new(html_lang()));
+
+    test_random_edits(text, registry, language, rng);
+}
+
+fn test_random_edits(
+    text: String,
+    registry: Arc<LanguageRegistry>,
+    language: Arc<Language>,
+    mut rng: StdRng,
+) {
+    let operations = env::var("OPERATIONS")
+        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+        .unwrap_or(10);
+
+    let mut buffer = Buffer::new(0, 0, text);
+
+    let mut syntax_map = SyntaxMap::new();
+    syntax_map.set_language_registry(registry.clone());
+    syntax_map.reparse(language.clone(), &buffer);
+
+    let mut reference_syntax_map = SyntaxMap::new();
+    reference_syntax_map.set_language_registry(registry.clone());
+
+    log::info!("initial text:\n{}", buffer.text());
+
+    for _ in 0..operations {
+        let prev_buffer = buffer.snapshot();
+        let prev_syntax_map = syntax_map.snapshot();
+
+        buffer.randomly_edit(&mut rng, 3);
+        log::info!("text:\n{}", buffer.text());
+
+        syntax_map.interpolate(&buffer);
+        check_interpolation(&prev_syntax_map, &syntax_map, &prev_buffer, &buffer);
+
+        syntax_map.reparse(language.clone(), &buffer);
+
+        reference_syntax_map.clear();
+        reference_syntax_map.reparse(language.clone(), &buffer);
+    }
+
+    for i in 0..operations {
+        let i = operations - i - 1;
+        buffer.undo();
+        log::info!("undoing operation {}", i);
+        log::info!("text:\n{}", buffer.text());
+
+        syntax_map.interpolate(&buffer);
+        syntax_map.reparse(language.clone(), &buffer);
+
+        reference_syntax_map.clear();
+        reference_syntax_map.reparse(language.clone(), &buffer);
+        assert_eq!(
+            syntax_map.layers(&buffer).len(),
+            reference_syntax_map.layers(&buffer).len(),
+            "wrong number of layers after undoing edit {i}"
+        );
+    }
+
+    let layers = syntax_map.layers(&buffer);
+    let reference_layers = reference_syntax_map.layers(&buffer);
+    for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) {
+        assert_eq!(
+            edited_layer.node().to_sexp(),
+            reference_layer.node().to_sexp()
+        );
+        assert_eq!(edited_layer.node().range(), reference_layer.node().range());
+    }
+}
+
+fn check_interpolation(
+    old_syntax_map: &SyntaxSnapshot,
+    new_syntax_map: &SyntaxSnapshot,
+    old_buffer: &BufferSnapshot,
+    new_buffer: &BufferSnapshot,
+) {
+    let edits = new_buffer
+        .edits_since::<usize>(&old_buffer.version())
+        .collect::<Vec<_>>();
+
+    for (old_layer, new_layer) in old_syntax_map
+        .layers
+        .iter()
+        .zip(new_syntax_map.layers.iter())
+    {
+        assert_eq!(old_layer.range, new_layer.range);
+        let Some(old_tree) = old_layer.content.tree() else {
+            continue;
+        };
+        let Some(new_tree) = new_layer.content.tree() else {
+            continue;
+        };
+        let old_start_byte = old_layer.range.start.to_offset(old_buffer);
+        let new_start_byte = new_layer.range.start.to_offset(new_buffer);
+        let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point();
+        let new_start_point = new_layer.range.start.to_point(new_buffer).to_ts_point();
+        let old_node = old_tree.root_node_with_offset(old_start_byte, old_start_point);
+        let new_node = new_tree.root_node_with_offset(new_start_byte, new_start_point);
+        check_node_edits(
+            old_layer.depth,
+            &old_layer.range,
+            old_node,
+            new_node,
+            old_buffer,
+            new_buffer,
+            &edits,
+        );
+    }
+
+    fn check_node_edits(
+        depth: usize,
+        range: &Range<Anchor>,
+        old_node: Node,
+        new_node: Node,
+        old_buffer: &BufferSnapshot,
+        new_buffer: &BufferSnapshot,
+        edits: &[text::Edit<usize>],
+    ) {
+        assert_eq!(old_node.kind(), new_node.kind());
+
+        let old_range = old_node.byte_range();
+        let new_range = new_node.byte_range();
+
+        let is_edited = edits
+            .iter()
+            .any(|edit| edit.new.start < new_range.end && edit.new.end > new_range.start);
+        if is_edited {
+            assert!(
+                new_node.has_changes(),
+                concat!(
+                    "failed to mark node as edited.\n",
+                    "layer depth: {}, old layer range: {:?}, new layer range: {:?},\n",
+                    "node kind: {}, old node range: {:?}, new node range: {:?}",
+                ),
+                depth,
+                range.to_offset(old_buffer),
+                range.to_offset(new_buffer),
+                new_node.kind(),
+                old_range,
+                new_range,
+            );
+        }
+
+        if !new_node.has_changes() {
+            assert_eq!(
+                old_buffer
+                    .text_for_range(old_range.clone())
+                    .collect::<String>(),
+                new_buffer
+                    .text_for_range(new_range.clone())
+                    .collect::<String>(),
+                concat!(
+                    "mismatched text for node\n",
+                    "layer depth: {}, old layer range: {:?}, new layer range: {:?},\n",
+                    "node kind: {}, old node range:{:?}, new node range:{:?}",
+                ),
+                depth,
+                range.to_offset(old_buffer),
+                range.to_offset(new_buffer),
+                new_node.kind(),
+                old_range,
+                new_range,
+            );
+        }
+
+        for i in 0..new_node.child_count() {
+            check_node_edits(
+                depth,
+                range,
+                old_node.child(i).unwrap(),
+                new_node.child(i).unwrap(),
+                old_buffer,
+                new_buffer,
+                edits,
+            )
+        }
+    }
+}
+
+fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap) {
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(Arc::new(elixir_lang()));
+    registry.add(Arc::new(heex_lang()));
+    registry.add(Arc::new(rust_lang()));
+    registry.add(Arc::new(ruby_lang()));
+    registry.add(Arc::new(html_lang()));
+    registry.add(Arc::new(erb_lang()));
+    registry.add(Arc::new(markdown_lang()));
+
+    let language = registry
+        .language_for_name(language_name)
+        .now_or_never()
+        .unwrap()
+        .unwrap();
+    let mut buffer = Buffer::new(0, 0, Default::default());
+
+    let mut mutated_syntax_map = SyntaxMap::new();
+    mutated_syntax_map.set_language_registry(registry.clone());
+    mutated_syntax_map.reparse(language.clone(), &buffer);
+
+    for (i, marked_string) in steps.into_iter().enumerate() {
+        let marked_string = marked_string.unindent();
+        log::info!("incremental parse {i}: {marked_string:?}");
+        buffer.edit_via_marked_text(&marked_string);
+
+        // Reparse the syntax map
+        mutated_syntax_map.interpolate(&buffer);
+        mutated_syntax_map.reparse(language.clone(), &buffer);
+
+        // Create a second syntax map from scratch
+        log::info!("fresh parse {i}: {marked_string:?}");
+        let mut reference_syntax_map = SyntaxMap::new();
+        reference_syntax_map.set_language_registry(registry.clone());
+        reference_syntax_map.reparse(language.clone(), &buffer);
+
+        // Compare the mutated syntax map to the new syntax map
+        let mutated_layers = mutated_syntax_map.layers(&buffer);
+        let reference_layers = reference_syntax_map.layers(&buffer);
+        assert_eq!(
+            mutated_layers.len(),
+            reference_layers.len(),
+            "wrong number of layers at step {i}"
+        );
+        for (edited_layer, reference_layer) in
+            mutated_layers.into_iter().zip(reference_layers.into_iter())
+        {
+            assert_eq!(
+                edited_layer.node().to_sexp(),
+                reference_layer.node().to_sexp(),
+                "different layer at step {i}"
+            );
+            assert_eq!(
+                edited_layer.node().range(),
+                reference_layer.node().range(),
+                "different layer at step {i}"
+            );
+        }
+    }
+
+    (buffer, mutated_syntax_map)
+}
+
+fn html_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "HTML".into(),
+            path_suffixes: vec!["html".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_html::language()),
+    )
+    .with_highlights_query(
+        r#"
+            (tag_name) @tag
+            (erroneous_end_tag_name) @tag
+            (attribute_name) @property
+        "#,
+    )
+    .unwrap()
+}
+
+fn ruby_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Ruby".into(),
+            path_suffixes: vec!["rb".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_ruby::language()),
+    )
+    .with_highlights_query(
+        r#"
+            ["if" "do" "else" "end"] @keyword
+            (instance_variable) @ivar
+            (call method: (identifier) @method)
+        "#,
+    )
+    .unwrap()
+}
+
+fn erb_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "ERB".into(),
+            path_suffixes: vec!["erb".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_embedded_template::language()),
+    )
+    .with_highlights_query(
+        r#"
+            ["<%" "%>"] @keyword
+        "#,
+    )
+    .unwrap()
+    .with_injection_query(
+        r#"
+            (
+                (code) @content
+                (#set! "language" "ruby")
+                (#set! "combined")
+            )
+
+            (
+                (content) @content
+                (#set! "language" "html")
+                (#set! "combined")
+            )
+        "#,
+    )
+    .unwrap()
+}
+
+fn rust_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    )
+    .with_highlights_query(
+        r#"
+            (field_identifier) @field
+            (struct_expression) @struct
+        "#,
+    )
+    .unwrap()
+    .with_injection_query(
+        r#"
+            (macro_invocation
+                (token_tree) @content
+                (#set! "language" "rust"))
+        "#,
+    )
+    .unwrap()
+}
+
+fn markdown_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Markdown".into(),
+            path_suffixes: vec!["md".into()],
+            ..Default::default()
+        },
+        Some(tree_sitter_markdown::language()),
+    )
+    .with_injection_query(
+        r#"
+            (fenced_code_block
+                (info_string
+                    (language) @language)
+                (code_fence_content) @content)
+        "#,
+    )
+    .unwrap()
+}
+
+fn elixir_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Elixir".into(),
+            path_suffixes: vec!["ex".into()],
+            ..Default::default()
+        },
+        Some(tree_sitter_elixir::language()),
+    )
+    .with_highlights_query(
+        r#"
+
+        "#,
+    )
+    .unwrap()
+}
+
+fn heex_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "HEEx".into(),
+            path_suffixes: vec!["heex".into()],
+            ..Default::default()
+        },
+        Some(tree_sitter_heex::language()),
+    )
+    .with_injection_query(
+        r#"
+        (
+          (directive
+            [
+              (partial_expression_value)
+              (expression_value)
+              (ending_expression_value)
+            ] @content)
+          (#set! language "elixir")
+          (#set! combined)
+        )
+
+        ((expression (expression_value) @content)
+         (#set! language "elixir"))
+        "#,
+    )
+    .unwrap()
+}
+
+fn range_for_text(buffer: &Buffer, text: &str) -> Range<usize> {
+    let start = buffer.as_rope().to_string().find(text).unwrap();
+    start..start + text.len()
+}
+
+#[track_caller]
+fn assert_layers_for_range(
+    syntax_map: &SyntaxMap,
+    buffer: &BufferSnapshot,
+    range: Range<Point>,
+    expected_layers: &[&str],
+) {
+    let layers = syntax_map
+        .layers_for_range(range, &buffer)
+        .collect::<Vec<_>>();
+    assert_eq!(
+        layers.len(),
+        expected_layers.len(),
+        "wrong number of layers"
+    );
+    for (i, (layer, expected_s_exp)) in layers.iter().zip(expected_layers.iter()).enumerate() {
+        let actual_s_exp = layer.node().to_sexp();
+        assert!(
+            string_contains_sequence(
+                &actual_s_exp,
+                &expected_s_exp.split("...").collect::<Vec<_>>()
+            ),
+            "layer {i}:\n\nexpected: {expected_s_exp}\nactual:   {actual_s_exp}",
+        );
+    }
+}
+
+fn assert_capture_ranges(
+    syntax_map: &SyntaxMap,
+    buffer: &BufferSnapshot,
+    highlight_query_capture_names: &[&str],
+    marked_string: &str,
+) {
+    let mut actual_ranges = Vec::<Range<usize>>::new();
+    let captures = syntax_map.captures(0..buffer.len(), buffer, |grammar| {
+        grammar.highlights_query.as_ref()
+    });
+    let queries = captures
+        .grammars()
+        .iter()
+        .map(|grammar| grammar.highlights_query.as_ref().unwrap())
+        .collect::<Vec<_>>();
+    for capture in captures {
+        let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
+        if highlight_query_capture_names.contains(&name.as_str()) {
+            actual_ranges.push(capture.node.byte_range());
+        }
+    }
+
+    let (text, expected_ranges) = marked_text_ranges(&marked_string.unindent(), false);
+    assert_eq!(text, buffer.text());
+    assert_eq!(actual_ranges, expected_ranges);
+}
+
+pub fn string_contains_sequence(text: &str, parts: &[&str]) -> bool {
+    let mut last_part_end = 0;
+    for part in parts {
+        if let Some(start_ix) = text[last_part_end..].find(part) {
+            last_part_end = start_ix + part.len();
+        } else {
+            return false;
+        }
+    }
+    true
+}

crates/language_tools/src/lsp_log.rs 🔗

@@ -1,5 +1,5 @@
-use collections::HashMap;
-use editor::Editor;
+use collections::{HashMap, VecDeque};
+use editor::{Editor, MoveToEnd};
 use futures::{channel::mpsc, StreamExt};
 use gpui::{
     actions,
@@ -11,7 +11,7 @@ use gpui::{
     AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
     ViewContext, ViewHandle, WeakModelHandle,
 };
-use language::{Buffer, LanguageServerId, LanguageServerName};
+use language::{LanguageServerId, LanguageServerName};
 use lsp::IoKind;
 use project::{search::SearchQuery, Project};
 use std::{borrow::Cow, sync::Arc};
@@ -22,8 +22,9 @@ use workspace::{
     ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
 };
 
-const SEND_LINE: &str = "// Send:\n";
-const RECEIVE_LINE: &str = "// Receive:\n";
+const SEND_LINE: &str = "// Send:";
+const RECEIVE_LINE: &str = "// Receive:";
+const MAX_STORED_LOG_ENTRIES: usize = 2000;
 
 pub struct LogStore {
     projects: HashMap<WeakModelHandle<Project>, ProjectState>,
@@ -36,24 +37,25 @@ struct ProjectState {
 }
 
 struct LanguageServerState {
-    log_buffer: ModelHandle<Buffer>,
+    log_messages: VecDeque<String>,
     rpc_state: Option<LanguageServerRpcState>,
     _io_logs_subscription: Option<lsp::Subscription>,
     _lsp_logs_subscription: Option<lsp::Subscription>,
 }
 
 struct LanguageServerRpcState {
-    buffer: ModelHandle<Buffer>,
+    rpc_messages: VecDeque<String>,
     last_message_kind: Option<MessageKind>,
 }
 
 pub struct LspLogView {
     pub(crate) editor: ViewHandle<Editor>,
+    editor_subscription: Subscription,
     log_store: ModelHandle<LogStore>,
     current_server_id: Option<LanguageServerId>,
     is_showing_rpc_trace: bool,
     project: ModelHandle<Project>,
-    _log_store_subscription: Subscription,
+    _log_store_subscriptions: Vec<Subscription>,
 }
 
 pub struct LspLogToolbarItemView {
@@ -122,10 +124,9 @@ impl LogStore {
             io_tx,
         };
         cx.spawn_weak(|this, mut cx| async move {
-            while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await {
+            while let Some((project, server_id, io_kind, message)) = io_rx.next().await {
                 if let Some(this) = this.upgrade(&cx) {
                     this.update(&mut cx, |this, cx| {
-                        message.push('\n');
                         this.on_io(project, server_id, io_kind, &message, cx);
                     });
                 }
@@ -168,15 +169,13 @@ impl LogStore {
         project: &ModelHandle<Project>,
         id: LanguageServerId,
         cx: &mut ModelContext<Self>,
-    ) -> Option<ModelHandle<Buffer>> {
+    ) -> Option<&mut LanguageServerState> {
         let project_state = self.projects.get_mut(&project.downgrade())?;
         let server_state = project_state.servers.entry(id).or_insert_with(|| {
             cx.notify();
             LanguageServerState {
                 rpc_state: None,
-                log_buffer: cx
-                    .add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""))
-                    .clone(),
+                log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
                 _io_logs_subscription: None,
                 _lsp_logs_subscription: None,
             }
@@ -186,7 +185,7 @@ impl LogStore {
         if let Some(server) = server.as_deref() {
             if server.has_notification_handler::<lsp::notification::LogMessage>() {
                 // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
-                return Some(server_state.log_buffer.clone());
+                return Some(server_state);
             }
         }
 
@@ -215,7 +214,7 @@ impl LogStore {
                 }
             })
         });
-        Some(server_state.log_buffer.clone())
+        Some(server_state)
     }
 
     fn add_language_server_log(
@@ -225,24 +224,26 @@ impl LogStore {
         message: &str,
         cx: &mut ModelContext<Self>,
     ) -> Option<()> {
-        let buffer = match self
+        let language_server_state = match self
             .projects
             .get_mut(&project.downgrade())?
             .servers
-            .get(&id)
-            .map(|state| state.log_buffer.clone())
+            .get_mut(&id)
         {
-            Some(existing_buffer) => existing_buffer,
+            Some(existing_state) => existing_state,
             None => self.add_language_server(&project, id, cx)?,
         };
-        buffer.update(cx, |buffer, cx| {
-            let len = buffer.len();
-            let has_newline = message.ends_with("\n");
-            buffer.edit([(len..len, message)], None, cx);
-            if !has_newline {
-                let len = buffer.len();
-                buffer.edit([(len..len, "\n")], None, cx);
-            }
+
+        let log_lines = &mut language_server_state.log_messages;
+        while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
+            log_lines.pop_front();
+        }
+        let message = message.trim();
+        log_lines.push_back(message.to_string());
+        cx.emit(Event::NewServerLogEntry {
+            id,
+            entry: message.to_string(),
+            is_rpc: false,
         });
         cx.notify();
         Some(())
@@ -260,46 +261,32 @@ impl LogStore {
         Some(())
     }
 
-    pub fn log_buffer_for_server(
+    fn server_logs(
         &self,
         project: &ModelHandle<Project>,
         server_id: LanguageServerId,
-    ) -> Option<ModelHandle<Buffer>> {
+    ) -> Option<&VecDeque<String>> {
         let weak_project = project.downgrade();
         let project_state = self.projects.get(&weak_project)?;
         let server_state = project_state.servers.get(&server_id)?;
-        Some(server_state.log_buffer.clone())
+        Some(&server_state.log_messages)
     }
 
     fn enable_rpc_trace_for_language_server(
         &mut self,
         project: &ModelHandle<Project>,
         server_id: LanguageServerId,
-        cx: &mut ModelContext<Self>,
-    ) -> Option<ModelHandle<Buffer>> {
+    ) -> Option<&mut LanguageServerRpcState> {
         let weak_project = project.downgrade();
         let project_state = self.projects.get_mut(&weak_project)?;
         let server_state = project_state.servers.get_mut(&server_id)?;
-        let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
-            let language = project.read(cx).languages().language_for_name("JSON");
-            let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
-            cx.spawn_weak({
-                let buffer = buffer.clone();
-                |_, mut cx| async move {
-                    let language = language.await.ok();
-                    buffer.update(&mut cx, |buffer, cx| {
-                        buffer.set_language(language, cx);
-                    });
-                }
-            })
-            .detach();
-
-            LanguageServerRpcState {
-                buffer,
+        let rpc_state = server_state
+            .rpc_state
+            .get_or_insert_with(|| LanguageServerRpcState {
+                rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
                 last_message_kind: None,
-            }
-        });
-        Some(rpc_state.buffer.clone())
+            });
+        Some(rpc_state)
     }
 
     pub fn disable_rpc_trace_for_language_server(
@@ -328,7 +315,7 @@ impl LogStore {
             IoKind::StdIn => false,
             IoKind::StdErr => {
                 let project = project.upgrade(cx)?;
-                let message = format!("stderr: {}\n", message.trim());
+                let message = format!("stderr: {}", message.trim());
                 self.add_language_server_log(&project, language_server_id, &message, cx);
                 return Some(());
             }
@@ -341,24 +328,37 @@ impl LogStore {
             .get_mut(&language_server_id)?
             .rpc_state
             .as_mut()?;
-        state.buffer.update(cx, |buffer, cx| {
-            let kind = if is_received {
-                MessageKind::Receive
-            } else {
-                MessageKind::Send
+        let kind = if is_received {
+            MessageKind::Receive
+        } else {
+            MessageKind::Send
+        };
+
+        let rpc_log_lines = &mut state.rpc_messages;
+        if state.last_message_kind != Some(kind) {
+            let line_before_message = match kind {
+                MessageKind::Send => SEND_LINE,
+                MessageKind::Receive => RECEIVE_LINE,
             };
-            if state.last_message_kind != Some(kind) {
-                let len = buffer.len();
-                let line = match kind {
-                    MessageKind::Send => SEND_LINE,
-                    MessageKind::Receive => RECEIVE_LINE,
-                };
-                buffer.edit([(len..len, line)], None, cx);
-                state.last_message_kind = Some(kind);
-            }
-            let len = buffer.len();
-            buffer.edit([(len..len, message)], None, cx);
+            rpc_log_lines.push_back(line_before_message.to_string());
+            cx.emit(Event::NewServerLogEntry {
+                id: language_server_id,
+                entry: line_before_message.to_string(),
+                is_rpc: true,
+            });
+        }
+
+        while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
+            rpc_log_lines.pop_front();
+        }
+        let message = message.trim();
+        rpc_log_lines.push_back(message.to_string());
+        cx.emit(Event::NewServerLogEntry {
+            id: language_server_id,
+            entry: message.to_string(),
+            is_rpc: true,
         });
+        cx.notify();
         Some(())
     }
 }
@@ -374,8 +374,7 @@ impl LspLogView {
             .projects
             .get(&project.downgrade())
             .and_then(|project| project.servers.keys().copied().next());
-        let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
-        let _log_store_subscription = cx.observe(&log_store, |this, store, cx| {
+        let model_changes_subscription = cx.observe(&log_store, |this, store, cx| {
             (|| -> Option<()> {
                 let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
                 if let Some(current_lsp) = this.current_server_id {
@@ -411,13 +410,31 @@ impl LspLogView {
 
             cx.notify();
         });
+        let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
+            Event::NewServerLogEntry { id, entry, is_rpc } => {
+                if log_view.current_server_id == Some(*id) {
+                    if (*is_rpc && log_view.is_showing_rpc_trace)
+                        || (!*is_rpc && !log_view.is_showing_rpc_trace)
+                    {
+                        log_view.editor.update(cx, |editor, cx| {
+                            editor.set_read_only(false);
+                            editor.handle_input(entry.trim(), cx);
+                            editor.handle_input("\n", cx);
+                            editor.set_read_only(true);
+                        });
+                    }
+                }
+            }
+        });
+        let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx);
         let mut this = Self {
-            editor: Self::editor_for_buffer(project.clone(), buffer, cx),
+            editor,
+            editor_subscription,
             project,
             log_store,
             current_server_id: None,
             is_showing_rpc_trace: false,
-            _log_store_subscription,
+            _log_store_subscriptions: vec![model_changes_subscription, events_subscriptions],
         };
         if let Some(server_id) = server_id {
             this.show_logs_for_server(server_id, cx);
@@ -425,20 +442,19 @@ impl LspLogView {
         this
     }
 
-    fn editor_for_buffer(
-        project: ModelHandle<Project>,
-        buffer: ModelHandle<Buffer>,
+    fn editor_for_logs(
+        log_contents: String,
         cx: &mut ViewContext<Self>,
-    ) -> ViewHandle<Editor> {
+    ) -> (ViewHandle<Editor>, Subscription) {
         let editor = cx.add_view(|cx| {
-            let mut editor = Editor::for_buffer(buffer, Some(project), cx);
+            let mut editor = Editor::multi_line(None, cx);
+            editor.set_text(log_contents, cx);
+            editor.move_to_end(&MoveToEnd, cx);
             editor.set_read_only(true);
-            editor.move_to_end(&Default::default(), cx);
             editor
         });
-        cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
-            .detach();
-        editor
+        let editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()));
+        (editor, editor_subscription)
     }
 
     pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
@@ -487,14 +503,17 @@ impl LspLogView {
     }
 
     fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
-        let buffer = self
+        let log_contents = self
             .log_store
             .read(cx)
-            .log_buffer_for_server(&self.project, server_id);
-        if let Some(buffer) = buffer {
+            .server_logs(&self.project, server_id)
+            .map(log_contents);
+        if let Some(log_contents) = log_contents {
             self.current_server_id = Some(server_id);
             self.is_showing_rpc_trace = false;
-            self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
+            let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx);
+            self.editor = editor;
+            self.editor_subscription = editor_subscription;
             cx.notify();
         }
     }
@@ -504,13 +523,37 @@ impl LspLogView {
         server_id: LanguageServerId,
         cx: &mut ViewContext<Self>,
     ) {
-        let buffer = self.log_store.update(cx, |log_set, cx| {
-            log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
+        let rpc_log = self.log_store.update(cx, |log_store, _| {
+            log_store
+                .enable_rpc_trace_for_language_server(&self.project, server_id)
+                .map(|state| log_contents(&state.rpc_messages))
         });
-        if let Some(buffer) = buffer {
+        if let Some(rpc_log) = rpc_log {
             self.current_server_id = Some(server_id);
             self.is_showing_rpc_trace = true;
-            self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
+            let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx);
+            let language = self.project.read(cx).languages().language_for_name("JSON");
+            editor
+                .read(cx)
+                .buffer()
+                .read(cx)
+                .as_singleton()
+                .expect("log buffer should be a singleton")
+                .update(cx, |_, cx| {
+                    cx.spawn_weak({
+                        let buffer = cx.handle();
+                        |_, mut cx| async move {
+                            let language = language.await.ok();
+                            buffer.update(&mut cx, |buffer, cx| {
+                                buffer.set_language(language, cx);
+                            });
+                        }
+                    })
+                    .detach();
+                });
+
+            self.editor = editor;
+            self.editor_subscription = editor_subscription;
             cx.notify();
         }
     }
@@ -523,7 +566,7 @@ impl LspLogView {
     ) {
         self.log_store.update(cx, |log_store, cx| {
             if enabled {
-                log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
+                log_store.enable_rpc_trace_for_language_server(&self.project, server_id);
             } else {
                 log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
             }
@@ -535,6 +578,16 @@ impl LspLogView {
     }
 }
 
+fn log_contents(lines: &VecDeque<String>) -> String {
+    let (a, b) = lines.as_slices();
+    let log_contents = a.join("\n");
+    if b.is_empty() {
+        log_contents
+    } else {
+        log_contents + "\n" + &b.join("\n")
+    }
+}
+
 impl View for LspLogView {
     fn ui_name() -> &'static str {
         "LspLogView"
@@ -685,6 +738,7 @@ impl View for LspLogToolbarItemView {
         });
         let server_selected = current_server.is_some();
 
+        enum LspLogScroll {}
         enum Menu {}
         let lsp_menu = Stack::new()
             .with_child(Self::render_language_server_menu_header(
@@ -697,7 +751,7 @@ impl View for LspLogToolbarItemView {
                     Overlay::new(
                         MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
                             Flex::column()
-                                .scrollable::<Self>(0, None, cx)
+                                .scrollable::<LspLogScroll>(0, None, cx)
                                 .with_children(menu_rows.into_iter().map(|row| {
                                     Self::render_language_server_menu_item(
                                         row.server_id,
@@ -876,6 +930,7 @@ impl LspLogToolbarItemView {
     ) -> impl Element<Self> {
         enum ActivateLog {}
         enum ActivateRpcTrace {}
+        enum LanguageServerCheckbox {}
 
         Flex::column()
             .with_child({
@@ -921,7 +976,7 @@ impl LspLogToolbarItemView {
                                 .with_height(theme.toolbar_dropdown_menu.row_height),
                         )
                         .with_child(
-                            ui::checkbox_with_label::<Self, _, Self, _>(
+                            ui::checkbox_with_label::<LanguageServerCheckbox, _, Self, _>(
                                 Empty::new(),
                                 &theme.welcome.checkbox,
                                 rpc_trace_enabled,
@@ -947,8 +1002,16 @@ impl LspLogToolbarItemView {
     }
 }
 
+pub enum Event {
+    NewServerLogEntry {
+        id: LanguageServerId,
+        entry: String,
+        is_rpc: bool,
+    },
+}
+
 impl Entity for LogStore {
-    type Event = ();
+    type Event = Event;
 }
 
 impl Entity for LspLogView {

crates/live_kit_client/examples/test_app.rs 🔗

@@ -61,7 +61,7 @@ fn main() {
 
             let mut audio_track_updates = room_b.remote_audio_track_updates();
             let audio_track = LocalAudioTrack::create();
-            let audio_track_publication = room_a.publish_audio_track(&audio_track).await.unwrap();
+            let audio_track_publication = room_a.publish_audio_track(audio_track).await.unwrap();
 
             if let RemoteAudioTrackUpdate::Subscribed(track, _) =
                 audio_track_updates.next().await.unwrap()
@@ -132,10 +132,8 @@ fn main() {
             let display = displays.into_iter().next().unwrap();
 
             let local_video_track = LocalVideoTrack::screen_share_for_display(&display);
-            let local_video_track_publication = room_a
-                .publish_video_track(&local_video_track)
-                .await
-                .unwrap();
+            let local_video_track_publication =
+                room_a.publish_video_track(local_video_track).await.unwrap();
 
             if let RemoteVideoTrackUpdate::Subscribed(track) =
                 video_track_updates.next().await.unwrap()

crates/live_kit_client/src/prod.rs 🔗

@@ -229,7 +229,7 @@ impl Room {
 
     pub fn publish_video_track(
         self: &Arc<Self>,
-        track: &LocalVideoTrack,
+        track: LocalVideoTrack,
     ) -> impl Future<Output = Result<LocalTrackPublication>> {
         let (tx, rx) = oneshot::channel::<Result<LocalTrackPublication>>();
         extern "C" fn callback(tx: *mut c_void, publication: *mut c_void, error: CFStringRef) {
@@ -255,7 +255,7 @@ impl Room {
 
     pub fn publish_audio_track(
         self: &Arc<Self>,
-        track: &LocalAudioTrack,
+        track: LocalAudioTrack,
     ) -> impl Future<Output = Result<LocalTrackPublication>> {
         let (tx, rx) = oneshot::channel::<Result<LocalTrackPublication>>();
         extern "C" fn callback(tx: *mut c_void, publication: *mut c_void, error: CFStringRef) {
@@ -621,6 +621,7 @@ impl Drop for RoomDelegate {
 }
 
 pub struct LocalAudioTrack(*const c_void);
+unsafe impl Send for LocalAudioTrack {}
 
 impl LocalAudioTrack {
     pub fn create() -> Self {
@@ -635,6 +636,7 @@ impl Drop for LocalAudioTrack {
 }
 
 pub struct LocalVideoTrack(*const c_void);
+unsafe impl Send for LocalVideoTrack {}
 
 impl LocalVideoTrack {
     pub fn screen_share_for_display(display: &MacOSDisplay) -> Self {
@@ -649,6 +651,7 @@ impl Drop for LocalVideoTrack {
 }
 
 pub struct LocalTrackPublication(*const c_void);
+unsafe impl Send for LocalTrackPublication {}
 
 impl LocalTrackPublication {
     pub fn new(native_track_publication: *const c_void) -> Self {
@@ -692,6 +695,8 @@ impl Drop for LocalTrackPublication {
 
 pub struct RemoteTrackPublication(*const c_void);
 
+unsafe impl Send for RemoteTrackPublication {}
+
 impl RemoteTrackPublication {
     pub fn new(native_track_publication: *const c_void) -> Self {
         unsafe {
@@ -747,6 +752,8 @@ pub struct RemoteAudioTrack {
     publisher_id: String,
 }
 
+unsafe impl Send for RemoteAudioTrack {}
+
 impl RemoteAudioTrack {
     fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
         unsafe {
@@ -783,6 +790,8 @@ pub struct RemoteVideoTrack {
     publisher_id: String,
 }
 
+unsafe impl Send for RemoteVideoTrack {}
+
 impl RemoteVideoTrack {
     fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
         unsafe {
@@ -864,6 +873,8 @@ pub enum RemoteAudioTrackUpdate {
 
 pub struct MacOSDisplay(*const c_void);
 
+unsafe impl Send for MacOSDisplay {}
+
 impl MacOSDisplay {
     fn new(ptr: *const c_void) -> Self {
         unsafe {

crates/live_kit_client/src/test.rs 🔗

@@ -306,6 +306,16 @@ impl live_kit_server::api::Client for TestApiClient {
             token::VideoGrant::to_join(room),
         )
     }
+
+    fn guest_token(&self, room: &str, identity: &str) -> Result<String> {
+        let server = TestServer::get(&self.url)?;
+        token::create(
+            &server.api_key,
+            &server.secret_key,
+            Some(identity),
+            token::VideoGrant::for_guest(room),
+        )
+    }
 }
 
 pub type Sid = String;
@@ -371,7 +381,7 @@ impl Room {
 
     pub fn publish_video_track(
         self: &Arc<Self>,
-        track: &LocalVideoTrack,
+        track: LocalVideoTrack,
     ) -> impl Future<Output = Result<LocalTrackPublication>> {
         let this = self.clone();
         let track = track.clone();
@@ -384,7 +394,7 @@ impl Room {
     }
     pub fn publish_audio_track(
         self: &Arc<Self>,
-        track: &LocalAudioTrack,
+        track: LocalAudioTrack,
     ) -> impl Future<Output = Result<LocalTrackPublication>> {
         let this = self.clone();
         let track = track.clone();

crates/live_kit_server/src/api.rs 🔗

@@ -12,6 +12,7 @@ pub trait Client: Send + Sync {
     async fn delete_room(&self, name: String) -> Result<()>;
     async fn remove_participant(&self, room: String, identity: String) -> Result<()>;
     fn room_token(&self, room: &str, identity: &str) -> Result<String>;
+    fn guest_token(&self, room: &str, identity: &str) -> Result<String>;
 }
 
 #[derive(Clone)]
@@ -138,4 +139,13 @@ impl Client for LiveKitClient {
             token::VideoGrant::to_join(room),
         )
     }
+
+    fn guest_token(&self, room: &str, identity: &str) -> Result<String> {
+        token::create(
+            &self.key,
+            &self.secret,
+            Some(identity),
+            token::VideoGrant::for_guest(room),
+        )
+    }
 }

crates/live_kit_server/src/token.rs 🔗

@@ -57,6 +57,15 @@ impl<'a> VideoGrant<'a> {
             ..Default::default()
         }
     }
+
+    pub fn for_guest(room: &'a str) -> Self {
+        Self {
+            room: Some(Cow::Borrowed(room)),
+            room_join: Some(true),
+            can_subscribe: Some(true),
+            ..Default::default()
+        }
+    }
 }
 
 pub fn create(

crates/lsp/src/lsp.rs 🔗

@@ -136,6 +136,7 @@ struct Error {
 
 impl LanguageServer {
     pub fn new(
+        stderr_capture: Arc<Mutex<Option<String>>>,
         server_id: LanguageServerId,
         binary: LanguageServerBinary,
         root_path: &Path,
@@ -165,6 +166,7 @@ impl LanguageServer {
             stdin,
             stdout,
             Some(stderr),
+            stderr_capture,
             Some(server),
             root_path,
             code_action_kinds,
@@ -197,6 +199,7 @@ impl LanguageServer {
         stdin: Stdin,
         stdout: Stdout,
         stderr: Option<Stderr>,
+        stderr_capture: Arc<Mutex<Option<String>>>,
         server: Option<Child>,
         root_path: &Path,
         code_action_kinds: Option<Vec<CodeActionKind>>,
@@ -218,20 +221,23 @@ impl LanguageServer {
         let io_handlers = Arc::new(Mutex::new(HashMap::default()));
 
         let stdout_input_task = cx.spawn(|cx| {
-            {
-                Self::handle_input(
-                    stdout,
-                    on_unhandled_notification.clone(),
-                    notification_handlers.clone(),
-                    response_handlers.clone(),
-                    io_handlers.clone(),
-                    cx,
-                )
-            }
+            Self::handle_input(
+                stdout,
+                on_unhandled_notification.clone(),
+                notification_handlers.clone(),
+                response_handlers.clone(),
+                io_handlers.clone(),
+                cx,
+            )
             .log_err()
         });
         let stderr_input_task = stderr
-            .map(|stderr| cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err()))
+            .map(|stderr| {
+                cx.spawn(|_| {
+                    Self::handle_stderr(stderr, io_handlers.clone(), stderr_capture.clone())
+                        .log_err()
+                })
+            })
             .unwrap_or_else(|| Task::Ready(Some(None)));
         let input_task = cx.spawn(|_| async move {
             let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task);
@@ -353,12 +359,14 @@ impl LanguageServer {
     async fn handle_stderr<Stderr>(
         stderr: Stderr,
         io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
+        stderr_capture: Arc<Mutex<Option<String>>>,
     ) -> anyhow::Result<()>
     where
         Stderr: AsyncRead + Unpin + Send + 'static,
     {
         let mut stderr = BufReader::new(stderr);
         let mut buffer = Vec::new();
+
         loop {
             buffer.clear();
             stderr.read_until(b'\n', &mut buffer).await?;
@@ -367,6 +375,10 @@ impl LanguageServer {
                 for handler in io_handlers.lock().values_mut() {
                     handler(IoKind::StdErr, message);
                 }
+
+                if let Some(stderr) = stderr_capture.lock().as_mut() {
+                    stderr.push_str(message);
+                }
             }
 
             // Don't starve the main thread when receiving lots of messages at once.
@@ -466,7 +478,10 @@ impl LanguageServer {
                         completion_item: Some(CompletionItemCapability {
                             snippet_support: Some(true),
                             resolve_support: Some(CompletionItemCapabilityResolveSupport {
-                                properties: vec!["additionalTextEdits".to_string()],
+                                properties: vec![
+                                    "documentation".to_string(),
+                                    "additionalTextEdits".to_string(),
+                                ],
                             }),
                             ..Default::default()
                         }),
@@ -748,6 +763,15 @@ impl LanguageServer {
         )
     }
 
+    // some child of string literal (be it "" or ``) which is the child of an attribute
+
+    // <Foo className="bar" />
+    // <Foo className={`bar`} />
+    // <Foo className={something + "bar"} />
+    // <Foo className={something + "bar"} />
+    // const classes = "awesome ";
+    // <Foo className={classes} />
+
     fn request_internal<T: request::Request>(
         next_id: &AtomicUsize,
         response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
@@ -926,6 +950,7 @@ impl LanguageServer {
             stdin_writer,
             stdout_reader,
             None::<async_pipe::PipeReader>,
+            Arc::new(Mutex::new(None)),
             None,
             Path::new("/"),
             None,
@@ -938,6 +963,7 @@ impl LanguageServer {
                 stdout_writer,
                 stdin_reader,
                 None::<async_pipe::PipeReader>,
+                Arc::new(Mutex::new(None)),
                 None,
                 Path::new("/"),
                 None,

crates/multi_buffer/Cargo.toml 🔗

@@ -0,0 +1,80 @@
+[package]
+name = "multi_buffer"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/multi_buffer.rs"
+doctest = false
+
+[features]
+test-support = [
+    "copilot/test-support",
+    "text/test-support",
+    "language/test-support",
+    "gpui/test-support",
+    "util/test-support",
+    "tree-sitter-rust",
+    "tree-sitter-typescript"
+]
+
+[dependencies]
+client = { path = "../client" }
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
+git = { path = "../git" }
+gpui = { path = "../gpui" }
+language = { path = "../language" }
+lsp = { path = "../lsp" }
+rich_text = { path = "../rich_text" }
+settings = { path = "../settings" }
+snippet = { path = "../snippet" }
+sum_tree = { path = "../sum_tree" }
+text = { path = "../text" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+
+aho-corasick = "1.1"
+anyhow.workspace = true
+convert_case = "0.6.0"
+futures.workspace = true
+indoc = "1.0.4"
+itertools = "0.10"
+lazy_static.workspace = true
+log.workspace = true
+ordered-float.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+pulldown-cmark = { version = "0.9.2", default-features = false }
+rand.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smallvec.workspace = true
+smol.workspace = true
+
+tree-sitter-rust = { workspace = true, optional = true }
+tree-sitter-html = { workspace = true, optional = true }
+tree-sitter-typescript = { workspace = true, optional = true }
+
+[dev-dependencies]
+copilot = { path = "../copilot", features = ["test-support"] }
+text = { path = "../text", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+
+ctor.workspace = true
+env_logger.workspace = true
+rand.workspace = true
+unindent.workspace = true
+tree-sitter.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-html.workspace = true
+tree-sitter-typescript.workspace = true

crates/editor/src/multi_buffer/anchor.rs → crates/multi_buffer/src/anchor.rs 🔗

@@ -8,9 +8,9 @@ use sum_tree::Bias;
 
 #[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
 pub struct Anchor {
-    pub(crate) buffer_id: Option<u64>,
-    pub(crate) excerpt_id: ExcerptId,
-    pub(crate) text_anchor: text::Anchor,
+    pub buffer_id: Option<u64>,
+    pub excerpt_id: ExcerptId,
+    pub text_anchor: text::Anchor,
 }
 
 impl Anchor {
@@ -30,10 +30,6 @@ impl Anchor {
         }
     }
 
-    pub fn excerpt_id(&self) -> ExcerptId {
-        self.excerpt_id
-    }
-
     pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
         let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot);
         if excerpt_id_cmp.is_eq() {

crates/editor/src/multi_buffer.rs → crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -303,7 +303,7 @@ impl MultiBuffer {
         self.snapshot.borrow().clone()
     }
 
-    pub(crate) fn read(&self, cx: &AppContext) -> Ref<MultiBufferSnapshot> {
+    pub fn read(&self, cx: &AppContext) -> Ref<MultiBufferSnapshot> {
         self.sync(cx);
         self.snapshot.borrow()
     }
@@ -498,84 +498,98 @@ impl MultiBuffer {
             }
         }
 
-        for (buffer_id, mut edits) in buffer_edits {
-            edits.sort_unstable_by_key(|edit| edit.range.start);
-            self.buffers.borrow()[&buffer_id]
-                .buffer
-                .update(cx, |buffer, cx| {
-                    let mut edits = edits.into_iter().peekable();
-                    let mut insertions = Vec::new();
-                    let mut original_indent_columns = Vec::new();
-                    let mut deletions = Vec::new();
-                    let empty_str: Arc<str> = "".into();
-                    while let Some(BufferEdit {
-                        mut range,
-                        new_text,
-                        mut is_insertion,
-                        original_indent_column,
-                    }) = edits.next()
-                    {
+        drop(cursor);
+        drop(snapshot);
+        // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR.
+        fn tail(
+            this: &mut MultiBuffer,
+            buffer_edits: HashMap<u64, Vec<BufferEdit>>,
+            autoindent_mode: Option<AutoindentMode>,
+            edited_excerpt_ids: Vec<ExcerptId>,
+            cx: &mut ModelContext<MultiBuffer>,
+        ) {
+            for (buffer_id, mut edits) in buffer_edits {
+                edits.sort_unstable_by_key(|edit| edit.range.start);
+                this.buffers.borrow()[&buffer_id]
+                    .buffer
+                    .update(cx, |buffer, cx| {
+                        let mut edits = edits.into_iter().peekable();
+                        let mut insertions = Vec::new();
+                        let mut original_indent_columns = Vec::new();
+                        let mut deletions = Vec::new();
+                        let empty_str: Arc<str> = "".into();
                         while let Some(BufferEdit {
-                            range: next_range,
-                            is_insertion: next_is_insertion,
-                            ..
-                        }) = edits.peek()
+                            mut range,
+                            new_text,
+                            mut is_insertion,
+                            original_indent_column,
+                        }) = edits.next()
                         {
-                            if range.end >= next_range.start {
-                                range.end = cmp::max(next_range.end, range.end);
-                                is_insertion |= *next_is_insertion;
-                                edits.next();
-                            } else {
-                                break;
+                            while let Some(BufferEdit {
+                                range: next_range,
+                                is_insertion: next_is_insertion,
+                                ..
+                            }) = edits.peek()
+                            {
+                                if range.end >= next_range.start {
+                                    range.end = cmp::max(next_range.end, range.end);
+                                    is_insertion |= *next_is_insertion;
+                                    edits.next();
+                                } else {
+                                    break;
+                                }
                             }
-                        }
 
-                        if is_insertion {
-                            original_indent_columns.push(original_indent_column);
-                            insertions.push((
-                                buffer.anchor_before(range.start)..buffer.anchor_before(range.end),
-                                new_text.clone(),
-                            ));
-                        } else if !range.is_empty() {
-                            deletions.push((
-                                buffer.anchor_before(range.start)..buffer.anchor_before(range.end),
-                                empty_str.clone(),
-                            ));
+                            if is_insertion {
+                                original_indent_columns.push(original_indent_column);
+                                insertions.push((
+                                    buffer.anchor_before(range.start)
+                                        ..buffer.anchor_before(range.end),
+                                    new_text.clone(),
+                                ));
+                            } else if !range.is_empty() {
+                                deletions.push((
+                                    buffer.anchor_before(range.start)
+                                        ..buffer.anchor_before(range.end),
+                                    empty_str.clone(),
+                                ));
+                            }
                         }
-                    }
 
-                    let deletion_autoindent_mode =
-                        if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
-                            Some(AutoindentMode::Block {
-                                original_indent_columns: Default::default(),
-                            })
-                        } else {
-                            None
-                        };
-                    let insertion_autoindent_mode =
-                        if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
-                            Some(AutoindentMode::Block {
-                                original_indent_columns,
-                            })
-                        } else {
-                            None
-                        };
+                        let deletion_autoindent_mode =
+                            if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
+                                Some(AutoindentMode::Block {
+                                    original_indent_columns: Default::default(),
+                                })
+                            } else {
+                                None
+                            };
+                        let insertion_autoindent_mode =
+                            if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
+                                Some(AutoindentMode::Block {
+                                    original_indent_columns,
+                                })
+                            } else {
+                                None
+                            };
 
-                    buffer.edit(deletions, deletion_autoindent_mode, cx);
-                    buffer.edit(insertions, insertion_autoindent_mode, cx);
-                })
-        }
+                        buffer.edit(deletions, deletion_autoindent_mode, cx);
+                        buffer.edit(insertions, insertion_autoindent_mode, cx);
+                    })
+            }
 
-        cx.emit(Event::ExcerptsEdited {
-            ids: edited_excerpt_ids,
-        });
+            cx.emit(Event::ExcerptsEdited {
+                ids: edited_excerpt_ids,
+            });
+        }
+        tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx);
     }
 
     pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
         self.start_transaction_at(Instant::now(), cx)
     }
 
-    pub(crate) fn start_transaction_at(
+    pub fn start_transaction_at(
         &mut self,
         now: Instant,
         cx: &mut ModelContext<Self>,
@@ -594,7 +608,7 @@ impl MultiBuffer {
         self.end_transaction_at(Instant::now(), cx)
     }
 
-    pub(crate) fn end_transaction_at(
+    pub fn end_transaction_at(
         &mut self,
         now: Instant,
         cx: &mut ModelContext<Self>,
@@ -1494,7 +1508,7 @@ impl MultiBuffer {
         "untitled".into()
     }
 
-    #[cfg(test)]
+    #[cfg(any(test, feature = "test-support"))]
     pub fn is_parsing(&self, cx: &AppContext) -> bool {
         self.as_singleton().unwrap().read(cx).is_parsing()
     }
@@ -3184,7 +3198,7 @@ impl MultiBufferSnapshot {
         theme: Option<&SyntaxTheme>,
     ) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
         let anchor = self.anchor_before(offset);
-        let excerpt_id = anchor.excerpt_id();
+        let excerpt_id = anchor.excerpt_id;
         let excerpt = self.excerpt(excerpt_id)?;
         Some((
             excerpt.buffer_id,
@@ -4115,17 +4129,13 @@ where
 
 #[cfg(test)]
 mod tests {
-    use crate::editor_tests::init_test;
-
     use super::*;
     use futures::StreamExt;
     use gpui::{AppContext, TestAppContext};
     use language::{Buffer, Rope};
-    use project::{FakeFs, Project};
     use rand::prelude::*;
     use settings::SettingsStore;
     use std::{env, rc::Rc};
-    use unindent::Unindent;
     use util::test::sample_text;
 
     #[gpui::test]
@@ -4824,190 +4834,6 @@ mod tests {
         );
     }
 
-    #[gpui::test]
-    async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
-        use git::diff::DiffHunkStatus;
-        init_test(cx, |_| {});
-
-        let fs = FakeFs::new(cx.background());
-        let project = Project::test(fs, [], cx).await;
-
-        // buffer has two modified hunks with two rows each
-        let buffer_1 = project
-            .update(cx, |project, cx| {
-                project.create_buffer(
-                    "
-                        1.zero
-                        1.ONE
-                        1.TWO
-                        1.three
-                        1.FOUR
-                        1.FIVE
-                        1.six
-                    "
-                    .unindent()
-                    .as_str(),
-                    None,
-                    cx,
-                )
-            })
-            .unwrap();
-        buffer_1.update(cx, |buffer, cx| {
-            buffer.set_diff_base(
-                Some(
-                    "
-                        1.zero
-                        1.one
-                        1.two
-                        1.three
-                        1.four
-                        1.five
-                        1.six
-                    "
-                    .unindent(),
-                ),
-                cx,
-            );
-        });
-
-        // buffer has a deletion hunk and an insertion hunk
-        let buffer_2 = project
-            .update(cx, |project, cx| {
-                project.create_buffer(
-                    "
-                        2.zero
-                        2.one
-                        2.two
-                        2.three
-                        2.four
-                        2.five
-                        2.six
-                    "
-                    .unindent()
-                    .as_str(),
-                    None,
-                    cx,
-                )
-            })
-            .unwrap();
-        buffer_2.update(cx, |buffer, cx| {
-            buffer.set_diff_base(
-                Some(
-                    "
-                        2.zero
-                        2.one
-                        2.one-and-a-half
-                        2.two
-                        2.three
-                        2.four
-                        2.six
-                    "
-                    .unindent(),
-                ),
-                cx,
-            );
-        });
-
-        cx.foreground().run_until_parked();
-
-        let multibuffer = cx.add_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(0);
-            multibuffer.push_excerpts(
-                buffer_1.clone(),
-                [
-                    // excerpt ends in the middle of a modified hunk
-                    ExcerptRange {
-                        context: Point::new(0, 0)..Point::new(1, 5),
-                        primary: Default::default(),
-                    },
-                    // excerpt begins in the middle of a modified hunk
-                    ExcerptRange {
-                        context: Point::new(5, 0)..Point::new(6, 5),
-                        primary: Default::default(),
-                    },
-                ],
-                cx,
-            );
-            multibuffer.push_excerpts(
-                buffer_2.clone(),
-                [
-                    // excerpt ends at a deletion
-                    ExcerptRange {
-                        context: Point::new(0, 0)..Point::new(1, 5),
-                        primary: Default::default(),
-                    },
-                    // excerpt starts at a deletion
-                    ExcerptRange {
-                        context: Point::new(2, 0)..Point::new(2, 5),
-                        primary: Default::default(),
-                    },
-                    // excerpt fully contains a deletion hunk
-                    ExcerptRange {
-                        context: Point::new(1, 0)..Point::new(2, 5),
-                        primary: Default::default(),
-                    },
-                    // excerpt fully contains an insertion hunk
-                    ExcerptRange {
-                        context: Point::new(4, 0)..Point::new(6, 5),
-                        primary: Default::default(),
-                    },
-                ],
-                cx,
-            );
-            multibuffer
-        });
-
-        let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
-
-        assert_eq!(
-            snapshot.text(),
-            "
-                1.zero
-                1.ONE
-                1.FIVE
-                1.six
-                2.zero
-                2.one
-                2.two
-                2.one
-                2.two
-                2.four
-                2.five
-                2.six"
-                .unindent()
-        );
-
-        let expected = [
-            (DiffHunkStatus::Modified, 1..2),
-            (DiffHunkStatus::Modified, 2..3),
-            //TODO: Define better when and where removed hunks show up at range extremities
-            (DiffHunkStatus::Removed, 6..6),
-            (DiffHunkStatus::Removed, 8..8),
-            (DiffHunkStatus::Added, 10..11),
-        ];
-
-        assert_eq!(
-            snapshot
-                .git_diff_hunks_in_range(0..12)
-                .map(|hunk| (hunk.status(), hunk.buffer_range))
-                .collect::<Vec<_>>(),
-            &expected,
-        );
-
-        assert_eq!(
-            snapshot
-                .git_diff_hunks_in_range_rev(0..12)
-                .map(|hunk| (hunk.status(), hunk.buffer_range))
-                .collect::<Vec<_>>(),
-            expected
-                .iter()
-                .rev()
-                .cloned()
-                .collect::<Vec<_>>()
-                .as_slice(),
-        );
-    }
-
     #[gpui::test(iterations = 100)]
     fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) {
         let operations = env::var("OPERATIONS")

crates/node_runtime/src/node_runtime.rs 🔗

@@ -220,96 +220,31 @@ impl NodeRuntime for RealNodeRuntime {
     }
 }
 
-pub struct FakeNodeRuntime(Option<PrettierSupport>);
-
-struct PrettierSupport {
-    plugins: Vec<&'static str>,
-}
+pub struct FakeNodeRuntime;
 
 impl FakeNodeRuntime {
     pub fn new() -> Arc<dyn NodeRuntime> {
-        Arc::new(FakeNodeRuntime(None))
-    }
-
-    pub fn with_prettier_support(plugins: &[&'static str]) -> Arc<dyn NodeRuntime> {
-        Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins))))
+        Arc::new(Self)
     }
 }
 
 #[async_trait::async_trait]
 impl NodeRuntime for FakeNodeRuntime {
     async fn binary_path(&self) -> anyhow::Result<PathBuf> {
-        if let Some(prettier_support) = &self.0 {
-            prettier_support.binary_path().await
-        } else {
-            unreachable!()
-        }
+        unreachable!()
     }
 
     async fn run_npm_subcommand(
         &self,
-        directory: Option<&Path>,
+        _: Option<&Path>,
         subcommand: &str,
         args: &[&str],
     ) -> anyhow::Result<Output> {
-        if let Some(prettier_support) = &self.0 {
-            prettier_support
-                .run_npm_subcommand(directory, subcommand, args)
-                .await
-        } else {
-            unreachable!()
-        }
-    }
-
-    async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
-        if let Some(prettier_support) = &self.0 {
-            prettier_support.npm_package_latest_version(name).await
-        } else {
-            unreachable!()
-        }
-    }
-
-    async fn npm_install_packages(
-        &self,
-        directory: &Path,
-        packages: &[(&str, &str)],
-    ) -> anyhow::Result<()> {
-        if let Some(prettier_support) = &self.0 {
-            prettier_support
-                .npm_install_packages(directory, packages)
-                .await
-        } else {
-            unreachable!()
-        }
-    }
-}
-
-impl PrettierSupport {
-    const PACKAGE_VERSION: &str = "0.0.1";
-
-    fn new(plugins: &[&'static str]) -> Self {
-        Self {
-            plugins: plugins.to_vec(),
-        }
-    }
-}
-
-#[async_trait::async_trait]
-impl NodeRuntime for PrettierSupport {
-    async fn binary_path(&self) -> anyhow::Result<PathBuf> {
-        Ok(PathBuf::from("prettier_fake_node"))
-    }
-
-    async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
-        unreachable!()
+        unreachable!("Should not run npm subcommand '{subcommand}' with args {args:?}")
     }
 
     async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
-        if name == "prettier" || self.plugins.contains(&name) {
-            Ok(Self::PACKAGE_VERSION.to_string())
-        } else {
-            panic!("Unexpected package name: {name}")
-        }
+        unreachable!("Should not query npm package '{name}' for latest version")
     }
 
     async fn npm_install_packages(
@@ -317,32 +252,6 @@ impl NodeRuntime for PrettierSupport {
         _: &Path,
         packages: &[(&str, &str)],
     ) -> anyhow::Result<()> {
-        assert_eq!(
-            packages.len(),
-            self.plugins.len() + 1,
-            "Unexpected packages length to install: {:?}, expected `prettier` + {:?}",
-            packages,
-            self.plugins
-        );
-        for (name, version) in packages {
-            assert!(
-                name == &"prettier" || self.plugins.contains(name),
-                "Unexpected package `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
-                name,
-                packages,
-                Self::PACKAGE_VERSION,
-                self.plugins
-            );
-            assert_eq!(
-                version,
-                &Self::PACKAGE_VERSION,
-                "Unexpected package version `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
-                version,
-                packages,
-                Self::PACKAGE_VERSION,
-                self.plugins
-            );
-        }
-        Ok(())
+        unreachable!("Should not install packages {packages:?}")
     }
 }

crates/notifications/Cargo.toml 🔗

@@ -0,0 +1,42 @@
+[package]
+name = "notifications"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/notification_store.rs"
+doctest = false
+
+[features]
+test-support = [
+    "channel/test-support",
+    "collections/test-support",
+    "gpui/test-support",
+    "rpc/test-support",
+]
+
+[dependencies]
+channel = { path = "../channel" }
+client = { path = "../client" }
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+db = { path = "../db" }
+feature_flags = { path = "../feature_flags" }
+gpui = { path = "../gpui" }
+rpc = { path = "../rpc" }
+settings = { path = "../settings" }
+sum_tree = { path = "../sum_tree" }
+text = { path = "../text" }
+util = { path = "../util" }
+
+anyhow.workspace = true
+time.workspace = true
+
+[dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
+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/notifications/src/notification_store.rs 🔗

@@ -0,0 +1,459 @@
+use anyhow::Result;
+use channel::{ChannelMessage, ChannelMessageId, ChannelStore};
+use client::{Client, UserStore};
+use collections::HashMap;
+use db::smol::stream::StreamExt;
+use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
+use rpc::{proto, Notification, TypedEnvelope};
+use std::{ops::Range, sync::Arc};
+use sum_tree::{Bias, SumTree};
+use time::OffsetDateTime;
+use util::ResultExt;
+
+pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
+    let notification_store = cx.add_model(|cx| NotificationStore::new(client, user_store, cx));
+    cx.set_global(notification_store);
+}
+
+pub struct NotificationStore {
+    client: Arc<Client>,
+    user_store: ModelHandle<UserStore>,
+    channel_messages: HashMap<u64, ChannelMessage>,
+    channel_store: ModelHandle<ChannelStore>,
+    notifications: SumTree<NotificationEntry>,
+    loaded_all_notifications: bool,
+    _watch_connection_status: Task<Option<()>>,
+    _subscriptions: Vec<client::Subscription>,
+}
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum NotificationEvent {
+    NotificationsUpdated {
+        old_range: Range<usize>,
+        new_count: usize,
+    },
+    NewNotification {
+        entry: NotificationEntry,
+    },
+    NotificationRemoved {
+        entry: NotificationEntry,
+    },
+    NotificationRead {
+        entry: NotificationEntry,
+    },
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct NotificationEntry {
+    pub id: u64,
+    pub notification: Notification,
+    pub timestamp: OffsetDateTime,
+    pub is_read: bool,
+    pub response: Option<bool>,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct NotificationSummary {
+    max_id: u64,
+    count: usize,
+    unread_count: usize,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct Count(usize);
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct UnreadCount(usize);
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct NotificationId(u64);
+
+impl NotificationStore {
+    pub fn global(cx: &AppContext) -> ModelHandle<Self> {
+        cx.global::<ModelHandle<Self>>().clone()
+    }
+
+    pub fn new(
+        client: Arc<Client>,
+        user_store: ModelHandle<UserStore>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        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 {
+                let this = this.upgrade(&cx)?;
+                match status {
+                    client::Status::Connected { .. } => {
+                        if let Some(task) = this.update(&mut cx, |this, cx| this.handle_connect(cx))
+                        {
+                            task.await.log_err()?;
+                        }
+                    }
+                    _ => this.update(&mut cx, |this, cx| this.handle_disconnect(cx)),
+                }
+            }
+            Some(())
+        });
+
+        Self {
+            channel_store: ChannelStore::global(cx),
+            notifications: Default::default(),
+            loaded_all_notifications: false,
+            channel_messages: Default::default(),
+            _watch_connection_status: watch_connection_status,
+            _subscriptions: vec![
+                client.add_message_handler(cx.handle(), Self::handle_new_notification),
+                client.add_message_handler(cx.handle(), Self::handle_delete_notification),
+            ],
+            user_store,
+            client,
+        }
+    }
+
+    pub fn notification_count(&self) -> usize {
+        self.notifications.summary().count
+    }
+
+    pub fn unread_notification_count(&self) -> usize {
+        self.notifications.summary().unread_count
+    }
+
+    pub fn channel_message_for_id(&self, id: u64) -> Option<&ChannelMessage> {
+        self.channel_messages.get(&id)
+    }
+
+    // Get the nth newest notification.
+    pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> {
+        let count = self.notifications.summary().count;
+        if ix >= count {
+            return None;
+        }
+        let ix = count - 1 - ix;
+        let mut cursor = self.notifications.cursor::<Count>();
+        cursor.seek(&Count(ix), Bias::Right, &());
+        cursor.item()
+    }
+
+    pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> {
+        let mut cursor = self.notifications.cursor::<NotificationId>();
+        cursor.seek(&NotificationId(id), Bias::Left, &());
+        if let Some(item) = cursor.item() {
+            if item.id == id {
+                return Some(item);
+            }
+        }
+        None
+    }
+
+    pub fn load_more_notifications(
+        &self,
+        clear_old: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.loaded_all_notifications && !clear_old {
+            return None;
+        }
+
+        let before_id = if clear_old {
+            None
+        } else {
+            self.notifications.first().map(|entry| entry.id)
+        };
+        let request = self.client.request(proto::GetNotifications { before_id });
+        Some(cx.spawn(|this, mut cx| async move {
+            let response = request.await?;
+            this.update(&mut cx, |this, _| {
+                this.loaded_all_notifications = response.done
+            });
+            Self::add_notifications(
+                this,
+                response.notifications,
+                AddNotificationsOptions {
+                    is_new: false,
+                    clear_old,
+                    includes_first: response.done,
+                },
+                cx,
+            )
+            .await?;
+            Ok(())
+        }))
+    }
+
+    fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Result<()>>> {
+        self.notifications = Default::default();
+        self.channel_messages = Default::default();
+        cx.notify();
+        self.load_more_notifications(true, cx)
+    }
+
+    fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
+        cx.notify()
+    }
+
+    async fn handle_new_notification(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::AddNotification>,
+        _: Arc<Client>,
+        cx: AsyncAppContext,
+    ) -> Result<()> {
+        Self::add_notifications(
+            this,
+            envelope.payload.notification.into_iter().collect(),
+            AddNotificationsOptions {
+                is_new: true,
+                clear_old: false,
+                includes_first: false,
+            },
+            cx,
+        )
+        .await
+    }
+
+    async fn handle_delete_notification(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::DeleteNotification>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            this.splice_notifications([(envelope.payload.notification_id, None)], false, cx);
+            Ok(())
+        })
+    }
+
+    async fn add_notifications(
+        this: ModelHandle<Self>,
+        notifications: Vec<proto::Notification>,
+        options: AddNotificationsOptions,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let mut user_ids = Vec::new();
+        let mut message_ids = Vec::new();
+
+        let notifications = notifications
+            .into_iter()
+            .filter_map(|message| {
+                Some(NotificationEntry {
+                    id: message.id,
+                    is_read: message.is_read,
+                    timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)
+                        .ok()?,
+                    notification: Notification::from_proto(&message)?,
+                    response: message.response,
+                })
+            })
+            .collect::<Vec<_>>();
+        if notifications.is_empty() {
+            return Ok(());
+        }
+
+        for entry in &notifications {
+            match entry.notification {
+                Notification::ChannelInvitation { inviter_id, .. } => {
+                    user_ids.push(inviter_id);
+                }
+                Notification::ContactRequest {
+                    sender_id: requester_id,
+                } => {
+                    user_ids.push(requester_id);
+                }
+                Notification::ContactRequestAccepted {
+                    responder_id: contact_id,
+                } => {
+                    user_ids.push(contact_id);
+                }
+                Notification::ChannelMessageMention {
+                    sender_id,
+                    message_id,
+                    ..
+                } => {
+                    user_ids.push(sender_id);
+                    message_ids.push(message_id);
+                }
+            }
+        }
+
+        let (user_store, channel_store) = this.read_with(&cx, |this, _| {
+            (this.user_store.clone(), this.channel_store.clone())
+        });
+
+        user_store
+            .update(&mut cx, |store, cx| store.get_users(user_ids, cx))
+            .await?;
+        let messages = channel_store
+            .update(&mut cx, |store, cx| {
+                store.fetch_channel_messages(message_ids, cx)
+            })
+            .await?;
+        this.update(&mut cx, |this, cx| {
+            if options.clear_old {
+                cx.emit(NotificationEvent::NotificationsUpdated {
+                    old_range: 0..this.notifications.summary().count,
+                    new_count: 0,
+                });
+                this.notifications = SumTree::default();
+                this.channel_messages.clear();
+                this.loaded_all_notifications = false;
+            }
+
+            if options.includes_first {
+                this.loaded_all_notifications = true;
+            }
+
+            this.channel_messages
+                .extend(messages.into_iter().filter_map(|message| {
+                    if let ChannelMessageId::Saved(id) = message.id {
+                        Some((id, message))
+                    } else {
+                        None
+                    }
+                }));
+
+            this.splice_notifications(
+                notifications
+                    .into_iter()
+                    .map(|notification| (notification.id, Some(notification))),
+                options.is_new,
+                cx,
+            );
+        });
+
+        Ok(())
+    }
+
+    fn splice_notifications(
+        &mut self,
+        notifications: impl IntoIterator<Item = (u64, Option<NotificationEntry>)>,
+        is_new: bool,
+        cx: &mut ModelContext<'_, NotificationStore>,
+    ) {
+        let mut cursor = self.notifications.cursor::<(NotificationId, Count)>();
+        let mut new_notifications = SumTree::new();
+        let mut old_range = 0..0;
+
+        for (i, (id, new_notification)) in notifications.into_iter().enumerate() {
+            new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left, &()), &());
+
+            if i == 0 {
+                old_range.start = cursor.start().1 .0;
+            }
+
+            let old_notification = cursor.item();
+            if let Some(old_notification) = old_notification {
+                if old_notification.id == id {
+                    cursor.next(&());
+
+                    if let Some(new_notification) = &new_notification {
+                        if new_notification.is_read {
+                            cx.emit(NotificationEvent::NotificationRead {
+                                entry: new_notification.clone(),
+                            });
+                        }
+                    } else {
+                        cx.emit(NotificationEvent::NotificationRemoved {
+                            entry: old_notification.clone(),
+                        });
+                    }
+                }
+            } else if let Some(new_notification) = &new_notification {
+                if is_new {
+                    cx.emit(NotificationEvent::NewNotification {
+                        entry: new_notification.clone(),
+                    });
+                }
+            }
+
+            if let Some(notification) = new_notification {
+                new_notifications.push(notification, &());
+            }
+        }
+
+        old_range.end = cursor.start().1 .0;
+        let new_count = new_notifications.summary().count - old_range.start;
+        new_notifications.append(cursor.suffix(&()), &());
+        drop(cursor);
+
+        self.notifications = new_notifications;
+        cx.emit(NotificationEvent::NotificationsUpdated {
+            old_range,
+            new_count,
+        });
+    }
+
+    pub fn respond_to_notification(
+        &mut self,
+        notification: Notification,
+        response: bool,
+        cx: &mut ModelContext<Self>,
+    ) {
+        match notification {
+            Notification::ContactRequest { sender_id } => {
+                self.user_store
+                    .update(cx, |store, cx| {
+                        store.respond_to_contact_request(sender_id, response, cx)
+                    })
+                    .detach();
+            }
+            Notification::ChannelInvitation { channel_id, .. } => {
+                self.channel_store
+                    .update(cx, |store, cx| {
+                        store.respond_to_channel_invite(channel_id, response, cx)
+                    })
+                    .detach();
+            }
+            _ => {}
+        }
+    }
+}
+
+impl Entity for NotificationStore {
+    type Event = NotificationEvent;
+}
+
+impl sum_tree::Item for NotificationEntry {
+    type Summary = NotificationSummary;
+
+    fn summary(&self) -> Self::Summary {
+        NotificationSummary {
+            max_id: self.id,
+            count: 1,
+            unread_count: if self.is_read { 0 } else { 1 },
+        }
+    }
+}
+
+impl sum_tree::Summary for NotificationSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &()) {
+        self.max_id = self.max_id.max(summary.max_id);
+        self.count += summary.count;
+        self.unread_count += summary.unread_count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, NotificationSummary> for NotificationId {
+    fn add_summary(&mut self, summary: &NotificationSummary, _: &()) {
+        debug_assert!(summary.max_id > self.0);
+        self.0 = summary.max_id;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, NotificationSummary> for Count {
+    fn add_summary(&mut self, summary: &NotificationSummary, _: &()) {
+        self.0 += summary.count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, NotificationSummary> for UnreadCount {
+    fn add_summary(&mut self, summary: &NotificationSummary, _: &()) {
+        self.0 += summary.unread_count;
+    }
+}
+
+struct AddNotificationsOptions {
+    is_new: bool,
+    clear_old: bool,
+    includes_first: bool,
+}

crates/prettier/Cargo.toml 🔗

@@ -27,6 +27,7 @@ serde_derive.workspace = true
 serde_json.workspace = true
 anyhow.workspace = true
 futures.workspace = true
+parking_lot.workspace = true
 
 [dev-dependencies]
 language = { path = "../language", features = ["test-support"] }

crates/prettier/src/prettier.rs 🔗

@@ -3,11 +3,11 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::Context;
-use collections::{HashMap, HashSet};
+use collections::HashMap;
 use fs::Fs;
 use gpui::{AsyncAppContext, ModelHandle};
 use language::language_settings::language_settings;
-use language::{Buffer, BundledFormatter, Diff};
+use language::{Buffer, Diff};
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
@@ -44,6 +44,9 @@ pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
 const PRETTIER_PACKAGE_NAME: &str = "prettier";
 const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
 
+#[cfg(any(test, feature = "test-support"))]
+pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
+
 impl Prettier {
     pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
         ".prettierrc",
@@ -60,98 +63,43 @@ impl Prettier {
         ".editorconfig",
     ];
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
-
     pub async fn locate(
         starting_path: Option<LocateStart>,
         fs: Arc<dyn Fs>,
     ) -> anyhow::Result<PathBuf> {
+        fn is_node_modules(path_component: &std::path::Component<'_>) -> bool {
+            path_component.as_os_str().to_string_lossy() == "node_modules"
+        }
+
         let paths_to_check = match starting_path.as_ref() {
             Some(starting_path) => {
                 let worktree_root = starting_path
                     .worktree_root_path
                     .components()
                     .into_iter()
-                    .take_while(|path_component| {
-                        path_component.as_os_str().to_string_lossy() != "node_modules"
-                    })
+                    .take_while(|path_component| !is_node_modules(path_component))
                     .collect::<PathBuf>();
-
                 if worktree_root != starting_path.worktree_root_path.as_ref() {
                     vec![worktree_root]
                 } else {
-                    let (worktree_root_metadata, start_path_metadata) = if starting_path
-                        .starting_path
-                        .as_ref()
-                        == Path::new("")
-                    {
-                        let worktree_root_data =
-                            fs.metadata(&worktree_root).await.with_context(|| {
-                                format!(
-                                    "FS metadata fetch for worktree root path {worktree_root:?}",
-                                )
-                            })?;
-                        (worktree_root_data.unwrap_or_else(|| {
-                            panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
-                        }), None)
+                    if starting_path.starting_path.as_ref() == Path::new("") {
+                        worktree_root
+                            .parent()
+                            .map(|path| vec![path.to_path_buf()])
+                            .unwrap_or_default()
                     } else {
-                        let full_starting_path = worktree_root.join(&starting_path.starting_path);
-                        let (worktree_root_data, start_path_data) = futures::try_join!(
-                            fs.metadata(&worktree_root),
-                            fs.metadata(&full_starting_path),
-                        )
-                        .with_context(|| {
-                            format!("FS metadata fetch for starting path {full_starting_path:?}",)
-                        })?;
-                        (
-                            worktree_root_data.unwrap_or_else(|| {
-                                panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
-                            }),
-                            start_path_data,
-                        )
-                    };
-
-                    match start_path_metadata {
-                        Some(start_path_metadata) => {
-                            anyhow::ensure!(worktree_root_metadata.is_dir,
-                                "For non-empty start path, worktree root {starting_path:?} should be a directory");
-                            anyhow::ensure!(
-                                !start_path_metadata.is_dir,
-                                "For non-empty start path, it should not be a directory {starting_path:?}"
-                            );
-                            anyhow::ensure!(
-                                !start_path_metadata.is_symlink,
-                                "For non-empty start path, it should not be a symlink {starting_path:?}"
-                            );
-
-                            let file_to_format = starting_path.starting_path.as_ref();
-                            let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
-                            let mut current_path = worktree_root;
-                            for path_component in file_to_format.components().into_iter() {
-                                current_path = current_path.join(path_component);
-                                paths_to_check.push_front(current_path.clone());
-                                if path_component.as_os_str().to_string_lossy() == "node_modules" {
-                                    break;
-                                }
+                        let file_to_format = starting_path.starting_path.as_ref();
+                        let mut paths_to_check = VecDeque::new();
+                        let mut current_path = worktree_root;
+                        for path_component in file_to_format.components().into_iter() {
+                            let new_path = current_path.join(path_component);
+                            let old_path = std::mem::replace(&mut current_path, new_path);
+                            paths_to_check.push_front(old_path);
+                            if is_node_modules(&path_component) {
+                                break;
                             }
-                            paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
-                            Vec::from(paths_to_check)
-                        }
-                        None => {
-                            anyhow::ensure!(
-                                !worktree_root_metadata.is_dir,
-                                "For empty start path, worktree root should not be a directory {starting_path:?}"
-                            );
-                            anyhow::ensure!(
-                                !worktree_root_metadata.is_symlink,
-                                "For empty start path, worktree root should not be a symlink {starting_path:?}"
-                            );
-                            worktree_root
-                                .parent()
-                                .map(|path| vec![path.to_path_buf()])
-                                .unwrap_or_default()
                         }
+                        Vec::from(paths_to_check)
                     }
                 }
             }
@@ -210,6 +158,7 @@ impl Prettier {
             .spawn(async move { node.binary_path().await })
             .await?;
         let server = LanguageServer::new(
+            Arc::new(parking_lot::Mutex::new(None)),
             server_id,
             LanguageServerBinary {
                 path: node_path,
@@ -242,40 +191,16 @@ impl Prettier {
             Self::Real(local) => {
                 let params = buffer.read_with(cx, |buffer, cx| {
                     let buffer_language = buffer.language();
-                    let parsers_with_plugins = buffer_language
-                        .into_iter()
-                        .flat_map(|language| {
-                            language
-                                .lsp_adapters()
-                                .iter()
-                                .flat_map(|adapter| adapter.enabled_formatters())
-                                .filter_map(|formatter| match formatter {
-                                    BundledFormatter::Prettier {
-                                        parser_name,
-                                        plugin_names,
-                                    } => Some((parser_name, plugin_names)),
-                                })
-                        })
-                        .fold(
-                            HashMap::default(),
-                            |mut parsers_with_plugins, (parser_name, plugins)| {
-                                match parser_name {
-                                    Some(parser_name) => parsers_with_plugins
-                                        .entry(parser_name)
-                                        .or_insert_with(HashSet::default)
-                                        .extend(plugins),
-                                    None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
-                                        existing_plugins.extend(plugins.iter());
-                                    }),
-                                }
-                                parsers_with_plugins
-                            },
-                        );
-
-                    let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
-                    if parsers_with_plugins.len() > 1 {
-                        log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
-                    }
+                    let parser_with_plugins = buffer_language.and_then(|l| {
+                        let prettier_parser = l.prettier_parser_name()?;
+                        let mut prettier_plugins = l
+                            .lsp_adapters()
+                            .iter()
+                            .flat_map(|adapter| adapter.prettier_plugins())
+                            .collect::<Vec<_>>();
+                        prettier_plugins.dedup();
+                        Some((prettier_parser, prettier_plugins))
+                    });
 
                     let prettier_node_modules = self.prettier_dir().join("node_modules");
                     anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
@@ -296,7 +221,7 @@ impl Prettier {
                         }
                         None
                     };
-                    let (parser, located_plugins) = match selected_parser_with_plugins {
+                    let (parser, located_plugins) = match parser_with_plugins {
                         Some((parser, plugins)) => {
                             // Tailwind plugin requires being added last
                             // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
@@ -373,7 +298,7 @@ impl Prettier {
             #[cfg(any(test, feature = "test-support"))]
             Self::Test(_) => Ok(buffer
                 .read_with(cx, |buffer, cx| {
-                    let formatted_text = buffer.text() + Self::FORMAT_SUFFIX;
+                    let formatted_text = buffer.text() + FORMAT_SUFFIX;
                     buffer.diff(formatted_text, cx)
                 })
                 .await),

crates/prettier2/src/prettier2.rs 🔗

@@ -67,91 +67,39 @@ impl Prettier {
         starting_path: Option<LocateStart>,
         fs: Arc<dyn Fs>,
     ) -> anyhow::Result<PathBuf> {
+        fn is_node_modules(path_component: &std::path::Component<'_>) -> bool {
+            path_component.as_os_str().to_string_lossy() == "node_modules"
+        }
+
         let paths_to_check = match starting_path.as_ref() {
             Some(starting_path) => {
                 let worktree_root = starting_path
                     .worktree_root_path
                     .components()
                     .into_iter()
-                    .take_while(|path_component| {
-                        path_component.as_os_str().to_string_lossy() != "node_modules"
-                    })
+                    .take_while(|path_component| !is_node_modules(path_component))
                     .collect::<PathBuf>();
-
                 if worktree_root != starting_path.worktree_root_path.as_ref() {
                     vec![worktree_root]
                 } else {
-                    let (worktree_root_metadata, start_path_metadata) = if starting_path
-                        .starting_path
-                        .as_ref()
-                        == Path::new("")
-                    {
-                        let worktree_root_data =
-                            fs.metadata(&worktree_root).await.with_context(|| {
-                                format!(
-                                    "FS metadata fetch for worktree root path {worktree_root:?}",
-                                )
-                            })?;
-                        (worktree_root_data.unwrap_or_else(|| {
-                            panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
-                        }), None)
+                    if starting_path.starting_path.as_ref() == Path::new("") {
+                        worktree_root
+                            .parent()
+                            .map(|path| vec![path.to_path_buf()])
+                            .unwrap_or_default()
                     } else {
-                        let full_starting_path = worktree_root.join(&starting_path.starting_path);
-                        let (worktree_root_data, start_path_data) = futures::try_join!(
-                            fs.metadata(&worktree_root),
-                            fs.metadata(&full_starting_path),
-                        )
-                        .with_context(|| {
-                            format!("FS metadata fetch for starting path {full_starting_path:?}",)
-                        })?;
-                        (
-                            worktree_root_data.unwrap_or_else(|| {
-                                panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
-                            }),
-                            start_path_data,
-                        )
-                    };
-
-                    match start_path_metadata {
-                        Some(start_path_metadata) => {
-                            anyhow::ensure!(worktree_root_metadata.is_dir,
-                                "For non-empty start path, worktree root {starting_path:?} should be a directory");
-                            anyhow::ensure!(
-                                !start_path_metadata.is_dir,
-                                "For non-empty start path, it should not be a directory {starting_path:?}"
-                            );
-                            anyhow::ensure!(
-                                !start_path_metadata.is_symlink,
-                                "For non-empty start path, it should not be a symlink {starting_path:?}"
-                            );
-
-                            let file_to_format = starting_path.starting_path.as_ref();
-                            let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
-                            let mut current_path = worktree_root;
-                            for path_component in file_to_format.components().into_iter() {
-                                current_path = current_path.join(path_component);
-                                paths_to_check.push_front(current_path.clone());
-                                if path_component.as_os_str().to_string_lossy() == "node_modules" {
-                                    break;
-                                }
+                        let file_to_format = starting_path.starting_path.as_ref();
+                        let mut paths_to_check = VecDeque::new();
+                        let mut current_path = worktree_root;
+                        for path_component in file_to_format.components().into_iter() {
+                            let new_path = current_path.join(path_component);
+                            let old_path = std::mem::replace(&mut current_path, new_path);
+                            paths_to_check.push_front(old_path);
+                            if is_node_modules(&path_component) {
+                                break;
                             }
-                            paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
-                            Vec::from(paths_to_check)
-                        }
-                        None => {
-                            anyhow::ensure!(
-                                !worktree_root_metadata.is_dir,
-                                "For empty start path, worktree root should not be a directory {starting_path:?}"
-                            );
-                            anyhow::ensure!(
-                                !worktree_root_metadata.is_symlink,
-                                "For empty start path, worktree root should not be a symlink {starting_path:?}"
-                            );
-                            worktree_root
-                                .parent()
-                                .map(|path| vec![path.to_path_buf()])
-                                .unwrap_or_default()
                         }
+                        Vec::from(paths_to_check)
                     }
                 }
             }

crates/project/src/lsp_command.rs 🔗

@@ -10,7 +10,7 @@ use futures::future;
 use gpui::{AppContext, AsyncAppContext, ModelHandle};
 use language::{
     language_settings::{language_settings, InlayHintKind},
-    point_from_lsp, point_to_lsp,
+    point_from_lsp, point_to_lsp, prepare_completion_documentation,
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
     range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
     CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
@@ -1341,7 +1341,7 @@ impl LspCommand for GetCompletions {
     async fn response_from_lsp(
         self,
         completions: Option<lsp::CompletionResponse>,
-        _: ModelHandle<Project>,
+        project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
         server_id: LanguageServerId,
         cx: AsyncAppContext,
@@ -1358,10 +1358,11 @@ impl LspCommand for GetCompletions {
                 }
             }
         } else {
-            Default::default()
+            Vec::new()
         };
 
-        let completions = buffer.read_with(&cx, |buffer, _| {
+        let completions = buffer.read_with(&cx, |buffer, cx| {
+            let language_registry = project.read(cx).languages().clone();
             let language = buffer.language().cloned();
             let snapshot = buffer.snapshot();
             let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
@@ -1370,6 +1371,14 @@ impl LspCommand for GetCompletions {
             completions
                 .into_iter()
                 .filter_map(move |mut lsp_completion| {
+                    if let Some(response_list) = &response_list {
+                        if let Some(item_defaults) = &response_list.item_defaults {
+                            if let Some(data) = &item_defaults.data {
+                                lsp_completion.data = Some(data.clone());
+                            }
+                        }
+                    }
+
                     let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
                         // If the language server provides a range to overwrite, then
                         // check that the range is valid.
@@ -1445,14 +1454,30 @@ impl LspCommand for GetCompletions {
                         }
                     };
 
-                    let language = language.clone();
                     LineEnding::normalize(&mut new_text);
+                    let language_registry = language_registry.clone();
+                    let language = language.clone();
+
                     Some(async move {
                         let mut label = None;
-                        if let Some(language) = language {
+                        if let Some(language) = language.as_ref() {
                             language.process_completion(&mut lsp_completion).await;
                             label = language.label_for_completion(&lsp_completion).await;
                         }
+
+                        let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
+                            Some(
+                                prepare_completion_documentation(
+                                    lsp_docs,
+                                    &language_registry,
+                                    language.clone(),
+                                )
+                                .await,
+                            )
+                        } else {
+                            None
+                        };
+
                         Completion {
                             old_range,
                             new_text,
@@ -1462,6 +1487,7 @@ impl LspCommand for GetCompletions {
                                     lsp_completion.filter_text.as_deref(),
                                 )
                             }),
+                            documentation,
                             server_id,
                             lsp_completion,
                         }

crates/project/src/project.rs 🔗

@@ -39,11 +39,11 @@ use language::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
         serialize_anchor, serialize_version, split_operations,
     },
-    range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter,
-    CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
-    Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
-    LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
-    TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
+    range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
+    CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
+    File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
+    OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
+    ToOffset, ToPointUtf16, Transaction, Unclipped,
 };
 use log::error;
 use lsp::{
@@ -52,8 +52,9 @@ use lsp::{
 };
 use lsp_command::*;
 use node_runtime::NodeRuntime;
+use parking_lot::Mutex;
 use postage::watch;
-use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS};
+use prettier::{LocateStart, Prettier};
 use project_settings::{LspSettings, ProjectSettings};
 use rand::prelude::*;
 use search::SearchQuery;
@@ -79,18 +80,19 @@ use std::{
     time::{Duration, Instant},
 };
 use terminals::Terminals;
-use text::{Anchor, LineEnding, Rope};
+use text::Anchor;
 use util::{
-    debug_panic, defer,
-    http::HttpClient,
-    merge_json_value_into,
-    paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
-    post_inc, ResultExt, TryFutureExt as _,
+    debug_panic, defer, http::HttpClient, merge_json_value_into,
+    paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
 };
 
 pub use fs::*;
+#[cfg(any(test, feature = "test-support"))]
+pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
 pub use worktree::*;
 
+const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4;
+
 pub trait Item {
     fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
@@ -592,6 +594,7 @@ impl Project {
         client.add_model_request_handler(Self::handle_apply_code_action);
         client.add_model_request_handler(Self::handle_on_type_formatting);
         client.add_model_request_handler(Self::handle_inlay_hints);
+        client.add_model_request_handler(Self::handle_resolve_completion_documentation);
         client.add_model_request_handler(Self::handle_resolve_inlay_hint);
         client.add_model_request_handler(Self::handle_refresh_inlay_hints);
         client.add_model_request_handler(Self::handle_reload_buffers);
@@ -835,16 +838,6 @@ impl Project {
         project
     }
 
-    /// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes.
-    /// Instead, if appends the suffix to every input, this suffix is returned by this method.
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str {
-        self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support(
-            plugins,
-        ));
-        Prettier::FORMAT_SUFFIX
-    }
-
     fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
         let mut language_servers_to_start = Vec::new();
         let mut language_formatters_to_check = Vec::new();
@@ -2731,12 +2724,18 @@ impl Project {
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
+        if adapter.reinstall_attempt_count.load(SeqCst) > MAX_SERVER_REINSTALL_ATTEMPT_COUNT {
+            return;
+        }
+
         let key = (worktree_id, adapter.name.clone());
         if self.language_server_ids.contains_key(&key) {
             return;
         }
 
+        let stderr_capture = Arc::new(Mutex::new(Some(String::new())));
         let pending_server = match self.languages.create_pending_language_server(
+            stderr_capture.clone(),
             language.clone(),
             adapter.clone(),
             worktree_path,
@@ -2751,15 +2750,6 @@ impl Project {
         let lsp = project_settings.lsp.get(&adapter.name.0);
         let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
 
-        let mut initialization_options = adapter.initialization_options.clone();
-        match (&mut initialization_options, override_options) {
-            (Some(initialization_options), Some(override_options)) => {
-                merge_json_value_into(override_options, initialization_options);
-            }
-            (None, override_options) => initialization_options = override_options,
-            _ => {}
-        }
-
         let server_id = pending_server.server_id;
         let container_dir = pending_server.container_dir.clone();
         let state = LanguageServerState::Starting({
@@ -2771,7 +2761,7 @@ impl Project {
             cx.spawn_weak(|this, mut cx| async move {
                 let result = Self::setup_and_insert_language_server(
                     this,
-                    initialization_options,
+                    override_options,
                     pending_server,
                     adapter.clone(),
                     language.clone(),
@@ -2782,29 +2772,41 @@ impl Project {
                 .await;
 
                 match result {
-                    Ok(server) => server,
+                    Ok(server) => {
+                        stderr_capture.lock().take();
+                        Some(server)
+                    }
 
                     Err(err) => {
-                        log::error!("failed to start language server {:?}: {}", server_name, err);
-
-                        if let Some(this) = this.upgrade(&cx) {
-                            if let Some(container_dir) = container_dir {
-                                let installation_test_binary = adapter
-                                    .installation_test_binary(container_dir.to_path_buf())
-                                    .await;
-
-                                this.update(&mut cx, |_, cx| {
-                                    Self::check_errored_server(
-                                        language,
-                                        adapter,
-                                        server_id,
-                                        installation_test_binary,
-                                        cx,
-                                    )
-                                });
-                            }
+                        log::error!("failed to start language server {server_name:?}: {err}");
+                        log::error!("server stderr: {:?}", stderr_capture.lock().take());
+
+                        let this = this.upgrade(&cx)?;
+                        let container_dir = container_dir?;
+
+                        let attempt_count = adapter.reinstall_attempt_count.fetch_add(1, SeqCst);
+                        if attempt_count >= MAX_SERVER_REINSTALL_ATTEMPT_COUNT {
+                            let max = MAX_SERVER_REINSTALL_ATTEMPT_COUNT;
+                            log::error!(
+                                "Hit {max} max reinstallation attempts for {server_name:?}"
+                            );
+                            return None;
                         }
 
+                        let installation_test_binary = adapter
+                            .installation_test_binary(container_dir.to_path_buf())
+                            .await;
+
+                        this.update(&mut cx, |_, cx| {
+                            Self::check_errored_server(
+                                language,
+                                adapter,
+                                server_id,
+                                installation_test_binary,
+                                cx,
+                            )
+                        });
+
                         None
                     }
                 }
@@ -2874,27 +2876,24 @@ impl Project {
 
     async fn setup_and_insert_language_server(
         this: WeakModelHandle<Self>,
-        initialization_options: Option<serde_json::Value>,
+        override_initialization_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
         language: Arc<Language>,
         server_id: LanguageServerId,
         key: (WorktreeId, LanguageServerName),
         cx: &mut AsyncAppContext,
-    ) -> Result<Option<Arc<LanguageServer>>> {
-        let setup = Self::setup_pending_language_server(
+    ) -> Result<Arc<LanguageServer>> {
+        let language_server = Self::setup_pending_language_server(
             this,
-            initialization_options,
+            override_initialization_options,
             pending_server,
             adapter.clone(),
             server_id,
             cx,
-        );
+        )
+        .await?;
 
-        let language_server = match setup.await? {
-            Some(language_server) => language_server,
-            None => return Ok(None),
-        };
         let this = match this.upgrade(cx) {
             Some(this) => this,
             None => return Err(anyhow!("failed to upgrade project handle")),
@@ -2911,22 +2910,19 @@ impl Project {
             )
         })?;
 
-        Ok(Some(language_server))
+        Ok(language_server)
     }
 
     async fn setup_pending_language_server(
         this: WeakModelHandle<Self>,
-        initialization_options: Option<serde_json::Value>,
+        override_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
         server_id: LanguageServerId,
         cx: &mut AsyncAppContext,
-    ) -> Result<Option<Arc<LanguageServer>>> {
+    ) -> Result<Arc<LanguageServer>> {
         let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await;
-        let language_server = match pending_server.task.await? {
-            Some(server) => server,
-            None => return Ok(None),
-        };
+        let language_server = pending_server.task.await?;
 
         language_server
             .on_notification::<lsp::notification::PublishDiagnostics, _>({
@@ -2934,8 +2930,8 @@ impl Project {
                 move |mut params, mut cx| {
                     let this = this;
                     let adapter = adapter.clone();
-                    adapter.process_diagnostics(&mut params);
                     if let Some(this) = this.upgrade(&cx) {
+                        adapter.process_diagnostics(&mut params);
                         this.update(&mut cx, |this, cx| {
                             this.update_diagnostics(
                                 server_id,
@@ -2997,6 +2993,7 @@ impl Project {
                 },
             )
             .detach();
+
         language_server
             .on_request::<lsp::request::RegisterCapability, _, _>({
                 move |params, mut cx| async move {
@@ -3063,6 +3060,15 @@ impl Project {
             })
             .detach();
 
+        let mut initialization_options = adapter.adapter.initialization_options().await;
+        match (&mut initialization_options, override_options) {
+            (Some(initialization_options), Some(override_options)) => {
+                merge_json_value_into(override_options, initialization_options);
+            }
+            (None, override_options) => initialization_options = override_options,
+            _ => {}
+        }
+
         let language_server = language_server.initialize(initialization_options).await?;
 
         language_server
@@ -3073,7 +3079,7 @@ impl Project {
             )
             .ok();
 
-        Ok(Some(language_server))
+        Ok(language_server)
     }
 
     fn insert_newly_running_language_server(
@@ -7353,6 +7359,40 @@ impl Project {
         })
     }
 
+    async fn handle_resolve_completion_documentation(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::ResolveCompletionDocumentation>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ResolveCompletionDocumentationResponse> {
+        let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?;
+
+        let completion = this
+            .read_with(&mut cx, |this, _| {
+                let id = LanguageServerId(envelope.payload.language_server_id as usize);
+                let Some(server) = this.language_server_for_id(id) else {
+                    return Err(anyhow!("No language server {id}"));
+                };
+
+                Ok(server.request::<lsp::request::ResolveCompletionItem>(lsp_completion))
+            })?
+            .await?;
+
+        let mut is_markdown = false;
+        let text = match completion.documentation {
+            Some(lsp::Documentation::String(text)) => text,
+
+            Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => {
+                is_markdown = kind == lsp::MarkupKind::Markdown;
+                value
+            }
+
+            _ => String::new(),
+        };
+
+        Ok(proto::ResolveCompletionDocumentationResponse { text, is_markdown })
+    }
+
     async fn handle_apply_code_action(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::ApplyCodeAction>,
@@ -8318,12 +8358,7 @@ impl Project {
         let Some(buffer_language) = buffer.language() else {
             return Task::ready(None);
         };
-        if !buffer_language
-            .lsp_adapters()
-            .iter()
-            .flat_map(|adapter| adapter.enabled_formatters())
-            .any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. }))
-        {
+        if buffer_language.prettier_parser_name().is_none() {
             return Task::ready(None);
         }
 
@@ -8460,6 +8495,18 @@ impl Project {
         }
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    fn install_default_formatters(
+        &self,
+        _worktree: Option<WorktreeId>,
+        _new_language: &Language,
+        _language_settings: &LanguageSettings,
+        _cx: &mut ModelContext<Self>,
+    ) -> Task<anyhow::Result<()>> {
+        return Task::ready(Ok(()));
+    }
+
+    #[cfg(not(any(test, feature = "test-support")))]
     fn install_default_formatters(
         &self,
         worktree: Option<WorktreeId>,
@@ -8476,22 +8523,21 @@ impl Project {
         };
 
         let mut prettier_plugins = None;
-        for formatter in new_language
-            .lsp_adapters()
-            .into_iter()
-            .flat_map(|adapter| adapter.enabled_formatters())
-        {
-            match formatter {
-                BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins
-                    .get_or_insert_with(|| HashSet::default())
-                    .extend(plugin_names),
-            }
+        if new_language.prettier_parser_name().is_some() {
+            prettier_plugins
+                .get_or_insert_with(|| HashSet::default())
+                .extend(
+                    new_language
+                        .lsp_adapters()
+                        .iter()
+                        .flat_map(|adapter| adapter.prettier_plugins()),
+                )
         }
         let Some(prettier_plugins) = prettier_plugins else {
             return Task::ready(Ok(()));
         };
 
-        let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
+        let default_prettier_dir = util::paths::DEFAULT_PRETTIER_DIR.as_path();
         let already_running_prettier = self
             .prettier_instances
             .get(&(worktree, default_prettier_dir.to_path_buf()))
@@ -8500,10 +8546,10 @@ impl Project {
         let fs = Arc::clone(&self.fs);
         cx.background()
             .spawn(async move {
-                let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE);
+                let prettier_wrapper_path = default_prettier_dir.join(prettier::PRETTIER_SERVER_FILE);
                 // method creates parent directory if it doesn't exist
-                fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await
-                .with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?;
+                fs.save(&prettier_wrapper_path, &text::Rope::from(prettier::PRETTIER_SERVER_JS), text::LineEnding::Unix).await
+                .with_context(|| format!("writing {} file at {prettier_wrapper_path:?}", prettier::PRETTIER_SERVER_FILE))?;
 
                 let packages_to_versions = future::try_join_all(
                     prettier_plugins

crates/project/src/worktree.rs 🔗

@@ -2027,11 +2027,16 @@ impl LocalSnapshot {
 
     fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
         let mut new_ignores = Vec::new();
-        for ancestor in abs_path.ancestors().skip(1) {
-            if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
-                new_ignores.push((ancestor, Some(ignore.clone())));
-            } else {
-                new_ignores.push((ancestor, None));
+        for (index, ancestor) in abs_path.ancestors().enumerate() {
+            if index > 0 {
+                if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
+                    new_ignores.push((ancestor, Some(ignore.clone())));
+                } else {
+                    new_ignores.push((ancestor, None));
+                }
+            }
+            if ancestor.join(&*DOT_GIT).is_dir() {
+                break;
             }
         }
 
@@ -2048,7 +2053,6 @@ impl LocalSnapshot {
         if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
             ignore_stack = IgnoreStack::all();
         }
-
         ignore_stack
     }
 
@@ -3064,14 +3068,21 @@ impl BackgroundScanner {
 
         // Populate ignores above the root.
         let root_abs_path = self.state.lock().snapshot.abs_path.clone();
-        for ancestor in root_abs_path.ancestors().skip(1) {
-            if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
-            {
-                self.state
-                    .lock()
-                    .snapshot
-                    .ignores_by_parent_abs_path
-                    .insert(ancestor.into(), (ignore.into(), false));
+        for (index, ancestor) in root_abs_path.ancestors().enumerate() {
+            if index != 0 {
+                if let Ok(ignore) =
+                    build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
+                {
+                    self.state
+                        .lock()
+                        .snapshot
+                        .ignores_by_parent_abs_path
+                        .insert(ancestor.into(), (ignore.into(), false));
+                }
+            }
+            if ancestor.join(&*DOT_GIT).is_dir() {
+                // Reached root of git repository.
+                break;
             }
         }
 

crates/project2/src/project2.rs 🔗

@@ -92,6 +92,8 @@ use util::{
 pub use fs::*;
 pub use worktree::*;
 
+const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4;
+
 pub trait Item {
     fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
@@ -876,16 +878,6 @@ impl Project {
     //     project
     // }
 
-    /// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes.
-    /// Instead, if appends the suffix to every input, this suffix is returned by this method.
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str {
-        self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support(
-            plugins,
-        ));
-        Prettier::FORMAT_SUFFIX
-    }
-
     fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
         let mut language_servers_to_start = Vec::new();
         let mut language_formatters_to_check = Vec::new();
@@ -2774,6 +2766,10 @@ impl Project {
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
+        if adapter.reinstall_attempt_count.load(SeqCst) > MAX_SERVER_REINSTALL_ATTEMPT_COUNT {
+            return;
+        }
+
         let key = (worktree_id, adapter.name.clone());
         if self.language_server_ids.contains_key(&key) {
             return;
@@ -2833,28 +2829,34 @@ impl Project {
                     }
 
                     Err(err) => {
-                        log::error!("failed to start language server {:?}: {}", server_name, err);
+                        log::error!("failed to start language server {server_name:?}: {err}");
                         log::error!("server stderr: {:?}", stderr_capture.lock().take());
 
-                        if let Some(this) = this.upgrade() {
-                            if let Some(container_dir) = container_dir {
-                                let installation_test_binary = adapter
-                                    .installation_test_binary(container_dir.to_path_buf())
-                                    .await;
-
-                                this.update(&mut cx, |_, cx| {
-                                    Self::check_errored_server(
-                                        language,
-                                        adapter,
-                                        server_id,
-                                        installation_test_binary,
-                                        cx,
-                                    )
-                                })
-                                .ok();
-                            }
+                        let this = this.upgrade()?;
+                        let container_dir = container_dir?;
+
+                        let attempt_count = adapter.reinstall_attempt_count.fetch_add(1, SeqCst);
+                        if attempt_count >= MAX_SERVER_REINSTALL_ATTEMPT_COUNT {
+                            let max = MAX_SERVER_REINSTALL_ATTEMPT_COUNT;
+                            log::error!("Hit {max} reinstallation attempts for {server_name:?}");
+                            return None;
                         }
 
+                        let installation_test_binary = adapter
+                            .installation_test_binary(container_dir.to_path_buf())
+                            .await;
+
+                        this.update(&mut cx, |_, cx| {
+                            Self::check_errored_server(
+                                language,
+                                adapter,
+                                server_id,
+                                installation_test_binary,
+                                cx,
+                            )
+                        })
+                        .ok();
+
                         None
                     }
                 }

crates/project_panel/src/file_associations.rs 🔗

@@ -4,7 +4,7 @@ use collections::HashMap;
 
 use gpui::{AppContext, AssetSource};
 use serde_derive::Deserialize;
-use util::{iife, paths::PathExt};
+use util::{maybe, paths::PathExt};
 
 #[derive(Deserialize, Debug)]
 struct TypeConfig {
@@ -42,12 +42,12 @@ impl FileAssociations {
     }
 
     pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
-        iife!({
+        maybe!({
             let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
             // FIXME: Associate a type with the languages and have the file's langauge
             //        override these associations
-            iife!({
+            maybe!({
                 let suffix = path.icon_suffix()?;
 
                 this.suffixes
@@ -61,7 +61,7 @@ impl FileAssociations {
     }
 
     pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
-        iife!({
+        maybe!({
             let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
             let key = if expanded {
@@ -78,7 +78,7 @@ impl FileAssociations {
     }
 
     pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
-        iife!({
+        maybe!({
             let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
             let key = if expanded {

crates/rich_text/src/rich_text.rs 🔗

@@ -1,20 +1,35 @@
 use std::{ops::Range, sync::Arc};
 
+use anyhow::bail;
 use futures::FutureExt;
 use gpui::{
-    color::Color,
     elements::Text,
-    fonts::{HighlightStyle, TextStyle, Underline, Weight},
+    fonts::{HighlightStyle, Underline, Weight},
     platform::{CursorStyle, MouseButton},
     AnyElement, CursorRegion, Element, MouseRegion, ViewContext,
 };
 use language::{HighlightId, Language, LanguageRegistry};
-use theme::SyntaxTheme;
+use theme::{RichTextStyle, SyntaxTheme};
+use util::RangeExt;
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum Highlight {
     Id(HighlightId),
     Highlight(HighlightStyle),
+    Mention,
+    SelfMention,
+}
+
+impl From<HighlightStyle> for Highlight {
+    fn from(style: HighlightStyle) -> Self {
+        Self::Highlight(style)
+    }
+}
+
+impl From<HighlightId> for Highlight {
+    fn from(style: HighlightId) -> Self {
+        Self::Id(style)
+    }
 }
 
 #[derive(Debug, Clone)]
@@ -25,18 +40,32 @@ pub struct RichText {
     pub regions: Vec<RenderedRegion>,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum BackgroundKind {
+    Code,
+    /// A mention background for non-self user.
+    Mention,
+    SelfMention,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct RenderedRegion {
-    code: bool,
-    link_url: Option<String>,
+    pub background_kind: Option<BackgroundKind>,
+    pub link_url: Option<String>,
+}
+
+/// Allows one to specify extra links to the rendered markdown, which can be used
+/// for e.g. mentions.
+pub struct Mention {
+    pub range: Range<usize>,
+    pub is_self_mention: bool,
 }
 
 impl RichText {
     pub fn element<V: 'static>(
         &self,
         syntax: Arc<SyntaxTheme>,
-        style: TextStyle,
-        code_span_background_color: Color,
+        style: RichTextStyle,
         cx: &mut ViewContext<V>,
     ) -> AnyElement<V> {
         let mut region_id = 0;
@@ -45,7 +74,7 @@ impl RichText {
         let regions = self.regions.clone();
 
         enum Markdown {}
-        Text::new(self.text.clone(), style.clone())
+        Text::new(self.text.clone(), style.text.clone())
             .with_highlights(
                 self.highlights
                     .iter()
@@ -53,6 +82,8 @@ impl RichText {
                         let style = match highlight {
                             Highlight::Id(id) => id.style(&syntax)?,
                             Highlight::Highlight(style) => style.clone(),
+                            Highlight::Mention => style.mention_highlight,
+                            Highlight::SelfMention => style.self_mention_highlight,
                         };
                         Some((range.clone(), style))
                     })
@@ -73,22 +104,55 @@ impl RichText {
                             }),
                     );
                 }
-                if region.code {
-                    cx.scene().push_quad(gpui::Quad {
-                        bounds,
-                        background: Some(code_span_background_color),
-                        border: Default::default(),
-                        corner_radii: (2.0).into(),
-                    });
+                if let Some(region_kind) = &region.background_kind {
+                    let background = match region_kind {
+                        BackgroundKind::Code => style.code_background,
+                        BackgroundKind::Mention => style.mention_background,
+                        BackgroundKind::SelfMention => style.self_mention_background,
+                    };
+                    if background.is_some() {
+                        cx.scene().push_quad(gpui::Quad {
+                            bounds,
+                            background,
+                            border: Default::default(),
+                            corner_radii: (2.0).into(),
+                        });
+                    }
                 }
             })
             .with_soft_wrap(true)
             .into_any()
     }
+
+    pub fn add_mention(
+        &mut self,
+        range: Range<usize>,
+        is_current_user: bool,
+        mention_style: HighlightStyle,
+    ) -> anyhow::Result<()> {
+        if range.end > self.text.len() {
+            bail!(
+                "Mention in range {range:?} is outside of bounds for a message of length {}",
+                self.text.len()
+            );
+        }
+
+        if is_current_user {
+            self.region_ranges.push(range.clone());
+            self.regions.push(RenderedRegion {
+                background_kind: Some(BackgroundKind::Mention),
+                link_url: None,
+            });
+        }
+        self.highlights
+            .push((range, Highlight::Highlight(mention_style)));
+        Ok(())
+    }
 }
 
 pub fn render_markdown_mut(
     block: &str,
+    mut mentions: &[Mention],
     language_registry: &Arc<LanguageRegistry>,
     language: Option<&Arc<Language>>,
     data: &mut RichText,
@@ -101,15 +165,40 @@ pub fn render_markdown_mut(
     let mut current_language = None;
     let mut list_stack = Vec::new();
 
-    for event in Parser::new_ext(&block, Options::all()) {
+    let options = Options::all();
+    for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() {
         let prev_len = data.text.len();
         match event {
             Event::Text(t) => {
                 if let Some(language) = &current_language {
                     render_code(&mut data.text, &mut data.highlights, t.as_ref(), language);
                 } else {
-                    data.text.push_str(t.as_ref());
+                    if let Some(mention) = mentions.first() {
+                        if source_range.contains_inclusive(&mention.range) {
+                            mentions = &mentions[1..];
+                            let range = (prev_len + mention.range.start - source_range.start)
+                                ..(prev_len + mention.range.end - source_range.start);
+                            data.highlights.push((
+                                range.clone(),
+                                if mention.is_self_mention {
+                                    Highlight::SelfMention
+                                } else {
+                                    Highlight::Mention
+                                },
+                            ));
+                            data.region_ranges.push(range);
+                            data.regions.push(RenderedRegion {
+                                background_kind: Some(if mention.is_self_mention {
+                                    BackgroundKind::SelfMention
+                                } else {
+                                    BackgroundKind::Mention
+                                }),
+                                link_url: None,
+                            });
+                        }
+                    }
 
+                    data.text.push_str(t.as_ref());
                     let mut style = HighlightStyle::default();
                     if bold_depth > 0 {
                         style.weight = Some(Weight::BOLD);
@@ -121,7 +210,7 @@ pub fn render_markdown_mut(
                         data.region_ranges.push(prev_len..data.text.len());
                         data.regions.push(RenderedRegion {
                             link_url: Some(link_url),
-                            code: false,
+                            background_kind: None,
                         });
                         style.underline = Some(Underline {
                             thickness: 1.0.into(),
@@ -162,7 +251,7 @@ pub fn render_markdown_mut(
                     ));
                 }
                 data.regions.push(RenderedRegion {
-                    code: true,
+                    background_kind: Some(BackgroundKind::Code),
                     link_url: link_url.clone(),
                 });
             }
@@ -228,6 +317,7 @@ pub fn render_markdown_mut(
 
 pub fn render_markdown(
     block: String,
+    mentions: &[Mention],
     language_registry: &Arc<LanguageRegistry>,
     language: Option<&Arc<Language>>,
 ) -> RichText {
@@ -238,7 +328,7 @@ pub fn render_markdown(
         regions: Default::default(),
     };
 
-    render_markdown_mut(&block, language_registry, language, &mut data);
+    render_markdown_mut(&block, mentions, language_registry, language, &mut data);
 
     data.text = data.text.trim().to_string();
 

crates/rpc/Cargo.toml 🔗

@@ -17,6 +17,7 @@ clock = { path = "../clock" }
 collections = { path = "../collections" }
 gpui = { path = "../gpui", optional = true }
 util = { path = "../util" }
+
 anyhow.workspace = true
 async-lock = "2.4"
 async-tungstenite = "0.16"
@@ -27,8 +28,10 @@ prost.workspace = true
 rand.workspace = true
 rsa = "0.4"
 serde.workspace = true
+serde_json.workspace = true
 serde_derive.workspace = true
 smol-timeout = "0.6"
+strum.workspace = true
 tracing = { version = "0.1.34", features = ["log"] }
 zstd = "0.11"
 

crates/rpc/proto/zed.proto 🔗

@@ -89,88 +89,96 @@ message Envelope {
         FormatBuffersResponse format_buffers_response = 70;
         GetCompletions get_completions = 71;
         GetCompletionsResponse get_completions_response = 72;
-        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
-        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
-        GetCodeActions get_code_actions = 75;
-        GetCodeActionsResponse get_code_actions_response = 76;
-        GetHover get_hover = 77;
-        GetHoverResponse get_hover_response = 78;
-        ApplyCodeAction apply_code_action = 79;
-        ApplyCodeActionResponse apply_code_action_response = 80;
-        PrepareRename prepare_rename = 81;
-        PrepareRenameResponse prepare_rename_response = 82;
-        PerformRename perform_rename = 83;
-        PerformRenameResponse perform_rename_response = 84;
-        SearchProject search_project = 85;
-        SearchProjectResponse search_project_response = 86;
-
-        UpdateContacts update_contacts = 87;
-        UpdateInviteInfo update_invite_info = 88;
-        ShowContacts show_contacts = 89;
-
-        GetUsers get_users = 90;
-        FuzzySearchUsers fuzzy_search_users = 91;
-        UsersResponse users_response = 92;
-        RequestContact request_contact = 93;
-        RespondToContactRequest respond_to_contact_request = 94;
-        RemoveContact remove_contact = 95;
-
-        Follow follow = 96;
-        FollowResponse follow_response = 97;
-        UpdateFollowers update_followers = 98;
-        Unfollow unfollow = 99;
-        GetPrivateUserInfo get_private_user_info = 100;
-        GetPrivateUserInfoResponse get_private_user_info_response = 101;
-        UpdateDiffBase update_diff_base = 102;
-
-        OnTypeFormatting on_type_formatting = 103;
-        OnTypeFormattingResponse on_type_formatting_response = 104;
-
-        UpdateWorktreeSettings update_worktree_settings = 105;
-
-        InlayHints inlay_hints = 106;
-        InlayHintsResponse inlay_hints_response = 107;
-        ResolveInlayHint resolve_inlay_hint = 108;
-        ResolveInlayHintResponse resolve_inlay_hint_response = 109;
-        RefreshInlayHints refresh_inlay_hints = 110;
-
-        CreateChannel create_channel = 111;
-        CreateChannelResponse create_channel_response = 112;
-        InviteChannelMember invite_channel_member = 113;
-        RemoveChannelMember remove_channel_member = 114;
-        RespondToChannelInvite respond_to_channel_invite = 115;
-        UpdateChannels update_channels = 116;
-        JoinChannel join_channel = 117;
-        DeleteChannel delete_channel = 118;
-        GetChannelMembers get_channel_members = 119;
-        GetChannelMembersResponse get_channel_members_response = 120;
-        SetChannelMemberAdmin set_channel_member_admin = 121;
-        RenameChannel rename_channel = 122;
-        RenameChannelResponse rename_channel_response = 123;
-
-        JoinChannelBuffer join_channel_buffer = 124;
-        JoinChannelBufferResponse join_channel_buffer_response = 125;
-        UpdateChannelBuffer update_channel_buffer = 126;
-        LeaveChannelBuffer leave_channel_buffer = 127;
-        UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
-        RejoinChannelBuffers rejoin_channel_buffers = 129;
-        RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
-        AckBufferOperation ack_buffer_operation = 143;
-
-        JoinChannelChat join_channel_chat = 131;
-        JoinChannelChatResponse join_channel_chat_response = 132;
-        LeaveChannelChat leave_channel_chat = 133;
-        SendChannelMessage send_channel_message = 134;
-        SendChannelMessageResponse send_channel_message_response = 135;
-        ChannelMessageSent channel_message_sent = 136;
-        GetChannelMessages get_channel_messages = 137;
-        GetChannelMessagesResponse get_channel_messages_response = 138;
-        RemoveChannelMessage remove_channel_message = 139;
-        AckChannelMessage ack_channel_message = 144;
-
-        LinkChannel link_channel = 140;
-        UnlinkChannel unlink_channel = 141;
-        MoveChannel move_channel = 142; // current max: 144
+        ResolveCompletionDocumentation resolve_completion_documentation = 73;
+        ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74;
+        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75;
+        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76;
+        GetCodeActions get_code_actions = 77;
+        GetCodeActionsResponse get_code_actions_response = 78;
+        GetHover get_hover = 79;
+        GetHoverResponse get_hover_response = 80;
+        ApplyCodeAction apply_code_action = 81;
+        ApplyCodeActionResponse apply_code_action_response = 82;
+        PrepareRename prepare_rename = 83;
+        PrepareRenameResponse prepare_rename_response = 84;
+        PerformRename perform_rename = 85;
+        PerformRenameResponse perform_rename_response = 86;
+        SearchProject search_project = 87;
+        SearchProjectResponse search_project_response = 88;
+
+        UpdateContacts update_contacts = 89;
+        UpdateInviteInfo update_invite_info = 90;
+        ShowContacts show_contacts = 91;
+
+        GetUsers get_users = 92;
+        FuzzySearchUsers fuzzy_search_users = 93;
+        UsersResponse users_response = 94;
+        RequestContact request_contact = 95;
+        RespondToContactRequest respond_to_contact_request = 96;
+        RemoveContact remove_contact = 97;
+
+        Follow follow = 98;
+        FollowResponse follow_response = 99;
+        UpdateFollowers update_followers = 100;
+        Unfollow unfollow = 101;
+        GetPrivateUserInfo get_private_user_info = 102;
+        GetPrivateUserInfoResponse get_private_user_info_response = 103;
+        UpdateDiffBase update_diff_base = 104;
+
+        OnTypeFormatting on_type_formatting = 105;
+        OnTypeFormattingResponse on_type_formatting_response = 106;
+
+        UpdateWorktreeSettings update_worktree_settings = 107;
+
+        InlayHints inlay_hints = 108;
+        InlayHintsResponse inlay_hints_response = 109;
+        ResolveInlayHint resolve_inlay_hint = 110;
+        ResolveInlayHintResponse resolve_inlay_hint_response = 111;
+        RefreshInlayHints refresh_inlay_hints = 112;
+
+        CreateChannel create_channel = 113;
+        CreateChannelResponse create_channel_response = 114;
+        InviteChannelMember invite_channel_member = 115;
+        RemoveChannelMember remove_channel_member = 116;
+        RespondToChannelInvite respond_to_channel_invite = 117;
+        UpdateChannels update_channels = 118;
+        JoinChannel join_channel = 119;
+        DeleteChannel delete_channel = 120;
+        GetChannelMembers get_channel_members = 121;
+        GetChannelMembersResponse get_channel_members_response = 122;
+        SetChannelMemberRole set_channel_member_role = 123;
+        RenameChannel rename_channel = 124;
+        RenameChannelResponse rename_channel_response = 125;
+
+        JoinChannelBuffer join_channel_buffer = 126;
+        JoinChannelBufferResponse join_channel_buffer_response = 127;
+        UpdateChannelBuffer update_channel_buffer = 128;
+        LeaveChannelBuffer leave_channel_buffer = 129;
+        UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130;
+        RejoinChannelBuffers rejoin_channel_buffers = 131;
+        RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132;
+        AckBufferOperation ack_buffer_operation = 133;
+
+        JoinChannelChat join_channel_chat = 134;
+        JoinChannelChatResponse join_channel_chat_response = 135;
+        LeaveChannelChat leave_channel_chat = 136;
+        SendChannelMessage send_channel_message = 137;
+        SendChannelMessageResponse send_channel_message_response = 138;
+        ChannelMessageSent channel_message_sent = 139;
+        GetChannelMessages get_channel_messages = 140;
+        GetChannelMessagesResponse get_channel_messages_response = 141;
+        RemoveChannelMessage remove_channel_message = 142;
+        AckChannelMessage ack_channel_message = 143;
+        GetChannelMessagesById get_channel_messages_by_id = 144;
+
+        MoveChannel move_channel = 147;
+        SetChannelVisibility set_channel_visibility = 148;
+
+        AddNotification add_notification = 149;
+        GetNotifications get_notifications = 150;
+        GetNotificationsResponse get_notifications_response = 151;
+        DeleteNotification delete_notification = 152;
+        MarkNotificationRead mark_notification_read = 153; // Current max
     }
 }
 
@@ -332,6 +340,7 @@ message RoomUpdated {
 message LiveKitConnectionInfo {
     string server_url = 1;
     string token = 2;
+    bool can_publish = 3;
 }
 
 message ShareProject {
@@ -832,6 +841,17 @@ message ResolveState {
     }
 }
 
+message ResolveCompletionDocumentation {
+    uint64 project_id = 1;
+    uint64 language_server_id = 2;
+    bytes lsp_completion = 3;
+}
+
+message ResolveCompletionDocumentationResponse {
+    string text = 1;
+    bool is_markdown = 2;
+}
+
 message ResolveInlayHint {
     uint64 project_id = 1;
     uint64 buffer_id = 2;
@@ -950,13 +970,10 @@ message LspDiskBasedDiagnosticsUpdated {}
 
 message UpdateChannels {
     repeated Channel channels = 1;
-    repeated ChannelEdge insert_edge = 2;
-    repeated ChannelEdge delete_edge = 3;
     repeated uint64 delete_channels = 4;
     repeated Channel channel_invitations = 5;
     repeated uint64 remove_channel_invitations = 6;
     repeated ChannelParticipants channel_participants = 7;
-    repeated ChannelPermission channel_permissions = 8;
     repeated UnseenChannelMessage unseen_channel_messages = 9;
     repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10;
 }
@@ -972,14 +989,9 @@ message UnseenChannelBufferChange {
     repeated VectorClockEntry version = 3;
 }
 
-message ChannelEdge {
-    uint64 channel_id = 1;
-    uint64 parent_id = 2;
-}
-
 message ChannelPermission {
     uint64 channel_id = 1;
-    bool is_admin = 2;
+    ChannelRole role = 3;
 }
 
 message ChannelParticipants {
@@ -1005,8 +1017,8 @@ message GetChannelMembersResponse {
 
 message ChannelMember {
     uint64 user_id = 1;
-    bool admin = 2;
     Kind kind = 3;
+    ChannelRole role = 4;
 
     enum Kind {
         Member = 0;
@@ -1028,7 +1040,7 @@ message CreateChannelResponse {
 message InviteChannelMember {
     uint64 channel_id = 1;
     uint64 user_id = 2;
-    bool admin = 3;
+    ChannelRole role = 4;
 }
 
 message RemoveChannelMember {
@@ -1036,10 +1048,22 @@ message RemoveChannelMember {
     uint64 user_id = 2;
 }
 
-message SetChannelMemberAdmin {
+enum ChannelRole {
+    Admin = 0;
+    Member = 1;
+    Guest = 2;
+    Banned = 3;
+}
+
+message SetChannelMemberRole {
     uint64 channel_id = 1;
     uint64 user_id = 2;
-    bool admin = 3;
+    ChannelRole role = 3;
+}
+
+message SetChannelVisibility {
+    uint64 channel_id = 1;
+    ChannelVisibility visibility = 2;
 }
 
 message RenameChannel {
@@ -1068,6 +1092,7 @@ message SendChannelMessage {
     uint64 channel_id = 1;
     string body = 2;
     Nonce nonce = 3;
+    repeated ChatMention mentions = 4;
 }
 
 message RemoveChannelMessage {
@@ -1099,20 +1124,13 @@ message GetChannelMessagesResponse {
     bool done = 2;
 }
 
-message LinkChannel {
-    uint64 channel_id = 1;
-    uint64 to = 2;
-}
-
-message UnlinkChannel {
-    uint64 channel_id = 1;
-    uint64 from = 2;
+message GetChannelMessagesById {
+    repeated uint64 message_ids = 1;
 }
 
 message MoveChannel {
     uint64 channel_id = 1;
-    uint64 from = 2;
-    uint64 to = 3;
+    optional uint64 to = 2;
 }
 
 message JoinChannelBuffer {
@@ -1125,6 +1143,12 @@ message ChannelMessage {
     uint64 timestamp = 3;
     uint64 sender_id = 4;
     Nonce nonce = 5;
+    repeated ChatMention mentions = 6;
+}
+
+message ChatMention {
+    Range range = 1;
+    uint64 user_id = 2;
 }
 
 message RejoinChannelBuffers {
@@ -1216,7 +1240,6 @@ message ShowContacts {}
 
 message IncomingContactRequest {
     uint64 requester_id = 1;
-    bool should_notify = 2;
 }
 
 message UpdateDiagnostics {
@@ -1533,16 +1556,23 @@ message Nonce {
     uint64 lower_half = 2;
 }
 
+enum ChannelVisibility {
+    Public = 0;
+    Members = 1;
+}
+
 message Channel {
     uint64 id = 1;
     string name = 2;
+    ChannelVisibility visibility = 3;
+    ChannelRole role = 4;
+    repeated uint64 parent_path = 5;
 }
 
 message Contact {
     uint64 user_id = 1;
     bool online = 2;
     bool busy = 3;
-    bool should_notify = 4;
 }
 
 message WorktreeMetadata {
@@ -1557,3 +1587,34 @@ message UpdateDiffBase {
     uint64 buffer_id = 2;
     optional string diff_base = 3;
 }
+
+message GetNotifications {
+    optional uint64 before_id = 1;
+}
+
+message AddNotification {
+    Notification notification = 1;
+}
+
+message GetNotificationsResponse {
+    repeated Notification notifications = 1;
+    bool done = 2;
+}
+
+message DeleteNotification {
+    uint64 notification_id = 1;
+}
+
+message MarkNotificationRead {
+    uint64 notification_id = 1;
+}
+
+message Notification {
+    uint64 id = 1;
+    uint64 timestamp = 2;
+    string kind = 3;
+    optional uint64 entity_id = 4;
+    string content = 5;
+    bool is_read = 6;
+    optional bool response = 7;
+}

crates/rpc/src/notification.rs 🔗

@@ -0,0 +1,105 @@
+use crate::proto;
+use serde::{Deserialize, Serialize};
+use serde_json::{map, Value};
+use strum::{EnumVariantNames, VariantNames as _};
+
+const KIND: &'static str = "kind";
+const ENTITY_ID: &'static str = "entity_id";
+
+/// A notification that can be stored, associated with a given recipient.
+///
+/// This struct is stored in the collab database as JSON, so it shouldn't be
+/// changed in a backward-incompatible way. For example, when renaming a
+/// variant, add a serde alias for the old name.
+///
+/// Most notification types have a special field which is aliased to
+/// `entity_id`. This field is stored in its own database column, and can
+/// be used to query the notification.
+#[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, Serialize, Deserialize)]
+#[serde(tag = "kind")]
+pub enum Notification {
+    ContactRequest {
+        #[serde(rename = "entity_id")]
+        sender_id: u64,
+    },
+    ContactRequestAccepted {
+        #[serde(rename = "entity_id")]
+        responder_id: u64,
+    },
+    ChannelInvitation {
+        #[serde(rename = "entity_id")]
+        channel_id: u64,
+        channel_name: String,
+        inviter_id: u64,
+    },
+    ChannelMessageMention {
+        #[serde(rename = "entity_id")]
+        message_id: u64,
+        sender_id: u64,
+        channel_id: u64,
+    },
+}
+
+impl Notification {
+    pub fn to_proto(&self) -> proto::Notification {
+        let mut value = serde_json::to_value(self).unwrap();
+        let mut entity_id = None;
+        let value = value.as_object_mut().unwrap();
+        let Some(Value::String(kind)) = value.remove(KIND) else {
+            unreachable!("kind is the enum tag")
+        };
+        if let map::Entry::Occupied(e) = value.entry(ENTITY_ID) {
+            if e.get().is_u64() {
+                entity_id = e.remove().as_u64();
+            }
+        }
+        proto::Notification {
+            kind,
+            entity_id,
+            content: serde_json::to_string(&value).unwrap(),
+            ..Default::default()
+        }
+    }
+
+    pub fn from_proto(notification: &proto::Notification) -> Option<Self> {
+        let mut value = serde_json::from_str::<Value>(&notification.content).ok()?;
+        let object = value.as_object_mut()?;
+        object.insert(KIND.into(), notification.kind.to_string().into());
+        if let Some(entity_id) = notification.entity_id {
+            object.insert(ENTITY_ID.into(), entity_id.into());
+        }
+        serde_json::from_value(value).ok()
+    }
+
+    pub fn all_variant_names() -> &'static [&'static str] {
+        Self::VARIANTS
+    }
+}
+
+#[test]
+fn test_notification() {
+    // Notifications can be serialized and deserialized.
+    for notification in [
+        Notification::ContactRequest { sender_id: 1 },
+        Notification::ContactRequestAccepted { responder_id: 2 },
+        Notification::ChannelInvitation {
+            channel_id: 100,
+            channel_name: "the-channel".into(),
+            inviter_id: 50,
+        },
+        Notification::ChannelMessageMention {
+            sender_id: 200,
+            channel_id: 30,
+            message_id: 1,
+        },
+    ] {
+        let message = notification.to_proto();
+        let deserialized = Notification::from_proto(&message).unwrap();
+        assert_eq!(deserialized, notification);
+    }
+
+    // When notifications are serialized, the `kind` and `actor_id` fields are
+    // stored separately, and do not appear redundantly in the JSON.
+    let notification = Notification::ContactRequest { sender_id: 1 };
+    assert_eq!(notification.to_proto().content, "{}");
+}

crates/rpc/src/proto.rs 🔗

@@ -133,6 +133,9 @@ impl fmt::Display for PeerId {
 
 messages!(
     (Ack, Foreground),
+    (AckBufferOperation, Background),
+    (AckChannelMessage, Background),
+    (AddNotification, Foreground),
     (AddProjectCollaborator, Foreground),
     (ApplyCodeAction, Background),
     (ApplyCodeActionResponse, Background),
@@ -143,57 +146,74 @@ messages!(
     (Call, Foreground),
     (CallCanceled, Foreground),
     (CancelCall, Foreground),
+    (ChannelMessageSent, Foreground),
     (CopyProjectEntry, Foreground),
     (CreateBufferForPeer, Foreground),
     (CreateChannel, Foreground),
     (CreateChannelResponse, Foreground),
-    (ChannelMessageSent, Foreground),
     (CreateProjectEntry, Foreground),
     (CreateRoom, Foreground),
     (CreateRoomResponse, Foreground),
     (DeclineCall, Foreground),
+    (DeleteChannel, Foreground),
+    (DeleteNotification, Foreground),
     (DeleteProjectEntry, Foreground),
     (Error, Foreground),
     (ExpandProjectEntry, Foreground),
+    (ExpandProjectEntryResponse, Foreground),
     (Follow, Foreground),
     (FollowResponse, Foreground),
     (FormatBuffers, Foreground),
     (FormatBuffersResponse, Foreground),
     (FuzzySearchUsers, Foreground),
-    (GetCodeActions, Background),
-    (GetCodeActionsResponse, Background),
-    (GetHover, Background),
-    (GetHoverResponse, Background),
+    (GetChannelMembers, Foreground),
+    (GetChannelMembersResponse, Foreground),
     (GetChannelMessages, Background),
+    (GetChannelMessagesById, Background),
     (GetChannelMessagesResponse, Background),
-    (SendChannelMessage, Background),
-    (SendChannelMessageResponse, Background),
+    (GetCodeActions, Background),
+    (GetCodeActionsResponse, Background),
     (GetCompletions, Background),
     (GetCompletionsResponse, Background),
     (GetDefinition, Background),
     (GetDefinitionResponse, Background),
-    (GetTypeDefinition, Background),
-    (GetTypeDefinitionResponse, Background),
     (GetDocumentHighlights, Background),
     (GetDocumentHighlightsResponse, Background),
-    (GetReferences, Background),
-    (GetReferencesResponse, Background),
+    (GetHover, Background),
+    (GetHoverResponse, Background),
+    (GetNotifications, Foreground),
+    (GetNotificationsResponse, Foreground),
+    (GetPrivateUserInfo, Foreground),
+    (GetPrivateUserInfoResponse, Foreground),
     (GetProjectSymbols, Background),
     (GetProjectSymbolsResponse, Background),
+    (GetReferences, Background),
+    (GetReferencesResponse, Background),
+    (GetTypeDefinition, Background),
+    (GetTypeDefinitionResponse, Background),
     (GetUsers, Foreground),
     (Hello, Foreground),
     (IncomingCall, Foreground),
+    (InlayHints, Background),
+    (InlayHintsResponse, Background),
     (InviteChannelMember, Foreground),
-    (UsersResponse, Foreground),
+    (JoinChannel, Foreground),
+    (JoinChannelBuffer, Foreground),
+    (JoinChannelBufferResponse, Foreground),
+    (JoinChannelChat, Foreground),
+    (JoinChannelChatResponse, Foreground),
     (JoinProject, Foreground),
     (JoinProjectResponse, Foreground),
     (JoinRoom, Foreground),
     (JoinRoomResponse, Foreground),
-    (JoinChannelChat, Foreground),
-    (JoinChannelChatResponse, Foreground),
+    (LeaveChannelBuffer, Background),
     (LeaveChannelChat, Foreground),
     (LeaveProject, Foreground),
     (LeaveRoom, Foreground),
+    (MarkNotificationRead, Foreground),
+    (MoveChannel, Foreground),
+    (OnTypeFormatting, Background),
+    (OnTypeFormattingResponse, Background),
     (OpenBufferById, Background),
     (OpenBufferByPath, Background),
     (OpenBufferForSymbol, Background),
@@ -201,58 +221,56 @@ messages!(
     (OpenBufferResponse, Background),
     (PerformRename, Background),
     (PerformRenameResponse, Background),
-    (OnTypeFormatting, Background),
-    (OnTypeFormattingResponse, Background),
-    (InlayHints, Background),
-    (InlayHintsResponse, Background),
-    (ResolveInlayHint, Background),
-    (ResolveInlayHintResponse, Background),
-    (RefreshInlayHints, Foreground),
     (Ping, Foreground),
     (PrepareRename, Background),
     (PrepareRenameResponse, Background),
-    (ExpandProjectEntryResponse, Foreground),
     (ProjectEntryResponse, Foreground),
+    (RefreshInlayHints, Foreground),
+    (RejoinChannelBuffers, Foreground),
+    (RejoinChannelBuffersResponse, Foreground),
     (RejoinRoom, Foreground),
     (RejoinRoomResponse, Foreground),
-    (RemoveContact, Foreground),
-    (RemoveChannelMember, Foreground),
-    (RemoveChannelMessage, Foreground),
     (ReloadBuffers, Foreground),
     (ReloadBuffersResponse, Foreground),
+    (RemoveChannelMember, Foreground),
+    (RemoveChannelMessage, Foreground),
+    (RemoveContact, Foreground),
     (RemoveProjectCollaborator, Foreground),
+    (RenameChannel, Foreground),
+    (RenameChannelResponse, Foreground),
     (RenameProjectEntry, Foreground),
     (RequestContact, Foreground),
-    (RespondToContactRequest, Foreground),
+    (ResolveCompletionDocumentation, Background),
+    (ResolveCompletionDocumentationResponse, Background),
+    (ResolveInlayHint, Background),
+    (ResolveInlayHintResponse, Background),
     (RespondToChannelInvite, Foreground),
-    (JoinChannel, Foreground),
+    (RespondToContactRequest, Foreground),
     (RoomUpdated, Foreground),
     (SaveBuffer, Foreground),
-    (RenameChannel, Foreground),
-    (RenameChannelResponse, Foreground),
-    (SetChannelMemberAdmin, Foreground),
+    (SetChannelMemberRole, Foreground),
+    (SetChannelVisibility, Foreground),
     (SearchProject, Background),
     (SearchProjectResponse, Background),
+    (SendChannelMessage, Background),
+    (SendChannelMessageResponse, Background),
     (ShareProject, Foreground),
     (ShareProjectResponse, Foreground),
     (ShowContacts, Foreground),
     (StartLanguageServer, Foreground),
     (SynchronizeBuffers, Foreground),
     (SynchronizeBuffersResponse, Foreground),
-    (RejoinChannelBuffers, Foreground),
-    (RejoinChannelBuffersResponse, Foreground),
     (Test, Foreground),
     (Unfollow, Foreground),
     (UnshareProject, Foreground),
     (UpdateBuffer, Foreground),
     (UpdateBufferFile, Foreground),
-    (UpdateContacts, Foreground),
-    (DeleteChannel, Foreground),
-    (MoveChannel, Foreground),
-    (LinkChannel, Foreground),
-    (UnlinkChannel, Foreground),
+    (UpdateChannelBuffer, Foreground),
+    (UpdateChannelBufferCollaborators, Foreground),
     (UpdateChannels, Foreground),
+    (UpdateContacts, Foreground),
     (UpdateDiagnosticSummary, Foreground),
+    (UpdateDiffBase, Foreground),
     (UpdateFollowers, Foreground),
     (UpdateInviteInfo, Foreground),
     (UpdateLanguageServer, Foreground),
@@ -261,18 +279,7 @@ messages!(
     (UpdateProjectCollaborator, Foreground),
     (UpdateWorktree, Foreground),
     (UpdateWorktreeSettings, Foreground),
-    (UpdateDiffBase, Foreground),
-    (GetPrivateUserInfo, Foreground),
-    (GetPrivateUserInfoResponse, Foreground),
-    (GetChannelMembers, Foreground),
-    (GetChannelMembersResponse, Foreground),
-    (JoinChannelBuffer, Foreground),
-    (JoinChannelBufferResponse, Foreground),
-    (LeaveChannelBuffer, Background),
-    (UpdateChannelBuffer, Foreground),
-    (UpdateChannelBufferCollaborators, Foreground),
-    (AckBufferOperation, Background),
-    (AckChannelMessage, Background),
+    (UsersResponse, Foreground),
 );
 
 request_messages!(
@@ -284,72 +291,78 @@ request_messages!(
     (Call, Ack),
     (CancelCall, Ack),
     (CopyProjectEntry, ProjectEntryResponse),
+    (CreateChannel, CreateChannelResponse),
     (CreateProjectEntry, ProjectEntryResponse),
     (CreateRoom, CreateRoomResponse),
-    (CreateChannel, CreateChannelResponse),
     (DeclineCall, Ack),
+    (DeleteChannel, Ack),
     (DeleteProjectEntry, ProjectEntryResponse),
     (ExpandProjectEntry, ExpandProjectEntryResponse),
     (Follow, FollowResponse),
     (FormatBuffers, FormatBuffersResponse),
+    (FuzzySearchUsers, UsersResponse),
+    (GetChannelMembers, GetChannelMembersResponse),
+    (GetChannelMessages, GetChannelMessagesResponse),
+    (GetChannelMessagesById, GetChannelMessagesResponse),
     (GetCodeActions, GetCodeActionsResponse),
-    (GetHover, GetHoverResponse),
     (GetCompletions, GetCompletionsResponse),
     (GetDefinition, GetDefinitionResponse),
-    (GetTypeDefinition, GetTypeDefinitionResponse),
     (GetDocumentHighlights, GetDocumentHighlightsResponse),
-    (GetReferences, GetReferencesResponse),
+    (GetHover, GetHoverResponse),
+    (GetNotifications, GetNotificationsResponse),
     (GetPrivateUserInfo, GetPrivateUserInfoResponse),
     (GetProjectSymbols, GetProjectSymbolsResponse),
-    (FuzzySearchUsers, UsersResponse),
+    (GetReferences, GetReferencesResponse),
+    (GetTypeDefinition, GetTypeDefinitionResponse),
     (GetUsers, UsersResponse),
+    (IncomingCall, Ack),
+    (InlayHints, InlayHintsResponse),
     (InviteChannelMember, Ack),
+    (JoinChannel, JoinRoomResponse),
+    (JoinChannelBuffer, JoinChannelBufferResponse),
+    (JoinChannelChat, JoinChannelChatResponse),
     (JoinProject, JoinProjectResponse),
     (JoinRoom, JoinRoomResponse),
-    (JoinChannelChat, JoinChannelChatResponse),
+    (LeaveChannelBuffer, Ack),
     (LeaveRoom, Ack),
-    (RejoinRoom, RejoinRoomResponse),
-    (IncomingCall, Ack),
+    (MarkNotificationRead, Ack),
+    (MoveChannel, Ack),
+    (OnTypeFormatting, OnTypeFormattingResponse),
     (OpenBufferById, OpenBufferResponse),
     (OpenBufferByPath, OpenBufferResponse),
     (OpenBufferForSymbol, OpenBufferForSymbolResponse),
-    (Ping, Ack),
     (PerformRename, PerformRenameResponse),
+    (Ping, Ack),
     (PrepareRename, PrepareRenameResponse),
-    (OnTypeFormatting, OnTypeFormattingResponse),
-    (InlayHints, InlayHintsResponse),
-    (ResolveInlayHint, ResolveInlayHintResponse),
     (RefreshInlayHints, Ack),
+    (RejoinChannelBuffers, RejoinChannelBuffersResponse),
+    (RejoinRoom, RejoinRoomResponse),
     (ReloadBuffers, ReloadBuffersResponse),
-    (RequestContact, Ack),
     (RemoveChannelMember, Ack),
-    (RemoveContact, Ack),
-    (RespondToContactRequest, Ack),
-    (RespondToChannelInvite, Ack),
-    (SetChannelMemberAdmin, Ack),
-    (SendChannelMessage, SendChannelMessageResponse),
-    (GetChannelMessages, GetChannelMessagesResponse),
-    (GetChannelMembers, GetChannelMembersResponse),
-    (JoinChannel, JoinRoomResponse),
     (RemoveChannelMessage, Ack),
-    (DeleteChannel, Ack),
-    (RenameProjectEntry, ProjectEntryResponse),
+    (RemoveContact, Ack),
     (RenameChannel, RenameChannelResponse),
-    (LinkChannel, Ack),
-    (UnlinkChannel, Ack),
-    (MoveChannel, Ack),
+    (RenameProjectEntry, ProjectEntryResponse),
+    (RequestContact, Ack),
+    (
+        ResolveCompletionDocumentation,
+        ResolveCompletionDocumentationResponse
+    ),
+    (ResolveInlayHint, ResolveInlayHintResponse),
+    (RespondToChannelInvite, Ack),
+    (RespondToContactRequest, Ack),
     (SaveBuffer, BufferSaved),
     (SearchProject, SearchProjectResponse),
+    (SendChannelMessage, SendChannelMessageResponse),
+    (SetChannelMemberRole, Ack),
+    (SetChannelVisibility, Ack),
     (ShareProject, ShareProjectResponse),
     (SynchronizeBuffers, SynchronizeBuffersResponse),
-    (RejoinChannelBuffers, RejoinChannelBuffersResponse),
     (Test, Test),
     (UpdateBuffer, Ack),
     (UpdateParticipantLocation, Ack),
     (UpdateProject, Ack),
     (UpdateWorktree, Ack),
-    (JoinChannelBuffer, JoinChannelBufferResponse),
-    (LeaveChannelBuffer, Ack)
 );
 
 entity_messages!(
@@ -368,25 +381,26 @@ entity_messages!(
     GetCodeActions,
     GetCompletions,
     GetDefinition,
-    GetTypeDefinition,
     GetDocumentHighlights,
     GetHover,
-    GetReferences,
     GetProjectSymbols,
+    GetReferences,
+    GetTypeDefinition,
+    InlayHints,
     JoinProject,
     LeaveProject,
+    OnTypeFormatting,
     OpenBufferById,
     OpenBufferByPath,
     OpenBufferForSymbol,
     PerformRename,
-    OnTypeFormatting,
-    InlayHints,
-    ResolveInlayHint,
-    RefreshInlayHints,
     PrepareRename,
+    RefreshInlayHints,
     ReloadBuffers,
     RemoveProjectCollaborator,
     RenameProjectEntry,
+    ResolveCompletionDocumentation,
+    ResolveInlayHint,
     SaveBuffer,
     SearchProject,
     StartLanguageServer,
@@ -395,19 +409,19 @@ entity_messages!(
     UpdateBuffer,
     UpdateBufferFile,
     UpdateDiagnosticSummary,
+    UpdateDiffBase,
     UpdateLanguageServer,
     UpdateProject,
     UpdateProjectCollaborator,
     UpdateWorktree,
     UpdateWorktreeSettings,
-    UpdateDiffBase
 );
 
 entity_messages!(
     channel_id,
     ChannelMessageSent,
-    UpdateChannelBuffer,
     RemoveChannelMessage,
+    UpdateChannelBuffer,
     UpdateChannelBufferCollaborators,
 );
 

crates/rpc/src/rpc.rs 🔗

@@ -1,9 +1,12 @@
 pub mod auth;
 mod conn;
+mod notification;
 mod peer;
 pub mod proto;
+
 pub use conn::Connection;
+pub use notification::*;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 64;
+pub const PROTOCOL_VERSION: u32 = 66;

crates/search/src/buffer_search.rs 🔗

@@ -537,6 +537,7 @@ impl BufferSearchBar {
         self.active_searchable_item
             .as_ref()
             .map(|searchable_item| searchable_item.query_suggestion(cx))
+            .filter(|suggestion| !suggestion.is_empty())
     }
 
     pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {

crates/search/src/project_search.rs 🔗

@@ -351,33 +351,32 @@ impl View for ProjectSearchView {
                     SemanticIndexStatus::NotAuthenticated => {
                         major_text = Cow::Borrowed("Not Authenticated");
                         show_minor_text = false;
-                        Some(
-                            "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables"
-                                .to_string(),
-                        )
+                        Some(vec![
+                            "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables."
+                                .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()])
                     }
-                    SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
+                    SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]),
                     SemanticIndexStatus::Indexing {
                         remaining_files,
                         rate_limit_expiry,
                     } => {
                         if remaining_files == 0 {
-                            Some(format!("Indexing..."))
+                            Some(vec![format!("Indexing...")])
                         } else {
                             if let Some(rate_limit_expiry) = rate_limit_expiry {
                                 let remaining_seconds =
                                     rate_limit_expiry.duration_since(Instant::now());
                                 if remaining_seconds > Duration::from_secs(0) {
-                                    Some(format!(
+                                    Some(vec![format!(
                                         "Remaining files to index (rate limit resets in {}s): {}",
                                         remaining_seconds.as_secs(),
                                         remaining_files
-                                    ))
+                                    )])
                                 } else {
-                                    Some(format!("Remaining files to index: {}", remaining_files))
+                                    Some(vec![format!("Remaining files to index: {}", remaining_files)])
                                 }
                             } else {
-                                Some(format!("Remaining files to index: {}", remaining_files))
+                                Some(vec![format!("Remaining files to index: {}", remaining_files)])
                             }
                         }
                     }
@@ -394,9 +393,11 @@ impl View for ProjectSearchView {
             } else {
                 match current_mode {
                     SearchMode::Semantic => {
-                        let mut minor_text = Vec::new();
+                        let mut minor_text: Vec<String> = Vec::new();
                         minor_text.push("".into());
-                        minor_text.extend(semantic_status);
+                        if let Some(semantic_status) = semantic_status {
+                            minor_text.extend(semantic_status);
+                        }
                         if show_minor_text {
                             minor_text
                                 .push("Simply explain the code you are looking to find.".into());

crates/semantic_index/Cargo.toml 🔗

@@ -51,7 +51,6 @@ workspace = { path = "../workspace", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"]}
 rust-embed = { version = "8.0", features = ["include-exclude"] }
 client = { path = "../client" }
-zed = { path = "../zed"}
 node_runtime = { path = "../node_runtime"}
 
 pretty_assertions.workspace = true
@@ -70,6 +69,3 @@ tree-sitter-elixir.workspace = true
 tree-sitter-lua.workspace = true
 tree-sitter-ruby.workspace = true
 tree-sitter-php.workspace = true
-
-[[example]]
-name = "eval"

crates/semantic_index/src/embedding_queue.rs 🔗

@@ -41,6 +41,7 @@ pub struct EmbeddingQueue {
     pending_batch_token_count: usize,
     finished_files_tx: channel::Sender<FileToEmbed>,
     finished_files_rx: channel::Receiver<FileToEmbed>,
+    api_key: Option<String>,
 }
 
 #[derive(Clone)]
@@ -50,7 +51,11 @@ pub struct FileFragmentToEmbed {
 }
 
 impl EmbeddingQueue {
-    pub fn new(embedding_provider: Arc<dyn EmbeddingProvider>, executor: Arc<Background>) -> Self {
+    pub fn new(
+        embedding_provider: Arc<dyn EmbeddingProvider>,
+        executor: Arc<Background>,
+        api_key: Option<String>,
+    ) -> Self {
         let (finished_files_tx, finished_files_rx) = channel::unbounded();
         Self {
             embedding_provider,
@@ -59,9 +64,14 @@ impl EmbeddingQueue {
             pending_batch_token_count: 0,
             finished_files_tx,
             finished_files_rx,
+            api_key,
         }
     }
 
+    pub fn set_api_key(&mut self, api_key: Option<String>) {
+        self.api_key = api_key
+    }
+
     pub fn push(&mut self, file: FileToEmbed) {
         if file.spans.is_empty() {
             self.finished_files_tx.try_send(file).unwrap();
@@ -108,6 +118,7 @@ impl EmbeddingQueue {
 
         let finished_files_tx = self.finished_files_tx.clone();
         let embedding_provider = self.embedding_provider.clone();
+        let api_key = self.api_key.clone();
 
         self.executor
             .spawn(async move {
@@ -132,7 +143,7 @@ impl EmbeddingQueue {
                     return;
                 };
 
-                match embedding_provider.embed_batch(spans).await {
+                match embedding_provider.embed_batch(spans, api_key).await {
                     Ok(embeddings) => {
                         let mut embeddings = embeddings.into_iter();
                         for fragment in batch {

crates/semantic_index/src/semantic_index.rs 🔗

@@ -123,6 +123,8 @@ pub struct SemanticIndex {
     _embedding_task: Task<()>,
     _parsing_files_tasks: Vec<Task<()>>,
     projects: HashMap<WeakModelHandle<Project>, ProjectState>,
+    api_key: Option<String>,
+    embedding_queue: Arc<Mutex<EmbeddingQueue>>,
 }
 
 struct ProjectState {
@@ -268,7 +270,7 @@ pub struct SearchResult {
 }
 
 impl SemanticIndex {
-    pub fn global(cx: &AppContext) -> Option<ModelHandle<SemanticIndex>> {
+    pub fn global(cx: &mut AppContext) -> Option<ModelHandle<SemanticIndex>> {
         if cx.has_global::<ModelHandle<Self>>() {
             Some(cx.global::<ModelHandle<SemanticIndex>>().clone())
         } else {
@@ -276,12 +278,26 @@ impl SemanticIndex {
         }
     }
 
+    pub fn authenticate(&mut self, cx: &AppContext) {
+        if self.api_key.is_none() {
+            self.api_key = self.embedding_provider.retrieve_credentials(cx);
+
+            self.embedding_queue
+                .lock()
+                .set_api_key(self.api_key.clone());
+        }
+    }
+
+    pub fn is_authenticated(&self) -> bool {
+        self.api_key.is_some()
+    }
+
     pub fn enabled(cx: &AppContext) -> bool {
         settings::get::<SemanticIndexSettings>(cx).enabled
     }
 
     pub fn status(&self, project: &ModelHandle<Project>) -> SemanticIndexStatus {
-        if !self.embedding_provider.is_authenticated() {
+        if !self.is_authenticated() {
             return SemanticIndexStatus::NotAuthenticated;
         }
 
@@ -323,7 +339,7 @@ impl SemanticIndex {
         Ok(cx.add_model(|cx| {
             let t0 = Instant::now();
             let embedding_queue =
-                EmbeddingQueue::new(embedding_provider.clone(), cx.background().clone());
+                EmbeddingQueue::new(embedding_provider.clone(), cx.background().clone(), None);
             let _embedding_task = cx.background().spawn({
                 let embedded_files = embedding_queue.finished_files();
                 let db = db.clone();
@@ -388,6 +404,8 @@ impl SemanticIndex {
                 _embedding_task,
                 _parsing_files_tasks,
                 projects: Default::default(),
+                api_key: None,
+                embedding_queue
             }
         }))
     }
@@ -702,12 +720,13 @@ impl SemanticIndex {
 
         let index = self.index_project(project.clone(), cx);
         let embedding_provider = self.embedding_provider.clone();
+        let api_key = self.api_key.clone();
 
         cx.spawn(|this, mut cx| async move {
             index.await?;
             let t0 = Instant::now();
             let query = embedding_provider
-                .embed_batch(vec![query])
+                .embed_batch(vec![query], api_key)
                 .await?
                 .pop()
                 .ok_or_else(|| anyhow!("could not embed query"))?;
@@ -925,6 +944,7 @@ impl SemanticIndex {
         let fs = self.fs.clone();
         let db_path = self.db.path().clone();
         let background = cx.background().clone();
+        let api_key = self.api_key.clone();
         cx.background().spawn(async move {
             let db = VectorDatabase::new(fs, db_path.clone(), background).await?;
             let mut results = Vec::<SearchResult>::new();
@@ -939,10 +959,15 @@ impl SemanticIndex {
                     .parse_file_with_template(None, &snapshot.text(), language)
                     .log_err()
                     .unwrap_or_default();
-                if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db)
-                    .await
-                    .log_err()
-                    .is_some()
+                if Self::embed_spans(
+                    &mut spans,
+                    embedding_provider.as_ref(),
+                    &db,
+                    api_key.clone(),
+                )
+                .await
+                .log_err()
+                .is_some()
                 {
                     for span in spans {
                         let similarity = span.embedding.unwrap().similarity(&query);
@@ -982,8 +1007,11 @@ impl SemanticIndex {
         project: ModelHandle<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
-        if !self.embedding_provider.is_authenticated() {
-            return Task::ready(Err(anyhow!("user is not authenticated")));
+        if self.api_key.is_none() {
+            self.authenticate(cx);
+            if self.api_key.is_none() {
+                return Task::ready(Err(anyhow!("user is not authenticated")));
+            }
         }
 
         if !self.projects.contains_key(&project.downgrade()) {
@@ -1164,6 +1192,7 @@ impl SemanticIndex {
         spans: &mut [Span],
         embedding_provider: &dyn EmbeddingProvider,
         db: &VectorDatabase,
+        api_key: Option<String>,
     ) -> Result<()> {
         let mut batch = Vec::new();
         let mut batch_tokens = 0;
@@ -1186,7 +1215,7 @@ impl SemanticIndex {
 
             if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() {
                 let batch_embeddings = embedding_provider
-                    .embed_batch(mem::take(&mut batch))
+                    .embed_batch(mem::take(&mut batch), api_key.clone())
                     .await?;
                 embeddings.extend(batch_embeddings);
                 batch_tokens = 0;
@@ -1198,7 +1227,7 @@ impl SemanticIndex {
 
         if !batch.is_empty() {
             let batch_embeddings = embedding_provider
-                .embed_batch(mem::take(&mut batch))
+                .embed_batch(mem::take(&mut batch), api_key)
                 .await?;
 
             embeddings.extend(batch_embeddings);

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -7,7 +7,7 @@ use crate::{
 use ai::embedding::{DummyEmbeddings, Embedding, EmbeddingProvider};
 use anyhow::Result;
 use async_trait::async_trait;
-use gpui::{executor::Deterministic, Task, TestAppContext};
+use gpui::{executor::Deterministic, AppContext, Task, TestAppContext};
 use language::{Language, LanguageConfig, LanguageRegistry, ToOffset};
 use parking_lot::Mutex;
 use pretty_assertions::assert_eq;
@@ -228,7 +228,7 @@ async fn test_embedding_batching(cx: &mut TestAppContext, mut rng: StdRng) {
 
     let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
 
-    let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background());
+    let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background(), None);
     for file in &files {
         queue.push(file.clone());
     }
@@ -1281,8 +1281,8 @@ impl FakeEmbeddingProvider {
 
 #[async_trait]
 impl EmbeddingProvider for FakeEmbeddingProvider {
-    fn is_authenticated(&self) -> bool {
-        true
+    fn retrieve_credentials(&self, _cx: &AppContext) -> Option<String> {
+        Some("Fake Credentials".to_string())
     }
     fn truncate(&self, span: &str) -> (String, usize) {
         (span.to_string(), 1)
@@ -1296,7 +1296,11 @@ impl EmbeddingProvider for FakeEmbeddingProvider {
         None
     }
 
-    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
+    async fn embed_batch(
+        &self,
+        spans: Vec<String>,
+        _api_key: Option<String>,
+    ) -> Result<Vec<Embedding>> {
         self.embedding_count
             .fetch_add(spans.len(), atomic::Ordering::SeqCst);
         Ok(spans.iter().map(|span| self.embed_sync(span)).collect())

crates/storybook2/src/stories/focus.rs 🔗

@@ -1,6 +1,6 @@
 use crate::themes::rose_pine;
 use gpui2::{
-    div, view, Context, Focusable, KeyBinding, ParentElement, StatelessInteractive, Styled, View,
+    div, Focusable, KeyBinding, ParentElement, StatelessInteractive, Styled, View, VisualContext,
     WindowContext,
 };
 use serde::Deserialize;
@@ -35,80 +35,83 @@ impl FocusStory {
         let color_4 = theme.lowest.accent.default.foreground;
         let color_5 = theme.lowest.variant.default.foreground;
         let color_6 = theme.highest.negative.default.foreground;
-
         let child_1 = cx.focus_handle();
         let child_2 = cx.focus_handle();
-        view(cx.entity(|cx| ()), move |_, cx| {
-            div()
-                .id("parent")
-                .focusable()
-                .context("parent")
-                .on_action(|_, action: &ActionA, phase, cx| {
-                    println!("Action A dispatched on parent during {:?}", phase);
-                })
-                .on_action(|_, action: &ActionB, phase, cx| {
-                    println!("Action B dispatched on parent during {:?}", phase);
-                })
-                .on_focus(|_, _, _| println!("Parent focused"))
-                .on_blur(|_, _, _| println!("Parent blurred"))
-                .on_focus_in(|_, _, _| println!("Parent focus_in"))
-                .on_focus_out(|_, _, _| println!("Parent focus_out"))
-                .on_key_down(|_, event, phase, _| {
-                    println!("Key down on parent {:?} {:?}", phase, event)
-                })
-                .on_key_up(|_, event, phase, _| {
-                    println!("Key up on parent {:?} {:?}", phase, event)
-                })
-                .size_full()
-                .bg(color_1)
-                .focus(|style| style.bg(color_2))
-                .focus_in(|style| style.bg(color_3))
-                .child(
-                    div()
-                        .track_focus(&child_1)
-                        .context("child-1")
-                        .on_action(|_, action: &ActionB, phase, cx| {
-                            println!("Action B dispatched on child 1 during {:?}", phase);
-                        })
-                        .w_full()
-                        .h_6()
-                        .bg(color_4)
-                        .focus(|style| style.bg(color_5))
-                        .in_focus(|style| style.bg(color_6))
-                        .on_focus(|_, _, _| println!("Child 1 focused"))
-                        .on_blur(|_, _, _| println!("Child 1 blurred"))
-                        .on_focus_in(|_, _, _| println!("Child 1 focus_in"))
-                        .on_focus_out(|_, _, _| println!("Child 1 focus_out"))
-                        .on_key_down(|_, event, phase, _| {
-                            println!("Key down on child 1 {:?} {:?}", phase, event)
-                        })
-                        .on_key_up(|_, event, phase, _| {
-                            println!("Key up on child 1 {:?} {:?}", phase, event)
-                        })
-                        .child("Child 1"),
-                )
-                .child(
-                    div()
-                        .track_focus(&child_2)
-                        .context("child-2")
-                        .on_action(|_, action: &ActionC, phase, cx| {
-                            println!("Action C dispatched on child 2 during {:?}", phase);
-                        })
-                        .w_full()
-                        .h_6()
-                        .bg(color_4)
-                        .on_focus(|_, _, _| println!("Child 2 focused"))
-                        .on_blur(|_, _, _| println!("Child 2 blurred"))
-                        .on_focus_in(|_, _, _| println!("Child 2 focus_in"))
-                        .on_focus_out(|_, _, _| println!("Child 2 focus_out"))
-                        .on_key_down(|_, event, phase, _| {
-                            println!("Key down on child 2 {:?} {:?}", phase, event)
-                        })
-                        .on_key_up(|_, event, phase, _| {
-                            println!("Key up on child 2 {:?} {:?}", phase, event)
-                        })
-                        .child("Child 2"),
-                )
-        })
+
+        cx.build_view(
+            |_| (),
+            move |_, cx| {
+                div()
+                    .id("parent")
+                    .focusable()
+                    .context("parent")
+                    .on_action(|_, action: &ActionA, phase, cx| {
+                        println!("Action A dispatched on parent during {:?}", phase);
+                    })
+                    .on_action(|_, action: &ActionB, phase, cx| {
+                        println!("Action B dispatched on parent during {:?}", phase);
+                    })
+                    .on_focus(|_, _, _| println!("Parent focused"))
+                    .on_blur(|_, _, _| println!("Parent blurred"))
+                    .on_focus_in(|_, _, _| println!("Parent focus_in"))
+                    .on_focus_out(|_, _, _| println!("Parent focus_out"))
+                    .on_key_down(|_, event, phase, _| {
+                        println!("Key down on parent {:?} {:?}", phase, event)
+                    })
+                    .on_key_up(|_, event, phase, _| {
+                        println!("Key up on parent {:?} {:?}", phase, event)
+                    })
+                    .size_full()
+                    .bg(color_1)
+                    .focus(|style| style.bg(color_2))
+                    .focus_in(|style| style.bg(color_3))
+                    .child(
+                        div()
+                            .track_focus(&child_1)
+                            .context("child-1")
+                            .on_action(|_, action: &ActionB, phase, cx| {
+                                println!("Action B dispatched on child 1 during {:?}", phase);
+                            })
+                            .w_full()
+                            .h_6()
+                            .bg(color_4)
+                            .focus(|style| style.bg(color_5))
+                            .in_focus(|style| style.bg(color_6))
+                            .on_focus(|_, _, _| println!("Child 1 focused"))
+                            .on_blur(|_, _, _| println!("Child 1 blurred"))
+                            .on_focus_in(|_, _, _| println!("Child 1 focus_in"))
+                            .on_focus_out(|_, _, _| println!("Child 1 focus_out"))
+                            .on_key_down(|_, event, phase, _| {
+                                println!("Key down on child 1 {:?} {:?}", phase, event)
+                            })
+                            .on_key_up(|_, event, phase, _| {
+                                println!("Key up on child 1 {:?} {:?}", phase, event)
+                            })
+                            .child("Child 1"),
+                    )
+                    .child(
+                        div()
+                            .track_focus(&child_2)
+                            .context("child-2")
+                            .on_action(|_, action: &ActionC, phase, cx| {
+                                println!("Action C dispatched on child 2 during {:?}", phase);
+                            })
+                            .w_full()
+                            .h_6()
+                            .bg(color_4)
+                            .on_focus(|_, _, _| println!("Child 2 focused"))
+                            .on_blur(|_, _, _| println!("Child 2 blurred"))
+                            .on_focus_in(|_, _, _| println!("Child 2 focus_in"))
+                            .on_focus_out(|_, _, _| println!("Child 2 focus_out"))
+                            .on_key_down(|_, event, phase, _| {
+                                println!("Key down on child 2 {:?} {:?}", phase, event)
+                            })
+                            .on_key_up(|_, event, phase, _| {
+                                println!("Key up on child 2 {:?} {:?}", phase, event)
+                            })
+                            .child("Child 2"),
+                    )
+            },
+        )
     }
 }

crates/storybook2/src/stories/kitchen_sink.rs 🔗

@@ -1,4 +1,4 @@
-use gpui2::{view, Context, View};
+use gpui2::{AppContext, Context, View};
 use strum::IntoEnumIterator;
 use ui::prelude::*;
 
@@ -12,8 +12,12 @@ impl KitchenSinkStory {
         Self {}
     }
 
-    pub fn view(cx: &mut WindowContext) -> View<Self> {
-        view(cx.entity(|cx| Self::new()), Self::render)
+    pub fn view(cx: &mut AppContext) -> View<Self> {
+        {
+            let state = cx.entity(|cx| Self::new());
+            let render = Self::render;
+            View::for_handle(state, render)
+        }
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {

crates/storybook2/src/stories/scroll.rs 🔗

@@ -1,6 +1,6 @@
 use crate::themes::rose_pine;
 use gpui2::{
-    div, px, view, Component, Context, ParentElement, SharedString, Styled, View, WindowContext,
+    div, px, Component, ParentElement, SharedString, Styled, View, VisualContext, WindowContext,
 };
 
 pub struct ScrollStory {
@@ -11,7 +11,9 @@ impl ScrollStory {
     pub fn view(cx: &mut WindowContext) -> View<()> {
         let theme = rose_pine();
 
-        view(cx.entity(|cx| ()), move |_, cx| checkerboard(1))
+        {
+            cx.build_view(|cx| (), move |_, cx| checkerboard(1))
+        }
     }
 }
 

crates/storybook2/src/stories/text.rs 🔗

@@ -1,4 +1,4 @@
-use gpui2::{div, view, white, Context, ParentElement, Styled, View, WindowContext};
+use gpui2::{div, white, ParentElement, Styled, View, VisualContext, WindowContext};
 
 pub struct TextStory {
     text: View<()>,
@@ -6,7 +6,7 @@ pub struct TextStory {
 
 impl TextStory {
     pub fn view(cx: &mut WindowContext) -> View<()> {
-        view(cx.entity(|cx| ()), |_, cx| {
+        cx.build_view(|cx| (), |_, cx| {
             div()
                 .size_full()
                 .bg(white())

crates/storybook2/src/stories/z_index.rs 🔗

@@ -1,4 +1,3 @@
-
 use gpui2::{px, rgb, Div, Hsla};
 use ui::prelude::*;
 
@@ -10,11 +9,7 @@ use crate::story::Story;
 pub struct ZIndexStory;
 
 impl ZIndexStory {
-    pub fn new() -> Self {
-        Self
-    }
-
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         Story::container(cx)
             .child(Story::title(cx, "z-index"))
             .child(

crates/storybook2/src/story_selector.rs 🔗

@@ -5,7 +5,7 @@ use crate::stories::*;
 use anyhow::anyhow;
 use clap::builder::PossibleValue;
 use clap::ValueEnum;
-use gpui2::{view, AnyView, Context};
+use gpui2::{AnyView, VisualContext};
 use strum::{EnumIter, EnumString, IntoEnumIterator};
 use ui::prelude::*;
 
@@ -27,31 +27,18 @@ pub enum ElementStory {
 impl ElementStory {
     pub fn story(&self, cx: &mut WindowContext) -> AnyView {
         match self {
-            Self::Avatar => {
-                view(cx.entity(|cx| ()), |_, _| ui::AvatarStory::new().render()).into_any()
+            Self::Avatar => { cx.build_view(|cx| (), |_, _| ui::AvatarStory.render()) }.into_any(),
+            Self::Button => { cx.build_view(|cx| (), |_, _| ui::ButtonStory.render()) }.into_any(),
+            Self::Details => {
+                { cx.build_view(|cx| (), |_, _| ui::DetailsStory.render()) }.into_any()
             }
-            Self::Button => {
-                view(cx.entity(|cx| ()), |_, _| ui::ButtonStory::new().render()).into_any()
-            }
-            Self::Details => view(cx.entity(|cx| ()), |_, _| {
-                ui::DetailsStory::new().render()
-            })
-            .into_any(),
             Self::Focus => FocusStory::view(cx).into_any(),
-            Self::Icon => {
-                view(cx.entity(|cx| ()), |_, _| ui::IconStory::new().render()).into_any()
-            }
-            Self::Input => {
-                view(cx.entity(|cx| ()), |_, _| ui::InputStory::new().render()).into_any()
-            }
-            Self::Label => {
-                view(cx.entity(|cx| ()), |_, _| ui::LabelStory::new().render()).into_any()
-            }
+            Self::Icon => { cx.build_view(|cx| (), |_, _| ui::IconStory.render()) }.into_any(),
+            Self::Input => { cx.build_view(|cx| (), |_, _| ui::InputStory.render()) }.into_any(),
+            Self::Label => { cx.build_view(|cx| (), |_, _| ui::LabelStory.render()) }.into_any(),
             Self::Scroll => ScrollStory::view(cx).into_any(),
             Self::Text => TextStory::view(cx).into_any(),
-            Self::ZIndex => {
-                view(cx.entity(|cx| ()), |_, _| ZIndexStory::new().render()).into_any()
-            }
+            Self::ZIndex => { cx.build_view(|cx| (), |_, _| ZIndexStory.render()) }.into_any(),
         }
     }
 }
@@ -90,96 +77,69 @@ pub enum ComponentStory {
 impl ComponentStory {
     pub fn story(&self, cx: &mut WindowContext) -> AnyView {
         match self {
-            Self::AssistantPanel => view(cx.entity(|cx| ()), |_, _| {
-                ui::AssistantPanelStory::new().render()
-            })
-            .into_any(),
-            Self::Buffer => {
-                view(cx.entity(|cx| ()), |_, _| ui::BufferStory::new().render()).into_any()
+            Self::AssistantPanel => {
+                { cx.build_view(|cx| (), |_, _| ui::AssistantPanelStory.render()) }.into_any()
+            }
+            Self::Buffer => { cx.build_view(|cx| (), |_, _| ui::BufferStory.render()) }.into_any(),
+            Self::Breadcrumb => {
+                { cx.build_view(|cx| (), |_, _| ui::BreadcrumbStory.render()) }.into_any()
+            }
+            Self::ChatPanel => {
+                { cx.build_view(|cx| (), |_, _| ui::ChatPanelStory.render()) }.into_any()
+            }
+            Self::CollabPanel => {
+                { cx.build_view(|cx| (), |_, _| ui::CollabPanelStory.render()) }.into_any()
+            }
+            Self::CommandPalette => {
+                { cx.build_view(|cx| (), |_, _| ui::CommandPaletteStory.render()) }.into_any()
+            }
+            Self::ContextMenu => {
+                { cx.build_view(|cx| (), |_, _| ui::ContextMenuStory.render()) }.into_any()
             }
-            Self::Breadcrumb => view(cx.entity(|cx| ()), |_, _| {
-                ui::BreadcrumbStory::new().render()
-            })
-            .into_any(),
-            Self::ChatPanel => view(cx.entity(|cx| ()), |_, _| {
-                ui::ChatPanelStory::new().render()
-            })
-            .into_any(),
-            Self::CollabPanel => view(cx.entity(|cx| ()), |_, _| {
-                ui::CollabPanelStory::new().render()
-            })
-            .into_any(),
-            Self::CommandPalette => view(cx.entity(|cx| ()), |_, _| {
-                ui::CommandPaletteStory::new().render()
-            })
-            .into_any(),
-            Self::ContextMenu => view(cx.entity(|cx| ()), |_, _| {
-                ui::ContextMenuStory::new().render()
-            })
-            .into_any(),
-            Self::Facepile => view(cx.entity(|cx| ()), |_, _| {
-                ui::FacepileStory::new().render()
-            })
-            .into_any(),
-            Self::Keybinding => view(cx.entity(|cx| ()), |_, _| {
-                ui::KeybindingStory::new().render()
-            })
-            .into_any(),
-            Self::LanguageSelector => view(cx.entity(|cx| ()), |_, _| {
-                ui::LanguageSelectorStory::new().render()
-            })
-            .into_any(),
-            Self::MultiBuffer => view(cx.entity(|cx| ()), |_, _| {
-                ui::MultiBufferStory::new().render()
-            })
-            .into_any(),
-            Self::NotificationsPanel => view(cx.entity(|cx| ()), |_, _| {
-                ui::NotificationsPanelStory::new().render()
-            })
-            .into_any(),
-            Self::Palette => view(cx.entity(|cx| ()), |_, _| {
-                ui::PaletteStory::new().render()
-            })
-            .into_any(),
-            Self::Panel => {
-                view(cx.entity(|cx| ()), |_, _| ui::PanelStory::new().render()).into_any()
+            Self::Facepile => {
+                { cx.build_view(|cx| (), |_, _| ui::FacepileStory.render()) }.into_any()
             }
-            Self::ProjectPanel => view(cx.entity(|cx| ()), |_, _| {
-                ui::ProjectPanelStory::new().render()
-            })
-            .into_any(),
-            Self::RecentProjects => view(cx.entity(|cx| ()), |_, _| {
-                ui::RecentProjectsStory::new().render()
-            })
-            .into_any(),
-            Self::Tab => view(cx.entity(|cx| ()), |_, _| ui::TabStory::new().render()).into_any(),
-            Self::TabBar => {
-                view(cx.entity(|cx| ()), |_, _| ui::TabBarStory::new().render()).into_any()
+            Self::Keybinding => {
+                { cx.build_view(|cx| (), |_, _| ui::KeybindingStory.render()) }.into_any()
+            }
+            Self::LanguageSelector => {
+                { cx.build_view(|cx| (), |_, _| ui::LanguageSelectorStory.render()) }.into_any()
+            }
+            Self::MultiBuffer => {
+                { cx.build_view(|cx| (), |_, _| ui::MultiBufferStory.render()) }.into_any()
+            }
+            Self::NotificationsPanel => {
+                { cx.build_view(|cx| (), |_, _| ui::NotificationsPanelStory.render()) }.into_any()
+            }
+            Self::Palette => {
+                { cx.build_view(|cx| (), |_, _| ui::PaletteStory.render()) }.into_any()
+            }
+            Self::Panel => { cx.build_view(|cx| (), |_, _| ui::PanelStory.render()) }.into_any(),
+            Self::ProjectPanel => {
+                { cx.build_view(|cx| (), |_, _| ui::ProjectPanelStory.render()) }.into_any()
+            }
+            Self::RecentProjects => {
+                { cx.build_view(|cx| (), |_, _| ui::RecentProjectsStory.render()) }.into_any()
+            }
+            Self::Tab => { cx.build_view(|cx| (), |_, _| ui::TabStory.render()) }.into_any(),
+            Self::TabBar => { cx.build_view(|cx| (), |_, _| ui::TabBarStory.render()) }.into_any(),
+            Self::Terminal => {
+                { cx.build_view(|cx| (), |_, _| ui::TerminalStory.render()) }.into_any()
+            }
+            Self::ThemeSelector => {
+                { cx.build_view(|cx| (), |_, _| ui::ThemeSelectorStory.render()) }.into_any()
             }
-            Self::Terminal => view(cx.entity(|cx| ()), |_, _| {
-                ui::TerminalStory::new().render()
-            })
-            .into_any(),
-            Self::ThemeSelector => view(cx.entity(|cx| ()), |_, _| {
-                ui::ThemeSelectorStory::new().render()
-            })
-            .into_any(),
             Self::TitleBar => ui::TitleBarStory::view(cx).into_any(),
-            Self::Toast => {
-                view(cx.entity(|cx| ()), |_, _| ui::ToastStory::new().render()).into_any()
+            Self::Toast => { cx.build_view(|cx| (), |_, _| ui::ToastStory.render()) }.into_any(),
+            Self::Toolbar => {
+                { cx.build_view(|cx| (), |_, _| ui::ToolbarStory.render()) }.into_any()
+            }
+            Self::TrafficLights => {
+                { cx.build_view(|cx| (), |_, _| ui::TrafficLightsStory.render()) }.into_any()
+            }
+            Self::Copilot => {
+                { cx.build_view(|cx| (), |_, _| ui::CopilotModalStory.render()) }.into_any()
             }
-            Self::Toolbar => view(cx.entity(|cx| ()), |_, _| {
-                ui::ToolbarStory::new().render()
-            })
-            .into_any(),
-            Self::TrafficLights => view(cx.entity(|cx| ()), |_, _| {
-                ui::TrafficLightsStory::new().render()
-            })
-            .into_any(),
-            Self::Copilot => view(cx.entity(|cx| ()), |_, _| {
-                ui::CopilotModalStory::new().render()
-            })
-            .into_any(),
             Self::Workspace => ui::WorkspaceStory::view(cx).into_any(),
         }
     }

crates/storybook2/src/storybook2.rs 🔗

@@ -10,7 +10,7 @@ use std::sync::Arc;
 
 use clap::Parser;
 use gpui2::{
-    div, px, size, view, AnyView, AppContext, Bounds, Context, ViewContext, WindowBounds,
+    div, px, size, AnyView, AppContext, Bounds, ViewContext, VisualContext, WindowBounds,
     WindowOptions,
 };
 use log::LevelFilter;
@@ -85,8 +85,8 @@ fn main() {
                 ..Default::default()
             },
             move |cx| {
-                view(
-                    cx.entity(|cx| StoryWrapper::new(selector.story(cx), theme)),
+                cx.build_view(
+                    |cx| StoryWrapper::new(selector.story(cx), theme),
                     StoryWrapper::render,
                 )
             },

crates/terminal_view/src/terminal_view.rs 🔗

@@ -150,11 +150,14 @@ impl TerminalView {
                 cx.notify();
                 cx.emit(Event::Wakeup);
             }
+
             Event::Bell => {
                 this.has_bell = true;
                 cx.emit(Event::Wakeup);
             }
+
             Event::BlinkChanged => this.blinking_on = !this.blinking_on,
+
             Event::TitleChanged => {
                 if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
                     let cwd = foreground_info.cwd.clone();
@@ -171,6 +174,7 @@ impl TerminalView {
                         .detach();
                 }
             }
+
             Event::NewNavigationTarget(maybe_navigation_target) => {
                 this.can_navigate_to_selected_word = match maybe_navigation_target {
                     Some(MaybeNavigationTarget::Url(_)) => true,
@@ -180,8 +184,10 @@ impl TerminalView {
                     None => false,
                 }
             }
+
             Event::Open(maybe_navigation_target) => match maybe_navigation_target {
                 MaybeNavigationTarget::Url(url) => cx.platform().open_url(url),
+
                 MaybeNavigationTarget::PathLike(maybe_path) => {
                     if !this.can_navigate_to_selected_word {
                         return;
@@ -246,6 +252,7 @@ impl TerminalView {
                     }
                 }
             },
+
             _ => cx.emit(event.clone()),
         })
         .detach();

crates/text/src/selection.rs 🔗

@@ -2,14 +2,15 @@ use crate::{Anchor, BufferSnapshot, TextDimension};
 use std::cmp::Ordering;
 use std::ops::Range;
 
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq)]
 pub enum SelectionGoal {
     None,
-    Column(u32),
-    ColumnRange { start: u32, end: u32 },
+    HorizontalPosition(f32),
+    HorizontalRange { start: f32, end: f32 },
+    WrappedHorizontalPosition((u32, f32)),
 }
 
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct Selection<T> {
     pub id: usize,
     pub start: T,

crates/theme/src/theme.rs 🔗

@@ -53,6 +53,7 @@ pub struct Theme {
     pub collab_panel: CollabPanel,
     pub project_panel: ProjectPanel,
     pub chat_panel: ChatPanel,
+    pub notification_panel: NotificationPanel,
     pub command_palette: CommandPalette,
     pub picker: Picker,
     pub editor: Editor,
@@ -249,6 +250,7 @@ pub struct CollabPanel {
     pub add_contact_button: Toggleable<Interactive<IconButton>>,
     pub add_channel_button: Toggleable<Interactive<IconButton>>,
     pub header_row: ContainedText,
+    pub dragged_over_header: ContainerStyle,
     pub subheader_row: Toggleable<Interactive<ContainedText>>,
     pub leave_call: Interactive<ContainedText>,
     pub contact_row: Toggleable<Interactive<ContainerStyle>>,
@@ -286,6 +288,8 @@ pub struct TabbedModal {
     pub header: ContainerStyle,
     pub body: ContainerStyle,
     pub title: ContainedText,
+    pub visibility_toggle: Interactive<ContainedText>,
+    pub channel_link: Interactive<ContainedText>,
     pub picker: Picker,
     pub max_height: f32,
     pub max_width: f32,
@@ -636,21 +640,43 @@ pub struct ChatPanel {
     pub input_editor: FieldEditor,
     pub avatar: AvatarStyle,
     pub avatar_container: ContainerStyle,
-    pub message: ChatMessage,
-    pub continuation_message: ChatMessage,
+    pub rich_text: RichTextStyle,
+    pub message_sender: ContainedText,
+    pub message_timestamp: ContainedText,
+    pub message: Interactive<ContainerStyle>,
+    pub continuation_message: Interactive<ContainerStyle>,
+    pub pending_message: Interactive<ContainerStyle>,
     pub last_message_bottom_spacing: f32,
-    pub pending_message: ChatMessage,
     pub sign_in_prompt: Interactive<TextStyle>,
     pub icon_button: Interactive<IconButton>,
 }
 
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct RichTextStyle {
+    pub text: TextStyle,
+    pub mention_highlight: HighlightStyle,
+    pub mention_background: Option<Color>,
+    pub self_mention_highlight: HighlightStyle,
+    pub self_mention_background: Option<Color>,
+    pub code_background: Option<Color>,
+}
+
 #[derive(Deserialize, Default, JsonSchema)]
-pub struct ChatMessage {
+pub struct NotificationPanel {
     #[serde(flatten)]
-    pub container: Interactive<ContainerStyle>,
-    pub body: TextStyle,
-    pub sender: ContainedText,
+    pub container: ContainerStyle,
+    pub title: ContainedText,
+    pub title_icon: SvgStyle,
+    pub title_height: f32,
+    pub list: ContainerStyle,
+    pub avatar: AvatarStyle,
+    pub avatar_container: ContainerStyle,
+    pub sign_in_prompt: Interactive<TextStyle>,
+    pub icon_button: Interactive<IconButton>,
+    pub unread_text: ContainedText,
+    pub read_text: ContainedText,
     pub timestamp: ContainedText,
+    pub button: Interactive<ContainedText>,
 }
 
 #[derive(Deserialize, Default, JsonSchema)]
@@ -867,9 +893,13 @@ pub struct AutocompleteStyle {
     pub selected_item: ContainerStyle,
     pub hovered_item: ContainerStyle,
     pub match_highlight: HighlightStyle,
-    pub server_name_container: ContainerStyle,
-    pub server_name_color: Color,
-    pub server_name_size_percent: f32,
+    pub completion_min_width: f32,
+    pub completion_max_width: f32,
+    pub inline_docs_container: ContainerStyle,
+    pub inline_docs_color: Color,
+    pub inline_docs_size_percent: f32,
+    pub alongside_docs_max_width: f32,
+    pub alongside_docs_container: ContainerStyle,
 }
 
 #[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
@@ -1195,6 +1225,15 @@ pub struct InlineAssistantStyle {
     pub disabled_editor: FieldEditor,
     pub pending_edit_background: Color,
     pub include_conversation: ToggleIconButtonStyle,
+    pub retrieve_context: ToggleIconButtonStyle,
+    pub context_status: ContextStatusStyle,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct ContextStatusStyle {
+    pub error_icon: Icon,
+    pub in_progress_icon: Icon,
+    pub complete_icon: Icon,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]

crates/theme2/src/theme2.rs 🔗

@@ -90,13 +90,22 @@ pub struct Theme {
 
 #[derive(Clone)]
 pub struct SyntaxTheme {
-    pub comment: Hsla,
-    pub string: Hsla,
-    pub function: Hsla,
-    pub keyword: Hsla,
     pub highlights: Vec<(String, HighlightStyle)>,
 }
 
+impl SyntaxTheme {
+    pub fn get(&self, name: &str) -> HighlightStyle {
+        self.highlights
+            .iter()
+            .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None })
+            .unwrap_or_default()
+    }
+
+    pub fn color(&self, name: &str) -> Hsla {
+        self.get(name).color.unwrap_or_default()
+    }
+}
+
 #[derive(Clone, Copy)]
 pub struct PlayerTheme {
     pub cursor: Hsla,

crates/theme2/src/themes/one_dark.rs 🔗

@@ -36,11 +36,47 @@ pub fn one_dark() -> Theme {
         text_accent: rgba(0x74ade8ff).into(),
         icon_muted: rgba(0x838994ff).into(),
         syntax: SyntaxTheme {
-            comment: rgba(0x5d636fff).into(),
-            string: rgba(0xa1c181ff).into(),
-            function: rgba(0x73ade9ff).into(),
-            keyword: rgba(0xb477cfff).into(),
-            highlights: vec![],
+            highlights: vec![
+                ("link_uri".into(), rgba(0x6eb4bfff).into()),
+                ("number".into(), rgba(0xbf956aff).into()),
+                ("property".into(), rgba(0xd07277ff).into()),
+                ("boolean".into(), rgba(0xbf956aff).into()),
+                ("label".into(), rgba(0x74ade8ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xd07277ff).into()),
+                ("keyword".into(), rgba(0xb477cfff).into()),
+                ("punctuation.delimiter".into(), rgba(0xb2b9c6ff).into()),
+                ("string.special".into(), rgba(0xbf956aff).into()),
+                ("constant".into(), rgba(0xdfc184ff).into()),
+                ("punctuation".into(), rgba(0xacb2beff).into()),
+                ("variable.special".into(), rgba(0xbf956aff).into()),
+                ("preproc".into(), rgba(0xc8ccd4ff).into()),
+                ("enum".into(), rgba(0xd07277ff).into()),
+                ("attribute".into(), rgba(0x74ade8ff).into()),
+                ("emphasis.strong".into(), rgba(0xbf956aff).into()),
+                ("title".into(), rgba(0xd07277ff).into()),
+                ("hint".into(), rgba(0x5a6f89ff).into()),
+                ("emphasis".into(), rgba(0x74ade8ff).into()),
+                ("string.regex".into(), rgba(0xbf956aff).into()),
+                ("link_text".into(), rgba(0x73ade9ff).into()),
+                ("string".into(), rgba(0xa1c181ff).into()),
+                ("comment.doc".into(), rgba(0x878e98ff).into()),
+                ("punctuation.special".into(), rgba(0xb1574bff).into()),
+                ("primary".into(), rgba(0xacb2beff).into()),
+                ("operator".into(), rgba(0x6eb4bfff).into()),
+                ("function".into(), rgba(0x73ade9ff).into()),
+                ("string.special.symbol".into(), rgba(0xbf956aff).into()),
+                ("type".into(), rgba(0x6eb4bfff).into()),
+                ("variant".into(), rgba(0x73ade9ff).into()),
+                ("tag".into(), rgba(0x74ade8ff).into()),
+                ("punctuation.bracket".into(), rgba(0xb2b9c6ff).into()),
+                ("embedded".into(), rgba(0xc8ccd4ff).into()),
+                ("string.escape".into(), rgba(0x878e98ff).into()),
+                ("variable".into(), rgba(0xc8ccd4ff).into()),
+                ("predictive".into(), rgba(0x5a6a87ff).into()),
+                ("comment".into(), rgba(0x5d636fff).into()),
+                ("text.literal".into(), rgba(0xa1c181ff).into()),
+                ("constructor".into(), rgba(0x73ade9ff).into()),
+            ],
         },
         status_bar: rgba(0x3b414dff).into(),
         title_bar: rgba(0x3b414dff).into(),

crates/theme2/src/themes/rose_pine.rs 🔗

@@ -36,11 +36,48 @@ pub fn rose_pine() -> Theme {
         text_accent: rgba(0x9bced6ff).into(),
         icon_muted: rgba(0x74708dff).into(),
         syntax: SyntaxTheme {
-            comment: rgba(0x6e6a86ff).into(),
-            string: rgba(0xf5c177ff).into(),
-            function: rgba(0xebbcbaff).into(),
-            keyword: rgba(0x30738fff).into(),
-            highlights: vec![],
+            highlights: vec![
+                ("text.literal".into(), rgba(0xc4a7e6ff).into()),
+                ("string".into(), rgba(0xf5c177ff).into()),
+                ("enum".into(), rgba(0xc4a7e6ff).into()),
+                ("number".into(), rgba(0x5cc1a3ff).into()),
+                ("attribute".into(), rgba(0x9bced6ff).into()),
+                ("property".into(), rgba(0x9bced6ff).into()),
+                ("function".into(), rgba(0xebbcbaff).into()),
+                ("embedded".into(), rgba(0xe0def4ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x9d99b6ff).into()),
+                ("variant".into(), rgba(0x9bced6ff).into()),
+                ("operator".into(), rgba(0x30738fff).into()),
+                ("comment".into(), rgba(0x6e6a86ff).into()),
+                ("type.builtin".into(), rgba(0x9ccfd8ff).into()),
+                ("label".into(), rgba(0x9bced6ff).into()),
+                ("string.escape".into(), rgba(0x76728fff).into()),
+                ("type".into(), rgba(0x9ccfd8ff).into()),
+                ("constructor".into(), rgba(0x9bced6ff).into()),
+                ("punctuation.bracket".into(), rgba(0x9d99b6ff).into()),
+                ("function.method".into(), rgba(0xebbcbaff).into()),
+                ("tag".into(), rgba(0x9ccfd8ff).into()),
+                ("link_text".into(), rgba(0x9ccfd8ff).into()),
+                ("string.special".into(), rgba(0xc4a7e6ff).into()),
+                ("string.regex".into(), rgba(0xc4a7e6ff).into()),
+                ("preproc".into(), rgba(0xe0def4ff).into()),
+                ("emphasis.strong".into(), rgba(0x9bced6ff).into()),
+                ("emphasis".into(), rgba(0x9bced6ff).into()),
+                ("comment.doc".into(), rgba(0x76728fff).into()),
+                ("boolean".into(), rgba(0xebbcbaff).into()),
+                ("punctuation.list_marker".into(), rgba(0x9d99b6ff).into()),
+                ("hint".into(), rgba(0x5e768cff).into()),
+                ("title".into(), rgba(0xf5c177ff).into()),
+                ("variable".into(), rgba(0xe0def4ff).into()),
+                ("string.special.symbol".into(), rgba(0xc4a7e6ff).into()),
+                ("primary".into(), rgba(0xe0def4ff).into()),
+                ("predictive".into(), rgba(0x556b81ff).into()),
+                ("punctuation".into(), rgba(0x908caaff).into()),
+                ("constant".into(), rgba(0x5cc1a3ff).into()),
+                ("punctuation.special".into(), rgba(0x9d99b6ff).into()),
+                ("link_uri".into(), rgba(0xebbcbaff).into()),
+                ("keyword".into(), rgba(0x30738fff).into()),
+            ],
         },
         status_bar: rgba(0x292738ff).into(),
         title_bar: rgba(0x292738ff).into(),
@@ -128,11 +165,48 @@ pub fn rose_pine_dawn() -> Theme {
         text_accent: rgba(0x57949fff).into(),
         icon_muted: rgba(0x706c8cff).into(),
         syntax: SyntaxTheme {
-            comment: rgba(0x9893a5ff).into(),
-            string: rgba(0xea9d34ff).into(),
-            function: rgba(0xd7827dff).into(),
-            keyword: rgba(0x276983ff).into(),
-            highlights: Vec::new(),
+            highlights: vec![
+                ("type".into(), rgba(0x55949fff).into()),
+                ("keyword".into(), rgba(0x276983ff).into()),
+                ("link_text".into(), rgba(0x55949fff).into()),
+                ("embedded".into(), rgba(0x575279ff).into()),
+                ("type.builtin".into(), rgba(0x55949fff).into()),
+                ("punctuation.delimiter".into(), rgba(0x635e82ff).into()),
+                ("text.literal".into(), rgba(0x9079a9ff).into()),
+                ("variant".into(), rgba(0x57949fff).into()),
+                ("string".into(), rgba(0xea9d34ff).into()),
+                ("hint".into(), rgba(0x7a92aaff).into()),
+                ("punctuation.special".into(), rgba(0x635e82ff).into()),
+                ("string.special".into(), rgba(0x9079a9ff).into()),
+                ("string.regex".into(), rgba(0x9079a9ff).into()),
+                ("operator".into(), rgba(0x276983ff).into()),
+                ("boolean".into(), rgba(0xd7827dff).into()),
+                ("constructor".into(), rgba(0x57949fff).into()),
+                ("punctuation".into(), rgba(0x797593ff).into()),
+                ("label".into(), rgba(0x57949fff).into()),
+                ("variable".into(), rgba(0x575279ff).into()),
+                ("tag".into(), rgba(0x55949fff).into()),
+                ("primary".into(), rgba(0x575279ff).into()),
+                ("link_uri".into(), rgba(0xd7827dff).into()),
+                ("punctuation.list_marker".into(), rgba(0x635e82ff).into()),
+                ("string.escape".into(), rgba(0x6e6a8bff).into()),
+                ("punctuation.bracket".into(), rgba(0x635e82ff).into()),
+                ("function".into(), rgba(0xd7827dff).into()),
+                ("preproc".into(), rgba(0x575279ff).into()),
+                ("function.method".into(), rgba(0xd7827dff).into()),
+                ("predictive".into(), rgba(0xa2acbeff).into()),
+                ("comment.doc".into(), rgba(0x6e6a8bff).into()),
+                ("comment".into(), rgba(0x9893a5ff).into()),
+                ("number".into(), rgba(0x3daa8eff).into()),
+                ("emphasis".into(), rgba(0x57949fff).into()),
+                ("title".into(), rgba(0xea9d34ff).into()),
+                ("enum".into(), rgba(0x9079a9ff).into()),
+                ("string.special.symbol".into(), rgba(0x9079a9ff).into()),
+                ("constant".into(), rgba(0x3daa8eff).into()),
+                ("emphasis.strong".into(), rgba(0x57949fff).into()),
+                ("property".into(), rgba(0x57949fff).into()),
+                ("attribute".into(), rgba(0x57949fff).into()),
+            ],
         },
         status_bar: rgba(0xdcd8d8ff).into(),
         title_bar: rgba(0xdcd8d8ff).into(),
@@ -220,11 +294,48 @@ pub fn rose_pine_moon() -> Theme {
         text_accent: rgba(0x9bced6ff).into(),
         icon_muted: rgba(0x85819eff).into(),
         syntax: SyntaxTheme {
-            comment: rgba(0x6e6a86ff).into(),
-            string: rgba(0xf5c177ff).into(),
-            function: rgba(0xea9a97ff).into(),
-            keyword: rgba(0x3d8fb0ff).into(),
-            highlights: Vec::new(),
+            highlights: vec![
+                ("embedded".into(), rgba(0xe0def4ff).into()),
+                ("link_uri".into(), rgba(0xea9a97ff).into()),
+                ("primary".into(), rgba(0xe0def4ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xaeabc6ff).into()),
+                ("string.escape".into(), rgba(0x8682a0ff).into()),
+                ("attribute".into(), rgba(0x9bced6ff).into()),
+                ("constant".into(), rgba(0x5cc1a3ff).into()),
+                ("keyword".into(), rgba(0x3d8fb0ff).into()),
+                ("predictive".into(), rgba(0x516b83ff).into()),
+                ("label".into(), rgba(0x9bced6ff).into()),
+                ("comment.doc".into(), rgba(0x8682a0ff).into()),
+                ("emphasis".into(), rgba(0x9bced6ff).into()),
+                ("string".into(), rgba(0xf5c177ff).into()),
+                ("type".into(), rgba(0x9ccfd8ff).into()),
+                ("string.special".into(), rgba(0xc4a7e6ff).into()),
+                ("function".into(), rgba(0xea9a97ff).into()),
+                ("constructor".into(), rgba(0x9bced6ff).into()),
+                ("comment".into(), rgba(0x6e6a86ff).into()),
+                ("preproc".into(), rgba(0xe0def4ff).into()),
+                ("enum".into(), rgba(0xc4a7e6ff).into()),
+                ("punctuation.bracket".into(), rgba(0xaeabc6ff).into()),
+                ("number".into(), rgba(0x5cc1a3ff).into()),
+                ("hint".into(), rgba(0x728aa2ff).into()),
+                ("variant".into(), rgba(0x9bced6ff).into()),
+                ("link_text".into(), rgba(0x9ccfd8ff).into()),
+                ("property".into(), rgba(0x9bced6ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xaeabc6ff).into()),
+                ("operator".into(), rgba(0x3d8fb0ff).into()),
+                ("title".into(), rgba(0xf5c177ff).into()),
+                ("punctuation".into(), rgba(0x908caaff).into()),
+                ("string.regex".into(), rgba(0xc4a7e6ff).into()),
+                ("tag".into(), rgba(0x9ccfd8ff).into()),
+                ("emphasis.strong".into(), rgba(0x9bced6ff).into()),
+                ("text.literal".into(), rgba(0xc4a7e6ff).into()),
+                ("punctuation.special".into(), rgba(0xaeabc6ff).into()),
+                ("boolean".into(), rgba(0xea9a97ff).into()),
+                ("type.builtin".into(), rgba(0x9ccfd8ff).into()),
+                ("function.method".into(), rgba(0xea9a97ff).into()),
+                ("variable".into(), rgba(0xe0def4ff).into()),
+                ("string.special.symbol".into(), rgba(0xc4a7e6ff).into()),
+            ],
         },
         status_bar: rgba(0x38354eff).into(),
         title_bar: rgba(0x38354eff).into(),

crates/theme2/src/themes/sandcastle.rs 🔗

@@ -36,11 +36,46 @@ pub fn sandcastle() -> Theme {
         text_accent: rgba(0x518b8bff).into(),
         icon_muted: rgba(0xa69782ff).into(),
         syntax: SyntaxTheme {
-            comment: rgba(0xff00ffff).into(),
-            string: rgba(0xff00ffff).into(),
-            function: rgba(0xff00ffff).into(),
-            keyword: rgba(0xff00ffff).into(),
-            highlights: vec![],
+            highlights: vec![
+                ("string.special.symbol".into(), rgba(0xa07d3aff).into()),
+                ("enum".into(), rgba(0xa07d3aff).into()),
+                ("punctuation.bracket".into(), rgba(0xd5c5a1ff).into()),
+                ("hint".into(), rgba(0x727d68ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xd5c5a1ff).into()),
+                ("comment".into(), rgba(0xa89984ff).into()),
+                ("embedded".into(), rgba(0xfdf4c1ff).into()),
+                ("string".into(), rgba(0xa07d3aff).into()),
+                ("string.escape".into(), rgba(0xa89984ff).into()),
+                ("comment.doc".into(), rgba(0xa89984ff).into()),
+                ("variant".into(), rgba(0x518b8bff).into()),
+                ("predictive".into(), rgba(0x5c6152ff).into()),
+                ("link_text".into(), rgba(0xa07d3aff).into()),
+                ("attribute".into(), rgba(0x518b8bff).into()),
+                ("title".into(), rgba(0xfdf4c1ff).into()),
+                ("emphasis.strong".into(), rgba(0x518b8bff).into()),
+                ("primary".into(), rgba(0xfdf4c1ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xd5c5a1ff).into()),
+                ("boolean".into(), rgba(0x83a598ff).into()),
+                ("function".into(), rgba(0xa07d3aff).into()),
+                ("punctuation.special".into(), rgba(0xd5c5a1ff).into()),
+                ("string.special".into(), rgba(0xa07d3aff).into()),
+                ("string.regex".into(), rgba(0xa07d3aff).into()),
+                ("tag".into(), rgba(0x518b8bff).into()),
+                ("keyword".into(), rgba(0x518b8bff).into()),
+                ("type".into(), rgba(0x83a598ff).into()),
+                ("text.literal".into(), rgba(0xa07d3aff).into()),
+                ("link_uri".into(), rgba(0x83a598ff).into()),
+                ("label".into(), rgba(0x518b8bff).into()),
+                ("property".into(), rgba(0x518b8bff).into()),
+                ("number".into(), rgba(0x83a598ff).into()),
+                ("constructor".into(), rgba(0x518b8bff).into()),
+                ("preproc".into(), rgba(0xfdf4c1ff).into()),
+                ("emphasis".into(), rgba(0x518b8bff).into()),
+                ("variable".into(), rgba(0xfdf4c1ff).into()),
+                ("operator".into(), rgba(0xa07d3aff).into()),
+                ("punctuation".into(), rgba(0xd5c5a1ff).into()),
+                ("constant".into(), rgba(0x83a598ff).into()),
+            ],
         },
         status_bar: rgba(0x333944ff).into(),
         title_bar: rgba(0x333944ff).into(),

crates/theme_converter/src/main.rs 🔗

@@ -24,9 +24,9 @@ fn main() -> Result<()> {
 
     let args = Args::parse();
 
-    let legacy_theme = load_theme(args.theme)?;
+    let (json_theme, legacy_theme) = load_theme(args.theme)?;
 
-    let theme = convert_theme(legacy_theme)?;
+    let theme = convert_theme(json_theme, legacy_theme)?;
 
     println!("{:#?}", ThemePrinter(theme));
 
@@ -89,129 +89,77 @@ impl From<PlayerThemeColors> for PlayerTheme {
     }
 }
 
-#[derive(Clone, Copy)]
-pub struct SyntaxColor {
-    pub comment: Hsla,
-    pub string: Hsla,
-    pub function: Hsla,
-    pub keyword: Hsla,
-}
-
-impl Debug for SyntaxColor {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("SyntaxColor")
-            .field("comment", &HslaPrinter(self.comment))
-            .field("string", &HslaPrinter(self.string))
-            .field("function", &HslaPrinter(self.function))
-            .field("keyword", &HslaPrinter(self.keyword))
-            .finish()
-    }
-}
-
-impl SyntaxColor {
-    pub fn new(theme: &LegacyTheme) -> Self {
-        Self {
-            comment: theme
-                .syntax
-                .get("comment")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            string: theme
-                .syntax
-                .get("string")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            function: theme
-                .syntax
-                .get("function")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-            keyword: theme
-                .syntax
-                .get("keyword")
-                .cloned()
-                .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
-        }
-    }
-}
-
-impl From<SyntaxColor> for SyntaxTheme {
-    fn from(value: SyntaxColor) -> Self {
-        Self {
-            comment: value.comment,
-            string: value.string,
-            keyword: value.keyword,
-            function: value.function,
-            highlights: Vec::new(),
-        }
-    }
-}
-
-fn convert_theme(theme: LegacyTheme) -> Result<theme2::Theme> {
+fn convert_theme(json_theme: JsonTheme, legacy_theme: LegacyTheme) -> Result<theme2::Theme> {
     let transparent = hsla(0.0, 0.0, 0.0, 0.0);
 
     let players: [PlayerTheme; 8] = [
-        PlayerThemeColors::new(&theme, 0).into(),
-        PlayerThemeColors::new(&theme, 1).into(),
-        PlayerThemeColors::new(&theme, 2).into(),
-        PlayerThemeColors::new(&theme, 3).into(),
-        PlayerThemeColors::new(&theme, 4).into(),
-        PlayerThemeColors::new(&theme, 5).into(),
-        PlayerThemeColors::new(&theme, 6).into(),
-        PlayerThemeColors::new(&theme, 7).into(),
+        PlayerThemeColors::new(&legacy_theme, 0).into(),
+        PlayerThemeColors::new(&legacy_theme, 1).into(),
+        PlayerThemeColors::new(&legacy_theme, 2).into(),
+        PlayerThemeColors::new(&legacy_theme, 3).into(),
+        PlayerThemeColors::new(&legacy_theme, 4).into(),
+        PlayerThemeColors::new(&legacy_theme, 5).into(),
+        PlayerThemeColors::new(&legacy_theme, 6).into(),
+        PlayerThemeColors::new(&legacy_theme, 7).into(),
     ];
 
     let theme = theme2::Theme {
         metadata: theme2::ThemeMetadata {
-            name: theme.name.clone().into(),
-            is_light: theme.is_light,
+            name: legacy_theme.name.clone().into(),
+            is_light: legacy_theme.is_light,
         },
         transparent,
         mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
         mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
         mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
-        border: theme.lowest.base.default.border,
-        border_variant: theme.lowest.variant.default.border,
-        border_focused: theme.lowest.accent.default.border,
+        border: legacy_theme.lowest.base.default.border,
+        border_variant: legacy_theme.lowest.variant.default.border,
+        border_focused: legacy_theme.lowest.accent.default.border,
         border_transparent: transparent,
-        elevated_surface: theme.lowest.base.default.background,
-        surface: theme.middle.base.default.background,
-        background: theme.lowest.base.default.background,
-        filled_element: theme.lowest.base.default.background,
+        elevated_surface: legacy_theme.lowest.base.default.background,
+        surface: legacy_theme.middle.base.default.background,
+        background: legacy_theme.lowest.base.default.background,
+        filled_element: legacy_theme.lowest.base.default.background,
         filled_element_hover: hsla(0.0, 0.0, 100.0, 0.12),
         filled_element_active: hsla(0.0, 0.0, 100.0, 0.16),
-        filled_element_selected: theme.lowest.accent.default.background,
+        filled_element_selected: legacy_theme.lowest.accent.default.background,
         filled_element_disabled: transparent,
         ghost_element: transparent,
         ghost_element_hover: hsla(0.0, 0.0, 100.0, 0.08),
         ghost_element_active: hsla(0.0, 0.0, 100.0, 0.12),
-        ghost_element_selected: theme.lowest.accent.default.background,
+        ghost_element_selected: legacy_theme.lowest.accent.default.background,
         ghost_element_disabled: transparent,
-        text: theme.lowest.base.default.foreground,
-        text_muted: theme.lowest.variant.default.foreground,
+        text: legacy_theme.lowest.base.default.foreground,
+        text_muted: legacy_theme.lowest.variant.default.foreground,
         /// TODO: map this to a real value
-        text_placeholder: theme.lowest.negative.default.foreground,
-        text_disabled: theme.lowest.base.disabled.foreground,
-        text_accent: theme.lowest.accent.default.foreground,
-        icon_muted: theme.lowest.variant.default.foreground,
-        syntax: SyntaxColor::new(&theme).into(),
-
-        status_bar: theme.lowest.base.default.background,
-        title_bar: theme.lowest.base.default.background,
-        toolbar: theme.highest.base.default.background,
-        tab_bar: theme.middle.base.default.background,
-        editor: theme.highest.base.default.background,
-        editor_subheader: theme.middle.base.default.background,
-        terminal: theme.highest.base.default.background,
-        editor_active_line: theme.highest.on.default.background,
-        image_fallback_background: theme.lowest.base.default.background,
-
-        git_created: theme.lowest.positive.default.foreground,
-        git_modified: theme.lowest.accent.default.foreground,
-        git_deleted: theme.lowest.negative.default.foreground,
-        git_conflict: theme.lowest.warning.default.foreground,
-        git_ignored: theme.lowest.base.disabled.foreground,
-        git_renamed: theme.lowest.warning.default.foreground,
+        text_placeholder: legacy_theme.lowest.negative.default.foreground,
+        text_disabled: legacy_theme.lowest.base.disabled.foreground,
+        text_accent: legacy_theme.lowest.accent.default.foreground,
+        icon_muted: legacy_theme.lowest.variant.default.foreground,
+        syntax: SyntaxTheme {
+            highlights: json_theme
+                .editor
+                .syntax
+                .iter()
+                .map(|(token, style)| (token.clone(), style.color.clone().into()))
+                .collect(),
+        },
+        status_bar: legacy_theme.lowest.base.default.background,
+        title_bar: legacy_theme.lowest.base.default.background,
+        toolbar: legacy_theme.highest.base.default.background,
+        tab_bar: legacy_theme.middle.base.default.background,
+        editor: legacy_theme.highest.base.default.background,
+        editor_subheader: legacy_theme.middle.base.default.background,
+        terminal: legacy_theme.highest.base.default.background,
+        editor_active_line: legacy_theme.highest.on.default.background,
+        image_fallback_background: legacy_theme.lowest.base.default.background,
+
+        git_created: legacy_theme.lowest.positive.default.foreground,
+        git_modified: legacy_theme.lowest.accent.default.foreground,
+        git_deleted: legacy_theme.lowest.negative.default.foreground,
+        git_conflict: legacy_theme.lowest.warning.default.foreground,
+        git_ignored: legacy_theme.lowest.base.disabled.foreground,
+        git_renamed: legacy_theme.lowest.warning.default.foreground,
 
         players,
     };
@@ -221,11 +169,22 @@ fn convert_theme(theme: LegacyTheme) -> Result<theme2::Theme> {
 
 #[derive(Deserialize)]
 struct JsonTheme {
+    pub editor: JsonEditorTheme,
     pub base_theme: serde_json::Value,
 }
 
+#[derive(Deserialize)]
+struct JsonEditorTheme {
+    pub syntax: HashMap<String, JsonSyntaxStyle>,
+}
+
+#[derive(Deserialize)]
+struct JsonSyntaxStyle {
+    pub color: Hsla,
+}
+
 /// Loads the [`Theme`] with the given name.
-pub fn load_theme(name: String) -> Result<LegacyTheme> {
+fn load_theme(name: String) -> Result<(JsonTheme, LegacyTheme)> {
     let theme_contents = Assets::get(&format!("themes/{name}.json"))
         .with_context(|| format!("theme file not found: '{name}'"))?;
 
@@ -235,7 +194,7 @@ pub fn load_theme(name: String) -> Result<LegacyTheme> {
     let legacy_theme: LegacyTheme = serde_json::from_value(json_theme.base_theme.clone())
         .context("failed to parse `base_theme`")?;
 
-    Ok(legacy_theme)
+    Ok((json_theme, legacy_theme))
 }
 
 #[derive(Deserialize, Clone, Default, Debug)]
@@ -491,11 +450,19 @@ pub struct SyntaxThemePrinter(SyntaxTheme);
 impl Debug for SyntaxThemePrinter {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("SyntaxTheme")
-            .field("comment", &HslaPrinter(self.0.comment))
-            .field("string", &HslaPrinter(self.0.string))
-            .field("function", &HslaPrinter(self.0.function))
-            .field("keyword", &HslaPrinter(self.0.keyword))
-            .field("highlights", &VecPrinter(&self.0.highlights))
+            .field(
+                "highlights",
+                &VecPrinter(
+                    &self
+                        .0
+                        .highlights
+                        .iter()
+                        .map(|(token, highlight)| {
+                            (IntoPrinter(token), HslaPrinter(highlight.color.unwrap()))
+                        })
+                        .collect(),
+                ),
+            )
             .finish()
     }
 }

crates/ui2/Cargo.toml 🔗

@@ -10,10 +10,8 @@ chrono = "0.4"
 gpui2 = { path = "../gpui2" }
 itertools = { version = "0.11.0", optional = true }
 serde.workspace = true
-settings = { path = "../settings" }
 smallvec.workspace = true
 strum = { version = "0.25.0", features = ["derive"] }
-theme = { path = "../theme" }
 theme2 = { path = "../theme2" }
 rand = "0.8"
 

crates/ui2/src/components/assistant_panel.rs 🔗

@@ -81,13 +81,9 @@ mod stories {
     use super::*;
 
     #[derive(Component)]
-    pub struct AssistantPanelStory {}
+    pub struct AssistantPanelStory;
 
     impl AssistantPanelStory {
-        pub fn new() -> Self {
-            Self {}
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, AssistantPanel>(cx))

crates/ui2/src/components/breadcrumb.rs 🔗

@@ -1,8 +1,8 @@
 use std::path::PathBuf;
 
-use gpui2::Div;
 use crate::prelude::*;
 use crate::{h_stack, HighlightedText};
+use gpui2::Div;
 
 #[derive(Clone)]
 pub struct Symbol(pub Vec<HighlightedText>);
@@ -15,10 +15,7 @@ pub struct Breadcrumb {
 
 impl Breadcrumb {
     pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
-        Self {
-            path,
-            symbols,
-        }
+        Self { path, symbols }
     }
 
     fn render_separator<V: 'static>(&self, cx: &WindowContext) -> Div<V> {
@@ -86,11 +83,11 @@ mod stories {
     pub struct BreadcrumbStory;
 
     impl BreadcrumbStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<V: 'static>(self, view_state: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render<V: 'static>(
+            self,
+            view_state: &mut V,
+            cx: &mut ViewContext<V>,
+        ) -> impl Component<V> {
             let theme = theme(cx);
 
             Story::container(cx)
@@ -102,21 +99,21 @@ mod stories {
                         Symbol(vec![
                             HighlightedText {
                                 text: "impl ".to_string(),
-                                color: theme.syntax.keyword,
+                                color: theme.syntax.color("keyword"),
                             },
                             HighlightedText {
                                 text: "BreadcrumbStory".to_string(),
-                                color: theme.syntax.function,
+                                color: theme.syntax.color("function"),
                             },
                         ]),
                         Symbol(vec![
                             HighlightedText {
                                 text: "fn ".to_string(),
-                                color: theme.syntax.keyword,
+                                color: theme.syntax.color("keyword"),
                             },
                             HighlightedText {
                                 text: "render".to_string(),
-                                color: theme.syntax.function,
+                                color: theme.syntax.color("function"),
                             },
                         ]),
                     ],

crates/ui2/src/components/buffer.rs 🔗

@@ -154,7 +154,7 @@ impl Buffer {
         self
     }
 
-    fn render_row<S: 'static>(row: BufferRow, cx: &WindowContext) -> impl Component<S> {
+    fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl Component<V> {
         let theme = theme(cx);
 
         let line_background = if row.current {
@@ -166,7 +166,7 @@ impl Buffer {
         let line_number_color = if row.current {
             theme.text
         } else {
-            theme.syntax.comment
+            theme.syntax.get("comment").color.unwrap_or_default()
         };
 
         h_stack()
@@ -204,7 +204,7 @@ impl Buffer {
             }))
     }
 
-    fn render_rows<S: 'static>(&self, cx: &WindowContext) -> Vec<impl Component<S>> {
+    fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl Component<V>> {
         match &self.rows {
             Some(rows) => rows
                 .rows
@@ -215,7 +215,7 @@ impl Buffer {
         }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
         let rows = self.render_rows(cx);
 
@@ -246,11 +246,7 @@ mod stories {
     pub struct BufferStory;
 
     impl BufferStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             let theme = theme(cx);
 
             Story::container(cx)

crates/ui2/src/components/buffer_search.rs 🔗

@@ -1,4 +1,4 @@
-use gpui2::{view, Context, View};
+use gpui2::{AppContext, Context, View};
 
 use crate::prelude::*;
 use crate::{h_stack, Icon, IconButton, IconColor, Input};
@@ -21,8 +21,12 @@ impl BufferSearch {
         cx.notify();
     }
 
-    pub fn view(cx: &mut WindowContext) -> View<Self> {
-        view(cx.entity(|cx| Self::new()), Self::render)
+    pub fn view(cx: &mut AppContext) -> View<Self> {
+        {
+            let state = cx.entity(|cx| Self::new());
+            let render = Self::render;
+            View::for_handle(state, render)
+        }
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {

crates/ui2/src/components/chat_panel.rs 🔗

@@ -22,7 +22,7 @@ impl ChatPanel {
         self
     }
 
-    fn render<S: 'static>(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div()
             .id(self.element_id.clone())
             .flex()
@@ -60,7 +60,7 @@ impl ChatPanel {
                             .flex_col()
                             .gap_3()
                             .overflow_y_scroll()
-                            .children(self.messages.drain(..)),
+                            .children(self.messages),
                     )
                     // Composer
                     .child(div().flex().my_2().child(Input::new("Message #design"))),
@@ -84,7 +84,7 @@ impl ChatMessage {
         }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div()
             .flex()
             .flex_col()
@@ -117,11 +117,7 @@ mod stories {
     pub struct ChatPanelStory;
 
     impl ChatPanelStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, ChatPanel>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/collab_panel.rs 🔗

@@ -14,7 +14,7 @@ impl CollabPanel {
         Self { id: id.into() }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         v_stack()
@@ -97,11 +97,7 @@ mod stories {
     pub struct CollabPanelStory;
 
     impl CollabPanelStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, CollabPanel>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/command_palette.rs 🔗

@@ -11,7 +11,7 @@ impl CommandPalette {
         Self { id: id.into() }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div().id(self.id.clone()).child(
             Palette::new("palette")
                 .items(example_editor_actions())
@@ -35,11 +35,7 @@ mod stories {
     pub struct CommandPaletteStory;
 
     impl CommandPaletteStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, CommandPalette>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/context_menu.rs 🔗

@@ -42,7 +42,8 @@ impl ContextMenu {
             items: items.into_iter().collect(),
         }
     }
-    fn render<S: 'static>(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         v_stack()
@@ -53,7 +54,7 @@ impl ContextMenu {
             .child(
                 List::new(
                     self.items
-                        .drain(..)
+                        .into_iter()
                         .map(ContextMenuItem::to_list_item)
                         .collect(),
                 )
@@ -75,11 +76,7 @@ mod stories {
     pub struct ContextMenuStory;
 
     impl ContextMenuStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, ContextMenu>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/copilot.rs 🔗

@@ -10,7 +10,7 @@ impl CopilotModal {
         Self { id: id.into() }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div().id(self.id.clone()).child(
             Modal::new("some-id")
                 .title("Connect Copilot to Zed")
@@ -33,11 +33,7 @@ mod stories {
     pub struct CopilotModalStory;
 
     impl CopilotModalStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, CopilotModal>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/editor_pane.rs 🔗

@@ -1,6 +1,6 @@
 use std::path::PathBuf;
 
-use gpui2::{view, Context, View};
+use gpui2::{AppContext, Context, View};
 
 use crate::prelude::*;
 use crate::{
@@ -20,7 +20,7 @@ pub struct EditorPane {
 
 impl EditorPane {
     pub fn new(
-        cx: &mut WindowContext,
+        cx: &mut AppContext,
         tabs: Vec<Tab>,
         path: PathBuf,
         symbols: Vec<Symbol>,
@@ -42,11 +42,12 @@ impl EditorPane {
         cx.notify();
     }
 
-    pub fn view(cx: &mut WindowContext) -> View<Self> {
-        view(
-            cx.entity(|cx| hello_world_rust_editor_with_status_example(cx)),
-            Self::render,
-        )
+    pub fn view(cx: &mut AppContext) -> View<Self> {
+        {
+            let state = cx.entity(|cx| hello_world_rust_editor_with_status_example(cx));
+            let render = Self::render;
+            View::for_handle(state, render)
+        }
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {

crates/ui2/src/components/facepile.rs 🔗

@@ -13,7 +13,7 @@ impl Facepile {
         }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let player_count = self.players.len();
         let player_list = self.players.iter().enumerate().map(|(ix, player)| {
             let isnt_last = ix < player_count - 1;
@@ -39,11 +39,7 @@ mod stories {
     pub struct FacepileStory;
 
     impl FacepileStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             let players = static_players();
 
             Story::container(cx)

crates/ui2/src/components/icon_button.rs 🔗

@@ -5,27 +5,27 @@ use gpui2::MouseButton;
 use crate::{h_stack, prelude::*};
 use crate::{ClickHandler, Icon, IconColor, IconElement};
 
-struct IconButtonHandlers<S: 'static> {
-    click: Option<ClickHandler<S>>,
+struct IconButtonHandlers<V: 'static> {
+    click: Option<ClickHandler<V>>,
 }
 
-impl<S: 'static> Default for IconButtonHandlers<S> {
+impl<V: 'static> Default for IconButtonHandlers<V> {
     fn default() -> Self {
         Self { click: None }
     }
 }
 
 #[derive(Component)]
-pub struct IconButton<S: 'static> {
+pub struct IconButton<V: 'static> {
     id: ElementId,
     icon: Icon,
     color: IconColor,
     variant: ButtonVariant,
     state: InteractionState,
-    handlers: IconButtonHandlers<S>,
+    handlers: IconButtonHandlers<V>,
 }
 
-impl<S: 'static> IconButton<S> {
+impl<V: 'static> IconButton<V> {
     pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
         Self {
             id: id.into(),
@@ -57,12 +57,15 @@ impl<S: 'static> IconButton<S> {
         self
     }
 
-    pub fn on_click(mut self, handler: impl 'static + Fn(&mut S, &mut ViewContext<S>) + Send + Sync) -> Self {
+    pub fn on_click(
+        mut self,
+        handler: impl 'static + Fn(&mut V, &mut ViewContext<V>) + Send + Sync,
+    ) -> Self {
         self.handlers.click = Some(Arc::new(handler));
         self
     }
 
-    fn render(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         let icon_color = match (self.state, self.color) {

crates/ui2/src/components/keybinding.rs 🔗

@@ -29,7 +29,7 @@ impl Keybinding {
         }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div()
             .flex()
             .gap_2()
@@ -59,7 +59,7 @@ impl Key {
         Self { key: key.into() }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         div()
@@ -168,10 +168,6 @@ mod stories {
     pub struct KeybindingStory;
 
     impl KeybindingStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             let all_modifier_permutations = ModifierKey::iter().permutations(2);
 

crates/ui2/src/components/language_selector.rs 🔗

@@ -11,7 +11,7 @@ impl LanguageSelector {
         Self { id: id.into() }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div().id(self.id.clone()).child(
             Palette::new("palette")
                 .items(vec![
@@ -46,11 +46,7 @@ mod stories {
     pub struct LanguageSelectorStory;
 
     impl LanguageSelectorStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, LanguageSelector>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/list.rs 🔗

@@ -55,7 +55,7 @@ impl ListHeader {
         self
     }
 
-    fn disclosure_control<S: 'static>(&self) -> Div<S> {
+    fn disclosure_control<V: 'static>(&self) -> Div<V> {
         let is_toggleable = self.toggleable != Toggleable::NotToggleable;
         let is_toggled = Toggleable::is_toggled(&self.toggleable);
 
@@ -88,7 +88,7 @@ impl ListHeader {
         }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         let is_toggleable = self.toggleable != Toggleable::NotToggleable;
@@ -151,7 +151,7 @@ impl ListSubHeader {
         self
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         h_stack().flex_1().w_full().relative().py_1().child(
             div()
                 .h_6()
@@ -192,39 +192,39 @@ pub enum ListEntrySize {
 }
 
 #[derive(Component)]
-pub enum ListItem<S: 'static> {
+pub enum ListItem<V: 'static> {
     Entry(ListEntry),
-    Details(ListDetailsEntry<S>),
+    Details(ListDetailsEntry<V>),
     Separator(ListSeparator),
     Header(ListSubHeader),
 }
 
-impl<S: 'static> From<ListEntry> for ListItem<S> {
+impl<V: 'static> From<ListEntry> for ListItem<V> {
     fn from(entry: ListEntry) -> Self {
         Self::Entry(entry)
     }
 }
 
-impl<S: 'static> From<ListDetailsEntry<S>> for ListItem<S> {
-    fn from(entry: ListDetailsEntry<S>) -> Self {
+impl<V: 'static> From<ListDetailsEntry<V>> for ListItem<V> {
+    fn from(entry: ListDetailsEntry<V>) -> Self {
         Self::Details(entry)
     }
 }
 
-impl<S: 'static> From<ListSeparator> for ListItem<S> {
+impl<V: 'static> From<ListSeparator> for ListItem<V> {
     fn from(entry: ListSeparator) -> Self {
         Self::Separator(entry)
     }
 }
 
-impl<S: 'static> From<ListSubHeader> for ListItem<S> {
+impl<V: 'static> From<ListSubHeader> for ListItem<V> {
     fn from(entry: ListSubHeader) -> Self {
         Self::Header(entry)
     }
 }
 
-impl<S: 'static> ListItem<S> {
-    fn render(self, view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+impl<V: 'static> ListItem<V> {
+    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         match self {
             ListItem::Entry(entry) => div().child(entry.render(view, cx)),
             ListItem::Separator(separator) => div().child(separator.render(view, cx)),
@@ -250,7 +250,7 @@ impl<S: 'static> ListItem<S> {
 pub struct ListEntry {
     disclosure_control_style: DisclosureControlVisibility,
     indent_level: u32,
-    label: Option<Label>,
+    label: Label,
     left_content: Option<LeftContent>,
     variant: ListItemVariant,
     size: ListEntrySize,
@@ -264,7 +264,7 @@ impl ListEntry {
         Self {
             disclosure_control_style: DisclosureControlVisibility::default(),
             indent_level: 0,
-            label: Some(label),
+            label,
             variant: ListItemVariant::default(),
             left_content: None,
             size: ListEntrySize::default(),
@@ -361,7 +361,7 @@ impl ListEntry {
         }
     }
 
-    fn render<S: 'static>(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let settings = user_settings(cx);
         let theme = theme(cx);
 
@@ -412,34 +412,34 @@ impl ListEntry {
                     .relative()
                     .children(self.disclosure_control(cx))
                     .children(left_content)
-                    .children(self.label.take()),
+                    .child(self.label),
             )
     }
 }
 
-struct ListDetailsEntryHandlers<S: 'static> {
-    click: Option<ClickHandler<S>>,
+struct ListDetailsEntryHandlers<V: 'static> {
+    click: Option<ClickHandler<V>>,
 }
 
-impl<S: 'static> Default for ListDetailsEntryHandlers<S> {
+impl<V: 'static> Default for ListDetailsEntryHandlers<V> {
     fn default() -> Self {
         Self { click: None }
     }
 }
 
 #[derive(Component)]
-pub struct ListDetailsEntry<S: 'static> {
+pub struct ListDetailsEntry<V: 'static> {
     label: SharedString,
     meta: Option<SharedString>,
     left_content: Option<LeftContent>,
-    handlers: ListDetailsEntryHandlers<S>,
-    actions: Option<Vec<Button<S>>>,
+    handlers: ListDetailsEntryHandlers<V>,
+    actions: Option<Vec<Button<V>>>,
     // TODO: make this more generic instead of
     // specifically for notifications
     seen: bool,
 }
 
-impl<S: 'static> ListDetailsEntry<S> {
+impl<V: 'static> ListDetailsEntry<V> {
     pub fn new(label: impl Into<SharedString>) -> Self {
         Self {
             label: label.into(),
@@ -461,17 +461,17 @@ impl<S: 'static> ListDetailsEntry<S> {
         self
     }
 
-    pub fn on_click(mut self, handler: ClickHandler<S>) -> Self {
+    pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
         self.handlers.click = Some(handler);
         self
     }
 
-    pub fn actions(mut self, actions: Vec<Button<S>>) -> Self {
+    pub fn actions(mut self, actions: Vec<Button<V>>) -> Self {
         self.actions = Some(actions);
         self
     }
 
-    fn render(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
         let settings = user_settings(cx);
 
@@ -504,14 +504,13 @@ impl<S: 'static> ListDetailsEntry<S> {
             .child(Label::new(self.label.clone()).color(label_color))
             .children(
                 self.meta
-                    .take()
                     .map(|meta| Label::new(meta).color(LabelColor::Muted)),
             )
             .child(
                 h_stack()
                     .gap_1()
                     .justify_end()
-                    .children(self.actions.take().unwrap_or_default().into_iter()),
+                    .children(self.actions.unwrap_or_default()),
             )
     }
 }
@@ -532,15 +531,15 @@ impl ListSeparator {
 }
 
 #[derive(Component)]
-pub struct List<S: 'static> {
-    items: Vec<ListItem<S>>,
+pub struct List<V: 'static> {
+    items: Vec<ListItem<V>>,
     empty_message: SharedString,
     header: Option<ListHeader>,
     toggleable: Toggleable,
 }
 
-impl<S: 'static> List<S> {
-    pub fn new(items: Vec<ListItem<S>>) -> Self {
+impl<V: 'static> List<V> {
+    pub fn new(items: Vec<ListItem<V>>) -> Self {
         Self {
             items,
             empty_message: "No items".into(),
@@ -564,13 +563,13 @@ impl<S: 'static> List<S> {
         self
     }
 
-    fn render(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let is_toggleable = self.toggleable != Toggleable::NotToggleable;
         let is_toggled = Toggleable::is_toggled(&self.toggleable);
 
         let list_content = match (self.items.is_empty(), is_toggled) {
             (_, false) => div(),
-            (false, _) => div().children(self.items.drain(..)),
+            (false, _) => div().children(self.items),
             (true, _) => {
                 div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
             }
@@ -578,11 +577,7 @@ impl<S: 'static> List<S> {
 
         v_stack()
             .py_1()
-            .children(
-                self.header
-                    .take()
-                    .map(|header| header.toggleable(self.toggleable)),
-            )
+            .children(self.header.map(|header| header.toggleable(self.toggleable)))
             .child(list_content)
     }
 }

crates/ui2/src/components/modal.rs 🔗

@@ -4,15 +4,15 @@ use smallvec::SmallVec;
 use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label};
 
 #[derive(Component)]
-pub struct Modal<S: 'static> {
+pub struct Modal<V: 'static> {
     id: ElementId,
     title: Option<SharedString>,
-    primary_action: Option<Button<S>>,
-    secondary_action: Option<Button<S>>,
-    children: SmallVec<[AnyElement<S>; 2]>,
+    primary_action: Option<Button<V>>,
+    secondary_action: Option<Button<V>>,
+    children: SmallVec<[AnyElement<V>; 2]>,
 }
 
-impl<S: 'static> Modal<S> {
+impl<V: 'static> Modal<V> {
     pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
             id: id.into(),
@@ -28,17 +28,17 @@ impl<S: 'static> Modal<S> {
         self
     }
 
-    pub fn primary_action(mut self, action: Button<S>) -> Self {
+    pub fn primary_action(mut self, action: Button<V>) -> Self {
         self.primary_action = Some(action);
         self
     }
 
-    pub fn secondary_action(mut self, action: Button<S>) -> Self {
+    pub fn secondary_action(mut self, action: Button<V>) -> Self {
         self.secondary_action = Some(action);
         self
     }
 
-    fn render(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         v_stack()
@@ -58,7 +58,7 @@ impl<S: 'static> Modal<S> {
                     .child(div().children(self.title.clone().map(|t| Label::new(t))))
                     .child(IconButton::new("close", Icon::Close)),
             )
-            .child(v_stack().p_1().children(self.children.drain(..)))
+            .child(v_stack().p_1().children(self.children))
             .when(
                 self.primary_action.is_some() || self.secondary_action.is_some(),
                 |this| {
@@ -68,16 +68,16 @@ impl<S: 'static> Modal<S> {
                             .border_color(theme.border)
                             .p_1()
                             .justify_end()
-                            .children(self.secondary_action.take())
-                            .children(self.primary_action.take()),
+                            .children(self.secondary_action)
+                            .children(self.primary_action),
                     )
                 },
             )
     }
 }
 
-impl<S: 'static> ParentElement<S> for Modal<S> {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<S>; 2]> {
+impl<V: 'static> ParentElement<V> for Modal<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }
 }

crates/ui2/src/components/multi_buffer.rs 🔗

@@ -48,10 +48,6 @@ mod stories {
     pub struct MultiBufferStory;
 
     impl MultiBufferStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             let theme = theme(cx);
 

crates/ui2/src/components/notification_toast.rs 🔗

@@ -10,10 +10,7 @@ pub struct NotificationToast {
 
 impl NotificationToast {
     pub fn new(label: SharedString) -> Self {
-        Self {
-            label,
-            icon: None,
-        }
+        Self { label, icon: None }
     }
 
     pub fn icon<I>(mut self, icon: I) -> Self
@@ -24,7 +21,7 @@ impl NotificationToast {
         self
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         h_stack()

crates/ui2/src/components/notifications_panel.rs 🔗

@@ -1,4 +1,3 @@
-
 use crate::{prelude::*, static_new_notification_items, static_read_notification_items};
 use crate::{List, ListHeader};
 
@@ -9,12 +8,10 @@ pub struct NotificationsPanel {
 
 impl NotificationsPanel {
     pub fn new(id: impl Into<ElementId>) -> Self {
-        Self {
-            id: id.into(),
-        }
+        Self { id: id.into() }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         div()
@@ -59,10 +56,6 @@ mod stories {
     pub struct NotificationsPanelStory;
 
     impl NotificationsPanelStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, NotificationsPanel>(cx))

crates/ui2/src/components/palette.rs 🔗

@@ -42,7 +42,7 @@ impl Palette {
         self
     }
 
-    fn render<S: 'static>(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         v_stack()
@@ -81,7 +81,7 @@ impl Palette {
                                 .into_iter()
                                 .flatten(),
                             )
-                            .children(self.items.drain(..).enumerate().map(|(index, item)| {
+                            .children(self.items.into_iter().enumerate().map(|(index, item)| {
                                 h_stack()
                                     .id(index)
                                     .justify_between()
@@ -131,7 +131,7 @@ impl PaletteItem {
         self
     }
 
-    fn render<S: 'static>(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div()
             .flex()
             .flex_row()
@@ -142,7 +142,7 @@ impl PaletteItem {
                     .child(Label::new(self.label.clone()))
                     .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))),
             )
-            .children(self.keybinding.take())
+            .children(self.keybinding)
     }
 }
 
@@ -160,10 +160,6 @@ mod stories {
     pub struct PaletteStory;
 
     impl PaletteStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, Palette>(cx))

crates/ui2/src/components/panel.rs 🔗

@@ -39,17 +39,17 @@ pub enum PanelSide {
 use std::collections::HashSet;
 
 #[derive(Component)]
-pub struct Panel<S: 'static> {
+pub struct Panel<V: 'static> {
     id: ElementId,
     current_side: PanelSide,
     /// Defaults to PanelAllowedSides::LeftAndRight
     allowed_sides: PanelAllowedSides,
     initial_width: AbsoluteLength,
     width: Option<AbsoluteLength>,
-    children: SmallVec<[AnyElement<S>; 2]>,
+    children: SmallVec<[AnyElement<V>; 2]>,
 }
 
-impl<S: 'static> Panel<S> {
+impl<V: 'static> Panel<V> {
     pub fn new(id: impl Into<ElementId>, cx: &mut WindowContext) -> Self {
         let settings = user_settings(cx);
 
@@ -92,7 +92,7 @@ impl<S: 'static> Panel<S> {
         self
     }
 
-    fn render(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         let current_size = self.width.unwrap_or(self.initial_width);
@@ -113,12 +113,12 @@ impl<S: 'static> Panel<S> {
             })
             .bg(theme.surface)
             .border_color(theme.border)
-            .children(self.children.drain(..))
+            .children(self.children)
     }
 }
 
-impl<S: 'static> ParentElement<S> for Panel<S> {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<S>; 2]> {
+impl<V: 'static> ParentElement<V> for Panel<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }
 }
@@ -136,13 +136,9 @@ mod stories {
     pub struct PanelStory;
 
     impl PanelStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
-                .child(Story::title_for::<_, Panel<S>>(cx))
+                .child(Story::title_for::<_, Panel<V>>(cx))
                 .child(Story::label(cx, "Default"))
                 .child(
                     Panel::new("panel", cx).child(

crates/ui2/src/components/panes.rs 🔗

@@ -96,7 +96,7 @@ impl<V: 'static> PaneGroup<V> {
         }
     }
 
-    fn render(mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         if !self.panes.is_empty() {
@@ -106,7 +106,7 @@ impl<V: 'static> PaneGroup<V> {
                 .gap_px()
                 .w_full()
                 .h_full()
-                .children(self.panes.drain(..).map(|pane| pane.render(view, cx)));
+                .children(self.panes.into_iter().map(|pane| pane.render(view, cx)));
 
             if self.split_direction == SplitDirection::Horizontal {
                 return el;
@@ -123,7 +123,7 @@ impl<V: 'static> PaneGroup<V> {
                 .w_full()
                 .h_full()
                 .bg(theme.editor)
-                .children(self.groups.drain(..).map(|group| group.render(view, cx)));
+                .children(self.groups.into_iter().map(|group| group.render(view, cx)));
 
             if self.split_direction == SplitDirection::Horizontal {
                 return el;

crates/ui2/src/components/player_stack.rs 🔗

@@ -13,7 +13,7 @@ impl PlayerStack {
         }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
         let player = self.player_with_call_status.get_player();
         self.player_with_call_status.get_call_status();

crates/ui2/src/components/project_panel.rs 🔗

@@ -13,7 +13,7 @@ impl ProjectPanel {
         Self { id: id.into() }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         div()
@@ -65,11 +65,7 @@ mod stories {
     pub struct ProjectPanelStory;
 
     impl ProjectPanelStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, ProjectPanel>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/recent_projects.rs 🔗

@@ -11,7 +11,7 @@ impl RecentProjects {
         Self { id: id.into() }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div().id(self.id.clone()).child(
             Palette::new("palette")
                 .items(vec![
@@ -42,11 +42,7 @@ mod stories {
     pub struct RecentProjectsStory;
 
     impl RecentProjectsStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, RecentProjects>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/status_bar.rs 🔗

@@ -39,7 +39,6 @@ pub struct StatusBar {
 impl StatusBar {
     pub fn new() -> Self {
         Self {
-            // state_type: PhantomData,
             left_tools: None,
             right_tools: None,
             bottom_tools: None,

crates/ui2/src/components/tab.rs 🔗

@@ -176,10 +176,6 @@ mod stories {
     pub struct TabStory;
 
     impl TabStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             let git_statuses = GitStatus::iter();
             let fs_statuses = FileSystemStatus::iter();

crates/ui2/src/components/tab_bar.rs 🔗

@@ -23,7 +23,7 @@ impl TabBar {
         self
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         let (can_navigate_back, can_navigate_forward) = self.can_navigate;
@@ -100,11 +100,7 @@ mod stories {
     pub struct TabBarStory;
 
     impl TabBarStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, TabBar>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/terminal.rs 🔗

@@ -11,7 +11,7 @@ impl Terminal {
         Self
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         let can_navigate_back = true;
@@ -91,11 +91,7 @@ mod stories {
     pub struct TerminalStory;
 
     impl TerminalStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, Terminal>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/theme_selector.rs 🔗

@@ -11,7 +11,7 @@ impl ThemeSelector {
         Self { id: id.into() }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div().child(
             Palette::new(self.id.clone())
                 .items(vec![
@@ -47,11 +47,7 @@ mod stories {
     pub struct ThemeSelectorStory;
 
     impl ThemeSelectorStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, ThemeSelector>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/components/title_bar.rs 🔗

@@ -1,7 +1,7 @@
 use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 
-use gpui2::{view, Context, View};
+use gpui2::{AppContext, Context, ModelContext, View};
 
 use crate::prelude::*;
 use crate::settings::user_settings;
@@ -28,7 +28,7 @@ pub struct TitleBar {
 }
 
 impl TitleBar {
-    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(cx: &mut ModelContext<Self>) -> Self {
         let is_active = Arc::new(AtomicBool::new(true));
         let active = is_active.clone();
 
@@ -80,11 +80,12 @@ impl TitleBar {
         cx.notify();
     }
 
-    pub fn view(cx: &mut WindowContext, livestream: Option<Livestream>) -> View<Self> {
-        view(
-            cx.entity(|cx| Self::new(cx).set_livestream(livestream)),
-            Self::render,
-        )
+    pub fn view(cx: &mut AppContext, livestream: Option<Livestream>) -> View<Self> {
+        {
+            let state = cx.entity(|cx| Self::new(cx).set_livestream(livestream));
+            let render = Self::render;
+            View::for_handle(state, render)
+        }
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
@@ -195,13 +196,14 @@ mod stories {
     }
 
     impl TitleBarStory {
-        pub fn view(cx: &mut WindowContext) -> View<Self> {
-            view(
-                cx.entity(|cx| Self {
+        pub fn view(cx: &mut AppContext) -> View<Self> {
+            {
+                let state = cx.entity(|cx| Self {
                     title_bar: TitleBar::view(cx, None),
-                }),
-                Self::render,
-            )
+                });
+                let render = Self::render;
+                View::for_handle(state, render)
+            }
         }
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {

crates/ui2/src/components/toast.rs 🔗

@@ -23,12 +23,12 @@ pub enum ToastOrigin {
 ///
 /// Only one toast may be visible at a time.
 #[derive(Component)]
-pub struct Toast<S: 'static> {
+pub struct Toast<V: 'static> {
     origin: ToastOrigin,
-    children: SmallVec<[AnyElement<S>; 2]>,
+    children: SmallVec<[AnyElement<V>; 2]>,
 }
 
-impl<S: 'static> Toast<S> {
+impl<V: 'static> Toast<V> {
     pub fn new(origin: ToastOrigin) -> Self {
         Self {
             origin,
@@ -36,7 +36,7 @@ impl<S: 'static> Toast<S> {
         }
     }
 
-    fn render(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         let mut div = div();
@@ -61,8 +61,8 @@ impl<S: 'static> Toast<S> {
     }
 }
 
-impl<S: 'static> ParentElement<S> for Toast<S> {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<S>; 2]> {
+impl<V: 'static> ParentElement<V> for Toast<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }
 }
@@ -80,13 +80,9 @@ mod stories {
     pub struct ToastStory;
 
     impl ToastStory {
-        pub fn new() -> Self {
-            Self
-        }
-
-        fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
-                .child(Story::title_for::<_, Toast<S>>(cx))
+                .child(Story::title_for::<_, Toast<V>>(cx))
                 .child(Story::label(cx, "Default"))
                 .child(Toast::new(ToastOrigin::Bottom).child(Label::new("label")))
         }

crates/ui2/src/components/toolbar.rs 🔗

@@ -7,12 +7,12 @@ use crate::prelude::*;
 pub struct ToolbarItem {}
 
 #[derive(Component)]
-pub struct Toolbar<S: 'static> {
-    left_items: SmallVec<[AnyElement<S>; 2]>,
-    right_items: SmallVec<[AnyElement<S>; 2]>,
+pub struct Toolbar<V: 'static> {
+    left_items: SmallVec<[AnyElement<V>; 2]>,
+    right_items: SmallVec<[AnyElement<V>; 2]>,
 }
 
-impl<S: 'static> Toolbar<S> {
+impl<V: 'static> Toolbar<V> {
     pub fn new() -> Self {
         Self {
             left_items: SmallVec::new(),
@@ -20,7 +20,7 @@ impl<S: 'static> Toolbar<S> {
         }
     }
 
-    pub fn left_item(mut self, child: impl Component<S>) -> Self
+    pub fn left_item(mut self, child: impl Component<V>) -> Self
     where
         Self: Sized,
     {
@@ -28,7 +28,7 @@ impl<S: 'static> Toolbar<S> {
         self
     }
 
-    pub fn left_items(mut self, iter: impl IntoIterator<Item = impl Component<S>>) -> Self
+    pub fn left_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
     where
         Self: Sized,
     {
@@ -37,7 +37,7 @@ impl<S: 'static> Toolbar<S> {
         self
     }
 
-    pub fn right_item(mut self, child: impl Component<S>) -> Self
+    pub fn right_item(mut self, child: impl Component<V>) -> Self
     where
         Self: Sized,
     {
@@ -45,7 +45,7 @@ impl<S: 'static> Toolbar<S> {
         self
     }
 
-    pub fn right_items(mut self, iter: impl IntoIterator<Item = impl Component<S>>) -> Self
+    pub fn right_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
     where
         Self: Sized,
     {
@@ -54,7 +54,7 @@ impl<S: 'static> Toolbar<S> {
         self
     }
 
-    fn render(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         div()
@@ -62,8 +62,8 @@ impl<S: 'static> Toolbar<S> {
             .p_2()
             .flex()
             .justify_between()
-            .child(div().flex().children(self.left_items.drain(..)))
-            .child(div().flex().children(self.right_items.drain(..)))
+            .child(div().flex().children(self.left_items))
+            .child(div().flex().children(self.right_items))
     }
 }
 
@@ -83,10 +83,6 @@ mod stories {
     pub struct ToolbarStory;
 
     impl ToolbarStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             let theme = theme(cx);
 
@@ -101,21 +97,21 @@ mod stories {
                                 Symbol(vec![
                                     HighlightedText {
                                         text: "impl ".to_string(),
-                                        color: theme.syntax.keyword,
+                                        color: theme.syntax.color("keyword"),
                                     },
                                     HighlightedText {
                                         text: "ToolbarStory".to_string(),
-                                        color: theme.syntax.function,
+                                        color: theme.syntax.color("function"),
                                     },
                                 ]),
                                 Symbol(vec![
                                     HighlightedText {
                                         text: "fn ".to_string(),
-                                        color: theme.syntax.keyword,
+                                        color: theme.syntax.color("keyword"),
                                     },
                                     HighlightedText {
                                         text: "render".to_string(),
-                                        color: theme.syntax.function,
+                                        color: theme.syntax.color("function"),
                                     },
                                 ]),
                             ],

crates/ui2/src/components/traffic_lights.rs 🔗

@@ -21,7 +21,7 @@ impl TrafficLight {
         }
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         let fill = match (self.window_has_focus, self.color) {
@@ -52,7 +52,7 @@ impl TrafficLights {
         self
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div()
             .flex()
             .items_center()
@@ -85,10 +85,6 @@ mod stories {
     pub struct TrafficLightsStory;
 
     impl TrafficLightsStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, TrafficLights>(cx))

crates/ui2/src/components/workspace.rs 🔗

@@ -1,7 +1,7 @@
 use std::sync::Arc;
 
 use chrono::DateTime;
-use gpui2::{px, relative, rems, view, Context, Size, View};
+use gpui2::{px, relative, rems, AppContext, Context, Size, View};
 
 use crate::{
     old_theme, static_livestream, user_settings_mut, v_stack, AssistantPanel, Button, ChatMessage,
@@ -44,7 +44,7 @@ pub struct Workspace {
 }
 
 impl Workspace {
-    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(cx: &mut AppContext) -> Self {
         Self {
             title_bar: TitleBar::view(cx, None),
             editor_1: EditorPane::view(cx),
@@ -170,8 +170,12 @@ impl Workspace {
         cx.notify();
     }
 
-    pub fn view(cx: &mut WindowContext) -> View<Self> {
-        view(cx.entity(|cx| Self::new(cx)), Self::render)
+    pub fn view(cx: &mut AppContext) -> View<Self> {
+        {
+            let state = cx.entity(|cx| Self::new(cx));
+            let render = Self::render;
+            View::for_handle(state, render)
+        }
     }
 
     pub fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
@@ -351,6 +355,8 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
+    use gpui2::VisualContext;
+
     use super::*;
 
     pub struct WorkspaceStory {
@@ -359,10 +365,10 @@ mod stories {
 
     impl WorkspaceStory {
         pub fn view(cx: &mut WindowContext) -> View<Self> {
-            view(
-                cx.entity(|cx| Self {
+            cx.build_view(
+                |cx| Self {
                     workspace: Workspace::view(cx),
-                }),
+                },
                 |view, cx| view.workspace.clone(),
             )
         }

crates/ui2/src/element_ext.rs 🔗

@@ -1,25 +0,0 @@
-use gpui2::Element;
-
-pub trait ElementExt<S: 'static>: Element<S> {
-    // fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
-    // where
-    //     Self: Sized,
-    // {
-    //     if condition {
-    //         self = then(self);
-    //     }
-    //     self
-    // }
-
-    // fn when_some<T, U>(mut self, option: Option<T>, then: impl FnOnce(Self, T) -> U) -> U
-    // where
-    //     Self: Sized,
-    // {
-    //     if let Some(value) = option {
-    //         self = then(self, value);
-    //     }
-    //     self
-    // }
-}
-
-impl<S: 'static, E: Element<S>> ElementExt<S> for E {}

crates/ui2/src/elements/avatar.rs 🔗

@@ -51,10 +51,6 @@ mod stories {
     pub struct AvatarStory;
 
     impl AvatarStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, Avatar>(cx))

crates/ui2/src/elements/button.rs 🔗

@@ -50,23 +50,23 @@ impl ButtonVariant {
 
 pub type ClickHandler<S> = Arc<dyn Fn(&mut S, &mut ViewContext<S>) + Send + Sync>;
 
-struct ButtonHandlers<S: 'static> {
-    click: Option<ClickHandler<S>>,
+struct ButtonHandlers<V: 'static> {
+    click: Option<ClickHandler<V>>,
 }
 
 unsafe impl<S> Send for ButtonHandlers<S> {}
 unsafe impl<S> Sync for ButtonHandlers<S> {}
 
-impl<S: 'static> Default for ButtonHandlers<S> {
+impl<V: 'static> Default for ButtonHandlers<V> {
     fn default() -> Self {
         Self { click: None }
     }
 }
 
 #[derive(Component)]
-pub struct Button<S: 'static> {
+pub struct Button<V: 'static> {
     disabled: bool,
-    handlers: ButtonHandlers<S>,
+    handlers: ButtonHandlers<V>,
     icon: Option<Icon>,
     icon_position: Option<IconPosition>,
     label: SharedString,
@@ -74,7 +74,7 @@ pub struct Button<S: 'static> {
     width: Option<DefiniteLength>,
 }
 
-impl<S: 'static> Button<S> {
+impl<V: 'static> Button<V> {
     pub fn new(label: impl Into<SharedString>) -> Self {
         Self {
             disabled: false,
@@ -114,7 +114,7 @@ impl<S: 'static> Button<S> {
         self
     }
 
-    pub fn on_click(mut self, handler: ClickHandler<S>) -> Self {
+    pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
         self.handlers.click = Some(handler);
         self
     }
@@ -150,7 +150,7 @@ impl<S: 'static> Button<S> {
         self.icon.map(|i| IconElement::new(i).color(icon_color))
     }
 
-    pub fn render(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let icon_color = self.icon_color();
 
         let mut button = h_stack()
@@ -230,10 +230,6 @@ mod stories {
     pub struct ButtonStory;
 
     impl ButtonStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             let states = InteractionState::iter();
 

crates/ui2/src/elements/details.rs 🔗

@@ -7,7 +7,7 @@ pub struct Details<V: 'static> {
     actions: Option<ButtonGroup<V>>,
 }
 
-impl<S: 'static> Details<S> {
+impl<V: 'static> Details<V> {
     pub fn new(text: &'static str) -> Self {
         Self {
             text,
@@ -21,12 +21,12 @@ impl<S: 'static> Details<S> {
         self
     }
 
-    pub fn actions(mut self, actions: ButtonGroup<S>) -> Self {
+    pub fn actions(mut self, actions: ButtonGroup<V>) -> Self {
         self.actions = Some(actions);
         self
     }
 
-    fn render(mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         v_stack()
@@ -37,7 +37,7 @@ impl<S: 'static> Details<S> {
             .size_full()
             .child(self.text)
             .children(self.meta.map(|m| m))
-            .children(self.actions.take().map(|a| a))
+            .children(self.actions.map(|a| a))
     }
 }
 
@@ -54,10 +54,6 @@ mod stories {
     pub struct DetailsStory;
 
     impl DetailsStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, Details<V>>(cx))

crates/ui2/src/elements/icon.rs 🔗

@@ -202,10 +202,6 @@ mod stories {
     pub struct IconStory;
 
     impl IconStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             let icons = Icon::iter();
 

crates/ui2/src/elements/input.rs 🔗

@@ -56,7 +56,7 @@ impl Input {
         self
     }
 
-    fn render<S: 'static>(self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Component<S> {
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let theme = theme(cx);
 
         let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
@@ -120,10 +120,6 @@ mod stories {
     pub struct InputStory;
 
     impl InputStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, Input>(cx))

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

@@ -209,10 +209,6 @@ mod stories {
     pub struct LabelStory;
 
     impl LabelStory {
-        pub fn new() -> Self {
-            Self
-        }
-
         fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
             Story::container(cx)
                 .child(Story::title_for::<_, Label>(cx))

crates/ui2/src/elements/player.rs 🔗

@@ -138,12 +138,12 @@ impl Player {
         self
     }
 
-    pub fn cursor_color<S: 'static>(&self, cx: &mut ViewContext<S>) -> Hsla {
+    pub fn cursor_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
         let theme = theme(cx);
         theme.players[self.index].cursor
     }
 
-    pub fn selection_color<S: 'static>(&self, cx: &mut ViewContext<S>) -> Hsla {
+    pub fn selection_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
         let theme = theme(cx);
         theme.players[self.index].selection
     }

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

@@ -14,18 +14,18 @@ pub trait Stack: Styled + Sized {
     }
 }
 
-impl<S: 'static> Stack for Div<S> {}
+impl<V: 'static> Stack for Div<V> {}
 
 /// Horizontally stacks elements.
 ///
 /// Sets `flex()`, `flex_row()`, `items_center()`
-pub fn h_stack<S: 'static>() -> Div<S> {
+pub fn h_stack<V: 'static>() -> Div<V> {
     div().h_stack()
 }
 
 /// Vertically stacks elements.
 ///
 /// Sets `flex()`, `flex_col()`
-pub fn v_stack<S: 'static>() -> Div<S> {
+pub fn v_stack<V: 'static>() -> Div<V> {
     div().v_stack()
 }

crates/ui2/src/lib.rs 🔗

@@ -18,7 +18,6 @@
 #![allow(dead_code, unused_variables)]
 
 mod components;
-mod element_ext;
 mod elements;
 mod elevation;
 pub mod prelude;
@@ -27,7 +26,6 @@ mod static_data;
 mod theme;
 
 pub use components::*;
-pub use element_ext::*;
 pub use elements::*;
 pub use prelude::*;
 pub use static_data::*;

crates/ui2/src/prelude.rs 🔗

@@ -1,11 +1,11 @@
 pub use gpui2::{
-    div, Element, ElementId, Component, ParentElement, SharedString, StatefulInteractive,
+    div, Component, Element, ElementId, ParentElement, SharedString, StatefulInteractive,
     StatelessInteractive, Styled, ViewContext, WindowContext,
 };
 
 pub use crate::elevation::*;
 use crate::settings::user_settings;
-pub use crate::{old_theme, theme, ButtonVariant, ElementExt, Theme};
+pub use crate::{old_theme, theme, ButtonVariant, Theme};
 
 use gpui2::{rems, Hsla, Rems};
 use strum::EnumIter;

crates/ui2/src/static_data.rs 🔗

@@ -1,7 +1,7 @@
 use std::path::PathBuf;
 use std::str::FromStr;
 
-use gpui2::WindowContext;
+use gpui2::{AppContext, WindowContext};
 use rand::Rng;
 use theme2::Theme;
 
@@ -325,7 +325,7 @@ pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
     ]
 }
 
-pub fn static_new_notification_items<S: 'static>() -> Vec<ListItem<S>> {
+pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
     vec![
         ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
             .meta("4 people in stream."),
@@ -336,7 +336,7 @@ pub fn static_new_notification_items<S: 'static>() -> Vec<ListItem<S>> {
     .collect()
 }
 
-pub fn static_read_notification_items<S: 'static>() -> Vec<ListItem<S>> {
+pub fn static_read_notification_items<V: 'static>() -> Vec<ListItem<V>> {
     vec![
         ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
             Button::new("Decline"),
@@ -352,7 +352,7 @@ pub fn static_read_notification_items<S: 'static>() -> Vec<ListItem<S>> {
     .collect()
 }
 
-pub fn static_project_panel_project_items<S: 'static>() -> Vec<ListItem<S>> {
+pub fn static_project_panel_project_items<V: 'static>() -> Vec<ListItem<V>> {
     vec![
         ListEntry::new(Label::new("zed"))
             .left_icon(Icon::FolderOpen.into())
@@ -479,7 +479,7 @@ pub fn static_project_panel_project_items<S: 'static>() -> Vec<ListItem<S>> {
     .collect()
 }
 
-pub fn static_project_panel_single_items<S: 'static>() -> Vec<ListItem<S>> {
+pub fn static_project_panel_single_items<V: 'static>() -> Vec<ListItem<V>> {
     vec![
         ListEntry::new(Label::new("todo.md"))
             .left_icon(Icon::FileDoc.into())
@@ -496,7 +496,7 @@ pub fn static_project_panel_single_items<S: 'static>() -> Vec<ListItem<S>> {
     .collect()
 }
 
-pub fn static_collab_panel_current_call<S: 'static>() -> Vec<ListItem<S>> {
+pub fn static_collab_panel_current_call<V: 'static>() -> Vec<ListItem<V>> {
     vec![
         ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
         ListEntry::new(Label::new("nathansobo"))
@@ -509,7 +509,7 @@ pub fn static_collab_panel_current_call<S: 'static>() -> Vec<ListItem<S>> {
     .collect()
 }
 
-pub fn static_collab_panel_channels<S: 'static>() -> Vec<ListItem<S>> {
+pub fn static_collab_panel_channels<V: 'static>() -> Vec<ListItem<V>> {
     vec![
         ListEntry::new(Label::new("zed"))
             .left_icon(Icon::Hash.into())
@@ -652,11 +652,11 @@ pub fn hello_world_rust_editor_example(cx: &mut WindowContext) -> EditorPane {
         vec![Symbol(vec![
             HighlightedText {
                 text: "fn ".to_string(),
-                color: theme.syntax.keyword,
+                color: theme.syntax.color("keyword"),
             },
             HighlightedText {
                 text: "main".to_string(),
-                color: theme.syntax.function,
+                color: theme.syntax.color("function"),
             },
         ])],
         hello_world_rust_buffer_example(&theme),
@@ -686,11 +686,11 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                 highlighted_texts: vec![
                     HighlightedText {
                         text: "fn ".to_string(),
-                        color: theme.syntax.keyword,
+                        color: theme.syntax.color("keyword"),
                     },
                     HighlightedText {
                         text: "main".to_string(),
-                        color: theme.syntax.function,
+                        color: theme.syntax.color("function"),
                     },
                     HighlightedText {
                         text: "() {".to_string(),
@@ -710,7 +710,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                 highlighted_texts: vec![HighlightedText {
                     text: "    // Statements here are executed when the compiled binary is called."
                         .to_string(),
-                    color: theme.syntax.comment,
+                    color: theme.syntax.color("comment"),
                 }],
             }),
             cursors: None,
@@ -733,7 +733,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "    // Print text to the console.".to_string(),
-                    color: theme.syntax.comment,
+                    color: theme.syntax.color("comment"),
                 }],
             }),
             cursors: None,
@@ -752,7 +752,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                     },
                     HighlightedText {
                         text: "\"Hello, world!\"".to_string(),
-                        color: theme.syntax.string,
+                        color: theme.syntax.color("string"),
                     },
                     HighlightedText {
                         text: ");".to_string(),
@@ -781,7 +781,7 @@ pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
     ]
 }
 
-pub fn hello_world_rust_editor_with_status_example(cx: &mut WindowContext) -> EditorPane {
+pub fn hello_world_rust_editor_with_status_example(cx: &mut AppContext) -> EditorPane {
     let theme = theme(cx);
 
     EditorPane::new(
@@ -791,11 +791,11 @@ pub fn hello_world_rust_editor_with_status_example(cx: &mut WindowContext) -> Ed
         vec![Symbol(vec![
             HighlightedText {
                 text: "fn ".to_string(),
-                color: theme.syntax.keyword,
+                color: theme.syntax.color("keyword"),
             },
             HighlightedText {
                 text: "main".to_string(),
-                color: theme.syntax.function,
+                color: theme.syntax.color("function"),
             },
         ])],
         hello_world_rust_buffer_with_status_example(&theme),
@@ -825,11 +825,11 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
                 highlighted_texts: vec![
                     HighlightedText {
                         text: "fn ".to_string(),
-                        color: theme.syntax.keyword,
+                        color: theme.syntax.color("keyword"),
                     },
                     HighlightedText {
                         text: "main".to_string(),
-                        color: theme.syntax.function,
+                        color: theme.syntax.color("function"),
                     },
                     HighlightedText {
                         text: "() {".to_string(),
@@ -849,7 +849,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
                 highlighted_texts: vec![HighlightedText {
                     text: "// Statements here are executed when the compiled binary is called."
                         .to_string(),
-                    color: theme.syntax.comment,
+                    color: theme.syntax.color("comment"),
                 }],
             }),
             cursors: None,
@@ -872,7 +872,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "    // Print text to the console.".to_string(),
-                    color: theme.syntax.comment,
+                    color: theme.syntax.color("comment"),
                 }],
             }),
             cursors: None,
@@ -891,7 +891,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
                     },
                     HighlightedText {
                         text: "\"Hello, world!\"".to_string(),
-                        color: theme.syntax.string,
+                        color: theme.syntax.color("string"),
                     },
                     HighlightedText {
                         text: ");".to_string(),
@@ -938,7 +938,7 @@ pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow>
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "// Marshall and Nate were here".to_string(),
-                    color: theme.syntax.comment,
+                    color: theme.syntax.color("comment"),
                 }],
             }),
             cursors: None,
@@ -969,7 +969,7 @@ pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                 highlighted_texts: vec![
                     HighlightedText {
                         text: "maxdeviant ".to_string(),
-                        color: theme.syntax.keyword,
+                        color: theme.syntax.color("keyword"),
                     },
                     HighlightedText {
                         text: "in ".to_string(),
@@ -977,7 +977,7 @@ pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                     },
                     HighlightedText {
                         text: "profaned-capital ".to_string(),
-                        color: theme.syntax.function,
+                        color: theme.syntax.color("function"),
                     },
                     HighlightedText {
                         text: "in ".to_string(),
@@ -985,7 +985,7 @@ pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                     },
                     HighlightedText {
                         text: "~/p/zed ".to_string(),
-                        color: theme.syntax.function,
+                        color: theme.syntax.color("function"),
                     },
                     HighlightedText {
                         text: "on ".to_string(),
@@ -993,7 +993,7 @@ pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
                     },
                     HighlightedText {
                         text: " gpui2-ui ".to_string(),
-                        color: theme.syntax.keyword,
+                        color: theme.syntax.color("keyword"),
                     },
                 ],
             }),
@@ -1008,7 +1008,7 @@ pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "λ ".to_string(),
-                    color: theme.syntax.string,
+                    color: theme.syntax.color("string"),
                 }],
             }),
             cursors: None,

crates/ui2/src/story.rs 🔗

@@ -5,7 +5,7 @@ use crate::prelude::*;
 pub struct Story {}
 
 impl Story {
-    pub fn container<S: 'static>(cx: &mut ViewContext<S>) -> Div<S> {
+    pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
         let theme = theme(cx);
 
         div()
@@ -18,10 +18,7 @@ impl Story {
             .bg(theme.background)
     }
 
-    pub fn title<S: 'static>(
-        cx: &mut ViewContext<S>,
-        title: &str,
-    ) -> impl Component<S> {
+    pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Component<V> {
         let theme = theme(cx);
 
         div()
@@ -30,14 +27,11 @@ impl Story {
             .child(title.to_owned())
     }
 
-    pub fn title_for<S: 'static, T>(cx: &mut ViewContext<S>) -> impl Component<S> {
+    pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Component<V> {
         Self::title(cx, std::any::type_name::<T>())
     }
 
-    pub fn label<S: 'static>(
-        cx: &mut ViewContext<S>,
-        label: &str,
-    ) -> impl Component<S> {
+    pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Component<V> {
         let theme = theme(cx);
 
         div()

crates/ui2/src/theme.rs 🔗

@@ -1,6 +1,6 @@
 use gpui2::{
-    AnyElement, Bounds, Component, Element, Hsla, LayoutId, Pixels, Result, ViewContext,
-    WindowContext,
+    AnyElement, AppContext, Bounds, Component, Element, Hsla, LayoutId, Pixels, Result,
+    ViewContext, WindowContext,
 };
 use serde::{de::Visitor, Deserialize, Deserializer};
 use std::collections::HashMap;
@@ -152,8 +152,8 @@ pub struct Themed<E> {
 impl<V, E> Component<V> for Themed<E>
 where
     V: 'static,
-    E: 'static + Element<V> + Send + Sync,
-    E::ElementState: Send + Sync,
+    E: 'static + Element<V> + Send,
+    E::ElementState: Send,
 {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
@@ -163,10 +163,10 @@ where
 #[derive(Default)]
 struct ThemeStack(Vec<Theme>);
 
-impl<V, E: 'static + Element<V> + Send + Sync> Element<V> for Themed<E>
+impl<V, E: 'static + Element<V> + Send> Element<V> for Themed<E>
 where
     V: 'static,
-    E::ElementState: Send + Sync,
+    E::ElementState: Send,
 {
     type ElementState = E::ElementState;
 
@@ -220,6 +220,6 @@ pub fn old_theme(cx: &WindowContext) -> Arc<Theme> {
     Arc::new(cx.global::<Theme>().clone())
 }
 
-pub fn theme(cx: &WindowContext) -> Arc<theme2::Theme> {
+pub fn theme(cx: &AppContext) -> Arc<theme2::Theme> {
     theme2::active_theme(cx).clone()
 }

crates/util/src/github.rs 🔗

@@ -1,5 +1,5 @@
 use crate::http::HttpClient;
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use futures::AsyncReadExt;
 use serde::Deserialize;
 use std::sync::Arc;
@@ -16,6 +16,7 @@ pub struct GithubRelease {
     pub pre_release: bool,
     pub assets: Vec<GithubReleaseAsset>,
     pub tarball_url: String,
+    pub zipball_url: String,
 }
 
 #[derive(Deserialize, Debug)]
@@ -45,6 +46,14 @@ pub async fn latest_github_release(
         .await
         .context("error reading latest release")?;
 
+    if response.status().is_client_error() {
+        let text = String::from_utf8_lossy(body.as_slice());
+        bail!(
+            "status error {}, response: {text:?}",
+            response.status().as_u16()
+        );
+    }
+
     let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
         Ok(releases) => releases,
 

crates/util/src/util.rs 🔗

@@ -350,19 +350,19 @@ pub fn unzip_option<T, U>(option: Option<(T, U)>) -> (Option<T>, Option<U>) {
     }
 }
 
-/// Immediately invoked function expression. Good for using the ? operator
+/// Evaluates to an immediately invoked function expression. Good for using the ? operator
 /// in functions which do not return an Option or Result
 #[macro_export]
-macro_rules! iife {
+macro_rules! maybe {
     ($block:block) => {
         (|| $block)()
     };
 }
 
-/// Async Immediately invoked function expression. Good for using the ? operator
-/// in functions which do not return an Option or Result. Async version of above
+/// Evaluates to an immediately invoked function expression. Good for using the ? operator
+/// in functions which do not return an Option or Result, but async.
 #[macro_export]
-macro_rules! async_iife {
+macro_rules! async_maybe {
     ($block:block) => {
         (|| async move { $block })()
     };
@@ -449,7 +449,7 @@ mod tests {
             None
         }
 
-        let foo = iife!({
+        let foo = maybe!({
             option_returning_function()?;
             Some(())
         });

crates/vcs_menu/Cargo.toml 🔗

@@ -7,6 +7,7 @@ publish = false
 
 [dependencies]
 fuzzy = {path = "../fuzzy"}
+fs = {path = "../fs"}
 gpui = {path = "../gpui"}
 picker = {path = "../picker"}
 util = {path = "../util"}

crates/vcs_menu/src/lib.rs 🔗

@@ -1,4 +1,5 @@
 use anyhow::{anyhow, bail, Result};
+use fs::repository::Branch;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     actions,
@@ -15,59 +16,42 @@ actions!(branches, [OpenRecent]);
 
 pub fn init(cx: &mut AppContext) {
     Picker::<BranchListDelegate>::init(cx);
-    cx.add_async_action(toggle);
+    cx.add_action(toggle);
 }
 pub type BranchList = Picker<BranchListDelegate>;
 
 pub fn build_branch_list(
     workspace: ViewHandle<Workspace>,
     cx: &mut ViewContext<BranchList>,
-) -> BranchList {
-    Picker::new(
-        BranchListDelegate {
-            matches: vec![],
-            workspace,
-            selected_index: 0,
-            last_query: String::default(),
-            branch_name_trailoff_after: 29,
-        },
-        cx,
-    )
-    .with_theme(|theme| theme.picker.clone())
+) -> Result<BranchList> {
+    let delegate = workspace.read_with(cx, |workspace, cx| {
+        BranchListDelegate::new(workspace, cx.handle(), 29, cx)
+    })?;
+
+    Ok(Picker::new(delegate, cx).with_theme(|theme| theme.picker.clone()))
 }
 
 fn toggle(
-    _: &mut Workspace,
+    workspace: &mut Workspace,
     _: &OpenRecent,
     cx: &mut ViewContext<Workspace>,
-) -> Option<Task<Result<()>>> {
-    Some(cx.spawn(|workspace, mut cx| async move {
-        workspace.update(&mut cx, |workspace, cx| {
-            workspace.toggle_modal(cx, |_, cx| {
-                let workspace = cx.handle();
-                cx.add_view(|cx| {
-                    Picker::new(
-                        BranchListDelegate {
-                            matches: vec![],
-                            workspace,
-                            selected_index: 0,
-                            last_query: String::default(),
-                            /// Modal branch picker has a longer trailoff than a popover one.
-                            branch_name_trailoff_after: 70,
-                        },
-                        cx,
-                    )
-                    .with_theme(|theme| theme.picker.clone())
-                    .with_max_size(800., 1200.)
-                })
-            });
-        })?;
-        Ok(())
-    }))
+) -> Result<()> {
+    // Modal branch picker has a longer trailoff than a popover one.
+    let delegate = BranchListDelegate::new(workspace, cx.handle(), 70, cx)?;
+    workspace.toggle_modal(cx, |_, cx| {
+        cx.add_view(|cx| {
+            Picker::new(delegate, cx)
+                .with_theme(|theme| theme.picker.clone())
+                .with_max_size(800., 1200.)
+        })
+    });
+
+    Ok(())
 }
 
 pub struct BranchListDelegate {
     matches: Vec<StringMatch>,
+    all_branches: Vec<Branch>,
     workspace: ViewHandle<Workspace>,
     selected_index: usize,
     last_query: String,
@@ -76,6 +60,33 @@ pub struct BranchListDelegate {
 }
 
 impl BranchListDelegate {
+    fn new(
+        workspace: &Workspace,
+        handle: ViewHandle<Workspace>,
+        branch_name_trailoff_after: usize,
+        cx: &AppContext,
+    ) -> Result<Self> {
+        let project = workspace.project().read(&cx);
+        let Some(worktree) = project.visible_worktrees(cx).next() else {
+            bail!("Cannot update branch list as there are no visible worktrees")
+        };
+
+        let mut cwd = worktree.read(cx).abs_path().to_path_buf();
+        cwd.push(".git");
+        let Some(repo) = project.fs().open_repo(&cwd) else {
+            bail!("Project does not have associated git repository.")
+        };
+        let all_branches = repo.lock().branches()?;
+        Ok(Self {
+            matches: vec![],
+            workspace: handle,
+            all_branches,
+            selected_index: 0,
+            last_query: Default::default(),
+            branch_name_trailoff_after,
+        })
+    }
+
     fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
         const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
         self.workspace.update(cx, |model, ctx| {
@@ -83,6 +94,7 @@ impl BranchListDelegate {
         });
     }
 }
+
 impl PickerDelegate for BranchListDelegate {
     fn placeholder_text(&self) -> Arc<str> {
         "Select branch...".into()
@@ -102,45 +114,28 @@ impl PickerDelegate for BranchListDelegate {
 
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
         cx.spawn(move |picker, mut cx| async move {
-            let Some(candidates) = picker
-                .read_with(&mut cx, |view, cx| {
-                    let delegate = view.delegate();
-                    let project = delegate.workspace.read(cx).project().read(&cx);
-
-                    let Some(worktree) = project.visible_worktrees(cx).next() else {
-                        bail!("Cannot update branch list as there are no visible worktrees")
-                    };
-                    let mut cwd = worktree.read(cx).abs_path().to_path_buf();
-                    cwd.push(".git");
-                    let Some(repo) = project.fs().open_repo(&cwd) else {
-                        bail!("Project does not have associated git repository.")
-                    };
-                    let mut branches = repo.lock().branches()?;
-                    const RECENT_BRANCHES_COUNT: usize = 10;
-                    if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
-                        // Truncate list of recent branches
-                        // Do a partial sort to show recent-ish branches first.
-                        branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
-                            rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
-                        });
-                        branches.truncate(RECENT_BRANCHES_COUNT);
-                        branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
-                    }
-                    Ok(branches
-                        .iter()
-                        .cloned()
-                        .enumerate()
-                        .map(|(ix, command)| StringMatchCandidate {
-                            id: ix,
-                            char_bag: command.name.chars().collect(),
-                            string: command.name.into(),
-                        })
-                        .collect::<Vec<_>>())
-                })
-                .log_err()
-            else {
-                return;
-            };
+            let candidates = picker.read_with(&mut cx, |view, _| {
+                const RECENT_BRANCHES_COUNT: usize = 10;
+                let mut branches = view.delegate().all_branches.clone();
+                if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
+                    // Truncate list of recent branches
+                    // Do a partial sort to show recent-ish branches first.
+                    branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
+                        rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
+                    });
+                    branches.truncate(RECENT_BRANCHES_COUNT);
+                    branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
+                }
+                branches
+                    .into_iter()
+                    .enumerate()
+                    .map(|(ix, command)| StringMatchCandidate {
+                        id: ix,
+                        char_bag: command.name.chars().collect(),
+                        string: command.name.into(),
+                    })
+                    .collect::<Vec<StringMatchCandidate>>()
+            });
             let Some(candidates) = candidates.log_err() else {
                 return;
             };

crates/vim/src/motion.rs 🔗

@@ -1,9 +1,7 @@
-use std::cmp;
-
 use editor::{
     char_kind,
     display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
-    movement::{self, find_boundary, find_preceding_boundary, FindRange},
+    movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
     Bias, CharKind, DisplayPoint, ToOffset,
 };
 use gpui::{actions, impl_actions, AppContext, WindowContext};
@@ -42,6 +40,7 @@ pub enum Motion {
     NextLineStart,
     StartOfLineDownward,
     EndOfLineDownward,
+    GoToColumn,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -121,6 +120,7 @@ actions!(
         NextLineStart,
         StartOfLineDownward,
         EndOfLineDownward,
+        GoToColumn,
     ]
 );
 impl_actions!(
@@ -217,6 +217,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
         motion(Motion::EndOfLineDownward, cx)
     });
+    cx.add_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
     cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
         repeat_motion(action.backwards, cx)
     })
@@ -294,6 +295,7 @@ impl Motion {
             | Right
             | StartOfLine { .. }
             | EndOfLineDownward
+            | GoToColumn
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace { .. }
@@ -319,6 +321,7 @@ impl Motion {
             | EndOfParagraph
             | StartOfLineDownward
             | EndOfLineDownward
+            | GoToColumn
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace { .. }
@@ -348,6 +351,7 @@ impl Motion {
             | StartOfLineDownward
             | StartOfParagraph
             | EndOfParagraph
+            | GoToColumn
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace { .. }
@@ -361,6 +365,7 @@ impl Motion {
         point: DisplayPoint,
         goal: SelectionGoal,
         maybe_times: Option<usize>,
+        text_layout_details: &TextLayoutDetails,
     ) -> Option<(DisplayPoint, SelectionGoal)> {
         let times = maybe_times.unwrap_or(1);
         use Motion::*;
@@ -370,16 +375,16 @@ impl Motion {
             Backspace => (backspace(map, point, times), SelectionGoal::None),
             Down {
                 display_lines: false,
-            } => down(map, point, goal, times),
+            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
             Down {
                 display_lines: true,
-            } => down_display(map, point, goal, times),
+            } => down_display(map, point, goal, times, &text_layout_details),
             Up {
                 display_lines: false,
-            } => up(map, point, goal, times),
+            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
             Up {
                 display_lines: true,
-            } => up_display(map, point, goal, times),
+            } => up_display(map, point, goal, times, &text_layout_details),
             Right => (right(map, point, times), SelectionGoal::None),
             NextWordStart { ignore_punctuation } => (
                 next_word_start(map, point, *ignore_punctuation, times),
@@ -430,6 +435,7 @@ impl Motion {
             NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
             StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
             EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
+            GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
         };
 
         (new_point != point || infallible).then_some((new_point, goal))
@@ -442,10 +448,15 @@ impl Motion {
         selection: &mut Selection<DisplayPoint>,
         times: Option<usize>,
         expand_to_surrounding_newline: bool,
+        text_layout_details: &TextLayoutDetails,
     ) -> bool {
-        if let Some((new_head, goal)) =
-            self.move_point(map, selection.head(), selection.goal, times)
-        {
+        if let Some((new_head, goal)) = self.move_point(
+            map,
+            selection.head(),
+            selection.goal,
+            times,
+            &text_layout_details,
+        ) {
             selection.set_head(new_head, goal);
 
             if self.linewise() {
@@ -530,35 +541,85 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
     point
 }
 
-fn down(
+pub(crate) fn start_of_relative_buffer_row(
+    map: &DisplaySnapshot,
+    point: DisplayPoint,
+    times: isize,
+) -> DisplayPoint {
+    let start = map.display_point_to_fold_point(point, Bias::Left);
+    let target = start.row() as isize + times;
+    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
+
+    map.clip_point(
+        map.fold_point_to_display_point(
+            map.fold_snapshot
+                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
+        ),
+        Bias::Right,
+    )
+}
+
+fn up_down_buffer_rows(
     map: &DisplaySnapshot,
     point: DisplayPoint,
     mut goal: SelectionGoal,
-    times: usize,
+    times: isize,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
     let start = map.display_point_to_fold_point(point, Bias::Left);
+    let begin_folded_line = map.fold_point_to_display_point(
+        map.fold_snapshot
+            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
+    );
+    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 
-    let goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
+    let (goal_wrap, goal_x) = match goal {
+        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
+        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
+        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
         _ => {
-            goal = SelectionGoal::Column(start.column());
-            start.column()
+            let x = map.x_for_point(point, text_layout_details);
+            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
+            (select_nth_wrapped_row, x)
         }
     };
 
-    let new_row = cmp::min(
-        start.row() + times as u32,
-        map.fold_snapshot.max_point().row(),
-    );
-    let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
-    let point = map.fold_point_to_display_point(
+    let target = start.row() as isize + times;
+    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
+
+    let mut begin_folded_line = map.fold_point_to_display_point(
         map.fold_snapshot
-            .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
+            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
     );
 
-    // clip twice to "clip at end of line"
-    (map.clip_point(point, Bias::Left), goal)
+    let mut i = 0;
+    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
+        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
+        if map
+            .display_point_to_fold_point(next_folded_line, Bias::Right)
+            .row()
+            == new_row
+        {
+            i += 1;
+            begin_folded_line = next_folded_line;
+        } else {
+            break;
+        }
+    }
+
+    let new_col = if i == goal_wrap {
+        map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details)
+    } else {
+        map.line_len(begin_folded_line.row())
+    };
+
+    (
+        map.clip_point(
+            DisplayPoint::new(begin_folded_line.row(), new_col),
+            Bias::Left,
+        ),
+        goal,
+    )
 }
 
 fn down_display(
@@ -566,49 +627,24 @@ fn down_display(
     mut point: DisplayPoint,
     mut goal: SelectionGoal,
     times: usize,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
     for _ in 0..times {
-        (point, goal) = movement::down(map, point, goal, true);
+        (point, goal) = movement::down(map, point, goal, true, text_layout_details);
     }
 
     (point, goal)
 }
 
-pub(crate) fn up(
-    map: &DisplaySnapshot,
-    point: DisplayPoint,
-    mut goal: SelectionGoal,
-    times: usize,
-) -> (DisplayPoint, SelectionGoal) {
-    let start = map.display_point_to_fold_point(point, Bias::Left);
-
-    let goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
-        _ => {
-            goal = SelectionGoal::Column(start.column());
-            start.column()
-        }
-    };
-
-    let new_row = start.row().saturating_sub(times as u32);
-    let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
-    let point = map.fold_point_to_display_point(
-        map.fold_snapshot
-            .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
-    );
-
-    (map.clip_point(point, Bias::Left), goal)
-}
-
 fn up_display(
     map: &DisplaySnapshot,
     mut point: DisplayPoint,
     mut goal: SelectionGoal,
     times: usize,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
     for _ in 0..times {
-        (point, goal) = movement::up(map, point, goal, true);
+        (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
     }
 
     (point, goal)
@@ -707,7 +743,7 @@ fn previous_word_start(
     point
 }
 
-fn first_non_whitespace(
+pub(crate) fn first_non_whitespace(
     map: &DisplaySnapshot,
     display_lines: bool,
     from: DisplayPoint,
@@ -886,13 +922,22 @@ fn find_backward(
 }
 
 fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
-    let correct_line = down(map, point, SelectionGoal::None, times).0;
+    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
     first_non_whitespace(map, false, correct_line)
 }
 
-fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
+fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
+    let correct_line = start_of_relative_buffer_row(map, point, 0);
+    right(map, correct_line, times.saturating_sub(1))
+}
+
+pub(crate) fn next_line_end(
+    map: &DisplaySnapshot,
+    mut point: DisplayPoint,
+    times: usize,
+) -> DisplayPoint {
     if times > 1 {
-        point = down(map, point, SelectionGoal::None, times - 1).0;
+        point = start_of_relative_buffer_row(map, point, times as isize - 1);
     }
     end_of_line(map, false, point)
 }

crates/vim/src/normal.rs 🔗

@@ -12,7 +12,7 @@ mod yank;
 use std::sync::Arc;
 
 use crate::{
-    motion::{self, Motion},
+    motion::{self, first_non_whitespace, next_line_end, right, Motion},
     object::Object,
     state::{Mode, Operator},
     Vim,
@@ -179,10 +179,11 @@ pub(crate) fn move_cursor(
     cx: &mut WindowContext,
 ) {
     vim.update_active_editor(cx, |editor, cx| {
+        let text_layout_details = editor.text_layout_details(cx);
         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_cursors_with(|map, cursor, goal| {
                 motion
-                    .move_point(map, cursor, goal, times)
+                    .move_point(map, cursor, goal, times, &text_layout_details)
                     .unwrap_or((cursor, goal))
             })
         })
@@ -195,9 +196,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::Right.move_point(map, cursor, goal, None)
-                });
+                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
             });
         });
     });
@@ -220,11 +219,11 @@ fn insert_first_non_whitespace(
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::FirstNonWhitespace {
-                        display_lines: false,
-                    }
-                    .move_point(map, cursor, goal, None)
+                s.move_cursors_with(|map, cursor, _| {
+                    (
+                        first_non_whitespace(map, false, cursor),
+                        SelectionGoal::None,
+                    )
                 });
             });
         });
@@ -237,8 +236,8 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::CurrentLine.move_point(map, cursor, goal, None)
+                s.move_cursors_with(|map, cursor, _| {
+                    (next_line_end(map, cursor, 1), SelectionGoal::None)
                 });
             });
         });
@@ -268,7 +267,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
                 editor.edit_with_autoindent(edits, cx);
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.move_cursors_with(|map, cursor, _| {
-                        let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
+                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
                         let insert_point = motion::end_of_line(map, false, previous_line);
                         (insert_point, SelectionGoal::None)
                     });
@@ -283,6 +282,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
         vim.start_recording(cx);
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
+            let text_layout_details = editor.text_layout_details(cx);
             editor.transact(cx, |editor, cx| {
                 let (map, old_selections) = editor.selections.all_display(cx);
 
@@ -301,7 +301,13 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
                 });
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.maybe_move_cursors_with(|map, cursor, goal| {
-                        Motion::CurrentLine.move_point(map, cursor, goal, None)
+                        Motion::CurrentLine.move_point(
+                            map,
+                            cursor,
+                            goal,
+                            None,
+                            &text_layout_details,
+                        )
                     });
                 });
                 editor.edit_with_autoindent(edits, cx);
@@ -399,12 +405,26 @@ mod test {
 
     #[gpui::test]
     async fn test_j(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
-        cx.assert_all(indoc! {"
-            ˇThe qˇuick broˇwn
-            ˇfox jumps"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+                    aaˇaa
+                    😃😃"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j"]).await;
+        cx.assert_shared_state(indoc! {"
+                    aaaa
+                    😃ˇ😃"
         })
         .await;
+
+        for marked_position in cx.each_marked_position(indoc! {"
+                    ˇThe qˇuick broˇwn
+                    ˇfox jumps"
+        }) {
+            cx.assert_neovim_compatible(&marked_position, ["j"]).await;
+        }
     }
 
     #[gpui::test]

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

@@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_
 use editor::{
     char_kind,
     display_map::DisplaySnapshot,
-    movement::{self, FindRange},
+    movement::{self, FindRange, TextLayoutDetails},
     scroll::autoscroll::Autoscroll,
     CharKind, DisplayPoint,
 };
@@ -20,6 +20,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
             | Motion::StartOfLine { .. }
     );
     vim.update_active_editor(cx, |editor, cx| {
+        let text_layout_details = editor.text_layout_details(cx);
         editor.transact(cx, |editor, cx| {
             // We are swapping to insert mode anyway. Just set the line end clipping behavior now
             editor.set_clip_at_line_ends(false, cx);
@@ -27,9 +28,15 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
                 s.move_with(|map, selection| {
                     motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
                     {
-                        expand_changed_word_selection(map, selection, times, ignore_punctuation)
+                        expand_changed_word_selection(
+                            map,
+                            selection,
+                            times,
+                            ignore_punctuation,
+                            &text_layout_details,
+                        )
                     } else {
-                        motion.expand_selection(map, selection, times, false)
+                        motion.expand_selection(map, selection, times, false, &text_layout_details)
                     };
                 });
             });
@@ -81,6 +88,7 @@ fn expand_changed_word_selection(
     selection: &mut Selection<DisplayPoint>,
     times: Option<usize>,
     ignore_punctuation: bool,
+    text_layout_details: &TextLayoutDetails,
 ) -> bool {
     if times.is_none() || times.unwrap() == 1 {
         let scope = map
@@ -103,11 +111,22 @@ fn expand_changed_word_selection(
                 });
             true
         } else {
-            Motion::NextWordStart { ignore_punctuation }
-                .expand_selection(map, selection, None, false)
+            Motion::NextWordStart { ignore_punctuation }.expand_selection(
+                map,
+                selection,
+                None,
+                false,
+                &text_layout_details,
+            )
         }
     } else {
-        Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
+        Motion::NextWordStart { ignore_punctuation }.expand_selection(
+            map,
+            selection,
+            times,
+            false,
+            &text_layout_details,
+        )
     }
 }
 

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

@@ -7,6 +7,7 @@ use language::Point;
 pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     vim.stop_recording();
     vim.update_active_editor(cx, |editor, cx| {
+        let text_layout_details = editor.text_layout_details(cx);
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
             let mut original_columns: HashMap<_, _> = Default::default();
@@ -14,7 +15,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
                 s.move_with(|map, selection| {
                     let original_head = selection.head();
                     original_columns.insert(selection.id, original_head.column());
-                    motion.expand_selection(map, selection, times, true);
+                    motion.expand_selection(map, selection, times, true, &text_layout_details);
 
                     // Motion::NextWordStart on an empty line should delete it.
                     if let Motion::NextWordStart {
@@ -192,10 +193,10 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_delete_e(cx: &mut gpui::TestAppContext) {
+    async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "e"]);
-        cx.assert("Teˇst Test").await;
-        cx.assert("Tˇest test").await;
+        // cx.assert("Teˇst Test").await;
+        // cx.assert("Tˇest test").await;
         cx.assert(indoc! {"
             Test teˇst
             test"})

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

@@ -255,8 +255,18 @@ mod test {
             4
             5"})
             .await;
-        cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g", "g", "ctrl-x"])
+
+        cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            «1ˇ»
+            «2ˇ»
+            «3ˇ»  2
+            «4ˇ»
+            «5ˇ»"})
             .await;
+
+        cx.simulate_shared_keystrokes(["g", "ctrl-x"]).await;
         cx.assert_shared_state(indoc! {"
             ˇ0
             0

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

@@ -30,6 +30,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.record_current_action(cx);
         vim.update_active_editor(cx, |editor, cx| {
+            let text_layout_details = editor.text_layout_details(cx);
             editor.transact(cx, |editor, cx| {
                 editor.set_clip_at_line_ends(false, cx);
 
@@ -168,8 +169,14 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
                             let mut cursor = anchor.to_display_point(map);
                             if *line_mode {
                                 if !before {
-                                    cursor =
-                                        movement::down(map, cursor, SelectionGoal::None, false).0;
+                                    cursor = movement::down(
+                                        map,
+                                        cursor,
+                                        SelectionGoal::None,
+                                        false,
+                                        &text_layout_details,
+                                    )
+                                    .0;
                                 }
                                 cursor = movement::indented_line_beginning(map, cursor, true);
                             } else if !is_multiline {

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

@@ -32,10 +32,17 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
     vim.update_active_editor(cx, |editor, cx| {
         editor.set_clip_at_line_ends(false, cx);
         editor.transact(cx, |editor, cx| {
+            let text_layout_details = editor.text_layout_details(cx);
             editor.change_selections(None, cx, |s| {
                 s.move_with(|map, selection| {
                     if selection.start == selection.end {
-                        Motion::Right.expand_selection(map, selection, count, true);
+                        Motion::Right.expand_selection(
+                            map,
+                            selection,
+                            count,
+                            true,
+                            &text_layout_details,
+                        );
                     }
                     if line_mode {
                         // in Visual mode when the selection contains the newline at the end
@@ -43,7 +50,13 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
                         if !selection.is_empty() && selection.end.column() == 0 {
                             selection.end = movement::left(map, selection.end);
                         }
-                        Motion::CurrentLine.expand_selection(map, selection, None, false);
+                        Motion::CurrentLine.expand_selection(
+                            map,
+                            selection,
+                            None,
+                            false,
+                            &text_layout_details,
+                        );
                         if let Some((point, _)) = (Motion::FirstNonWhitespace {
                             display_lines: false,
                         })
@@ -52,6 +65,7 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
                             selection.start,
                             selection.goal,
                             None,
+                            &text_layout_details,
                         ) {
                             selection.start = point;
                         }

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

@@ -4,6 +4,7 @@ use gpui::WindowContext;
 
 pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
+        let text_layout_details = editor.text_layout_details(cx);
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
             let mut original_positions: HashMap<_, _> = Default::default();
@@ -11,7 +12,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut
                 s.move_with(|map, selection| {
                     let original_position = (selection.head(), selection.goal);
                     original_positions.insert(selection.id, original_position);
-                    motion.expand_selection(map, selection, times, true);
+                    motion.expand_selection(map, selection, times, true, &text_layout_details);
                 });
             });
             copy_selections_content(editor, motion.linewise(), cx);

crates/vim/src/object.rs 🔗

@@ -2,7 +2,7 @@ use std::ops::Range;
 
 use editor::{
     char_kind,
-    display_map::DisplaySnapshot,
+    display_map::{DisplaySnapshot, ToDisplayPoint},
     movement::{self, FindRange},
     Bias, CharKind, DisplayPoint,
 };
@@ -20,6 +20,7 @@ pub enum Object {
     Quotes,
     BackQuotes,
     DoubleQuotes,
+    VerticalBars,
     Parentheses,
     SquareBrackets,
     CurlyBrackets,
@@ -40,6 +41,7 @@ actions!(
         Quotes,
         BackQuotes,
         DoubleQuotes,
+        VerticalBars,
         Parentheses,
         SquareBrackets,
         CurlyBrackets,
@@ -64,6 +66,7 @@ pub fn init(cx: &mut AppContext) {
     });
     cx.add_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx));
     cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx));
+    cx.add_action(|_: &mut Workspace, _: &VerticalBars, cx: _| object(Object::VerticalBars, cx));
 }
 
 fn object(object: Object, cx: &mut WindowContext) {
@@ -79,9 +82,11 @@ 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::Word { .. }
+            | Object::Quotes
+            | Object::BackQuotes
+            | Object::VerticalBars
+            | Object::DoubleQuotes => false,
             Object::Sentence
             | Object::Parentheses
             | Object::AngleBrackets
@@ -96,6 +101,7 @@ impl Object {
             Object::Quotes
             | Object::BackQuotes
             | Object::DoubleQuotes
+            | Object::VerticalBars
             | Object::Parentheses
             | Object::SquareBrackets
             | Object::CurlyBrackets
@@ -111,6 +117,7 @@ impl Object {
             | Object::Quotes
             | Object::BackQuotes
             | Object::DoubleQuotes
+            | Object::VerticalBars
             | Object::Parentheses
             | Object::SquareBrackets
             | Object::CurlyBrackets
@@ -142,6 +149,9 @@ impl Object {
             Object::DoubleQuotes => {
                 surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
             }
+            Object::VerticalBars => {
+                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
+            }
             Object::Parentheses => {
                 surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
             }
@@ -427,110 +437,151 @@ fn surrounding_markers(
     relative_to: DisplayPoint,
     around: bool,
     search_across_lines: bool,
-    start_marker: char,
-    end_marker: char,
+    open_marker: char,
+    close_marker: char,
 ) -> Option<Range<DisplayPoint>> {
-    let mut matched_ends = 0;
-    let mut start = None;
-    for (char, mut point) in map.reverse_chars_at(relative_to) {
-        if char == start_marker {
-            if matched_ends > 0 {
-                matched_ends -= 1;
-            } else {
-                if around {
-                    start = Some(point)
-                } else {
-                    *point.column_mut() += char.len_utf8() as u32;
-                    start = Some(point)
+    let point = relative_to.to_offset(map, Bias::Left);
+
+    let mut matched_closes = 0;
+    let mut opening = None;
+
+    if let Some((ch, range)) = movement::chars_after(map, point).next() {
+        if ch == open_marker {
+            if open_marker == close_marker {
+                let mut total = 0;
+                for (ch, _) in movement::chars_before(map, point) {
+                    if ch == '\n' {
+                        break;
+                    }
+                    if ch == open_marker {
+                        total += 1;
+                    }
                 }
-                break;
+                if total % 2 == 0 {
+                    opening = Some(range)
+                }
+            } else {
+                opening = Some(range)
             }
-        } else if char == end_marker {
-            matched_ends += 1;
-        } else if char == '\n' && !search_across_lines {
-            break;
         }
     }
 
-    let mut matched_starts = 0;
-    let mut end = None;
-    for (char, mut point) in map.chars_at(relative_to) {
-        if char == end_marker {
-            if start.is_none() {
+    if opening.is_none() {
+        for (ch, range) in movement::chars_before(map, point) {
+            if ch == '\n' && !search_across_lines {
                 break;
             }
 
-            if matched_starts > 0 {
-                matched_starts -= 1;
-            } else {
-                if around {
-                    *point.column_mut() += char.len_utf8() as u32;
-                    end = Some(point);
-                } else {
-                    end = Some(point);
+            if ch == open_marker {
+                if matched_closes == 0 {
+                    opening = Some(range);
+                    break;
                 }
-
-                break;
+                matched_closes -= 1;
+            } else if ch == close_marker {
+                matched_closes += 1
             }
         }
+    }
 
-        if char == start_marker {
-            if start.is_none() {
-                if around {
-                    start = Some(point);
-                } else {
-                    *point.column_mut() += char.len_utf8() as u32;
-                    start = Some(point);
-                }
-            } else {
-                matched_starts += 1;
+    if opening.is_none() {
+        for (ch, range) in movement::chars_after(map, point) {
+            if ch == open_marker {
+                opening = Some(range);
+                break;
+            } else if ch == close_marker {
+                break;
             }
         }
+    }
+
+    let Some(mut opening) = opening else {
+        return None;
+    };
 
-        if char == '\n' && !search_across_lines {
+    let mut matched_opens = 0;
+    let mut closing = None;
+
+    for (ch, range) in movement::chars_after(map, opening.end) {
+        if ch == '\n' && !search_across_lines {
             break;
         }
+
+        if ch == close_marker {
+            if matched_opens == 0 {
+                closing = Some(range);
+                break;
+            }
+            matched_opens -= 1;
+        } else if ch == open_marker {
+            matched_opens += 1;
+        }
     }
 
-    let (Some(mut start), Some(mut end)) = (start, end) else {
+    let Some(mut closing) = closing else {
         return None;
     };
 
-    if !around {
-        // if a block starts with a newline, move the start to after the newline.
-        let mut was_newline = false;
-        for (char, point) in map.chars_at(start) {
-            if was_newline {
-                start = point;
-            } else if char == '\n' {
-                was_newline = true;
-                continue;
+    if around && !search_across_lines {
+        let mut found = false;
+
+        for (ch, range) in movement::chars_after(map, closing.end) {
+            if ch.is_whitespace() && ch != '\n' {
+                found = true;
+                closing.end = range.end;
+            } else {
+                break;
             }
-            break;
         }
-        // if a block ends with a newline, then whitespace, then the delimeter,
-        // move the end to after the newline.
-        let mut new_end = end;
-        for (char, point) in map.reverse_chars_at(end) {
-            if char == '\n' {
-                end = new_end;
-                break;
+
+        if !found {
+            for (ch, range) in movement::chars_before(map, opening.start) {
+                if ch.is_whitespace() && ch != '\n' {
+                    opening.start = range.start
+                } else {
+                    break;
+                }
             }
-            if !char.is_whitespace() {
+        }
+    }
+
+    if !around && search_across_lines {
+        if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
+            if ch == '\n' {
+                opening.end = range.end
+            }
+        }
+
+        for (ch, range) in movement::chars_before(map, closing.start) {
+            if !ch.is_whitespace() {
                 break;
             }
-            new_end = point
+            if ch != '\n' {
+                closing.start = range.start
+            }
         }
     }
 
-    Some(start..end)
+    let result = if around {
+        opening.start..closing.end
+    } else {
+        opening.end..closing.start
+    };
+
+    Some(
+        map.clip_point(result.start.to_display_point(map), Bias::Left)
+            ..map.clip_point(result.end.to_display_point(map), Bias::Right),
+    )
 }
 
 #[cfg(test)]
 mod test {
     use indoc::indoc;
 
-    use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
+    use crate::{
+        state::Mode,
+        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
+    };
 
     const WORD_LOCATIONS: &'static str = indoc! {"
         The quick ˇbrowˇnˇ•••
@@ -765,13 +816,6 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
         for (start, end) in SURROUNDING_OBJECTS {
-            if ((start == &'\'' || start == &'`' || start == &'"')
-                && !ExemptionFeatures::QuotesSeekForward.supported())
-                || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported())
-            {
-                continue;
-            }
-
             let marked_string = SURROUNDING_MARKER_STRING
                 .replace('`', &start.to_string())
                 .replace('\'', &end.to_string());
@@ -786,6 +830,63 @@ mod test {
                 .await;
         }
     }
+    #[gpui::test]
+    async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_wrap(12).await;
+
+        cx.set_shared_state(indoc! {
+            "helˇlo \"world\"!"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
+        cx.assert_shared_state(indoc! {
+            "hello \"«worldˇ»\"!"
+        })
+        .await;
+
+        cx.set_shared_state(indoc! {
+            "hello \"wˇorld\"!"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
+        cx.assert_shared_state(indoc! {
+            "hello \"«worldˇ»\"!"
+        })
+        .await;
+
+        cx.set_shared_state(indoc! {
+            "hello \"wˇorld\"!"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
+        cx.assert_shared_state(indoc! {
+            "hello« \"world\"ˇ»!"
+        })
+        .await;
+
+        cx.set_shared_state(indoc! {
+            "hello \"wˇorld\" !"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
+        cx.assert_shared_state(indoc! {
+            "hello «\"world\" ˇ»!"
+        })
+        .await;
+
+        cx.set_shared_state(indoc! {
+            "hello \"wˇorld\"•
+            goodbye"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "a", "\""]).await;
+        cx.assert_shared_state(indoc! {
+            "hello «\"world\" ˇ»
+            goodbye"
+        })
+        .await;
+    }
 
     #[gpui::test]
     async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
@@ -827,6 +928,66 @@ mod test {
                  return false
             }"})
             .await;
+
+        cx.set_shared_state(indoc! {
+            "func empty(a string) bool {
+                 if a == \"\" ˇ{
+                     return true
+                 }
+                 return false
+            }"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "i", "{"]).await;
+        cx.assert_shared_state(indoc! {"
+            func empty(a string) bool {
+                 if a == \"\" {
+            «         return true
+            ˇ»     }
+                 return false
+            }"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.set_state(
+            indoc! {"
+            fn boop() {
+                baz(ˇ|a, b| { bar(|j, k| { })})
+            }"
+            },
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes(["c", "i", "|"]);
+        cx.assert_state(
+            indoc! {"
+            fn boop() {
+                baz(|ˇ| { bar(|j, k| { })})
+            }"
+            },
+            Mode::Insert,
+        );
+        cx.simulate_keystrokes(["escape", "1", "8", "|"]);
+        cx.assert_state(
+            indoc! {"
+            fn boop() {
+                baz(|| { bar(ˇ|j, k| { })})
+            }"
+            },
+            Mode::Normal,
+        );
+
+        cx.simulate_keystrokes(["v", "a", "|"]);
+        cx.assert_state(
+            indoc! {"
+            fn boop() {
+                baz(|| { bar(«|j, k| ˇ»{ })})
+            }"
+            },
+            Mode::Visual,
+        );
     }
 
     #[gpui::test]
@@ -834,12 +995,6 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
         for (start, end) in SURROUNDING_OBJECTS {
-            if ((start == &'\'' || start == &'`' || start == &'"')
-                && !ExemptionFeatures::QuotesSeekForward.supported())
-                || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported())
-            {
-                continue;
-            }
             let marked_string = SURROUNDING_MARKER_STRING
                 .replace('`', &start.to_string())
                 .replace('\'', &end.to_string());

crates/vim/src/test.rs 🔗

@@ -653,6 +653,63 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
         .await;
 }
 
+#[gpui::test]
+async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_wrap(12).await;
+
+    cx.set_shared_state(indoc! {"
+                aaˇaa
+                😃😃"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! {"
+                aaaa
+                😃ˇ😃"
+    })
+    .await;
+
+    cx.set_shared_state(indoc! {"
+                123456789012aaˇaa
+                123456789012😃😃"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! {"
+        123456789012aaaa
+        123456789012😃ˇ😃"
+    })
+    .await;
+
+    cx.set_shared_state(indoc! {"
+                123456789012aaˇaa
+                123456789012😃😃"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! {"
+        123456789012aaaa
+        123456789012😃ˇ😃"
+    })
+    .await;
+
+    cx.set_shared_state(indoc! {"
+        123456789012aaaaˇaaaaaaaa123456789012
+        wow
+        123456789012😃😃😃😃😃😃123456789012"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j", "j"]).await;
+    cx.assert_shared_state(indoc! {"
+        123456789012aaaaaaaaaaaa123456789012
+        wow
+        123456789012😃😃ˇ😃😃😃😃123456789012"
+    })
+    .await;
+}
+
 #[gpui::test]
 async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
     let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -677,3 +734,26 @@ async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
         two"})
         .await;
 }
+
+#[gpui::test]
+async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    cx.set_state(
+        indoc! {"
+        defmodule Test do
+            def test(a, ˇ[_, _] = b), do: IO.puts('hi')
+        end
+    "},
+        Mode::Normal,
+    );
+    cx.simulate_keystrokes(["g", "a"]);
+    cx.assert_state(
+        indoc! {"
+        defmodule Test do
+            def test(a, «[ˇ»_, _] = b), do: IO.puts('hi')
+        end
+    "},
+        Mode::Visual,
+    );
+}

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

@@ -1,15 +1,15 @@
 use editor::scroll::VERTICAL_SCROLL_MARGIN;
 use indoc::indoc;
 use settings::SettingsStore;
-use std::ops::{Deref, DerefMut, Range};
+use std::{
+    ops::{Deref, DerefMut},
+    panic, thread,
+};
 
 use collections::{HashMap, HashSet};
 use gpui::{geometry::vector::vec2f, ContextHandle};
-use language::{
-    language_settings::{AllLanguageSettings, SoftWrap},
-    OffsetRangeExt,
-};
-use util::test::{generate_marked_text, marked_text_offsets};
+use language::language_settings::{AllLanguageSettings, SoftWrap};
+use util::test::marked_text_offsets;
 
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
 use crate::state::Mode;
@@ -37,10 +37,6 @@ pub enum ExemptionFeatures {
     AroundSentenceStartingBetweenIncludesWrongWhitespace,
     // Non empty selection with text objects in visual mode
     NonEmptyVisualTextObjects,
-    // Quote style surrounding text objects don't seek forward properly
-    QuotesSeekForward,
-    // Neovim freezes up for some reason with angle brackets
-    AngleBracketsFreezeNeovim,
     // Sentence Doesn't backtrack when its at the end of the file
     SentenceAfterPunctuationAtEndOfFile,
 }
@@ -66,12 +62,22 @@ pub struct NeovimBackedTestContext<'a> {
 
 impl<'a> NeovimBackedTestContext<'a> {
     pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
-        let function_name = cx.function_name.clone();
-        let cx = VimTestContext::new(cx, true).await;
+        // rust stores the name of the test on the current thread.
+        // We use this to automatically name a file that will store
+        // the neovim connection's requests/responses so that we can
+        // run without neovim on CI.
+        let thread = thread::current();
+        let test_name = thread
+            .name()
+            .expect("thread is not named")
+            .split(":")
+            .last()
+            .unwrap()
+            .to_string();
         Self {
-            cx,
+            cx: VimTestContext::new(cx, true).await,
             exemptions: Default::default(),
-            neovim: NeovimConnection::new(function_name).await,
+            neovim: NeovimConnection::new(test_name).await,
 
             last_set_state: None,
             recent_keystrokes: Default::default(),
@@ -250,25 +256,13 @@ impl<'a> NeovimBackedTestContext<'a> {
     }
 
     pub async fn neovim_state(&mut self) -> String {
-        generate_marked_text(
-            self.neovim.text().await.as_str(),
-            &self.neovim_selections().await[..],
-            true,
-        )
+        self.neovim.marked_text().await
     }
 
     pub async fn neovim_mode(&mut self) -> Mode {
         self.neovim.mode().await.unwrap()
     }
 
-    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) {
         self.is_dirty = false;
         let neovim = self.neovim_state().await;

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

@@ -1,9 +1,9 @@
+use std::path::PathBuf;
 #[cfg(feature = "neovim")]
 use std::{
     cmp,
-    ops::{Deref, DerefMut},
+    ops::{Deref, DerefMut, Range},
 };
-use std::{ops::Range, path::PathBuf};
 
 #[cfg(feature = "neovim")]
 use async_compat::Compat;
@@ -12,6 +12,7 @@ use async_trait::async_trait;
 #[cfg(feature = "neovim")]
 use gpui::keymap_matcher::Keystroke;
 
+#[cfg(feature = "neovim")]
 use language::Point;
 
 #[cfg(feature = "neovim")]
@@ -109,7 +110,12 @@ impl NeovimConnection {
     // Sends a keystroke to the neovim process.
     #[cfg(feature = "neovim")]
     pub async fn send_keystroke(&mut self, keystroke_text: &str) {
-        let keystroke = Keystroke::parse(keystroke_text).unwrap();
+        let mut keystroke = Keystroke::parse(keystroke_text).unwrap();
+
+        if keystroke.key == "<" {
+            keystroke.key = "lt".to_string()
+        }
+
         let special = keystroke.shift
             || keystroke.ctrl
             || keystroke.alt
@@ -296,7 +302,7 @@ impl NeovimConnection {
     }
 
     #[cfg(feature = "neovim")]
-    pub async fn state(&mut self) -> (Option<Mode>, String, Vec<Range<Point>>) {
+    pub async fn state(&mut self) -> (Option<Mode>, String) {
         let nvim_buffer = self
             .nvim
             .get_current_buf()
@@ -405,37 +411,33 @@ impl NeovimConnection {
                 .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)),
         }
 
+        let ranges = encode_ranges(&text, &selections);
         let state = NeovimData::Get {
             mode,
-            state: encode_ranges(&text, &selections),
+            state: ranges.clone(),
         };
 
         if self.data.back() != Some(&state) {
             self.data.push_back(state.clone());
         }
 
-        (mode, text, selections)
+        (mode, ranges)
     }
 
     #[cfg(not(feature = "neovim"))]
-    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, ranges) = parse_state(text);
-            (*mode, text, ranges)
+    pub async fn state(&mut self) -> (Option<Mode>, String) {
+        if let Some(NeovimData::Get { state: raw, mode }) = self.data.front() {
+            (*mode, raw.to_string())
         } else {
             panic!("operation does not match recorded script. re-record with --features=neovim");
         }
     }
 
-    pub async fn selections(&mut self) -> Vec<Range<Point>> {
-        self.state().await.2
-    }
-
     pub async fn mode(&mut self) -> Option<Mode> {
         self.state().await.0
     }
 
-    pub async fn text(&mut self) -> String {
+    pub async fn marked_text(&mut self) -> String {
         self.state().await.1
     }
 
@@ -527,6 +529,7 @@ impl Handler for NvimHandler {
     }
 }
 
+#[cfg(feature = "neovim")]
 fn parse_state(marked_text: &str) -> (String, Vec<Range<Point>>) {
     let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
     let point_ranges = ranges

crates/vim/src/vim.rs 🔗

@@ -25,7 +25,7 @@ pub use mode_indicator::ModeIndicator;
 use motion::Motion;
 use normal::normal_replace;
 use serde::Deserialize;
-use settings::{Setting, SettingsStore};
+use settings::{update_settings_file, Setting, SettingsStore};
 use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
 use std::{ops::Range, sync::Arc};
 use visual::{visual_block_motion, visual_replace};
@@ -48,6 +48,7 @@ actions!(
     vim,
     [Tab, Enter, Object, InnerObject, FindForward, FindBackward]
 );
+actions!(workspace, [ToggleVimMode]);
 impl_actions!(vim, [Number, SwitchMode, PushOperator]);
 
 #[derive(Copy, Clone, Debug)]
@@ -88,6 +89,14 @@ pub fn init(cx: &mut AppContext) {
         Vim::active_editor_input_ignored("\n".into(), cx)
     });
 
+    cx.add_action(|workspace: &mut Workspace, _: &ToggleVimMode, cx| {
+        let fs = workspace.app_state().fs.clone();
+        let currently_enabled = settings::get::<VimModeSetting>(cx).0;
+        update_settings_file::<VimModeSetting>(fs, cx, move |setting| {
+            *setting = Some(!currently_enabled)
+        })
+    });
+
     // Any time settings change, update vim mode to match. The Vim struct
     // will be initialized as disabled by default, so we filter its commands
     // out when starting up.
@@ -581,7 +590,7 @@ impl Setting for VimModeSetting {
 fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
-            if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
+            if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
                 vim.switch_mode(Mode::VisualBlock, false, cx);
             } else {
                 vim.switch_mode(Mode::Visual, false, cx)

crates/vim/src/visual.rs 🔗

@@ -1,5 +1,5 @@
 use anyhow::Result;
-use std::{cmp, sync::Arc};
+use std::sync::Arc;
 
 use collections::HashMap;
 use editor::{
@@ -57,6 +57,7 @@ pub fn init(cx: &mut AppContext) {
 pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
+            let text_layout_details = editor.text_layout_details(cx);
             if vim.state().mode == Mode::VisualBlock
                 && !matches!(
                     motion,
@@ -67,7 +68,7 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
             {
                 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)
+                    motion.move_point(map, point, goal, times, &text_layout_details)
                 })
             } else {
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -89,9 +90,13 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
                             current_head = movement::left(map, selection.end)
                         }
 
-                        let Some((new_head, goal)) =
-                            motion.move_point(map, current_head, selection.goal, times)
-                        else {
+                        let Some((new_head, goal)) = motion.move_point(
+                            map,
+                            current_head,
+                            selection.goal,
+                            times,
+                            &text_layout_details,
+                        ) else {
                             return;
                         };
 
@@ -135,19 +140,23 @@ pub fn visual_block_motion(
         SelectionGoal,
     ) -> Option<(DisplayPoint, SelectionGoal)>,
 ) {
+    let text_layout_details = editor.text_layout_details(cx);
     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 head_x = map.x_for_point(head, &text_layout_details);
+        let mut tail_x = map.x_for_point(tail, &text_layout_details);
+
         let (start, end) = match s.newest_anchor().goal {
-            SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
-            SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
-            _ => (tail.column(), head.column()),
+            SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
+            SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
+            _ => (tail_x, head_x),
         };
-        let goal = SelectionGoal::ColumnRange { start, end };
+        let mut goal = SelectionGoal::HorizontalRange { start, end };
 
-        let was_reversed = tail.column() > head.column();
+        let was_reversed = tail_x > head_x;
         if !was_reversed && !preserve_goal {
             head = movement::saturating_left(map, head);
         }
@@ -156,32 +165,56 @@ pub fn visual_block_motion(
             return;
         };
         head = new_head;
+        head_x = map.x_for_point(head, &text_layout_details);
 
-        let is_reversed = tail.column() > head.column();
+        let is_reversed = tail_x > head_x;
         if was_reversed && !is_reversed {
-            tail = movement::left(map, tail)
+            tail = movement::saturating_left(map, tail);
+            tail_x = map.x_for_point(tail, &text_layout_details);
         } else if !was_reversed && is_reversed {
-            tail = movement::right(map, tail)
+            tail = movement::saturating_right(map, tail);
+            tail_x = map.x_for_point(tail, &text_layout_details);
         }
         if !is_reversed && !preserve_goal {
-            head = movement::saturating_right(map, head)
+            head = movement::saturating_right(map, head);
+            head_x = map.x_for_point(head, &text_layout_details);
         }
 
-        let columns = if is_reversed {
-            head.column()..tail.column()
-        } else if head.column() == tail.column() {
-            head.column()..(head.column() + 1)
+        let positions = if is_reversed {
+            head_x..tail_x
         } else {
-            tail.column()..head.column()
+            tail_x..head_x
         };
 
+        if !preserve_goal {
+            goal = SelectionGoal::HorizontalRange {
+                start: positions.start,
+                end: positions.end,
+            };
+        }
+
         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 layed_out_line = map.lay_out_line_for_row(row, &text_layout_details);
+            let start = DisplayPoint::new(
+                row,
+                layed_out_line.closest_index_for_x(positions.start) as u32,
+            );
+            let mut end = DisplayPoint::new(
+                row,
+                layed_out_line.closest_index_for_x(positions.end) as u32,
+            );
+            if end <= start {
+                if start.column() == map.line_len(start.row()) {
+                    end = start;
+                } else {
+                    end = movement::saturating_right(map, start);
+                }
+            }
+
+            if positions.start <= layed_out_line.width() {
                 let selection = Selection {
                     id: s.new_selection_id(),
                     start: start.to_point(map),
@@ -230,21 +263,13 @@ 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 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
-                                    };
+                                let expand_both_ways = object.always_expands_both_ways()
+                                    || selection.is_empty()
+                                    || movement::right(map, selection.start) == selection.end;
 
                                 if expand_both_ways {
-                                    selection.start = cmp::min(selection.start, range.start);
-                                    selection.end = cmp::max(selection.end, range.end);
+                                    selection.start = range.start;
+                                    selection.end = range.end;
                                 } else if selection.reversed {
                                     selection.start = range.start;
                                 } else {
@@ -888,6 +913,28 @@ mod test {
         .await;
     }
 
+    #[gpui::test]
+    async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {
+            "The ˇquick brown
+            fox jumps over
+            the lazy dog
+            "
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["ctrl-v", "right", "down"])
+            .await;
+        cx.assert_shared_state(indoc! {
+            "The «quˇ»ick brown
+            fox «juˇ»mps over
+            the lazy dog
+            "
+        })
+        .await;
+    }
+
     #[gpui::test]
     async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/test_data/test_change_surrounding_character_objects.json 🔗

@@ -1,3 +1,1023 @@
+{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck broˇ\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck broˇ\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck broˇ\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck broˇ\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
+{"Key":"c"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
+{"Key":"c"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
 {"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
 {"Key":"c"}
 {"Key":"i"}

crates/vim/test_data/test_delete_e.json → crates/vim/test_data/test_delete_next_word_end.json 🔗

@@ -1,11 +1,3 @@
-{"Put":{"state":"Teˇst Test"}}
-{"Key":"d"}
-{"Key":"e"}
-{"Get":{"state":"Teˇ Test","mode":"Normal"}}
-{"Put":{"state":"Tˇest test"}}
-{"Key":"d"}
-{"Key":"e"}
-{"Get":{"state":"Tˇ test","mode":"Normal"}}
 {"Put":{"state":"Test teˇst\ntest"}}
 {"Key":"d"}
 {"Key":"e"}

crates/vim/test_data/test_delete_surrounding_character_objects.json 🔗

@@ -1,3 +1,1023 @@
+{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck brˇo\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck brˇo\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"'"}
+{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck brˇo\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck brˇo\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"`"}
+{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
+{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}}
+{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
 {"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
 {"Key":"d"}
 {"Key":"i"}

crates/vim/test_data/test_e.json 🔗

@@ -1,32 +0,0 @@
-{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}}
-{"Key":"e"}
-{"Get":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
-{"Key":"e"}
-{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
-{"Key":"e"}
-{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
-{"Key":"e"}
-{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}}
-{"Key":"e"}
-{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}}
-{"Key":"e"}
-{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
-{"Key":"e"}
-{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
-{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}}
-{"Key":"shift-e"}
-{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
-{"Put":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe"}}
-{"Key":"shift-e"}
-{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
-{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}}
-{"Key":"shift-e"}
-{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
-{"Key":"shift-e"}
-{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}}
-{"Key":"shift-e"}
-{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}}
-{"Key":"shift-e"}
-{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
-{"Key":"shift-e"}
-{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}

crates/vim/test_data/test_increment_steps.json 🔗

@@ -9,6 +9,7 @@
 {"Key":"ctrl-v"}
 {"Key":"g"}
 {"Key":"g"}
+{"Get":{"state":"«1ˇ»\n«2ˇ»\n«3ˇ»  2\n«4ˇ»\n«5ˇ»","mode":"VisualBlock"}}
 {"Key":"g"}
 {"Key":"ctrl-x"}
 {"Get":{"state":"ˇ0\n0\n0  2\n0\n0","mode":"Normal"}}

crates/vim/test_data/test_j.json 🔗

@@ -1,3 +1,6 @@
+{"Put":{"state":"aaˇaa\n😃😃"}}
+{"Key":"j"}
+{"Get":{"state":"aaaa\n😃ˇ😃","mode":"Normal"}}
 {"Put":{"state":"ˇThe quick brown\nfox jumps"}}
 {"Key":"j"}
 {"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}

crates/vim/test_data/test_multiline_surrounding_character_objects.json 🔗

@@ -8,3 +8,8 @@
 {"Key":"i"}
 {"Key":"{"}
 {"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"}}

crates/vim/test_data/test_singleline_surrounding_character_objects.json 🔗

@@ -0,0 +1,27 @@
+{"SetOption":{"value":"wrap"}}
+{"SetOption":{"value":"columns=12"}}
+{"Put":{"state":"helˇlo \"world\"!"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}}
+{"Put":{"state":"hello \"wˇorld\"!"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}}
+{"Put":{"state":"hello \"wˇorld\"!"}}
+{"Key":"v"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"hello« \"world\"ˇ»!","mode":"Visual"}}
+{"Put":{"state":"hello \"wˇorld\" !"}}
+{"Key":"v"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"hello «\"world\" ˇ»!","mode":"Visual"}}
+{"Put":{"state":"hello \"wˇorld\"•\ngoodbye"}}
+{"Key":"v"}
+{"Key":"a"}
+{"Key":"\""}
+{"Get":{"state":"hello «\"world\" ˇ»\ngoodbye","mode":"Visual"}}

crates/vim/test_data/test_visual_block_issue_2123.json 🔗

@@ -0,0 +1,5 @@
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\n"}}
+{"Key":"ctrl-v"}
+{"Key":"right"}
+{"Key":"down"}
+{"Get":{"state":"The «quˇ»ick brown\nfox «juˇ»mps over\nthe lazy dog\n","mode":"VisualBlock"}}

crates/vim/test_data/test_visual_paste.json 🔗

@@ -1,26 +0,0 @@
-{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
-{"Key":"v"}
-{"Key":"i"}
-{"Key":"w"}
-{"Key":"y"}
-{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}}
-{"Key":"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_wrapped_motions.json 🔗

@@ -0,0 +1,15 @@
+{"SetOption":{"value":"wrap"}}
+{"SetOption":{"value":"columns=12"}}
+{"Put":{"state":"aaˇaa\n😃😃"}}
+{"Key":"j"}
+{"Get":{"state":"aaaa\n😃ˇ😃","mode":"Normal"}}
+{"Put":{"state":"123456789012aaˇaa\n123456789012😃😃"}}
+{"Key":"j"}
+{"Get":{"state":"123456789012aaaa\n123456789012😃ˇ😃","mode":"Normal"}}
+{"Put":{"state":"123456789012aaˇaa\n123456789012😃😃"}}
+{"Key":"j"}
+{"Get":{"state":"123456789012aaaa\n123456789012😃ˇ😃","mode":"Normal"}}
+{"Put":{"state":"123456789012aaaaˇaaaaaaaa123456789012\nwow\n123456789012😃😃😃😃😃😃123456789012"}}
+{"Key":"j"}
+{"Key":"j"}
+{"Get":{"state":"123456789012aaaaaaaaaaaa123456789012\nwow\n123456789012😃😃ˇ😃😃😃😃123456789012","mode":"Normal"}}

crates/workspace/src/workspace.rs 🔗

@@ -35,9 +35,9 @@ use gpui::{
         CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
         WindowBounds, WindowOptions,
     },
-    AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext,
-    Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle, WindowContext, WindowHandle,
+    AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
+    ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
+    WeakViewHandle, WindowContext, WindowHandle,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
 use itertools::Itertools;
@@ -289,6 +289,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.add_global_action(restart);
     cx.add_async_action(Workspace::save_all);
     cx.add_action(Workspace::add_folder_to_project);
+
     cx.add_action(
         |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
             let pane = workspace.active_pane().clone();
@@ -4237,6 +4238,10 @@ async fn join_channel_internal(
         })
         .await?;
 
+    let Some(room) = room else {
+        return anyhow::Ok(true);
+    };
+
     room.update(cx, |room, _| room.room_update_completed())
         .await;
 
@@ -4294,12 +4299,14 @@ pub fn join_channel(
         }
 
         if let Err(err) = result {
-            let prompt = active_window.unwrap().prompt(
-                PromptLevel::Critical,
-                &format!("Failed to join channel: {}", err),
-                &["Ok"],
-                &mut cx,
-            );
+            let prompt = active_window.unwrap().update(&mut cx, |_, cx| {
+                cx.prompt(
+                    PromptLevel::Critical,
+                    &format!("Failed to join channel: {}", err),
+                    &["Ok"],
+                )
+            });
+
             if let Some(mut prompt) = prompt {
                 prompt.next().await;
             } else {
@@ -4312,17 +4319,39 @@ pub fn join_channel(
     })
 }
 
-pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
+pub async fn get_any_active_workspace(
+    app_state: Arc<AppState>,
+    mut cx: AsyncAppContext,
+) -> Result<ViewHandle<Workspace>> {
+    // find an existing workspace to focus and show call controls
+    let active_window = activate_any_workspace_window(&mut cx);
+    if active_window.is_none() {
+        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))
+            .await;
+    }
+
+    let Some(active_window) = activate_any_workspace_window(&mut cx) else {
+        return Err(anyhow!("could not open zed"))?;
+    };
+
+    Ok(active_window)
+}
+
+pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<ViewHandle<Workspace>> {
     for window in cx.windows() {
-        let found = window.update(cx, |cx| {
-            let is_workspace = cx.root_view().clone().downcast::<Workspace>().is_some();
-            if is_workspace {
-                cx.activate_window();
-            }
-            is_workspace
-        });
-        if found == Some(true) {
-            return Some(window);
+        if let Some(workspace) = window
+            .update(cx, |cx| {
+                cx.root_view()
+                    .clone()
+                    .downcast::<Workspace>()
+                    .map(|workspace| {
+                        cx.activate_window();
+                        workspace
+                    })
+            })
+            .flatten()
+        {
+            return Some(workspace);
         }
     }
     None

crates/zed-actions/src/lib.rs 🔗

@@ -1,28 +1,41 @@
-use gpui::actions;
+use std::sync::Arc;
+
+use gpui::{actions, impl_actions};
+use serde::Deserialize;
 
 actions!(
     zed,
     [
         About,
+        DebugElements,
+        DecreaseBufferFontSize,
         Hide,
         HideOthers,
-        ShowAll,
+        IncreaseBufferFontSize,
         Minimize,
-        Zoom,
-        ToggleFullScreen,
-        Quit,
-        DebugElements,
-        OpenLog,
-        OpenLicenses,
-        OpenTelemetryLog,
+        OpenDefaultKeymap,
+        OpenDefaultSettings,
         OpenKeymap,
-        OpenSettings,
+        OpenLicenses,
         OpenLocalSettings,
-        OpenDefaultSettings,
-        OpenDefaultKeymap,
-        IncreaseBufferFontSize,
-        DecreaseBufferFontSize,
+        OpenLog,
+        OpenSettings,
+        OpenTelemetryLog,
+        Quit,
         ResetBufferFontSize,
         ResetDatabase,
+        ShowAll,
+        ToggleFullScreen,
+        Zoom,
     ]
 );
+
+#[derive(Deserialize, Clone, PartialEq)]
+pub struct OpenBrowser {
+    pub url: Arc<str>,
+}
+#[derive(Deserialize, Clone, PartialEq)]
+pub struct OpenZedURL {
+    pub url: String,
+}
+impl_actions!(zed, [OpenBrowser, OpenZedURL]);

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.109.0"
+version = "0.111.0"
 publish = false
 
 [lib]
@@ -15,6 +15,9 @@ doctest = false
 name = "Zed"
 path = "src/main.rs"
 
+[[example]]
+name = "semantic_index_eval"
+
 [dependencies]
 audio = { path = "../audio" }
 activity_indicator = { path = "../activity_indicator" }
@@ -50,6 +53,7 @@ language_selector = { path = "../language_selector" }
 lsp = { path = "../lsp" }
 language_tools = { path = "../language_tools" }
 node_runtime = { path = "../node_runtime" }
+notifications = { path = "../notifications" }
 assistant = { path = "../assistant" }
 outline = { path = "../outline" }
 plugin_runtime = { path = "../plugin_runtime",optional = true }
@@ -135,12 +139,14 @@ tree-sitter-yaml.workspace = true
 tree-sitter-lua.workspace = true
 tree-sitter-nix.workspace = true
 tree-sitter-nu.workspace = true
+tree-sitter-vue.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"
 uuid.workspace = true
 
 [dev-dependencies]
+ai = { path = "../ai" }
 call = { path = "../call", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }

crates/semantic_index/examples/eval.rs → crates/zed/examples/semantic_index_eval.rs 🔗

@@ -55,7 +55,7 @@ fn parse_eval() -> anyhow::Result<Vec<RepoEval>> {
         .as_path()
         .parent()
         .unwrap()
-        .join("crates/semantic_index/eval");
+        .join("zed/crates/semantic_index/eval");
 
     let mut repo_evals: Vec<RepoEval> = Vec::new();
     for entry in fs::read_dir(eval_folder)? {
@@ -469,6 +469,7 @@ fn main() {
             .join("embeddings_db");
 
         let languages = languages.clone();
+
         let fs = fs.clone();
         cx.spawn(|mut cx| async move {
             let semantic_index = SemanticIndex::new(

crates/zed/src/languages.rs 🔗

@@ -24,6 +24,7 @@ mod rust;
 mod svelte;
 mod tailwind;
 mod typescript;
+mod vue;
 mod yaml;
 
 // 1. Add tree-sitter-{language} parser to zed crate
@@ -75,7 +76,10 @@ pub fn init(
         elixir::ElixirLspSetting::ElixirLs => language(
             "elixir",
             tree_sitter_elixir::language(),
-            vec![Arc::new(elixir::ElixirLspAdapter)],
+            vec![
+                Arc::new(elixir::ElixirLspAdapter),
+                Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+            ],
         ),
         elixir::ElixirLspSetting::NextLs => language(
             "elixir",
@@ -100,7 +104,10 @@ pub fn init(
     language(
         "heex",
         tree_sitter_heex::language(),
-        vec![Arc::new(elixir::ElixirLspAdapter)],
+        vec![
+            Arc::new(elixir::ElixirLspAdapter),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
     );
     language(
         "json",
@@ -166,7 +173,10 @@ pub fn init(
     language(
         "erb",
         tree_sitter_embedded_template::language(),
-        vec![Arc::new(ruby::RubyLanguageServer)],
+        vec![
+            Arc::new(ruby::RubyLanguageServer),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
     );
     language("scheme", tree_sitter_scheme::language(), vec![]);
     language("racket", tree_sitter_racket::language(), vec![]);
@@ -183,20 +193,29 @@ pub fn init(
     language(
         "svelte",
         tree_sitter_svelte::language(),
-        vec![Arc::new(svelte::SvelteLspAdapter::new(
-            node_runtime.clone(),
-        ))],
+        vec![
+            Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
     );
     language(
         "php",
         tree_sitter_php::language(),
-        vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))],
+        vec![
+            Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
     );
 
     language("elm", tree_sitter_elm::language(), vec![]);
     language("glsl", tree_sitter_glsl::language(), vec![]);
     language("nix", tree_sitter_nix::language(), vec![]);
     language("nu", tree_sitter_nu::language(), vec![]);
+    language(
+        "vue",
+        tree_sitter_vue::language(),
+        vec![Arc::new(vue::VueLspAdapter::new(node_runtime))],
+    );
 }
 
 #[cfg(any(test, feature = "test-support"))]

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

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
@@ -96,10 +96,6 @@ impl LspAdapter for CssLspAdapter {
             "provideFormatter": true
         }))
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("css")]
-    }
 }
 
 async fn get_cached_server_binary(

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

@@ -19,7 +19,7 @@ use std::{
     },
 };
 use util::{
-    async_iife,
+    async_maybe,
     fs::remove_matching,
     github::{latest_github_release, GitHubLspBinaryVersion},
     ResultExt,
@@ -421,7 +421,7 @@ impl LspAdapter for NextLspAdapter {
 }
 
 async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
-    async_iife!({
+    async_maybe!({
         let mut last_binary_path = None;
         let mut entries = fs::read_dir(&container_dir).await?;
         while let Some(entry) = entries.next().await {

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

@@ -9,3 +9,8 @@ brackets = [
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
     { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

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

@@ -5,3 +5,4 @@ brackets = [
     { start = "<", end = ">", close = true, newline = true },
 ]
 block_comment = ["<%#", "%>"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]

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

@@ -5,3 +5,8 @@ brackets = [
     { start = "<", end = ">", close = true, newline = true },
 ]
 block_comment = ["<%!-- ", " --%>"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

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

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
@@ -96,10 +96,6 @@ impl LspAdapter for HtmlLspAdapter {
             "provideFormatter": true
         }))
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("html")]
-    }
 }
 
 async fn get_cached_server_binary(

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

@@ -11,3 +11,4 @@ brackets = [
     { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] },
 ]
 word_characters = ["-"]
+prettier_parser_name = "html"

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

@@ -15,6 +15,7 @@ brackets = [
 ]
 word_characters = ["$", "#"]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "babel"
 
 [overrides.element]
 line_comment = { remove = true }

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

@@ -4,9 +4,7 @@ use collections::HashMap;
 use feature_flags::FeatureFlagAppExt;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
-use language::{
-    BundledFormatter, LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate,
-};
+use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
@@ -146,10 +144,6 @@ impl LspAdapter for JsonLspAdapter {
     async fn language_ids(&self) -> HashMap<String, String> {
         [("JSON".into(), "jsonc".into())].into_iter().collect()
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("json")]
-    }
 }
 
 async fn get_cached_server_binary(

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

@@ -7,3 +7,4 @@ brackets = [
     { start = "[", end = "]", close = true, newline = true },
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
+prettier_parser_name = "json"

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

@@ -8,7 +8,7 @@ use lsp::LanguageServerBinary;
 use smol::fs;
 use std::{any::Any, env::consts, path::PathBuf};
 use util::{
-    async_iife,
+    async_maybe,
     github::{latest_github_release, GitHubLspBinaryVersion},
     ResultExt,
 };
@@ -106,7 +106,7 @@ impl super::LspAdapter for LuaLspAdapter {
 }
 
 async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
-    async_iife!({
+    async_maybe!({
         let mut last_binary_path = None;
         let mut entries = fs::read_dir(&container_dir).await?;
         while let Some(entry) = entries.next().await {

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

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
@@ -96,11 +96,8 @@ impl LspAdapter for SvelteLspAdapter {
         }))
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::Prettier {
-            parser_name: Some("svelte"),
-            plugin_names: vec!["prettier-plugin-svelte"],
-        }]
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &["prettier-plugin-svelte"]
     }
 }
 

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

@@ -12,7 +12,9 @@ brackets = [
     { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
     { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
 ]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "svelte"
 
-[overrides.element]
-line_comment = { remove = true }
-block_comment = ["{/* ", " */}"]
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

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

@@ -6,7 +6,7 @@ use futures::{
     FutureExt, StreamExt,
 };
 use gpui::AppContext;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::{json, Value};
@@ -117,22 +117,21 @@ impl LspAdapter for TailwindLspAdapter {
     }
 
     async fn language_ids(&self) -> HashMap<String, String> {
-        HashMap::from_iter(
-            [
-                ("HTML".to_string(), "html".to_string()),
-                ("CSS".to_string(), "css".to_string()),
-                ("JavaScript".to_string(), "javascript".to_string()),
-                ("TSX".to_string(), "typescriptreact".to_string()),
-            ]
-            .into_iter(),
-        )
+        HashMap::from_iter([
+            ("HTML".to_string(), "html".to_string()),
+            ("CSS".to_string(), "css".to_string()),
+            ("JavaScript".to_string(), "javascript".to_string()),
+            ("TSX".to_string(), "typescriptreact".to_string()),
+            ("Svelte".to_string(), "svelte".to_string()),
+            ("Elixir".to_string(), "phoenix-heex".to_string()),
+            ("HEEX".to_string(), "phoenix-heex".to_string()),
+            ("ERB".to_string(), "erb".to_string()),
+            ("PHP".to_string(), "php".to_string()),
+        ])
     }
 
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::Prettier {
-            parser_name: None,
-            plugin_names: vec!["prettier-plugin-tailwindcss"],
-        }]
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &["prettier-plugin-tailwindcss"]
     }
 }
 

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

@@ -14,6 +14,7 @@ brackets = [
 ]
 word_characters = ["#", "$"]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "typescript"
 
 [overrides.element]
 line_comment = { remove = true }

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

@@ -4,7 +4,7 @@ use async_tar::Archive;
 use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt};
 use gpui::AppContext;
-use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::{CodeActionKind, LanguageServerBinary};
 use node_runtime::NodeRuntime;
 use serde_json::{json, Value};
@@ -161,10 +161,6 @@ impl LspAdapter for TypeScriptLspAdapter {
             "provideFormatter": true
         }))
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("typescript")]
-    }
 }
 
 async fn get_cached_ts_server_binary(
@@ -313,10 +309,6 @@ impl LspAdapter for EsLintLspAdapter {
     async fn initialization_options(&self) -> Option<serde_json::Value> {
         None
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("babel")]
-    }
 }
 
 async fn get_cached_eslint_server_binary(

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

@@ -0,0 +1,220 @@
+use anyhow::{anyhow, ensure, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+pub use language::*;
+use lsp::{CodeActionKind, LanguageServerBinary};
+use node_runtime::NodeRuntime;
+use parking_lot::Mutex;
+use serde_json::Value;
+use smol::fs::{self};
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+pub struct VueLspVersion {
+    vue_version: String,
+    ts_version: String,
+}
+
+pub struct VueLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+    typescript_install_path: Mutex<Option<PathBuf>>,
+}
+
+impl VueLspAdapter {
+    const SERVER_PATH: &'static str =
+        "node_modules/@vue/language-server/bin/vue-language-server.js";
+    // TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options.
+    const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib";
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        let typescript_install_path = Mutex::new(None);
+        Self {
+            node,
+            typescript_install_path,
+        }
+    }
+}
+#[async_trait]
+impl super::LspAdapter for VueLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("vue-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "vue-language-server"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(VueLspVersion {
+            vue_version: self
+                .node
+                .npm_package_latest_version("@vue/language-server")
+                .await?,
+            ts_version: self.node.npm_package_latest_version("typescript").await?,
+        }) as Box<_>)
+    }
+    async fn initialization_options(&self) -> Option<Value> {
+        let typescript_sdk_path = self.typescript_install_path.lock();
+        let typescript_sdk_path = typescript_sdk_path
+            .as_ref()
+            .expect("initialization_options called without a container_dir for typescript");
+
+        Some(serde_json::json!({
+            "typescript": {
+                "tsdk": typescript_sdk_path
+            }
+        }))
+    }
+    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
+        // sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
+        Some(vec![
+            CodeActionKind::EMPTY,
+            CodeActionKind::QUICKFIX,
+            CodeActionKind::REFACTOR_REWRITE,
+        ])
+    }
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<VueLspVersion>().unwrap();
+        let server_path = container_dir.join(Self::SERVER_PATH);
+        let ts_path = container_dir.join(Self::TYPESCRIPT_PATH);
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("@vue/language-server", version.vue_version.as_str())],
+                )
+                .await?;
+        }
+        ensure!(
+            fs::metadata(&server_path).await.is_ok(),
+            "@vue/language-server package installation failed"
+        );
+        if fs::metadata(&ts_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("typescript", version.ts_version.as_str())],
+                )
+                .await?;
+        }
+
+        ensure!(
+            fs::metadata(&ts_path).await.is_ok(),
+            "typescript for Vue package installation failed"
+        );
+        *self.typescript_install_path.lock() = Some(ts_path);
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: vue_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?;
+        *self.typescript_install_path.lock() = Some(ts_path);
+        Some(server)
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone())
+            .await
+            .map(|(mut binary, ts_path)| {
+                binary.arguments = vec!["--help".into()];
+                (binary, ts_path)
+            })?;
+        *self.typescript_install_path.lock() = Some(ts_path);
+        Some(server)
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp::CompletionItem,
+        language: &Arc<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        use lsp::CompletionItemKind as Kind;
+        let len = item.label.len();
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
+            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
+            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
+            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
+            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"),
+            Kind::VARIABLE => grammar.highlight_id_for_name("type"),
+            Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
+            Kind::VALUE => grammar.highlight_id_for_name("tag"),
+            _ => None,
+        }?;
+
+        let text = match &item.detail {
+            Some(detail) => format!("{} {}", item.label, detail),
+            None => item.label.clone(),
+        };
+
+        Some(language::CodeLabel {
+            text,
+            runs: vec![(0..len, highlight_id)],
+            filter_range: 0..len,
+        })
+    }
+}
+
+fn vue_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+type TypescriptPath = PathBuf;
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: Arc<dyn NodeRuntime>,
+) -> Option<(LanguageServerBinary, TypescriptPath)> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH);
+        let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH);
+        if server_path.exists() && typescript_path.exists() {
+            Ok((
+                LanguageServerBinary {
+                    path: node.binary_path().await?,
+                    arguments: vue_server_binary_arguments(&server_path),
+                },
+                typescript_path,
+            ))
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

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

@@ -0,0 +1,14 @@
+name = "Vue.js"
+path_suffixes = ["vue"]
+block_comment = ["<!-- ", " -->"]
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = true, newline = true, not_in = ["string", "comment"] },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
+]
+word_characters = ["-"]

crates/zed/src/languages/vue/highlights.scm 🔗

@@ -0,0 +1,15 @@
+(attribute) @property
+(directive_attribute) @property
+(quoted_attribute_value) @string
+(interpolation) @punctuation.special
+(raw_text) @embedded
+
+((tag_name) @type
+ (#match? @type "^[A-Z]"))
+
+((directive_name) @keyword
+ (#match? @keyword "^v-"))
+
+(start_tag) @tag
+(end_tag) @tag
+(self_closing_tag) @tag

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

@@ -3,8 +3,7 @@ use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
 use language::{
-    language_settings::all_language_settings, BundledFormatter, LanguageServerName, LspAdapter,
-    LspAdapterDelegate,
+    language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
 };
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
@@ -109,10 +108,6 @@ impl LspAdapter for YamlLspAdapter {
         }))
         .boxed()
     }
-
-    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
-        vec![BundledFormatter::prettier("yaml")]
-    }
 }
 
 async fn get_cached_server_binary(

crates/zed/src/main.rs 🔗

@@ -3,22 +3,17 @@
 
 use anyhow::{anyhow, Context, Result};
 use backtrace::Backtrace;
-use cli::{
-    ipc::{self, IpcSender},
-    CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
-};
+use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
 use client::{
     self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
 };
+use collab_ui::channel_view::ChannelView;
 use db::kvp::KEY_VALUE_STORE;
-use editor::{scroll::autoscroll::Autoscroll, Editor};
-use futures::{
-    channel::{mpsc, oneshot},
-    FutureExt, SinkExt, StreamExt,
-};
+use editor::Editor;
+use futures::StreamExt;
 use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task};
 use isahc::{config::Configurable, Request};
-use language::{LanguageRegistry, Point};
+use language::LanguageRegistry;
 use log::LevelFilter;
 use node_runtime::RealNodeRuntime;
 use parking_lot::Mutex;
@@ -28,7 +23,6 @@ use settings::{default_settings, handle_settings_file_changes, watch_config_file
 use simplelog::ConfigBuilder;
 use smol::process::Command;
 use std::{
-    collections::HashMap,
     env,
     ffi::OsStr,
     fs::OpenOptions,
@@ -40,13 +34,11 @@ use std::{
         Arc, Weak,
     },
     thread,
-    time::{Duration, SystemTime, UNIX_EPOCH},
+    time::{SystemTime, UNIX_EPOCH},
 };
-use sum_tree::Bias;
 use util::{
     channel::{parse_zed_link, ReleaseChannel},
     http::{self, HttpClient},
-    paths::PathLikeWithPosition,
 };
 use uuid::Uuid;
 use welcome::{show_welcome_experience, FIRST_OPEN};
@@ -58,12 +50,9 @@ use zed::{
     assets::Assets,
     build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
     only_instance::{ensure_only_instance, IsOnlyInstance},
+    open_listener::{handle_cli_connection, OpenListener, OpenRequest},
 };
 
-use crate::open_listener::{OpenListener, OpenRequest};
-
-mod open_listener;
-
 fn main() {
     let http = http::client();
     init_paths();
@@ -113,6 +102,7 @@ fn main() {
 
     app.run(move |cx| {
         cx.set_global(*RELEASE_CHANNEL);
+        cx.set_global(listener.clone());
 
         let mut store = SettingsStore::default();
         store
@@ -202,6 +192,7 @@ fn main() {
         activity_indicator::init(cx);
         language_tools::init(cx);
         call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+        notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
         collab_ui::init(&app_state, cx);
         feedback::init(cx);
         welcome::init(cx);
@@ -250,6 +241,20 @@ fn main() {
                 })
                 .detach_and_log_err(cx)
             }
+            Ok(Some(OpenRequest::OpenChannelNotes { channel_id })) => {
+                triggered_authentication = true;
+                let app_state = app_state.clone();
+                let client = client.clone();
+                cx.spawn(|mut cx| async move {
+                    // ignore errors here, we'll show a generic "not signed in"
+                    let _ = authenticate(client, &cx).await;
+                    let workspace =
+                        workspace::get_any_active_workspace(app_state, cx.clone()).await?;
+                    cx.update(|cx| ChannelView::open(channel_id, workspace, cx))
+                        .await
+                })
+                .detach_and_log_err(cx)
+            }
             Ok(None) | Err(_) => cx
                 .spawn({
                     let app_state = app_state.clone();
@@ -264,8 +269,10 @@ fn main() {
                 while let Some(request) = open_rx.next().await {
                     match request {
                         OpenRequest::Paths { paths } => {
-                            cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
-                                .detach();
+                            cx.update(|cx| {
+                                workspace::open_paths(&paths, &app_state.clone(), None, cx)
+                            })
+                            .detach();
                         }
                         OpenRequest::CliConnection { connection } => {
                             cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
@@ -276,6 +283,16 @@ fn main() {
                                 workspace::join_channel(channel_id, app_state.clone(), None, cx)
                             })
                             .detach(),
+                        OpenRequest::OpenChannelNotes { channel_id } => {
+                            let app_state = app_state.clone();
+                            if let Ok(workspace) =
+                                workspace::get_any_active_workspace(app_state, cx.clone()).await
+                            {
+                                cx.update(|cx| {
+                                    ChannelView::open(channel_id, workspace, cx).detach();
+                                })
+                            }
+                        }
                     }
                 }
             }
@@ -667,7 +684,7 @@ fn load_embedded_fonts(app: &App) {
 #[cfg(debug_assertions)]
 async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
     let mut events = fs
-        .watch("styles/src".as_ref(), Duration::from_millis(100))
+        .watch("styles/src".as_ref(), std::time::Duration::from_millis(100))
         .await;
     while (events.next().await).is_some() {
         let output = Command::new("npm")
@@ -693,7 +710,7 @@ async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> O
     let mut events = fs
         .watch(
             "crates/zed/src/languages".as_ref(),
-            Duration::from_millis(100),
+            std::time::Duration::from_millis(100),
         )
         .await;
     while (events.next().await).is_some() {
@@ -708,7 +725,7 @@ fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {
         let mut events = fs
             .watch(
                 "assets/icons/file_icons/file_types.json".as_ref(),
-                Duration::from_millis(100),
+                std::time::Duration::from_millis(100),
             )
             .await;
         while (events.next().await).is_some() {
@@ -735,189 +752,6 @@ async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()>
 #[cfg(not(debug_assertions))]
 fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
 
-fn connect_to_cli(
-    server_name: &str,
-) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
-    let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
-        .context("error connecting to cli")?;
-    let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
-    let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
-
-    handshake_tx
-        .send(IpcHandshake {
-            requests: request_tx,
-            responses: response_rx,
-        })
-        .context("error sending ipc handshake")?;
-
-    let (mut async_request_tx, async_request_rx) =
-        futures::channel::mpsc::channel::<CliRequest>(16);
-    thread::spawn(move || {
-        while let Ok(cli_request) = request_rx.recv() {
-            if smol::block_on(async_request_tx.send(cli_request)).is_err() {
-                break;
-            }
-        }
-        Ok::<_, anyhow::Error>(())
-    });
-
-    Ok((async_request_rx, response_tx))
-}
-
-async fn handle_cli_connection(
-    (mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
-    app_state: Arc<AppState>,
-    mut cx: AsyncAppContext,
-) {
-    if let Some(request) = requests.next().await {
-        match request {
-            CliRequest::Open { paths, wait } => {
-                let mut caret_positions = HashMap::new();
-
-                let paths = if paths.is_empty() {
-                    workspace::last_opened_workspace_paths()
-                        .await
-                        .map(|location| location.paths().to_vec())
-                        .unwrap_or_default()
-                } else {
-                    paths
-                        .into_iter()
-                        .filter_map(|path_with_position_string| {
-                            let path_with_position = PathLikeWithPosition::parse_str(
-                                &path_with_position_string,
-                                |path_str| {
-                                    Ok::<_, std::convert::Infallible>(
-                                        Path::new(path_str).to_path_buf(),
-                                    )
-                                },
-                            )
-                            .expect("Infallible");
-                            let path = path_with_position.path_like;
-                            if let Some(row) = path_with_position.row {
-                                if path.is_file() {
-                                    let row = row.saturating_sub(1);
-                                    let col =
-                                        path_with_position.column.unwrap_or(0).saturating_sub(1);
-                                    caret_positions.insert(path.clone(), Point::new(row, col));
-                                }
-                            }
-                            Some(path)
-                        })
-                        .collect()
-                };
-
-                let mut errored = false;
-                match cx
-                    .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
-                    .await
-                {
-                    Ok((workspace, items)) => {
-                        let mut item_release_futures = Vec::new();
-
-                        for (item, path) in items.into_iter().zip(&paths) {
-                            match item {
-                                Some(Ok(item)) => {
-                                    if let Some(point) = caret_positions.remove(path) {
-                                        if let Some(active_editor) = item.downcast::<Editor>() {
-                                            active_editor
-                                                .downgrade()
-                                                .update(&mut cx, |editor, cx| {
-                                                    let snapshot =
-                                                        editor.snapshot(cx).display_snapshot;
-                                                    let point = snapshot
-                                                        .buffer_snapshot
-                                                        .clip_point(point, Bias::Left);
-                                                    editor.change_selections(
-                                                        Some(Autoscroll::center()),
-                                                        cx,
-                                                        |s| s.select_ranges([point..point]),
-                                                    );
-                                                })
-                                                .log_err();
-                                        }
-                                    }
-
-                                    let released = oneshot::channel();
-                                    cx.update(|cx| {
-                                        item.on_release(
-                                            cx,
-                                            Box::new(move |_| {
-                                                let _ = released.0.send(());
-                                            }),
-                                        )
-                                        .detach();
-                                    });
-                                    item_release_futures.push(released.1);
-                                }
-                                Some(Err(err)) => {
-                                    responses
-                                        .send(CliResponse::Stderr {
-                                            message: format!("error opening {:?}: {}", path, err),
-                                        })
-                                        .log_err();
-                                    errored = true;
-                                }
-                                None => {}
-                            }
-                        }
-
-                        if wait {
-                            let background = cx.background();
-                            let wait = async move {
-                                if paths.is_empty() {
-                                    let (done_tx, done_rx) = oneshot::channel();
-                                    if let Some(workspace) = workspace.upgrade(&cx) {
-                                        let _subscription = cx.update(|cx| {
-                                            cx.observe_release(&workspace, move |_, _| {
-                                                let _ = done_tx.send(());
-                                            })
-                                        });
-                                        drop(workspace);
-                                        let _ = done_rx.await;
-                                    }
-                                } else {
-                                    let _ =
-                                        futures::future::try_join_all(item_release_futures).await;
-                                };
-                            }
-                            .fuse();
-                            futures::pin_mut!(wait);
-
-                            loop {
-                                // Repeatedly check if CLI is still open to avoid wasting resources
-                                // waiting for files or workspaces to close.
-                                let mut timer = background.timer(Duration::from_secs(1)).fuse();
-                                futures::select_biased! {
-                                    _ = wait => break,
-                                    _ = timer => {
-                                        if responses.send(CliResponse::Ping).is_err() {
-                                            break;
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                    Err(error) => {
-                        errored = true;
-                        responses
-                            .send(CliResponse::Stderr {
-                                message: format!("error opening {:?}: {}", paths, error),
-                            })
-                            .log_err();
-                    }
-                }
-
-                responses
-                    .send(CliResponse::Exit {
-                        status: i32::from(errored),
-                    })
-                    .log_err();
-            }
-        }
-    }
-}
-
 pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
     &[
         ("Go to file", &file_finder::Toggle),

crates/zed/src/open_listener.rs 🔗

@@ -1,15 +1,26 @@
-use anyhow::anyhow;
+use anyhow::{anyhow, Context, Result};
+use cli::{ipc, IpcHandshake};
 use cli::{ipc::IpcSender, CliRequest, CliResponse};
-use futures::channel::mpsc;
+use editor::scroll::autoscroll::Autoscroll;
+use editor::Editor;
 use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
+use futures::channel::{mpsc, oneshot};
+use futures::{FutureExt, SinkExt, StreamExt};
+use gpui::AsyncAppContext;
+use language::{Bias, Point};
+use std::collections::HashMap;
 use std::ffi::OsStr;
 use std::os::unix::prelude::OsStrExt;
+use std::path::Path;
 use std::sync::atomic::Ordering;
+use std::sync::Arc;
+use std::thread;
+use std::time::Duration;
 use std::{path::PathBuf, sync::atomic::AtomicBool};
 use util::channel::parse_zed_link;
+use util::paths::PathLikeWithPosition;
 use util::ResultExt;
-
-use crate::connect_to_cli;
+use workspace::AppState;
 
 pub enum OpenRequest {
     Paths {
@@ -21,6 +32,9 @@ pub enum OpenRequest {
     JoinChannel {
         channel_id: u64,
     },
+    OpenChannelNotes {
+        channel_id: u64,
+    },
 }
 
 pub struct OpenListener {
@@ -74,7 +88,11 @@ impl OpenListener {
             if let Some(slug) = parts.next() {
                 if let Some(id_str) = slug.split("-").last() {
                     if let Ok(channel_id) = id_str.parse::<u64>() {
-                        return Some(OpenRequest::JoinChannel { channel_id });
+                        if Some("notes") == parts.next() {
+                            return Some(OpenRequest::OpenChannelNotes { channel_id });
+                        } else {
+                            return Some(OpenRequest::JoinChannel { channel_id });
+                        }
                     }
                 }
             }
@@ -96,3 +114,186 @@ impl OpenListener {
         Some(OpenRequest::Paths { paths })
     }
 }
+
+fn connect_to_cli(
+    server_name: &str,
+) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
+    let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
+        .context("error connecting to cli")?;
+    let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
+    let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
+
+    handshake_tx
+        .send(IpcHandshake {
+            requests: request_tx,
+            responses: response_rx,
+        })
+        .context("error sending ipc handshake")?;
+
+    let (mut async_request_tx, async_request_rx) =
+        futures::channel::mpsc::channel::<CliRequest>(16);
+    thread::spawn(move || {
+        while let Ok(cli_request) = request_rx.recv() {
+            if smol::block_on(async_request_tx.send(cli_request)).is_err() {
+                break;
+            }
+        }
+        Ok::<_, anyhow::Error>(())
+    });
+
+    Ok((async_request_rx, response_tx))
+}
+
+pub async fn handle_cli_connection(
+    (mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
+    app_state: Arc<AppState>,
+    mut cx: AsyncAppContext,
+) {
+    if let Some(request) = requests.next().await {
+        match request {
+            CliRequest::Open { paths, wait } => {
+                let mut caret_positions = HashMap::new();
+
+                let paths = if paths.is_empty() {
+                    workspace::last_opened_workspace_paths()
+                        .await
+                        .map(|location| location.paths().to_vec())
+                        .unwrap_or_default()
+                } else {
+                    paths
+                        .into_iter()
+                        .filter_map(|path_with_position_string| {
+                            let path_with_position = PathLikeWithPosition::parse_str(
+                                &path_with_position_string,
+                                |path_str| {
+                                    Ok::<_, std::convert::Infallible>(
+                                        Path::new(path_str).to_path_buf(),
+                                    )
+                                },
+                            )
+                            .expect("Infallible");
+                            let path = path_with_position.path_like;
+                            if let Some(row) = path_with_position.row {
+                                if path.is_file() {
+                                    let row = row.saturating_sub(1);
+                                    let col =
+                                        path_with_position.column.unwrap_or(0).saturating_sub(1);
+                                    caret_positions.insert(path.clone(), Point::new(row, col));
+                                }
+                            }
+                            Some(path)
+                        })
+                        .collect()
+                };
+
+                let mut errored = false;
+                match cx
+                    .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
+                    .await
+                {
+                    Ok((workspace, items)) => {
+                        let mut item_release_futures = Vec::new();
+
+                        for (item, path) in items.into_iter().zip(&paths) {
+                            match item {
+                                Some(Ok(item)) => {
+                                    if let Some(point) = caret_positions.remove(path) {
+                                        if let Some(active_editor) = item.downcast::<Editor>() {
+                                            active_editor
+                                                .downgrade()
+                                                .update(&mut cx, |editor, cx| {
+                                                    let snapshot =
+                                                        editor.snapshot(cx).display_snapshot;
+                                                    let point = snapshot
+                                                        .buffer_snapshot
+                                                        .clip_point(point, Bias::Left);
+                                                    editor.change_selections(
+                                                        Some(Autoscroll::center()),
+                                                        cx,
+                                                        |s| s.select_ranges([point..point]),
+                                                    );
+                                                })
+                                                .log_err();
+                                        }
+                                    }
+
+                                    let released = oneshot::channel();
+                                    cx.update(|cx| {
+                                        item.on_release(
+                                            cx,
+                                            Box::new(move |_| {
+                                                let _ = released.0.send(());
+                                            }),
+                                        )
+                                        .detach();
+                                    });
+                                    item_release_futures.push(released.1);
+                                }
+                                Some(Err(err)) => {
+                                    responses
+                                        .send(CliResponse::Stderr {
+                                            message: format!("error opening {:?}: {}", path, err),
+                                        })
+                                        .log_err();
+                                    errored = true;
+                                }
+                                None => {}
+                            }
+                        }
+
+                        if wait {
+                            let background = cx.background();
+                            let wait = async move {
+                                if paths.is_empty() {
+                                    let (done_tx, done_rx) = oneshot::channel();
+                                    if let Some(workspace) = workspace.upgrade(&cx) {
+                                        let _subscription = cx.update(|cx| {
+                                            cx.observe_release(&workspace, move |_, _| {
+                                                let _ = done_tx.send(());
+                                            })
+                                        });
+                                        drop(workspace);
+                                        let _ = done_rx.await;
+                                    }
+                                } else {
+                                    let _ =
+                                        futures::future::try_join_all(item_release_futures).await;
+                                };
+                            }
+                            .fuse();
+                            futures::pin_mut!(wait);
+
+                            loop {
+                                // Repeatedly check if CLI is still open to avoid wasting resources
+                                // waiting for files or workspaces to close.
+                                let mut timer = background.timer(Duration::from_secs(1)).fuse();
+                                futures::select_biased! {
+                                    _ = wait => break,
+                                    _ = timer => {
+                                        if responses.send(CliResponse::Ping).is_err() {
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    Err(error) => {
+                        errored = true;
+                        responses
+                            .send(CliResponse::Stderr {
+                                message: format!("error opening {:?}: {}", paths, error),
+                            })
+                            .log_err();
+                    }
+                }
+
+                responses
+                    .send(CliResponse::Exit {
+                        status: i32::from(errored),
+                    })
+                    .log_err();
+            }
+        }
+    }
+}

crates/zed/src/zed.rs 🔗

@@ -2,6 +2,7 @@ pub mod assets;
 pub mod languages;
 pub mod menus;
 pub mod only_instance;
+pub mod open_listener;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
@@ -28,6 +29,7 @@ use gpui::{
     AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
 };
 pub use lsp;
+use open_listener::OpenListener;
 pub use project;
 use project_panel::ProjectPanel;
 use quick_action_bar::QuickActionBar;
@@ -87,6 +89,10 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         },
     );
     cx.add_global_action(quit);
+    cx.add_global_action(move |action: &OpenZedURL, cx| {
+        cx.global::<Arc<OpenListener>>()
+            .open_urls(vec![action.url.clone()])
+    });
     cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
     cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
         theme::adjust_font_size(cx, |size| *size += 1.0)
@@ -221,6 +227,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
             workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
         },
     );
+    cx.add_action(
+        |workspace: &mut Workspace,
+         _: &collab_ui::notification_panel::ToggleFocus,
+         cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(cx);
+        },
+    );
     cx.add_action(
         |workspace: &mut Workspace,
          _: &terminal_panel::ToggleFocus,
@@ -275,9 +288,8 @@ pub fn initialize_workspace(
                                     QuickActionBar::new(buffer_search_bar, workspace)
                                 });
                                 toolbar.add_item(quick_action_bar, cx);
-                                let diagnostic_editor_controls = cx.add_view(|_| {
-                                    diagnostics::ToolbarControls::new()
-                                });
+                                let diagnostic_editor_controls =
+                                    cx.add_view(|_| diagnostics::ToolbarControls::new());
                                 toolbar.add_item(diagnostic_editor_controls, cx);
                                 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
                                 toolbar.add_item(project_search_bar, cx);
@@ -351,12 +363,24 @@ pub fn initialize_workspace(
             collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
         let chat_panel =
             collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
-        let (project_panel, terminal_panel, assistant_panel, channels_panel, chat_panel) = futures::try_join!(
+        let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
+            workspace_handle.clone(),
+            cx.clone(),
+        );
+        let (
+            project_panel,
+            terminal_panel,
+            assistant_panel,
+            channels_panel,
+            chat_panel,
+            notification_panel,
+        ) = futures::try_join!(
             project_panel,
             terminal_panel,
             assistant_panel,
             channels_panel,
             chat_panel,
+            notification_panel,
         )?;
         workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
@@ -377,6 +401,7 @@ pub fn initialize_workspace(
             workspace.add_panel(assistant_panel, cx);
             workspace.add_panel(channels_panel, cx);
             workspace.add_panel(chat_panel, cx);
+            workspace.add_panel(notification_panel, cx);
 
             if !was_deserialized
                 && workspace
@@ -2426,6 +2451,7 @@ mod tests {
             audio::init((), cx);
             channel::init(&app_state.client, app_state.user_store.clone(), cx);
             call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
             workspace::init(app_state.clone(), cx);
             Project::init_settings(cx);
             language::init(cx);

crates/zed2/Cargo.toml 🔗

@@ -43,7 +43,7 @@ fuzzy = { path = "../fuzzy" }
 # go_to_line = { path = "../go_to_line" }
 gpui2 = { path = "../gpui2" }
 install_cli = { path = "../install_cli" }
-# journal = { path = "../journal" }
+journal2 = { path = "../journal2" }
 language2 = { path = "../language2" }
 # language_selector = { path = "../language_selector" }
 lsp = { path = "../lsp" }

crates/zed2/build.rs 🔗

@@ -0,0 +1,24 @@
+fn main() {
+    println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
+
+    if let Ok(value) = std::env::var("ZED_PREVIEW_CHANNEL") {
+        println!("cargo:rustc-env=ZED_PREVIEW_CHANNEL={value}");
+    }
+
+    if std::env::var("ZED_BUNDLE").ok().as_deref() == Some("true") {
+        // Find WebRTC.framework in the Frameworks folder when running as part of an application bundle.
+        println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks");
+    } else {
+        // Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
+        println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
+    }
+
+    // Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+.
+    println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit");
+
+    // Seems to be required to enable Swift concurrency
+    println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
+
+    // Register exported Objective-C selectors, protocols, etc
+    println!("cargo:rustc-link-arg=-Wl,-ObjC");
+}

crates/zed2/src/main.rs 🔗

@@ -188,7 +188,7 @@ fn main() {
         // workspace::init(app_state.clone(), cx);
         // recent_projects::init(cx);
 
-        // journal::init(app_state.clone(), cx);
+        // journal2::init(app_state.clone(), cx);
         // language_selector::init(cx);
         // theme_selector::init(cx);
         // activity_indicator::init(cx);

script/bundle 🔗

@@ -134,6 +134,8 @@ else
     cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/"
 fi
 
+cp crates/zed/contents/$channel/embedded.provisionprofile "${app_path}/Contents/"
+
 if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
     echo "Signing bundle with Apple-issued certificate"
     security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo ""
@@ -143,14 +145,33 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR
     security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
     rm /tmp/zed-certificate.p12
     security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
-    /usr/bin/codesign --force --deep --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v
+
+    # sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514
+    /usr/bin/codesign --deep --force --timestamp --sign "Zed Industries, Inc." "${app_path}/Contents/Frameworks/WebRTC.framework" -v
+    /usr/bin/codesign --deep --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/cli" -v
+    /usr/bin/codesign --deep --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/zed" -v
+    /usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v
+
     security default-keychain -s login.keychain
 else
     echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_USERNAME, APPLE_NOTARIZATION_PASSWORD"
-    echo "Performing an ad-hoc signature, but this bundle should not be distributed"
-    echo "If you see 'The application cannot be opened for an unexpected reason,' you likely don't have the necessary entitlements to run the application in your signing keychain"
-    echo "You will need to download a new signing key from developer.apple.com, add it to keychain, and export MACOS_SIGNING_KEY=<email address of signing key>"
-    codesign --force --deep --entitlements crates/zed/resources/zed.entitlements --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v
+    if [[ "$local_only" = false ]]; then
+        echo "To create a self-signed local build use ./scripts/build.sh -ldf"
+        exit 1
+    fi
+
+    echo "====== WARNING ======"
+    echo "This bundle is being signed without all entitlements, some features (e.g. universal links) will not work"
+    echo "====== WARNING ======"
+
+    # NOTE: if you need to test universal links you have a few paths forward:
+    # - create a PR and tag it with the `run-build-dmg` label, and download the .dmg file from there.
+    # - get a signing key for the MQ55VZLNZQ team from Nathan.
+    # - create your own signing key, and update references to MQ55VZLNZQ to your own team ID
+    # then comment out this line.
+    cat crates/zed/resources/zed.entitlements | sed '/com.apple.developer.associated-domains/,+1d' > "${app_path}/Contents/Resources/zed.entitlements"
+
+    codesign --force --deep --entitlements "${app_path}/Contents/Resources/zed.entitlements" --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v
 fi
 
 if [[ "$target_dir" = "debug" && "$local_only" = false ]]; then

script/evaluate_semantic_index 🔗

@@ -1,3 +1,3 @@
 #!/bin/bash
 
-RUST_LOG=semantic_index=trace cargo run -p semantic_index --example eval --release
+RUST_LOG=semantic_index=trace cargo run --example semantic_index_eval --release

script/zed-local 🔗

@@ -55,6 +55,8 @@ let users = [
   'iamnbutler'
 ]
 
+const RUST_LOG = process.env.RUST_LOG || 'info'
+
 // If a user is specified, make sure it's first in the list
 const user = process.env.ZED_IMPERSONATE
 if (user) {
@@ -81,7 +83,9 @@ setTimeout(() => {
         ZED_ALWAYS_ACTIVE: '1',
         ZED_SERVER_URL: 'http://localhost:8080',
         ZED_ADMIN_API_TOKEN: 'secret',
-        ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`
+        ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`,
+        PATH: process.env.PATH,
+        RUST_LOG,
       }
     })
   }

styles/src/style_tree/app.ts 🔗

@@ -13,6 +13,7 @@ import project_shared_notification from "./project_shared_notification"
 import tooltip from "./tooltip"
 import terminal from "./terminal"
 import chat_panel from "./chat_panel"
+import notification_panel from "./notification_panel"
 import collab_panel from "./collab_panel"
 import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
 import incoming_call_notification from "./incoming_call_notification"
@@ -57,6 +58,7 @@ export default function app(): any {
         assistant: assistant(),
         feedback: feedback(),
         chat_panel: chat_panel(),
+        notification_panel: notification_panel(),
         component_test: component_test(),
     }
 }

styles/src/style_tree/assistant.ts 🔗

@@ -79,6 +79,80 @@ export default function assistant(): any {
                 },
             },
             pending_edit_background: background(theme.highest, "positive"),
+            context_status: {
+                error_icon: {
+                    margin: { left: 8, right: 18 },
+                    color: foreground(theme.highest, "negative"),
+                    width: 12,
+                },
+                in_progress_icon: {
+                    margin: { left: 8, right: 18 },
+                    color: foreground(theme.highest, "positive"),
+                    width: 12,
+                },
+                complete_icon: {
+                    margin: { left: 8, right: 18 },
+                    color: foreground(theme.highest, "positive"),
+                    width: 12,
+                }
+            },
+            retrieve_context: toggleable({
+                base: interactive({
+                    base: {
+                        icon_size: 12,
+                        color: foreground(theme.highest, "variant"),
+
+                        button_width: 12,
+                        background: background(theme.highest, "on"),
+                        corner_radius: 2,
+                        border: {
+                            width: 1., color: background(theme.highest, "on")
+                        },
+                        margin: { left: 2 },
+                        padding: {
+                            left: 4,
+                            right: 4,
+                            top: 4,
+                            bottom: 4,
+                        },
+                    },
+                    state: {
+                        hovered: {
+                            ...text(theme.highest, "mono", "variant", "hovered"),
+                            background: background(theme.highest, "on", "hovered"),
+                            border: {
+                                width: 1., color: background(theme.highest, "on", "hovered")
+                            },
+                        },
+                        clicked: {
+                            ...text(theme.highest, "mono", "variant", "pressed"),
+                            background: background(theme.highest, "on", "pressed"),
+                            border: {
+                                width: 1., color: background(theme.highest, "on", "pressed")
+                            },
+                        },
+                    },
+                }),
+                state: {
+                    active: {
+                        default: {
+                            icon_size: 12,
+                            button_width: 12,
+                            color: foreground(theme.highest, "variant"),
+                            background: background(theme.highest, "accent"),
+                            border: border(theme.highest, "accent"),
+                        },
+                        hovered: {
+                            background: background(theme.highest, "accent", "hovered"),
+                            border: border(theme.highest, "accent", "hovered"),
+                        },
+                        clicked: {
+                            background: background(theme.highest, "accent", "pressed"),
+                            border: border(theme.highest, "accent", "pressed"),
+                        },
+                    },
+                },
+            }),
             include_conversation: toggleable({
                 base: interactive({
                     base: {

styles/src/style_tree/chat_panel.ts 🔗

@@ -1,10 +1,6 @@
-import {
-    background,
-    border,
-    text,
-} from "./components"
+import { background, border, foreground, text } from "./components"
 import { icon_button } from "../component/icon_button"
-import { useTheme } from "../theme"
+import { useTheme, with_opacity } from "../theme"
 import { interactive } from "../element"
 
 export default function chat_panel(): any {
@@ -41,15 +37,13 @@ export default function chat_panel(): any {
                 left: 2,
                 top: 2,
                 bottom: 2,
-            }
-        },
-        list: {
-
+            },
         },
+        list: {},
         channel_select: {
             header: {
                 ...channel_name,
-                border: border(layer, { bottom: true })
+                border: border(layer, { bottom: true }),
             },
             item: channel_name,
             active_item: {
@@ -62,8 +56,8 @@ export default function chat_panel(): any {
             },
             menu: {
                 background: background(layer, "on"),
-                border: border(layer, { bottom: true })
-            }
+                border: border(layer, { bottom: true }),
+            },
         },
         icon_button: icon_button({
             variant: "ghost",
@@ -91,6 +85,21 @@ export default function chat_panel(): any {
                 top: 4,
             },
         },
+
+        rich_text: {
+            text: text(layer, "sans", "base"),
+            code_background: with_opacity(foreground(layer, "accent"), 0.1),
+            mention_highlight: { weight: "bold" },
+            self_mention_highlight: { weight: "bold" },
+            self_mention_background: background(layer, "active"),
+        },
+        message_sender: {
+            margin: {
+                right: 8,
+            },
+            ...text(layer, "sans", "base", { weight: "bold" }),
+        },
+        message_timestamp: text(layer, "sans", "base", "disabled"),
         message: {
             ...interactive({
                 base: {
@@ -100,7 +109,7 @@ export default function chat_panel(): any {
                         bottom: 4,
                         left: SPACING / 2,
                         right: SPACING / 3,
-                    }
+                    },
                 },
                 state: {
                     hovered: {
@@ -108,25 +117,9 @@ export default function chat_panel(): any {
                     },
                 },
             }),
-            body: text(layer, "sans", "base"),
-            sender: {
-                margin: {
-                    right: 8,
-                },
-                ...text(layer, "sans", "base", { weight: "bold" }),
-            },
-            timestamp: text(layer, "sans", "base", "disabled"),
         },
         last_message_bottom_spacing: SPACING,
         continuation_message: {
-            body: text(layer, "sans", "base"),
-            sender: {
-                margin: {
-                    right: 8,
-                },
-                ...text(layer, "sans", "base", { weight: "bold" }),
-            },
-            timestamp: text(layer, "sans", "base", "disabled"),
             ...interactive({
                 base: {
                     padding: {
@@ -134,7 +127,7 @@ export default function chat_panel(): any {
                         bottom: 4,
                         left: SPACING / 2,
                         right: SPACING / 3,
-                    }
+                    },
                 },
                 state: {
                     hovered: {
@@ -144,14 +137,6 @@ export default function chat_panel(): any {
             }),
         },
         pending_message: {
-            body: text(layer, "sans", "base"),
-            sender: {
-                margin: {
-                    right: 8,
-                },
-                ...text(layer, "sans", "base", "disabled"),
-            },
-            timestamp: text(layer, "sans", "base"),
             ...interactive({
                 base: {
                     padding: {
@@ -159,7 +144,7 @@ export default function chat_panel(): any {
                         bottom: 4,
                         left: SPACING / 2,
                         right: SPACING / 3,
-                    }
+                    },
                 },
                 state: {
                     hovered: {
@@ -170,6 +155,6 @@ export default function chat_panel(): any {
         },
         sign_in_prompt: {
             default: text(layer, "sans", "base"),
-        }
+        },
     }
 }

styles/src/style_tree/collab_modals.ts 🔗

@@ -1,10 +1,11 @@
-import { useTheme } from "../theme"
+import { StyleSets, useTheme } from "../theme"
 import { background, border, foreground, text } from "./components"
 import picker from "./picker"
 import { input } from "../component/input"
 import contact_finder from "./contact_finder"
 import { tab } from "../component/tab"
 import { icon_button } from "../component/icon_button"
+import { interactive } from "../element/interactive"
 
 export default function channel_modal(): any {
     const theme = useTheme()
@@ -27,6 +28,24 @@ export default function channel_modal(): any {
 
     const picker_input = input()
 
+    const interactive_text = (styleset: StyleSets) =>
+        interactive({
+            base: {
+                padding: {
+                    left: 8,
+                    top: 8
+                },
+                ...text(theme.middle, "sans", styleset, "default"),
+            }, state: {
+                hovered: {
+                    ...text(theme.middle, "sans", styleset, "hovered"),
+                },
+                clicked: {
+                    ...text(theme.middle, "sans", styleset, "active"),
+                }
+            }
+        })
+
     const member_icon_style = icon_button({
         variant: "ghost",
         size: "sm",
@@ -88,6 +107,8 @@ export default function channel_modal(): any {
                     left: BUTTON_OFFSET,
                 },
             },
+            visibility_toggle: interactive_text("base"),
+            channel_link: interactive_text("accent"),
             picker: {
                 empty_container: {},
                 item: {

styles/src/style_tree/collab_panel.ts 🔗

@@ -210,6 +210,14 @@ export default function contacts_panel(): any {
                 right: SPACING,
             },
         },
+        dragged_over_header: {
+            margin: { top: SPACING },
+            padding: {
+                left: SPACING,
+                right: SPACING,
+            },
+            background: background(layer, "hovered"),
+        },
         subheader_row,
         leave_call: interactive({
             base: {
@@ -279,7 +287,7 @@ export default function contacts_panel(): any {
                 margin: {
                     left: CHANNEL_SPACING,
                 },
-            }
+            },
         },
         list_empty_label_container: {
             margin: {

styles/src/style_tree/editor.ts 🔗

@@ -206,9 +206,13 @@ export default function editor(): any {
                 match_highlight: foreground(theme.middle, "accent", "active"),
                 background: background(theme.middle, "active"),
             },
-            server_name_container: { padding: { left: 40 } },
-            server_name_color: text(theme.middle, "sans", "disabled", {}).color,
-            server_name_size_percent: 0.75,
+            completion_min_width: 300,
+            completion_max_width: 700,
+            inline_docs_container: { padding: { left: 40 } },
+            inline_docs_color: text(theme.middle, "sans", "disabled", {}).color,
+            inline_docs_size_percent: 0.75,
+            alongside_docs_max_width: 700,
+            alongside_docs_container: { padding: autocomplete_item.padding }
         },
         diagnostic_header: {
             background: background(theme.middle),

styles/src/style_tree/notification_panel.ts 🔗

@@ -0,0 +1,75 @@
+import { background, border, text } from "./components"
+import { icon_button } from "../component/icon_button"
+import { useTheme, with_opacity } from "../theme"
+import { text_button } from "../component"
+
+export default function (): any {
+    const theme = useTheme()
+    const layer = theme.middle
+
+    const notification_text = {
+        padding: { top: 4, bottom: 4 },
+        ...text(layer, "sans", "base"),
+    }
+
+    const notification_read_text_color = with_opacity(
+        theme.middle.base.default.foreground,
+        0.6
+    )
+
+    return {
+        background: background(layer),
+        avatar: {
+            icon_width: 24,
+            icon_height: 24,
+            corner_radius: 12,
+            outer_width: 24,
+            outer_corner_radius: 24,
+        },
+        title: {
+            ...text(layer, "sans", "default"),
+            padding: { left: 8, right: 8 },
+            border: border(layer, { bottom: true }),
+        },
+        title_height: 32,
+        title_icon: {
+            asset: "icons/feedback.svg",
+            color: text(theme.lowest, "sans", "default").color,
+            dimensions: {
+                width: 16,
+                height: 16,
+            },
+        },
+        read_text: {
+            ...notification_text,
+            color: notification_read_text_color,
+        },
+        unread_text: notification_text,
+        button: text_button({
+            variant: "ghost",
+        }),
+        timestamp: text(layer, "sans", "base", "disabled"),
+        avatar_container: {
+            padding: {
+                right: 8,
+                left: 2,
+                top: 4,
+                bottom: 2,
+            },
+        },
+        list: {
+            padding: {
+                left: 8,
+                right: 8,
+            },
+        },
+        icon_button: icon_button({
+            variant: "ghost",
+            color: "variant",
+            size: "sm",
+        }),
+        sign_in_prompt: {
+            default: text(layer, "sans", "base"),
+        },
+    }
+}

styles/src/style_tree/search.ts 🔗

@@ -2,7 +2,6 @@ import { with_opacity } from "../theme/color"
 import { background, border, foreground, text } from "./components"
 import { interactive, toggleable } from "../element"
 import { useTheme } from "../theme"
-import { text_button } from "../component/text_button"
 
 const search_results = () => {
     const theme = useTheme()
@@ -36,7 +35,7 @@ export default function search(): any {
             left: 10,
             right: 4,
         },
-        margin: { right: SEARCH_ROW_SPACING }
+        margin: { right: SEARCH_ROW_SPACING },
     }
 
     const include_exclude_editor = {
@@ -378,7 +377,7 @@ export default function search(): any {
         modes_container: {
             padding: {
                 right: SEARCH_ROW_SPACING,
-            }
+            },
         },
         replace_icon: {
             icon: {