Merge branch 'main' into zed2-project-test

Max Brunsfeld and Marshall created

Co-authored-by: Marshall <marshall@zed.dev>

Change summary

.github/workflows/release_actions.yml                                               |    4 
Cargo.lock                                                                          |  193 
Cargo.toml                                                                          |    9 
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/Cargo.toml                                                                   |   38 
crates/ai/Cargo.toml                                                                |    4 
crates/ai/src/ai.rs                                                                 |    6 
crates/ai/src/auth.rs                                                               |   15 
crates/ai/src/completion.rs                                                         |  213 
crates/ai/src/embedding.rs                                                          |  312 
crates/ai/src/models.rs                                                             |   16 
crates/ai/src/prompts/base.rs                                                       |  330 
crates/ai/src/prompts/file_context.rs                                               |  164 
crates/ai/src/prompts/generate.rs                                                   |   99 
crates/ai/src/prompts/mod.rs                                                        |    5 
crates/ai/src/prompts/preamble.rs                                                   |   52 
crates/ai/src/prompts/repository_context.rs                                         |   94 
crates/ai/src/providers/mod.rs                                                      |    1 
crates/ai/src/providers/open_ai/completion.rs                                       |  298 
crates/ai/src/providers/open_ai/embedding.rs                                        |  306 
crates/ai/src/providers/open_ai/mod.rs                                              |    9 
crates/ai/src/providers/open_ai/model.rs                                            |   57 
crates/ai/src/providers/open_ai/new.rs                                              |   11 
crates/ai/src/test.rs                                                               |  191 
crates/ai2/Cargo.toml                                                               |   38 
crates/ai2/src/ai2.rs                                                               |    8 
crates/ai2/src/auth.rs                                                              |   17 
crates/ai2/src/completion.rs                                                        |   23 
crates/ai2/src/embedding.rs                                                         |  123 
crates/ai2/src/models.rs                                                            |   16 
crates/ai2/src/prompts/base.rs                                                      |  330 
crates/ai2/src/prompts/file_context.rs                                              |  164 
crates/ai2/src/prompts/generate.rs                                                  |   99 
crates/ai2/src/prompts/mod.rs                                                       |    5 
crates/ai2/src/prompts/preamble.rs                                                  |   52 
crates/ai2/src/prompts/repository_context.rs                                        |   98 
crates/ai2/src/providers/mod.rs                                                     |    1 
crates/ai2/src/providers/open_ai/completion.rs                                      |  306 
crates/ai2/src/providers/open_ai/embedding.rs                                       |  313 
crates/ai2/src/providers/open_ai/mod.rs                                             |    9 
crates/ai2/src/providers/open_ai/model.rs                                           |   57 
crates/ai2/src/providers/open_ai/new.rs                                             |   11 
crates/ai2/src/test.rs                                                              |  193 
crates/assistant/Cargo.toml                                                         |    9 
crates/assistant/src/assistant.rs                                                   |    2 
crates/assistant/src/assistant_panel.rs                                             |  614 
crates/assistant/src/codegen.rs                                                     |   83 
crates/assistant/src/prompts.rs                                                     |  132 
crates/call/src/call.rs                                                             |  107 
crates/call/src/room.rs                                                             |   76 
crates/call2/src/call2.rs                                                           |   50 
crates/call2/src/participant.rs                                                     |   10 
crates/call2/src/room.rs                                                            |  969 
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                                                       |   56 
crates/client2/src/telemetry.rs                                                     |   19 
crates/client2/src/test.rs                                                          |    6 
crates/client2/src/user.rs                                                          |   16 
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/copilot2/src/copilot2.rs                                                     |   32 
crates/db/src/db.rs                                                                 |    4 
crates/db2/src/db2.rs                                                               |    4 
crates/diagnostics/src/items.rs                                                     |    4 
crates/editor/Cargo.toml                                                            |    4 
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                                                         |  650 
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                                                             |  157 
crates/gpui2/src/app/async_context.rs                                               |   46 
crates/gpui2/src/app/entity_map.rs                                                  |  249 
crates/gpui2/src/app/model_context.rs                                               |  118 
crates/gpui2/src/app/test_context.rs                                                |   49 
crates/gpui2/src/element.rs                                                         |   32 
crates/gpui2/src/executor.rs                                                        |   23 
crates/gpui2/src/focusable.rs                                                       |   23 
crates/gpui2/src/gpui2.rs                                                           |  124 
crates/gpui2/src/interactive.rs                                                     |  166 
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/view.rs                                                            |  359 
crates/gpui2/src/window.rs                                                          |  439 
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/src/buffer.rs                                                      |    6 
crates/language2/src/buffer_tests.rs                                                |  327 
crates/language2/src/highlight_map.rs                                               |   60 
crates/language2/src/language2.rs                                                   |   50 
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/prod.rs                                                  |   18 
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/menu2/Cargo.toml                                                             |   12 
crates/menu2/src/menu2.rs                                                           |   25 
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/prettier/src/prettier_server.js                                              |    9 
crates/prettier2/src/prettier2.rs                                                   |  342 
crates/project/src/lsp_command.rs                                                   |   38 
crates/project/src/project.rs                                                       |  359 
crates/project/src/worktree.rs                                                      |   51 
crates/project2/Cargo.toml                                                          |    1 
crates/project2/src/lsp_command.rs                                                  |  158 
crates/project2/src/project2.rs                                                     |  299 
crates/project2/src/project_tests.rs                                                |    4 
crates/project2/src/terminals.rs                                                    |   12 
crates/project2/src/worktree.rs                                                     |   38 
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                                                    |    5 
crates/semantic_index/src/parsing.rs                                                |   33 
crates/semantic_index/src/semantic_index.rs                                         |   30 
crates/semantic_index/src/semantic_index_tests.rs                                   |   85 
crates/settings2/src/keymap_file.rs                                                 |    4 
crates/storybook2/src/stories.rs                                                    |    2 
crates/storybook2/src/stories/colors.rs                                             |   38 
crates/storybook2/src/stories/focus.rs                                              |  174 
crates/storybook2/src/stories/kitchen_sink.rs                                       |   23 
crates/storybook2/src/stories/scroll.rs                                             |   90 
crates/storybook2/src/stories/text.rs                                               |   31 
crates/storybook2/src/stories/z_index.rs                                            |   12 
crates/storybook2/src/story_selector.rs                                             |  160 
crates/storybook2/src/storybook2.rs                                                 |   38 
crates/storybook2/src/themes.rs                                                     |   30 
crates/storybook2/src/themes/rose_pine.rs                                           | 1686 
crates/terminal_view/src/terminal_view.rs                                           |    7 
crates/text/src/selection.rs                                                        |    9 
crates/theme/src/theme.rs                                                           |   59 
crates/theme2/src/default.rs                                                        | 2118 
crates/theme2/src/registry.rs                                                       |   49 
crates/theme2/src/scale.rs                                                          |  164 
crates/theme2/src/theme2.rs                                                         |   49 
crates/theme2/src/themes/andromeda.rs                                               |  130 
crates/theme2/src/themes/atelier_cave_dark.rs                                       |  136 
crates/theme2/src/themes/atelier_cave_light.rs                                      |  136 
crates/theme2/src/themes/atelier_dune_dark.rs                                       |  136 
crates/theme2/src/themes/atelier_dune_light.rs                                      |  136 
crates/theme2/src/themes/atelier_estuary_dark.rs                                    |  136 
crates/theme2/src/themes/atelier_estuary_light.rs                                   |  136 
crates/theme2/src/themes/atelier_forest_dark.rs                                     |  136 
crates/theme2/src/themes/atelier_forest_light.rs                                    |  136 
crates/theme2/src/themes/atelier_heath_dark.rs                                      |  136 
crates/theme2/src/themes/atelier_heath_light.rs                                     |  136 
crates/theme2/src/themes/atelier_lakeside_dark.rs                                   |  136 
crates/theme2/src/themes/atelier_lakeside_light.rs                                  |  136 
crates/theme2/src/themes/atelier_plateau_dark.rs                                    |  136 
crates/theme2/src/themes/atelier_plateau_light.rs                                   |  136 
crates/theme2/src/themes/atelier_savanna_dark.rs                                    |  136 
crates/theme2/src/themes/atelier_savanna_light.rs                                   |  136 
crates/theme2/src/themes/atelier_seaside_dark.rs                                    |  136 
crates/theme2/src/themes/atelier_seaside_light.rs                                   |  136 
crates/theme2/src/themes/atelier_sulphurpool_dark.rs                                |  136 
crates/theme2/src/themes/atelier_sulphurpool_light.rs                               |  136 
crates/theme2/src/themes/ayu_dark.rs                                                |  130 
crates/theme2/src/themes/ayu_light.rs                                               |  130 
crates/theme2/src/themes/ayu_mirage.rs                                              |  130 
crates/theme2/src/themes/gruvbox_dark.rs                                            |  131 
crates/theme2/src/themes/gruvbox_dark_hard.rs                                       |  131 
crates/theme2/src/themes/gruvbox_dark_soft.rs                                       |  131 
crates/theme2/src/themes/gruvbox_light.rs                                           |  131 
crates/theme2/src/themes/gruvbox_light_hard.rs                                      |  131 
crates/theme2/src/themes/gruvbox_light_soft.rs                                      |  131 
crates/theme2/src/themes/mod.rs                                                     |   72 
crates/theme2/src/themes/one_dark.rs                                                |   46 
crates/theme2/src/themes/one_light.rs                                               |  131 
crates/theme2/src/themes/rose_pine.rs                                               |  231 
crates/theme2/src/themes/rose_pine_dawn.rs                                          |  132 
crates/theme2/src/themes/rose_pine_moon.rs                                          |  132 
crates/theme2/src/themes/sandcastle.rs                                              |   45 
crates/theme2/src/themes/solarized_dark.rs                                          |  130 
crates/theme2/src/themes/solarized_light.rs                                         |  130 
crates/theme2/src/themes/summercamp.rs                                              |  130 
crates/theme_converter/Cargo.toml                                                   |    1 
crates/theme_converter/src/main.rs                                                  |  406 
crates/theme_converter/src/theme_printer.rs                                         |  174 
crates/ui2/Cargo.toml                                                               |    2 
crates/ui2/src/components/assistant_panel.rs                                        |   19 
crates/ui2/src/components/breadcrumb.rs                                             |   31 
crates/ui2/src/components/buffer.rs                                                 |   17 
crates/ui2/src/components/buffer_search.rs                                          |   10 
crates/ui2/src/components/chat_panel.rs                                             |   10 
crates/ui2/src/components/collab_panel.rs                                           |   13 
crates/ui2/src/components/command_palette.rs                                        |   11 
crates/ui2/src/components/context_menu.rs                                           |   13 
crates/ui2/src/components/copilot.rs                                                |   11 
crates/ui2/src/components/editor_pane.rs                                            |   15 
crates/ui2/src/components/facepile.rs                                               |   13 
crates/ui2/src/components/icon_button.rs                                            |    5 
crates/ui2/src/components/keybinding.rs                                             |   16 
crates/ui2/src/components/language_selector.rs                                      |   13 
crates/ui2/src/components/multi_buffer.rs                                           |   13 
crates/ui2/src/components/notifications_panel.rs                                    |   13 
crates/ui2/src/components/palette.rs                                                |  103 
crates/ui2/src/components/panel.rs                                                  |   15 
crates/ui2/src/components/panes.rs                                                  |   13 
crates/ui2/src/components/project_panel.rs                                          |   13 
crates/ui2/src/components/recent_projects.rs                                        |   13 
crates/ui2/src/components/tab.rs                                                    |   33 
crates/ui2/src/components/tab_bar.rs                                                |   13 
crates/ui2/src/components/terminal.rs                                               |   14 
crates/ui2/src/components/theme_selector.rs                                         |   11 
crates/ui2/src/components/title_bar.rs                                              |   31 
crates/ui2/src/components/toast.rs                                                  |   13 
crates/ui2/src/components/toolbar.rs                                                |   21 
crates/ui2/src/components/traffic_lights.rs                                         |   11 
crates/ui2/src/components/workspace.rs                                              |   46 
crates/ui2/src/elements/avatar.rs                                                   |   13 
crates/ui2/src/elements/button.rs                                                   |   19 
crates/ui2/src/elements/details.rs                                                  |   15 
crates/ui2/src/elements/icon.rs                                                     |   31 
crates/ui2/src/elements/input.rs                                                    |   13 
crates/ui2/src/elements/label.rs                                                    |   27 
crates/ui2/src/lib.rs                                                               |    2 
crates/ui2/src/prelude.rs                                                           |    3 
crates/ui2/src/static_data.rs                                                       |   48 
crates/ui2/src/theme.rs                                                             |  225 
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/workspace2/src/item.rs                                                       | 1096 
crates/workspace2/src/pane.rs                                                       | 2754 
crates/workspace2/src/pane_group.rs                                                 |  993 
crates/workspace2/src/persistence/model.rs                                          |  340 
crates/workspace2/src/workspace2.rs                                                 | 5535 
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                                          |    7 
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                                                  |    8 
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                                                              |    8 
crates/zed2/contents/dev/embedded.provisionprofile                                  |    0 
crates/zed2/contents/preview/embedded.provisionprofile                              |    0 
crates/zed2/contents/stable/embedded.provisionprofile                               |    0 
crates/zed2/src/languages.rs                                                        |  273 
crates/zed2/src/languages/bash/brackets.scm                                         |    3 
crates/zed2/src/languages/bash/config.toml                                          |    9 
crates/zed2/src/languages/bash/highlights.scm                                       |   59 
crates/zed2/src/languages/c.rs                                                      |  321 
crates/zed2/src/languages/c/brackets.scm                                            |    3 
crates/zed2/src/languages/c/config.toml                                             |   12 
crates/zed2/src/languages/c/embedding.scm                                           |   43 
crates/zed2/src/languages/c/highlights.scm                                          |  109 
crates/zed2/src/languages/c/indents.scm                                             |    9 
crates/zed2/src/languages/c/injections.scm                                          |    7 
crates/zed2/src/languages/c/outline.scm                                             |   70 
crates/zed2/src/languages/c/overrides.scm                                           |    2 
crates/zed2/src/languages/cpp/brackets.scm                                          |    3 
crates/zed2/src/languages/cpp/config.toml                                           |   12 
crates/zed2/src/languages/cpp/embedding.scm                                         |   61 
crates/zed2/src/languages/cpp/highlights.scm                                        |  158 
crates/zed2/src/languages/cpp/indents.scm                                           |    7 
crates/zed2/src/languages/cpp/injections.scm                                        |    7 
crates/zed2/src/languages/cpp/outline.scm                                           |  149 
crates/zed2/src/languages/cpp/overrides.scm                                         |    2 
crates/zed2/src/languages/css.rs                                                    |  130 
crates/zed2/src/languages/css/brackets.scm                                          |    3 
crates/zed2/src/languages/css/config.toml                                           |   13 
crates/zed2/src/languages/css/highlights.scm                                        |   78 
crates/zed2/src/languages/css/indents.scm                                           |    1 
crates/zed2/src/languages/css/overrides.scm                                         |    2 
crates/zed2/src/languages/elixir.rs                                                 |  546 
crates/zed2/src/languages/elixir/brackets.scm                                       |    5 
crates/zed2/src/languages/elixir/config.toml                                        |   16 
crates/zed2/src/languages/elixir/embedding.scm                                      |   27 
crates/zed2/src/languages/elixir/highlights.scm                                     |  153 
crates/zed2/src/languages/elixir/indents.scm                                        |    6 
crates/zed2/src/languages/elixir/injections.scm                                     |    7 
crates/zed2/src/languages/elixir/outline.scm                                        |   26 
crates/zed2/src/languages/elixir/overrides.scm                                      |    2 
crates/zed2/src/languages/elm/config.toml                                           |   11 
crates/zed2/src/languages/elm/highlights.scm                                        |   72 
crates/zed2/src/languages/elm/injections.scm                                        |    2 
crates/zed2/src/languages/elm/outline.scm                                           |   22 
crates/zed2/src/languages/erb/config.toml                                           |    8 
crates/zed2/src/languages/erb/highlights.scm                                        |   12 
crates/zed2/src/languages/erb/injections.scm                                        |    7 
crates/zed2/src/languages/glsl/config.toml                                          |    9 
crates/zed2/src/languages/glsl/highlights.scm                                       |  118 
crates/zed2/src/languages/go.rs                                                     |  464 
crates/zed2/src/languages/go/brackets.scm                                           |    3 
crates/zed2/src/languages/go/config.toml                                            |   12 
crates/zed2/src/languages/go/embedding.scm                                          |   24 
crates/zed2/src/languages/go/highlights.scm                                         |  107 
crates/zed2/src/languages/go/indents.scm                                            |    9 
crates/zed2/src/languages/go/outline.scm                                            |   43 
crates/zed2/src/languages/go/overrides.scm                                          |    6 
crates/zed2/src/languages/heex/config.toml                                          |   12 
crates/zed2/src/languages/heex/highlights.scm                                       |   57 
crates/zed2/src/languages/heex/injections.scm                                       |   13 
crates/zed2/src/languages/heex/overrides.scm                                        |    4 
crates/zed2/src/languages/html.rs                                                   |  130 
crates/zed2/src/languages/html/brackets.scm                                         |    2 
crates/zed2/src/languages/html/config.toml                                          |   14 
crates/zed2/src/languages/html/highlights.scm                                       |   15 
crates/zed2/src/languages/html/indents.scm                                          |    6 
crates/zed2/src/languages/html/injections.scm                                       |    7 
crates/zed2/src/languages/html/outline.scm                                          |    0 
crates/zed2/src/languages/html/overrides.scm                                        |    2 
crates/zed2/src/languages/javascript/brackets.scm                                   |    5 
crates/zed2/src/languages/javascript/config.toml                                    |   26 
crates/zed2/src/languages/javascript/contexts.scm                                   |    0 
crates/zed2/src/languages/javascript/embedding.scm                                  |   71 
crates/zed2/src/languages/javascript/highlights.scm                                 |  217 
crates/zed2/src/languages/javascript/indents.scm                                    |   15 
crates/zed2/src/languages/javascript/outline.scm                                    |   62 
crates/zed2/src/languages/javascript/overrides.scm                                  |   13 
crates/zed2/src/languages/json.rs                                                   |  184 
crates/zed2/src/languages/json/brackets.scm                                         |    3 
crates/zed2/src/languages/json/config.toml                                          |   10 
crates/zed2/src/languages/json/embedding.scm                                        |   14 
crates/zed2/src/languages/json/highlights.scm                                       |   21 
crates/zed2/src/languages/json/indents.scm                                          |    2 
crates/zed2/src/languages/json/outline.scm                                          |    2 
crates/zed2/src/languages/json/overrides.scm                                        |    1 
crates/zed2/src/languages/language_plugin.rs                                        |  168 
crates/zed2/src/languages/lua.rs                                                    |  135 
crates/zed2/src/languages/lua/brackets.scm                                          |    3 
crates/zed2/src/languages/lua/config.toml                                           |   10 
crates/zed2/src/languages/lua/embedding.scm                                         |   10 
crates/zed2/src/languages/lua/highlights.scm                                        |  198 
crates/zed2/src/languages/lua/indents.scm                                           |   10 
crates/zed2/src/languages/lua/outline.scm                                           |    3 
crates/zed2/src/languages/markdown/config.toml                                      |   11 
crates/zed2/src/languages/markdown/highlights.scm                                   |   24 
crates/zed2/src/languages/markdown/injections.scm                                   |    4 
crates/zed2/src/languages/nix/config.toml                                           |   11 
crates/zed2/src/languages/nix/highlights.scm                                        |   89 
crates/zed2/src/languages/nu/brackets.scm                                           |    4 
crates/zed2/src/languages/nu/config.toml                                            |    9 
crates/zed2/src/languages/nu/highlights.scm                                         |  302 
crates/zed2/src/languages/nu/indents.scm                                            |    3 
crates/zed2/src/languages/php.rs                                                    |  137 
crates/zed2/src/languages/php/config.toml                                           |   14 
crates/zed2/src/languages/php/embedding.scm                                         |   36 
crates/zed2/src/languages/php/highlights.scm                                        |  123 
crates/zed2/src/languages/php/injections.scm                                        |    3 
crates/zed2/src/languages/php/outline.scm                                           |   29 
crates/zed2/src/languages/php/tags.scm                                              |   40 
crates/zed2/src/languages/python.rs                                                 |  296 
crates/zed2/src/languages/python/brackets.scm                                       |    3 
crates/zed2/src/languages/python/config.toml                                        |   16 
crates/zed2/src/languages/python/embedding.scm                                      |    9 
crates/zed2/src/languages/python/highlights.scm                                     |  125 
crates/zed2/src/languages/python/indents.scm                                        |    3 
crates/zed2/src/languages/python/outline.scm                                        |    9 
crates/zed2/src/languages/python/overrides.scm                                      |    2 
crates/zed2/src/languages/racket/brackets.scm                                       |    3 
crates/zed2/src/languages/racket/config.toml                                        |    9 
crates/zed2/src/languages/racket/highlights.scm                                     |   34 
crates/zed2/src/languages/racket/indents.scm                                        |    3 
crates/zed2/src/languages/racket/outline.scm                                        |   10 
crates/zed2/src/languages/ruby.rs                                                   |  160 
crates/zed2/src/languages/ruby/brackets.scm                                         |   14 
crates/zed2/src/languages/ruby/config.toml                                          |   13 
crates/zed2/src/languages/ruby/embedding.scm                                        |   22 
crates/zed2/src/languages/ruby/highlights.scm                                       |  181 
crates/zed2/src/languages/ruby/indents.scm                                          |   17 
crates/zed2/src/languages/ruby/outline.scm                                          |   17 
crates/zed2/src/languages/ruby/overrides.scm                                        |    2 
crates/zed2/src/languages/rust.rs                                                   |  568 
crates/zed2/src/languages/rust/brackets.scm                                         |    6 
crates/zed2/src/languages/rust/config.toml                                          |   13 
crates/zed2/src/languages/rust/embedding.scm                                        |   32 
crates/zed2/src/languages/rust/highlights.scm                                       |  116 
crates/zed2/src/languages/rust/indents.scm                                          |   14 
crates/zed2/src/languages/rust/injections.scm                                       |    7 
crates/zed2/src/languages/rust/outline.scm                                          |   63 
crates/zed2/src/languages/rust/overrides.scm                                        |    8 
crates/zed2/src/languages/scheme/brackets.scm                                       |    3 
crates/zed2/src/languages/scheme/config.toml                                        |    9 
crates/zed2/src/languages/scheme/highlights.scm                                     |   28 
crates/zed2/src/languages/scheme/indents.scm                                        |    3 
crates/zed2/src/languages/scheme/outline.scm                                        |   10 
crates/zed2/src/languages/scheme/overrides.scm                                      |    6 
crates/zed2/src/languages/svelte.rs                                                 |  133 
crates/zed2/src/languages/svelte/config.toml                                        |   20 
crates/zed2/src/languages/svelte/folds.scm                                          |    9 
crates/zed2/src/languages/svelte/highlights.scm                                     |   42 
crates/zed2/src/languages/svelte/indents.scm                                        |    8 
crates/zed2/src/languages/svelte/injections.scm                                     |   28 
crates/zed2/src/languages/svelte/overrides.scm                                      |    7 
crates/zed2/src/languages/tailwind.rs                                               |  167 
crates/zed2/src/languages/toml/brackets.scm                                         |    3 
crates/zed2/src/languages/toml/config.toml                                          |   10 
crates/zed2/src/languages/toml/highlights.scm                                       |   37 
crates/zed2/src/languages/toml/indents.scm                                          |    0 
crates/zed2/src/languages/toml/outline.scm                                          |   15 
crates/zed2/src/languages/toml/overrides.scm                                        |    2 
crates/zed2/src/languages/tsx/brackets.scm                                          |    1 
crates/zed2/src/languages/tsx/config.toml                                           |   25 
crates/zed2/src/languages/tsx/embedding.scm                                         |   85 
crates/zed2/src/languages/tsx/highlights-jsx.scm                                    |    0 
crates/zed2/src/languages/tsx/highlights.scm                                        |    1 
crates/zed2/src/languages/tsx/indents.scm                                           |    1 
crates/zed2/src/languages/tsx/outline.scm                                           |    1 
crates/zed2/src/languages/tsx/overrides.scm                                         |   13 
crates/zed2/src/languages/typescript.rs                                             |  384 
crates/zed2/src/languages/typescript/brackets.scm                                   |    5 
crates/zed2/src/languages/typescript/config.toml                                    |   16 
crates/zed2/src/languages/typescript/embedding.scm                                  |   85 
crates/zed2/src/languages/typescript/highlights.scm                                 |  221 
crates/zed2/src/languages/typescript/indents.scm                                    |   15 
crates/zed2/src/languages/typescript/outline.scm                                    |   65 
crates/zed2/src/languages/typescript/overrides.scm                                  |    2 
crates/zed2/src/languages/vue.rs                                                    |  220 
crates/zed2/src/languages/vue/brackets.scm                                          |    2 
crates/zed2/src/languages/vue/config.toml                                           |   14 
crates/zed2/src/languages/vue/highlights.scm                                        |   15 
crates/zed2/src/languages/vue/injections.scm                                        |    7 
crates/zed2/src/languages/yaml.rs                                                   |  142 
crates/zed2/src/languages/yaml/brackets.scm                                         |    3 
crates/zed2/src/languages/yaml/config.toml                                          |   12 
crates/zed2/src/languages/yaml/highlights.scm                                       |   49 
crates/zed2/src/languages/yaml/outline.scm                                          |    1 
crates/zed2/src/main.rs                                                             |   12 
crates/zed2/src/zed2.rs                                                             |    5 
script/bundle                                                                       |   31 
script/evaluate_semantic_index                                                      |    2 
script/zed-2-progress-report.py                                                     |   27 
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 
607 files changed, 51,097 insertions(+), 13,645 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,34 @@ dependencies = [
  "rusqlite",
  "serde",
  "serde_json",
- "tiktoken-rs 0.5.4",
+ "tiktoken-rs",
+ "util",
+]
+
+[[package]]
+name = "ai2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "bincode",
+ "futures 0.3.28",
+ "gpui2",
+ "isahc",
+ "language2",
+ "lazy_static",
+ "log",
+ "matrixmultiply",
+ "ordered-float 2.10.0",
+ "parking_lot 0.11.2",
+ "parse_duration",
+ "postage",
+ "rand 0.8.5",
+ "regex",
+ "rusqlite",
+ "serde",
+ "serde_json",
+ "tiktoken-rs",
  "util",
 ]
 
@@ -309,6 +337,7 @@ dependencies = [
  "language",
  "log",
  "menu",
+ "multi_buffer",
  "ordered-float 2.10.0",
  "parking_lot 0.11.2",
  "project",
@@ -316,12 +345,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 +1603,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.24.0"
+version = "0.27.0"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1609,6 +1639,7 @@ dependencies = [
  "lsp",
  "nanoid",
  "node_runtime",
+ "notifications",
  "parking_lot 0.11.2",
  "pretty_assertions",
  "project",
@@ -1664,20 +1695,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 +1768,7 @@ dependencies = [
  "theme",
  "util",
  "workspace",
+ "zed-actions",
 ]
 
 [[package]]
@@ -1810,6 +1848,7 @@ dependencies = [
  "log",
  "lsp",
  "node_runtime",
+ "parking_lot 0.11.2",
  "rpc",
  "serde",
  "serde_derive",
@@ -2556,11 +2595,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 +4198,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 +4301,7 @@ dependencies = [
  "lsp",
  "parking_lot 0.11.2",
  "postage",
+ "pulldown-cmark",
  "rand 0.8.5",
  "regex",
  "rpc",
@@ -4764,6 +4822,13 @@ dependencies = [
  "gpui",
 ]
 
+[[package]]
+name = "menu2"
+version = "0.1.0"
+dependencies = [
+ "gpui2",
+]
+
 [[package]]
 name = "metal"
 version = "0.21.0"
@@ -4921,6 +4986,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"
@@ -5070,6 +5184,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"
@@ -5886,6 +6020,7 @@ dependencies = [
  "log",
  "lsp",
  "node_runtime",
+ "parking_lot 0.11.2",
  "serde",
  "serde_derive",
  "serde_json",
@@ -6831,8 +6966,10 @@ dependencies = [
  "rsa 0.4.0",
  "serde",
  "serde_derive",
+ "serde_json",
  "smol",
  "smol-timeout",
+ "strum",
  "tempdir",
  "tracing",
  "util",
@@ -7407,7 +7544,7 @@ dependencies = [
  "smol",
  "tempdir",
  "theme",
- "tiktoken-rs 0.5.4",
+ "tiktoken-rs",
  "tree-sitter",
  "tree-sitter-cpp",
  "tree-sitter-elixir",
@@ -7421,7 +7558,6 @@ dependencies = [
  "unindent",
  "util",
  "workspace",
- "zed",
 ]
 
 [[package]]
@@ -8638,6 +8774,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "clap 4.4.4",
+ "convert_case 0.6.0",
  "gpui2",
  "log",
  "rust-embed",
@@ -8713,21 +8850,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"
@@ -9148,8 +9270,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",
@@ -9388,6 +9510,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"
@@ -9469,10 +9600,8 @@ dependencies = [
  "itertools 0.11.0",
  "rand 0.8.5",
  "serde",
- "settings",
  "smallvec",
  "strum",
- "theme",
  "theme2",
 ]
 
@@ -9714,6 +9843,7 @@ name = "vcs_menu"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "fs",
  "fuzzy",
  "gpui",
  "picker",
@@ -10658,9 +10788,10 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.109.0"
+version = "0.111.0"
 dependencies = [
  "activity_indicator",
+ "ai",
  "anyhow",
  "assistant",
  "async-compression",
@@ -10712,6 +10843,7 @@ dependencies = [
  "log",
  "lsp",
  "node_runtime",
+ "notifications",
  "num_cpus",
  "outline",
  "parking_lot 0.11.2",
@@ -10773,6 +10905,7 @@ dependencies = [
  "tree-sitter-svelte",
  "tree-sitter-toml",
  "tree-sitter-typescript",
+ "tree-sitter-vue",
  "tree-sitter-yaml",
  "unindent",
  "url",
@@ -10790,12 +10923,14 @@ name = "zed-actions"
 version = "0.1.0"
 dependencies = [
  "gpui",
+ "serde",
 ]
 
 [[package]]
 name = "zed2"
 version = "0.109.0"
 dependencies = [
+ "ai2",
  "anyhow",
  "async-compression",
  "async-recursion 0.3.2",
@@ -10811,7 +10946,7 @@ dependencies = [
  "ctor",
  "db2",
  "env_logger 0.9.3",
- "feature_flags",
+ "feature_flags2",
  "fs2",
  "fsevent",
  "futures 0.3.28",
@@ -10822,12 +10957,13 @@ dependencies = [
  "indexmap 1.9.3",
  "install_cli",
  "isahc",
+ "journal2",
  "language2",
  "language_tools",
  "lazy_static",
  "libc",
  "log",
- "lsp",
+ "lsp2",
  "node_runtime",
  "num_cpus",
  "parking_lot 0.11.2",
@@ -10880,6 +11016,7 @@ dependencies = [
  "tree-sitter-svelte",
  "tree-sitter-toml",
  "tree-sitter-typescript",
+ "tree-sitter-vue",
  "tree-sitter-yaml",
  "unindent",
  "url",

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,10 @@ members = [
     "crates/lsp2",
     "crates/media",
     "crates/menu",
+    "crates/menu2",
+    "crates/multi_buffer",
     "crates/node_runtime",
+    "crates/notifications",
     "crates/outline",
     "crates/picker",
     "crates/plugin",
@@ -133,6 +137,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 +149,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 +175,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/Cargo.toml 🔗

@@ -0,0 +1,38 @@
+[package]
+name = "ai"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/ai.rs"
+doctest = false
+
+[features]
+test-support = []
+
+[dependencies]
+gpui = { path = "../gpui" }
+util = { path = "../util" }
+language = { path = "../language" }
+async-trait.workspace = true
+anyhow.workspace = true
+futures.workspace = true
+lazy_static.workspace = true
+ordered-float.workspace = true
+parking_lot.workspace = true
+isahc.workspace = true
+regex.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+postage.workspace = true
+rand.workspace = true
+log.workspace = true
+parse_duration = "2.1.1"
+tiktoken-rs = "0.5.0"
+matrixmultiply = "0.3.7"
+rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
+bincode = "1.3.3"
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }

crates/ai/Cargo.toml 🔗

@@ -8,9 +8,13 @@ publish = false
 path = "src/ai.rs"
 doctest = false
 
+[features]
+test-support = []
+
 [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,8 @@
+pub mod auth;
 pub mod completion;
 pub mod embedding;
+pub mod models;
+pub mod prompts;
+pub mod providers;
+#[cfg(any(test, feature = "test-support"))]
+pub mod test;

crates/ai/src/auth.rs 🔗

@@ -0,0 +1,15 @@
+use gpui::AppContext;
+
+#[derive(Clone, Debug)]
+pub enum ProviderCredential {
+    Credentials { api_key: String },
+    NoCredentials,
+    NotNeeded,
+}
+
+pub trait CredentialProvider: Send + Sync {
+    fn has_credentials(&self) -> bool;
+    fn retrieve_credentials(&self, cx: &AppContext) -> ProviderCredential;
+    fn save_credentials(&self, cx: &AppContext, credential: ProviderCredential);
+    fn delete_credentials(&self, cx: &AppContext);
+}

crates/ai/src/completion.rs 🔗

@@ -1,212 +1,23 @@
-use anyhow::{anyhow, Result};
-use futures::{
-    future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt,
-    Stream, StreamExt,
-};
-use gpui::executor::Background;
-use isahc::{http::StatusCode, Request, RequestExt};
-use serde::{Deserialize, Serialize};
-use std::{
-    fmt::{self, Display},
-    io,
-    sync::Arc,
-};
+use anyhow::Result;
+use futures::{future::BoxFuture, stream::BoxStream};
 
-pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
+use crate::{auth::CredentialProvider, models::LanguageModel};
 
-#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
-#[serde(rename_all = "lowercase")]
-pub enum Role {
-    User,
-    Assistant,
-    System,
+pub trait CompletionRequest: Send + Sync {
+    fn data(&self) -> serde_json::Result<String>;
 }
 
-impl Role {
-    pub fn cycle(&mut self) {
-        *self = match self {
-            Role::User => Role::Assistant,
-            Role::Assistant => Role::System,
-            Role::System => Role::User,
-        }
-    }
-}
-
-impl Display for Role {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Role::User => write!(f, "User"),
-            Role::Assistant => write!(f, "Assistant"),
-            Role::System => write!(f, "System"),
-        }
-    }
-}
-
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
-pub struct RequestMessage {
-    pub role: Role,
-    pub content: String,
-}
-
-#[derive(Debug, Default, Serialize)]
-pub struct OpenAIRequest {
-    pub model: String,
-    pub messages: Vec<RequestMessage>,
-    pub stream: bool,
-}
-
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
-pub struct ResponseMessage {
-    pub role: Option<Role>,
-    pub content: Option<String>,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct OpenAIUsage {
-    pub prompt_tokens: u32,
-    pub completion_tokens: u32,
-    pub total_tokens: u32,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct ChatChoiceDelta {
-    pub index: u32,
-    pub delta: ResponseMessage,
-    pub finish_reason: Option<String>,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct OpenAIResponseStreamEvent {
-    pub id: Option<String>,
-    pub object: String,
-    pub created: u32,
-    pub model: String,
-    pub choices: Vec<ChatChoiceDelta>,
-    pub usage: Option<OpenAIUsage>,
-}
-
-pub async fn stream_completion(
-    api_key: String,
-    executor: Arc<Background>,
-    mut request: OpenAIRequest,
-) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
-    request.stream = true;
-
-    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
-
-    let json_data = serde_json::to_string(&request)?;
-    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
-        .header("Content-Type", "application/json")
-        .header("Authorization", format!("Bearer {}", api_key))
-        .body(json_data)?
-        .send_async()
-        .await?;
-
-    let status = response.status();
-    if status == StatusCode::OK {
-        executor
-            .spawn(async move {
-                let mut lines = BufReader::new(response.body_mut()).lines();
-
-                fn parse_line(
-                    line: Result<String, io::Error>,
-                ) -> Result<Option<OpenAIResponseStreamEvent>> {
-                    if let Some(data) = line?.strip_prefix("data: ") {
-                        let event = serde_json::from_str(&data)?;
-                        Ok(Some(event))
-                    } else {
-                        Ok(None)
-                    }
-                }
-
-                while let Some(line) = lines.next().await {
-                    if let Some(event) = parse_line(line).transpose() {
-                        let done = event.as_ref().map_or(false, |event| {
-                            event
-                                .choices
-                                .last()
-                                .map_or(false, |choice| choice.finish_reason.is_some())
-                        });
-                        if tx.unbounded_send(event).is_err() {
-                            break;
-                        }
-
-                        if done {
-                            break;
-                        }
-                    }
-                }
-
-                anyhow::Ok(())
-            })
-            .detach();
-
-        Ok(rx)
-    } else {
-        let mut body = String::new();
-        response.body_mut().read_to_string(&mut body).await?;
-
-        #[derive(Deserialize)]
-        struct OpenAIResponse {
-            error: OpenAIError,
-        }
-
-        #[derive(Deserialize)]
-        struct OpenAIError {
-            message: String,
-        }
-
-        match serde_json::from_str::<OpenAIResponse>(&body) {
-            Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
-                "Failed to connect to OpenAI API: {}",
-                response.error.message,
-            )),
-
-            _ => Err(anyhow!(
-                "Failed to connect to OpenAI API: {} {}",
-                response.status(),
-                body,
-            )),
-        }
-    }
-}
-
-pub trait CompletionProvider {
+pub trait CompletionProvider: CredentialProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel>;
     fn complete(
         &self,
-        prompt: OpenAIRequest,
+        prompt: Box<dyn CompletionRequest>,
     ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
+    fn box_clone(&self) -> Box<dyn CompletionProvider>;
 }
 
-pub struct OpenAICompletionProvider {
-    api_key: String,
-    executor: Arc<Background>,
-}
-
-impl OpenAICompletionProvider {
-    pub fn new(api_key: String, executor: Arc<Background>) -> Self {
-        Self { api_key, executor }
-    }
-}
-
-impl CompletionProvider for OpenAICompletionProvider {
-    fn complete(
-        &self,
-        prompt: OpenAIRequest,
-    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
-        let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
-        async move {
-            let response = request.await?;
-            let stream = response
-                .filter_map(|response| async move {
-                    match response {
-                        Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
-                        Err(error) => Some(Err(error)),
-                    }
-                })
-                .boxed();
-            Ok(stream)
-        }
-        .boxed()
+impl Clone for Box<dyn CompletionProvider> {
+    fn clone(&self) -> Box<dyn CompletionProvider> {
+        self.box_clone()
     }
 }

crates/ai/src/embedding.rs 🔗

@@ -1,30 +1,13 @@
-use anyhow::{anyhow, Result};
+use std::time::Instant;
+
+use anyhow::Result;
 use async_trait::async_trait;
-use futures::AsyncReadExt;
-use gpui::executor::Background;
-use gpui::serde_json;
-use isahc::http::StatusCode;
-use isahc::prelude::Configurable;
-use isahc::{AsyncBody, Response};
-use lazy_static::lazy_static;
 use ordered_float::OrderedFloat;
-use parking_lot::Mutex;
-use parse_duration::parse;
-use postage::watch;
 use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
 use rusqlite::ToSql;
-use serde::{Deserialize, Serialize};
-use std::env;
-use std::ops::Add;
-use std::sync::Arc;
-use std::time::{Duration, Instant};
-use tiktoken_rs::{cl100k_base, CoreBPE};
-use util::http::{HttpClient, Request};
 
-lazy_static! {
-    static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
-    static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap();
-}
+use crate::auth::CredentialProvider;
+use crate::models::LanguageModel;
 
 #[derive(Debug, PartialEq, Clone)]
 pub struct Embedding(pub Vec<f32>);
@@ -85,295 +68,14 @@ 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>,
-    pub executor: Arc<Background>,
-    rate_limit_count_rx: watch::Receiver<Option<Instant>>,
-    rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
-}
-
-#[derive(Serialize)]
-struct OpenAIEmbeddingRequest<'a> {
-    model: &'static str,
-    input: Vec<&'a str>,
-}
-
-#[derive(Deserialize)]
-struct OpenAIEmbeddingResponse {
-    data: Vec<OpenAIEmbedding>,
-    usage: OpenAIEmbeddingUsage,
-}
-
-#[derive(Debug, Deserialize)]
-struct OpenAIEmbedding {
-    embedding: Vec<f32>,
-    index: usize,
-    object: String,
-}
-
-#[derive(Deserialize)]
-struct OpenAIEmbeddingUsage {
-    prompt_tokens: usize,
-    total_tokens: usize,
-}
-
 #[async_trait]
-pub trait EmbeddingProvider: Sync + Send {
-    fn is_authenticated(&self) -> bool;
+pub trait EmbeddingProvider: CredentialProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel>;
     async fn embed_batch(&self, spans: Vec<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>;
 }
 
-pub struct DummyEmbeddings {}
-
-#[async_trait]
-impl EmbeddingProvider for DummyEmbeddings {
-    fn is_authenticated(&self) -> bool {
-        true
-    }
-    fn rate_limit_expiration(&self) -> Option<Instant> {
-        None
-    }
-    async fn embed_batch(&self, spans: Vec<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]);
-        return Ok(vec![dummy_vec; spans.len()]);
-    }
-
-    fn max_tokens_per_batch(&self) -> usize {
-        OPENAI_INPUT_LIMIT
-    }
-
-    fn truncate(&self, span: &str) -> (String, usize) {
-        let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span);
-        let token_count = tokens.len();
-        let output = if token_count > OPENAI_INPUT_LIMIT {
-            tokens.truncate(OPENAI_INPUT_LIMIT);
-            let new_input = OPENAI_BPE_TOKENIZER.decode(tokens.clone());
-            new_input.ok().unwrap_or_else(|| span.to_string())
-        } else {
-            span.to_string()
-        };
-
-        (output, tokens.len())
-    }
-}
-
-const OPENAI_INPUT_LIMIT: usize = 8190;
-
-impl OpenAIEmbeddings {
-    pub fn new(client: Arc<dyn HttpClient>, executor: Arc<Background>) -> Self {
-        let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
-        let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
-
-        OpenAIEmbeddings {
-            client,
-            executor,
-            rate_limit_count_rx,
-            rate_limit_count_tx,
-        }
-    }
-
-    fn resolve_rate_limit(&self) {
-        let reset_time = *self.rate_limit_count_tx.lock().borrow();
-
-        if let Some(reset_time) = reset_time {
-            if Instant::now() >= reset_time {
-                *self.rate_limit_count_tx.lock().borrow_mut() = None
-            }
-        }
-
-        log::trace!(
-            "resolving reset time: {:?}",
-            *self.rate_limit_count_tx.lock().borrow()
-        );
-    }
-
-    fn update_reset_time(&self, reset_time: Instant) {
-        let original_time = *self.rate_limit_count_tx.lock().borrow();
-
-        let updated_time = if let Some(original_time) = original_time {
-            if reset_time < original_time {
-                Some(reset_time)
-            } else {
-                Some(original_time)
-            }
-        } else {
-            Some(reset_time)
-        };
-
-        log::trace!("updating rate limit time: {:?}", updated_time);
-
-        *self.rate_limit_count_tx.lock().borrow_mut() = updated_time;
-    }
-    async fn send_request(
-        &self,
-        api_key: &str,
-        spans: Vec<&str>,
-        request_timeout: u64,
-    ) -> Result<Response<AsyncBody>> {
-        let request = Request::post("https://api.openai.com/v1/embeddings")
-            .redirect_policy(isahc::config::RedirectPolicy::Follow)
-            .timeout(Duration::from_secs(request_timeout))
-            .header("Content-Type", "application/json")
-            .header("Authorization", format!("Bearer {}", api_key))
-            .body(
-                serde_json::to_string(&OpenAIEmbeddingRequest {
-                    input: spans.clone(),
-                    model: "text-embedding-ada-002",
-                })
-                .unwrap()
-                .into(),
-            )?;
-
-        Ok(self.client.send(request).await?)
-    }
-}
-
-#[async_trait]
-impl EmbeddingProvider for OpenAIEmbeddings {
-    fn is_authenticated(&self) -> bool {
-        OPENAI_API_KEY.as_ref().is_some()
-    }
-    fn max_tokens_per_batch(&self) -> usize {
-        50000
-    }
-
-    fn rate_limit_expiration(&self) -> Option<Instant> {
-        *self.rate_limit_count_rx.borrow()
-    }
-    fn truncate(&self, span: &str) -> (String, usize) {
-        let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span);
-        let output = if tokens.len() > OPENAI_INPUT_LIMIT {
-            tokens.truncate(OPENAI_INPUT_LIMIT);
-            OPENAI_BPE_TOKENIZER
-                .decode(tokens.clone())
-                .ok()
-                .unwrap_or_else(|| span.to_string())
-        } else {
-            span.to_string()
-        };
-
-        (output, tokens.len())
-    }
-
-    async fn embed_batch(&self, spans: Vec<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 mut request_number = 0;
-        let mut rate_limiting = false;
-        let mut request_timeout: u64 = 15;
-        let mut response: Response<AsyncBody>;
-        while request_number < MAX_RETRIES {
-            response = self
-                .send_request(
-                    api_key,
-                    spans.iter().map(|x| &**x).collect(),
-                    request_timeout,
-                )
-                .await?;
-            request_number += 1;
-
-            match response.status() {
-                StatusCode::REQUEST_TIMEOUT => {
-                    request_timeout += 5;
-                }
-                StatusCode::OK => {
-                    let mut body = String::new();
-                    response.body_mut().read_to_string(&mut body).await?;
-                    let response: OpenAIEmbeddingResponse = serde_json::from_str(&body)?;
-
-                    log::trace!(
-                        "openai embedding completed. tokens: {:?}",
-                        response.usage.total_tokens
-                    );
-
-                    // If we complete a request successfully that was previously rate_limited
-                    // resolve the rate limit
-                    if rate_limiting {
-                        self.resolve_rate_limit()
-                    }
-
-                    return Ok(response
-                        .data
-                        .into_iter()
-                        .map(|embedding| Embedding::from(embedding.embedding))
-                        .collect());
-                }
-                StatusCode::TOO_MANY_REQUESTS => {
-                    rate_limiting = true;
-                    let mut body = String::new();
-                    response.body_mut().read_to_string(&mut body).await?;
-
-                    let delay_duration = {
-                        let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64);
-                        if let Some(time_to_reset) =
-                            response.headers().get("x-ratelimit-reset-tokens")
-                        {
-                            if let Ok(time_str) = time_to_reset.to_str() {
-                                parse(time_str).unwrap_or(delay)
-                            } else {
-                                delay
-                            }
-                        } else {
-                            delay
-                        }
-                    };
-
-                    // If we've previously rate limited, increment the duration but not the count
-                    let reset_time = Instant::now().add(delay_duration);
-                    self.update_reset_time(reset_time);
-
-                    log::trace!(
-                        "openai rate limiting: waiting {:?} until lifted",
-                        &delay_duration
-                    );
-
-                    self.executor.timer(delay_duration).await;
-                }
-                _ => {
-                    let mut body = String::new();
-                    response.body_mut().read_to_string(&mut body).await?;
-                    return Err(anyhow!(
-                        "open ai bad request: {:?} {:?}",
-                        &response.status(),
-                        body
-                    ));
-                }
-            }
-        }
-        Err(anyhow!("openai max retries"))
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

crates/ai/src/models.rs 🔗

@@ -0,0 +1,16 @@
+pub enum TruncationDirection {
+    Start,
+    End,
+}
+
+pub trait LanguageModel {
+    fn name(&self) -> String;
+    fn count_tokens(&self, content: &str) -> anyhow::Result<usize>;
+    fn truncate(
+        &self,
+        content: &str,
+        length: usize,
+        direction: TruncationDirection,
+    ) -> anyhow::Result<String>;
+    fn capacity(&self) -> anyhow::Result<usize>;
+}

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

@@ -0,0 +1,330 @@
+use std::cmp::Reverse;
+use std::ops::Range;
+use std::sync::Arc;
+
+use language::BufferSnapshot;
+use util::ResultExt;
+
+use crate::models::LanguageModel;
+use crate::prompts::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 crate::models::TruncationDirection;
+    use crate::test::FakeLanguageModel;
+
+    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,
+                            TruncationDirection::End,
+                        )?;
+                        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,
+                            TruncationDirection::End,
+                        )?;
+                        token_count = max_token_length;
+                    }
+                }
+
+                anyhow::Ok((content, token_count))
+            }
+        }
+
+        let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel { 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(FakeLanguageModel { 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(FakeLanguageModel { 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(FakeLanguageModel { 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/prompts/file_context.rs 🔗

@@ -0,0 +1,164 @@
+use anyhow::anyhow;
+use language::BufferSnapshot;
+use language::ToOffset;
+
+use crate::models::LanguageModel;
+use crate::models::TruncationDirection;
+use crate::prompts::base::PromptArguments;
+use crate::prompts::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_window, start_goal_tokens, TruncationDirection::Start)?;
+                let truncated_end_window =
+                    model.truncate(&end_window, end_goal_tokens, TruncationDirection::End)?;
+                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, TruncationDirection::End)?;
+                }
+            }
+        }
+    }
+
+    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, TruncationDirection::End)?;
+            }
+
+            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/prompts/generate.rs 🔗

@@ -0,0 +1,99 @@
+use crate::prompts::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,
+                crate::models::TruncationDirection::End,
+            )?;
+        }
+
+        let token_count = args.model.count_tokens(&prompt)?;
+
+        anyhow::Ok((prompt, token_count))
+    }
+}

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

@@ -0,0 +1,52 @@
+use crate::prompts::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/prompts/repository_context.rs 🔗

@@ -0,0 +1,94 @@
+use crate::prompts::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/ai/src/providers/open_ai/completion.rs 🔗

@@ -0,0 +1,298 @@
+use anyhow::{anyhow, Result};
+use futures::{
+    future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt,
+    Stream, StreamExt,
+};
+use gpui::{executor::Background, AppContext};
+use isahc::{http::StatusCode, Request, RequestExt};
+use parking_lot::RwLock;
+use serde::{Deserialize, Serialize};
+use std::{
+    env,
+    fmt::{self, Display},
+    io,
+    sync::Arc,
+};
+use util::ResultExt;
+
+use crate::{
+    auth::{CredentialProvider, ProviderCredential},
+    completion::{CompletionProvider, CompletionRequest},
+    models::LanguageModel,
+};
+
+use crate::providers::open_ai::{OpenAILanguageModel, OPENAI_API_URL};
+
+#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum Role {
+    User,
+    Assistant,
+    System,
+}
+
+impl Role {
+    pub fn cycle(&mut self) {
+        *self = match self {
+            Role::User => Role::Assistant,
+            Role::Assistant => Role::System,
+            Role::System => Role::User,
+        }
+    }
+}
+
+impl Display for Role {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Role::User => write!(f, "User"),
+            Role::Assistant => write!(f, "Assistant"),
+            Role::System => write!(f, "System"),
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+pub struct RequestMessage {
+    pub role: Role,
+    pub content: String,
+}
+
+#[derive(Debug, Default, Serialize)]
+pub struct OpenAIRequest {
+    pub model: String,
+    pub messages: Vec<RequestMessage>,
+    pub stream: bool,
+    pub stop: Vec<String>,
+    pub temperature: f32,
+}
+
+impl CompletionRequest for OpenAIRequest {
+    fn data(&self) -> serde_json::Result<String> {
+        serde_json::to_string(self)
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+pub struct ResponseMessage {
+    pub role: Option<Role>,
+    pub content: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OpenAIUsage {
+    pub prompt_tokens: u32,
+    pub completion_tokens: u32,
+    pub total_tokens: u32,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ChatChoiceDelta {
+    pub index: u32,
+    pub delta: ResponseMessage,
+    pub finish_reason: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OpenAIResponseStreamEvent {
+    pub id: Option<String>,
+    pub object: String,
+    pub created: u32,
+    pub model: String,
+    pub choices: Vec<ChatChoiceDelta>,
+    pub usage: Option<OpenAIUsage>,
+}
+
+pub async fn stream_completion(
+    credential: ProviderCredential,
+    executor: Arc<Background>,
+    request: Box<dyn CompletionRequest>,
+) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
+    let api_key = match credential {
+        ProviderCredential::Credentials { api_key } => api_key,
+        _ => {
+            return Err(anyhow!("no credentials provider for completion"));
+        }
+    };
+
+    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
+
+    let json_data = request.data()?;
+    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
+        .header("Content-Type", "application/json")
+        .header("Authorization", format!("Bearer {}", api_key))
+        .body(json_data)?
+        .send_async()
+        .await?;
+
+    let status = response.status();
+    if status == StatusCode::OK {
+        executor
+            .spawn(async move {
+                let mut lines = BufReader::new(response.body_mut()).lines();
+
+                fn parse_line(
+                    line: Result<String, io::Error>,
+                ) -> Result<Option<OpenAIResponseStreamEvent>> {
+                    if let Some(data) = line?.strip_prefix("data: ") {
+                        let event = serde_json::from_str(&data)?;
+                        Ok(Some(event))
+                    } else {
+                        Ok(None)
+                    }
+                }
+
+                while let Some(line) = lines.next().await {
+                    if let Some(event) = parse_line(line).transpose() {
+                        let done = event.as_ref().map_or(false, |event| {
+                            event
+                                .choices
+                                .last()
+                                .map_or(false, |choice| choice.finish_reason.is_some())
+                        });
+                        if tx.unbounded_send(event).is_err() {
+                            break;
+                        }
+
+                        if done {
+                            break;
+                        }
+                    }
+                }
+
+                anyhow::Ok(())
+            })
+            .detach();
+
+        Ok(rx)
+    } else {
+        let mut body = String::new();
+        response.body_mut().read_to_string(&mut body).await?;
+
+        #[derive(Deserialize)]
+        struct OpenAIResponse {
+            error: OpenAIError,
+        }
+
+        #[derive(Deserialize)]
+        struct OpenAIError {
+            message: String,
+        }
+
+        match serde_json::from_str::<OpenAIResponse>(&body) {
+            Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
+                "Failed to connect to OpenAI API: {}",
+                response.error.message,
+            )),
+
+            _ => Err(anyhow!(
+                "Failed to connect to OpenAI API: {} {}",
+                response.status(),
+                body,
+            )),
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct OpenAICompletionProvider {
+    model: OpenAILanguageModel,
+    credential: Arc<RwLock<ProviderCredential>>,
+    executor: Arc<Background>,
+}
+
+impl OpenAICompletionProvider {
+    pub fn new(model_name: &str, executor: Arc<Background>) -> Self {
+        let model = OpenAILanguageModel::load(model_name);
+        let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
+        Self {
+            model,
+            credential,
+            executor,
+        }
+    }
+}
+
+impl CredentialProvider for OpenAICompletionProvider {
+    fn has_credentials(&self) -> bool {
+        match *self.credential.read() {
+            ProviderCredential::Credentials { .. } => true,
+            _ => false,
+        }
+    }
+    fn retrieve_credentials(&self, cx: &AppContext) -> ProviderCredential {
+        let mut credential = self.credential.write();
+        match *credential {
+            ProviderCredential::Credentials { .. } => {
+                return credential.clone();
+            }
+            _ => {
+                if let Ok(api_key) = env::var("OPENAI_API_KEY") {
+                    *credential = ProviderCredential::Credentials { api_key };
+                } else if let Some((_, api_key)) = cx
+                    .platform()
+                    .read_credentials(OPENAI_API_URL)
+                    .log_err()
+                    .flatten()
+                {
+                    if let Some(api_key) = String::from_utf8(api_key).log_err() {
+                        *credential = ProviderCredential::Credentials { api_key };
+                    }
+                } else {
+                };
+            }
+        }
+
+        credential.clone()
+    }
+
+    fn save_credentials(&self, cx: &AppContext, credential: ProviderCredential) {
+        match credential.clone() {
+            ProviderCredential::Credentials { api_key } => {
+                cx.platform()
+                    .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
+                    .log_err();
+            }
+            _ => {}
+        }
+
+        *self.credential.write() = credential;
+    }
+    fn delete_credentials(&self, cx: &AppContext) {
+        cx.platform().delete_credentials(OPENAI_API_URL).log_err();
+        *self.credential.write() = ProviderCredential::NoCredentials;
+    }
+}
+
+impl CompletionProvider for OpenAICompletionProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel> {
+        let model: Box<dyn LanguageModel> = Box::new(self.model.clone());
+        model
+    }
+    fn complete(
+        &self,
+        prompt: Box<dyn CompletionRequest>,
+    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
+        // Currently the CompletionRequest for OpenAI, includes a 'model' parameter
+        // This means that the model is determined by the CompletionRequest and not the CompletionProvider,
+        // which is currently model based, due to the langauge model.
+        // At some point in the future we should rectify this.
+        let credential = self.credential.read().clone();
+        let request = stream_completion(credential, self.executor.clone(), prompt);
+        async move {
+            let response = request.await?;
+            let stream = response
+                .filter_map(|response| async move {
+                    match response {
+                        Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
+                        Err(error) => Some(Err(error)),
+                    }
+                })
+                .boxed();
+            Ok(stream)
+        }
+        .boxed()
+    }
+    fn box_clone(&self) -> Box<dyn CompletionProvider> {
+        Box::new((*self).clone())
+    }
+}

crates/ai/src/providers/open_ai/embedding.rs 🔗

@@ -0,0 +1,306 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::AsyncReadExt;
+use gpui::executor::Background;
+use gpui::{serde_json, AppContext};
+use isahc::http::StatusCode;
+use isahc::prelude::Configurable;
+use isahc::{AsyncBody, Response};
+use lazy_static::lazy_static;
+use parking_lot::{Mutex, RwLock};
+use parse_duration::parse;
+use postage::watch;
+use serde::{Deserialize, Serialize};
+use std::env;
+use std::ops::Add;
+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::auth::{CredentialProvider, ProviderCredential};
+use crate::embedding::{Embedding, EmbeddingProvider};
+use crate::models::LanguageModel;
+use crate::providers::open_ai::OpenAILanguageModel;
+
+use crate::providers::open_ai::OPENAI_API_URL;
+
+lazy_static! {
+    static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap();
+}
+
+#[derive(Clone)]
+pub struct OpenAIEmbeddingProvider {
+    model: OpenAILanguageModel,
+    credential: Arc<RwLock<ProviderCredential>>,
+    pub client: Arc<dyn HttpClient>,
+    pub executor: Arc<Background>,
+    rate_limit_count_rx: watch::Receiver<Option<Instant>>,
+    rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
+}
+
+#[derive(Serialize)]
+struct OpenAIEmbeddingRequest<'a> {
+    model: &'static str,
+    input: Vec<&'a str>,
+}
+
+#[derive(Deserialize)]
+struct OpenAIEmbeddingResponse {
+    data: Vec<OpenAIEmbedding>,
+    usage: OpenAIEmbeddingUsage,
+}
+
+#[derive(Debug, Deserialize)]
+struct OpenAIEmbedding {
+    embedding: Vec<f32>,
+    index: usize,
+    object: String,
+}
+
+#[derive(Deserialize)]
+struct OpenAIEmbeddingUsage {
+    prompt_tokens: usize,
+    total_tokens: usize,
+}
+
+impl OpenAIEmbeddingProvider {
+    pub fn new(client: Arc<dyn HttpClient>, executor: Arc<Background>) -> Self {
+        let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
+        let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
+
+        let model = OpenAILanguageModel::load("text-embedding-ada-002");
+        let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
+
+        OpenAIEmbeddingProvider {
+            model,
+            credential,
+            client,
+            executor,
+            rate_limit_count_rx,
+            rate_limit_count_tx,
+        }
+    }
+
+    fn get_api_key(&self) -> Result<String> {
+        match self.credential.read().clone() {
+            ProviderCredential::Credentials { api_key } => Ok(api_key),
+            _ => Err(anyhow!("api credentials not provided")),
+        }
+    }
+
+    fn resolve_rate_limit(&self) {
+        let reset_time = *self.rate_limit_count_tx.lock().borrow();
+
+        if let Some(reset_time) = reset_time {
+            if Instant::now() >= reset_time {
+                *self.rate_limit_count_tx.lock().borrow_mut() = None
+            }
+        }
+
+        log::trace!(
+            "resolving reset time: {:?}",
+            *self.rate_limit_count_tx.lock().borrow()
+        );
+    }
+
+    fn update_reset_time(&self, reset_time: Instant) {
+        let original_time = *self.rate_limit_count_tx.lock().borrow();
+
+        let updated_time = if let Some(original_time) = original_time {
+            if reset_time < original_time {
+                Some(reset_time)
+            } else {
+                Some(original_time)
+            }
+        } else {
+            Some(reset_time)
+        };
+
+        log::trace!("updating rate limit time: {:?}", updated_time);
+
+        *self.rate_limit_count_tx.lock().borrow_mut() = updated_time;
+    }
+    async fn send_request(
+        &self,
+        api_key: &str,
+        spans: Vec<&str>,
+        request_timeout: u64,
+    ) -> Result<Response<AsyncBody>> {
+        let request = Request::post("https://api.openai.com/v1/embeddings")
+            .redirect_policy(isahc::config::RedirectPolicy::Follow)
+            .timeout(Duration::from_secs(request_timeout))
+            .header("Content-Type", "application/json")
+            .header("Authorization", format!("Bearer {}", api_key))
+            .body(
+                serde_json::to_string(&OpenAIEmbeddingRequest {
+                    input: spans.clone(),
+                    model: "text-embedding-ada-002",
+                })
+                .unwrap()
+                .into(),
+            )?;
+
+        Ok(self.client.send(request).await?)
+    }
+}
+
+impl CredentialProvider for OpenAIEmbeddingProvider {
+    fn has_credentials(&self) -> bool {
+        match *self.credential.read() {
+            ProviderCredential::Credentials { .. } => true,
+            _ => false,
+        }
+    }
+    fn retrieve_credentials(&self, cx: &AppContext) -> ProviderCredential {
+        let mut credential = self.credential.write();
+        match *credential {
+            ProviderCredential::Credentials { .. } => {
+                return credential.clone();
+            }
+            _ => {
+                if let Ok(api_key) = env::var("OPENAI_API_KEY") {
+                    *credential = ProviderCredential::Credentials { api_key };
+                } else if let Some((_, api_key)) = cx
+                    .platform()
+                    .read_credentials(OPENAI_API_URL)
+                    .log_err()
+                    .flatten()
+                {
+                    if let Some(api_key) = String::from_utf8(api_key).log_err() {
+                        *credential = ProviderCredential::Credentials { api_key };
+                    }
+                } else {
+                };
+            }
+        }
+
+        credential.clone()
+    }
+
+    fn save_credentials(&self, cx: &AppContext, credential: ProviderCredential) {
+        match credential.clone() {
+            ProviderCredential::Credentials { api_key } => {
+                cx.platform()
+                    .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
+                    .log_err();
+            }
+            _ => {}
+        }
+
+        *self.credential.write() = credential;
+    }
+    fn delete_credentials(&self, cx: &AppContext) {
+        cx.platform().delete_credentials(OPENAI_API_URL).log_err();
+        *self.credential.write() = ProviderCredential::NoCredentials;
+    }
+}
+
+#[async_trait]
+impl EmbeddingProvider for OpenAIEmbeddingProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel> {
+        let model: Box<dyn LanguageModel> = Box::new(self.model.clone());
+        model
+    }
+
+    fn max_tokens_per_batch(&self) -> usize {
+        50000
+    }
+
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        *self.rate_limit_count_rx.borrow()
+    }
+
+    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
+        const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
+        const MAX_RETRIES: usize = 4;
+
+        let api_key = self.get_api_key()?;
+
+        let mut request_number = 0;
+        let mut rate_limiting = false;
+        let mut request_timeout: u64 = 15;
+        let mut response: Response<AsyncBody>;
+        while request_number < MAX_RETRIES {
+            response = self
+                .send_request(
+                    &api_key,
+                    spans.iter().map(|x| &**x).collect(),
+                    request_timeout,
+                )
+                .await?;
+
+            request_number += 1;
+
+            match response.status() {
+                StatusCode::REQUEST_TIMEOUT => {
+                    request_timeout += 5;
+                }
+                StatusCode::OK => {
+                    let mut body = String::new();
+                    response.body_mut().read_to_string(&mut body).await?;
+                    let response: OpenAIEmbeddingResponse = serde_json::from_str(&body)?;
+
+                    log::trace!(
+                        "openai embedding completed. tokens: {:?}",
+                        response.usage.total_tokens
+                    );
+
+                    // If we complete a request successfully that was previously rate_limited
+                    // resolve the rate limit
+                    if rate_limiting {
+                        self.resolve_rate_limit()
+                    }
+
+                    return Ok(response
+                        .data
+                        .into_iter()
+                        .map(|embedding| Embedding::from(embedding.embedding))
+                        .collect());
+                }
+                StatusCode::TOO_MANY_REQUESTS => {
+                    rate_limiting = true;
+                    let mut body = String::new();
+                    response.body_mut().read_to_string(&mut body).await?;
+
+                    let delay_duration = {
+                        let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64);
+                        if let Some(time_to_reset) =
+                            response.headers().get("x-ratelimit-reset-tokens")
+                        {
+                            if let Ok(time_str) = time_to_reset.to_str() {
+                                parse(time_str).unwrap_or(delay)
+                            } else {
+                                delay
+                            }
+                        } else {
+                            delay
+                        }
+                    };
+
+                    // If we've previously rate limited, increment the duration but not the count
+                    let reset_time = Instant::now().add(delay_duration);
+                    self.update_reset_time(reset_time);
+
+                    log::trace!(
+                        "openai rate limiting: waiting {:?} until lifted",
+                        &delay_duration
+                    );
+
+                    self.executor.timer(delay_duration).await;
+                }
+                _ => {
+                    let mut body = String::new();
+                    response.body_mut().read_to_string(&mut body).await?;
+                    return Err(anyhow!(
+                        "open ai bad request: {:?} {:?}",
+                        &response.status(),
+                        body
+                    ));
+                }
+            }
+        }
+        Err(anyhow!("openai max retries"))
+    }
+}

crates/ai/src/providers/open_ai/mod.rs 🔗

@@ -0,0 +1,9 @@
+pub mod completion;
+pub mod embedding;
+pub mod model;
+
+pub use completion::*;
+pub use embedding::*;
+pub use model::OpenAILanguageModel;
+
+pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";

crates/ai/src/providers/open_ai/model.rs 🔗

@@ -0,0 +1,57 @@
+use anyhow::anyhow;
+use tiktoken_rs::CoreBPE;
+use util::ResultExt;
+
+use crate::models::{LanguageModel, TruncationDirection};
+
+#[derive(Clone)]
+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,
+        direction: TruncationDirection,
+    ) -> anyhow::Result<String> {
+        if let Some(bpe) = &self.bpe {
+            let tokens = bpe.encode_with_special_tokens(content);
+            if tokens.len() > length {
+                match direction {
+                    TruncationDirection::End => bpe.decode(tokens[..length].to_vec()),
+                    TruncationDirection::Start => 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/providers/open_ai/new.rs 🔗

@@ -0,0 +1,11 @@
+pub trait LanguageModel {
+    fn name(&self) -> String;
+    fn count_tokens(&self, content: &str) -> anyhow::Result<usize>;
+    fn truncate(
+        &self,
+        content: &str,
+        length: usize,
+        direction: TruncationDirection,
+    ) -> anyhow::Result<String>;
+    fn capacity(&self) -> anyhow::Result<usize>;
+}

crates/ai/src/test.rs 🔗

@@ -0,0 +1,191 @@
+use std::{
+    sync::atomic::{self, AtomicUsize, Ordering},
+    time::Instant,
+};
+
+use async_trait::async_trait;
+use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
+use gpui::AppContext;
+use parking_lot::Mutex;
+
+use crate::{
+    auth::{CredentialProvider, ProviderCredential},
+    completion::{CompletionProvider, CompletionRequest},
+    embedding::{Embedding, EmbeddingProvider},
+    models::{LanguageModel, TruncationDirection},
+};
+
+#[derive(Clone)]
+pub struct FakeLanguageModel {
+    pub capacity: usize,
+}
+
+impl LanguageModel for FakeLanguageModel {
+    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,
+        direction: TruncationDirection,
+    ) -> anyhow::Result<String> {
+        println!("TRYING TO TRUNCATE: {:?}", length.clone());
+
+        if length > self.count_tokens(content)? {
+            println!("NOT TRUNCATING");
+            return anyhow::Ok(content.to_string());
+        }
+
+        anyhow::Ok(match direction {
+            TruncationDirection::End => content.chars().collect::<Vec<char>>()[..length]
+                .into_iter()
+                .collect::<String>(),
+            TruncationDirection::Start => content.chars().collect::<Vec<char>>()[length..]
+                .into_iter()
+                .collect::<String>(),
+        })
+    }
+    fn capacity(&self) -> anyhow::Result<usize> {
+        anyhow::Ok(self.capacity)
+    }
+}
+
+pub struct FakeEmbeddingProvider {
+    pub embedding_count: AtomicUsize,
+}
+
+impl Clone for FakeEmbeddingProvider {
+    fn clone(&self) -> Self {
+        FakeEmbeddingProvider {
+            embedding_count: AtomicUsize::new(self.embedding_count.load(Ordering::SeqCst)),
+        }
+    }
+}
+
+impl Default for FakeEmbeddingProvider {
+    fn default() -> Self {
+        FakeEmbeddingProvider {
+            embedding_count: AtomicUsize::default(),
+        }
+    }
+}
+
+impl FakeEmbeddingProvider {
+    pub fn embedding_count(&self) -> usize {
+        self.embedding_count.load(atomic::Ordering::SeqCst)
+    }
+
+    pub fn embed_sync(&self, span: &str) -> Embedding {
+        let mut result = vec![1.0; 26];
+        for letter in span.chars() {
+            let letter = letter.to_ascii_lowercase();
+            if letter as u32 >= 'a' as u32 {
+                let ix = (letter as u32) - ('a' as u32);
+                if ix < 26 {
+                    result[ix as usize] += 1.0;
+                }
+            }
+        }
+
+        let norm = result.iter().map(|x| x * x).sum::<f32>().sqrt();
+        for x in &mut result {
+            *x /= norm;
+        }
+
+        result.into()
+    }
+}
+
+impl CredentialProvider for FakeEmbeddingProvider {
+    fn has_credentials(&self) -> bool {
+        true
+    }
+    fn retrieve_credentials(&self, _cx: &AppContext) -> ProviderCredential {
+        ProviderCredential::NotNeeded
+    }
+    fn save_credentials(&self, _cx: &AppContext, _credential: ProviderCredential) {}
+    fn delete_credentials(&self, _cx: &AppContext) {}
+}
+
+#[async_trait]
+impl EmbeddingProvider for FakeEmbeddingProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel> {
+        Box::new(FakeLanguageModel { capacity: 1000 })
+    }
+    fn max_tokens_per_batch(&self) -> usize {
+        1000
+    }
+
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        None
+    }
+
+    async fn embed_batch(&self, spans: Vec<String>) -> anyhow::Result<Vec<Embedding>> {
+        self.embedding_count
+            .fetch_add(spans.len(), atomic::Ordering::SeqCst);
+
+        anyhow::Ok(spans.iter().map(|span| self.embed_sync(span)).collect())
+    }
+}
+
+pub struct FakeCompletionProvider {
+    last_completion_tx: Mutex<Option<mpsc::Sender<String>>>,
+}
+
+impl Clone for FakeCompletionProvider {
+    fn clone(&self) -> Self {
+        Self {
+            last_completion_tx: Mutex::new(None),
+        }
+    }
+}
+
+impl FakeCompletionProvider {
+    pub fn new() -> Self {
+        Self {
+            last_completion_tx: Mutex::new(None),
+        }
+    }
+
+    pub fn send_completion(&self, completion: impl Into<String>) {
+        let mut tx = self.last_completion_tx.lock();
+        tx.as_mut().unwrap().try_send(completion.into()).unwrap();
+    }
+
+    pub fn finish_completion(&self) {
+        self.last_completion_tx.lock().take().unwrap();
+    }
+}
+
+impl CredentialProvider for FakeCompletionProvider {
+    fn has_credentials(&self) -> bool {
+        true
+    }
+    fn retrieve_credentials(&self, _cx: &AppContext) -> ProviderCredential {
+        ProviderCredential::NotNeeded
+    }
+    fn save_credentials(&self, _cx: &AppContext, _credential: ProviderCredential) {}
+    fn delete_credentials(&self, _cx: &AppContext) {}
+}
+
+impl CompletionProvider for FakeCompletionProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel> {
+        let model: Box<dyn LanguageModel> = Box::new(FakeLanguageModel { capacity: 8190 });
+        model
+    }
+    fn complete(
+        &self,
+        _prompt: Box<dyn CompletionRequest>,
+    ) -> BoxFuture<'static, anyhow::Result<BoxStream<'static, anyhow::Result<String>>>> {
+        let (tx, rx) = mpsc::channel(1);
+        *self.last_completion_tx.lock() = Some(tx);
+        async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed()
+    }
+    fn box_clone(&self) -> Box<dyn CompletionProvider> {
+        Box::new((*self).clone())
+    }
+}

crates/ai2/Cargo.toml 🔗

@@ -0,0 +1,38 @@
+[package]
+name = "ai2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/ai2.rs"
+doctest = false
+
+[features]
+test-support = []
+
+[dependencies]
+gpui2 = { path = "../gpui2" }
+util = { path = "../util" }
+language2 = { path = "../language2" }
+async-trait.workspace = true
+anyhow.workspace = true
+futures.workspace = true
+lazy_static.workspace = true
+ordered-float.workspace = true
+parking_lot.workspace = true
+isahc.workspace = true
+regex.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+postage.workspace = true
+rand.workspace = true
+log.workspace = true
+parse_duration = "2.1.1"
+tiktoken-rs = "0.5.0"
+matrixmultiply = "0.3.7"
+rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
+bincode = "1.3.3"
+
+[dev-dependencies]
+gpui2 = { path = "../gpui2", features = ["test-support"] }

crates/ai2/src/ai2.rs 🔗

@@ -0,0 +1,8 @@
+pub mod auth;
+pub mod completion;
+pub mod embedding;
+pub mod models;
+pub mod prompts;
+pub mod providers;
+#[cfg(any(test, feature = "test-support"))]
+pub mod test;

crates/ai2/src/auth.rs 🔗

@@ -0,0 +1,17 @@
+use async_trait::async_trait;
+use gpui2::AppContext;
+
+#[derive(Clone, Debug)]
+pub enum ProviderCredential {
+    Credentials { api_key: String },
+    NoCredentials,
+    NotNeeded,
+}
+
+#[async_trait]
+pub trait CredentialProvider: Send + Sync {
+    fn has_credentials(&self) -> bool;
+    async fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential;
+    async fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential);
+    async fn delete_credentials(&self, cx: &mut AppContext);
+}

crates/ai2/src/completion.rs 🔗

@@ -0,0 +1,23 @@
+use anyhow::Result;
+use futures::{future::BoxFuture, stream::BoxStream};
+
+use crate::{auth::CredentialProvider, models::LanguageModel};
+
+pub trait CompletionRequest: Send + Sync {
+    fn data(&self) -> serde_json::Result<String>;
+}
+
+pub trait CompletionProvider: CredentialProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel>;
+    fn complete(
+        &self,
+        prompt: Box<dyn CompletionRequest>,
+    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
+    fn box_clone(&self) -> Box<dyn CompletionProvider>;
+}
+
+impl Clone for Box<dyn CompletionProvider> {
+    fn clone(&self) -> Box<dyn CompletionProvider> {
+        self.box_clone()
+    }
+}

crates/ai2/src/embedding.rs 🔗

@@ -0,0 +1,123 @@
+use std::time::Instant;
+
+use anyhow::Result;
+use async_trait::async_trait;
+use ordered_float::OrderedFloat;
+use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
+use rusqlite::ToSql;
+
+use crate::auth::CredentialProvider;
+use crate::models::LanguageModel;
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Embedding(pub Vec<f32>);
+
+// This is needed for semantic index functionality
+// Unfortunately it has to live wherever the "Embedding" struct is created.
+// Keeping this in here though, introduces a 'rusqlite' dependency into AI
+// which is less than ideal
+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)))
+    }
+}
+impl From<Vec<f32>> for Embedding {
+    fn from(value: Vec<f32>) -> Self {
+        Embedding(value)
+    }
+}
+
+impl Embedding {
+    pub fn similarity(&self, other: &Self) -> OrderedFloat<f32> {
+        let len = self.0.len();
+        assert_eq!(len, other.0.len());
+
+        let mut result = 0.0;
+        unsafe {
+            matrixmultiply::sgemm(
+                1,
+                len,
+                1,
+                1.0,
+                self.0.as_ptr(),
+                len as isize,
+                1,
+                other.0.as_ptr(),
+                1,
+                len as isize,
+                0.0,
+                &mut result as *mut f32,
+                1,
+                1,
+            );
+        }
+        OrderedFloat(result)
+    }
+}
+
+#[async_trait]
+pub trait EmbeddingProvider: CredentialProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel>;
+    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
+    fn max_tokens_per_batch(&self) -> usize;
+    fn rate_limit_expiration(&self) -> Option<Instant>;
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use rand::prelude::*;
+
+    #[gpui2::test]
+    fn test_similarity(mut rng: StdRng) {
+        assert_eq!(
+            Embedding::from(vec![1., 0., 0., 0., 0.])
+                .similarity(&Embedding::from(vec![0., 1., 0., 0., 0.])),
+            0.
+        );
+        assert_eq!(
+            Embedding::from(vec![2., 0., 0., 0., 0.])
+                .similarity(&Embedding::from(vec![3., 1., 0., 0., 0.])),
+            6.
+        );
+
+        for _ in 0..100 {
+            let size = 1536;
+            let mut a = vec![0.; size];
+            let mut b = vec![0.; size];
+            for (a, b) in a.iter_mut().zip(b.iter_mut()) {
+                *a = rng.gen();
+                *b = rng.gen();
+            }
+            let a = Embedding::from(a);
+            let b = Embedding::from(b);
+
+            assert_eq!(
+                round_to_decimals(a.similarity(&b), 1),
+                round_to_decimals(reference_dot(&a.0, &b.0), 1)
+            );
+        }
+
+        fn round_to_decimals(n: OrderedFloat<f32>, decimal_places: i32) -> f32 {
+            let factor = (10.0 as f32).powi(decimal_places);
+            (n * factor).round() / factor
+        }
+
+        fn reference_dot(a: &[f32], b: &[f32]) -> OrderedFloat<f32> {
+            OrderedFloat(a.iter().zip(b.iter()).map(|(a, b)| a * b).sum())
+        }
+    }
+}

crates/ai2/src/models.rs 🔗

@@ -0,0 +1,16 @@
+pub enum TruncationDirection {
+    Start,
+    End,
+}
+
+pub trait LanguageModel {
+    fn name(&self) -> String;
+    fn count_tokens(&self, content: &str) -> anyhow::Result<usize>;
+    fn truncate(
+        &self,
+        content: &str,
+        length: usize,
+        direction: TruncationDirection,
+    ) -> anyhow::Result<String>;
+    fn capacity(&self) -> anyhow::Result<usize>;
+}

crates/ai2/src/prompts/base.rs 🔗

@@ -0,0 +1,330 @@
+use std::cmp::Reverse;
+use std::ops::Range;
+use std::sync::Arc;
+
+use language2::BufferSnapshot;
+use util::ResultExt;
+
+use crate::models::LanguageModel;
+use crate::prompts::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 crate::models::TruncationDirection;
+    use crate::test::FakeLanguageModel;
+
+    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,
+                            TruncationDirection::End,
+                        )?;
+                        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,
+                            TruncationDirection::End,
+                        )?;
+                        token_count = max_token_length;
+                    }
+                }
+
+                anyhow::Ok((content, token_count))
+            }
+        }
+
+        let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel { 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(FakeLanguageModel { 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(FakeLanguageModel { 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(FakeLanguageModel { 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/ai2/src/prompts/file_context.rs 🔗

@@ -0,0 +1,164 @@
+use anyhow::anyhow;
+use language2::BufferSnapshot;
+use language2::ToOffset;
+
+use crate::models::LanguageModel;
+use crate::models::TruncationDirection;
+use crate::prompts::base::PromptArguments;
+use crate::prompts::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_window, start_goal_tokens, TruncationDirection::Start)?;
+                let truncated_end_window =
+                    model.truncate(&end_window, end_goal_tokens, TruncationDirection::End)?;
+                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, TruncationDirection::End)?;
+                }
+            }
+        }
+    }
+
+    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, TruncationDirection::End)?;
+            }
+
+            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/ai2/src/prompts/generate.rs 🔗

@@ -0,0 +1,99 @@
+use crate::prompts::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,
+                crate::models::TruncationDirection::End,
+            )?;
+        }
+
+        let token_count = args.model.count_tokens(&prompt)?;
+
+        anyhow::Ok((prompt, token_count))
+    }
+}

crates/ai2/src/prompts/preamble.rs 🔗

@@ -0,0 +1,52 @@
+use crate::prompts::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/ai2/src/prompts/repository_context.rs 🔗

@@ -0,0 +1,98 @@
+use crate::prompts::base::{PromptArguments, PromptTemplate};
+use std::fmt::Write;
+use std::{ops::Range, path::PathBuf};
+
+use gpui2::{AsyncAppContext, Model};
+use language2::{Anchor, Buffer};
+
+#[derive(Clone)]
+pub struct PromptCodeSnippet {
+    path: Option<PathBuf>,
+    language_name: Option<String>,
+    content: String,
+}
+
+impl PromptCodeSnippet {
+    pub fn new(
+        buffer: Model<Buffer>,
+        range: Range<Anchor>,
+        cx: &mut AsyncAppContext,
+    ) -> anyhow::Result<Self> {
+        let (content, language_name, file_path) = buffer.update(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)
+        })?;
+
+        anyhow::Ok(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/ai2/src/providers/open_ai/completion.rs 🔗

@@ -0,0 +1,306 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::{
+    future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt,
+    Stream, StreamExt,
+};
+use gpui2::{AppContext, Executor};
+use isahc::{http::StatusCode, Request, RequestExt};
+use parking_lot::RwLock;
+use serde::{Deserialize, Serialize};
+use std::{
+    env,
+    fmt::{self, Display},
+    io,
+    sync::Arc,
+};
+use util::ResultExt;
+
+use crate::{
+    auth::{CredentialProvider, ProviderCredential},
+    completion::{CompletionProvider, CompletionRequest},
+    models::LanguageModel,
+};
+
+use crate::providers::open_ai::{OpenAILanguageModel, OPENAI_API_URL};
+
+#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum Role {
+    User,
+    Assistant,
+    System,
+}
+
+impl Role {
+    pub fn cycle(&mut self) {
+        *self = match self {
+            Role::User => Role::Assistant,
+            Role::Assistant => Role::System,
+            Role::System => Role::User,
+        }
+    }
+}
+
+impl Display for Role {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Role::User => write!(f, "User"),
+            Role::Assistant => write!(f, "Assistant"),
+            Role::System => write!(f, "System"),
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+pub struct RequestMessage {
+    pub role: Role,
+    pub content: String,
+}
+
+#[derive(Debug, Default, Serialize)]
+pub struct OpenAIRequest {
+    pub model: String,
+    pub messages: Vec<RequestMessage>,
+    pub stream: bool,
+    pub stop: Vec<String>,
+    pub temperature: f32,
+}
+
+impl CompletionRequest for OpenAIRequest {
+    fn data(&self) -> serde_json::Result<String> {
+        serde_json::to_string(self)
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+pub struct ResponseMessage {
+    pub role: Option<Role>,
+    pub content: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OpenAIUsage {
+    pub prompt_tokens: u32,
+    pub completion_tokens: u32,
+    pub total_tokens: u32,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ChatChoiceDelta {
+    pub index: u32,
+    pub delta: ResponseMessage,
+    pub finish_reason: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OpenAIResponseStreamEvent {
+    pub id: Option<String>,
+    pub object: String,
+    pub created: u32,
+    pub model: String,
+    pub choices: Vec<ChatChoiceDelta>,
+    pub usage: Option<OpenAIUsage>,
+}
+
+pub async fn stream_completion(
+    credential: ProviderCredential,
+    executor: Arc<Executor>,
+    request: Box<dyn CompletionRequest>,
+) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
+    let api_key = match credential {
+        ProviderCredential::Credentials { api_key } => api_key,
+        _ => {
+            return Err(anyhow!("no credentials provider for completion"));
+        }
+    };
+
+    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
+
+    let json_data = request.data()?;
+    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
+        .header("Content-Type", "application/json")
+        .header("Authorization", format!("Bearer {}", api_key))
+        .body(json_data)?
+        .send_async()
+        .await?;
+
+    let status = response.status();
+    if status == StatusCode::OK {
+        executor
+            .spawn(async move {
+                let mut lines = BufReader::new(response.body_mut()).lines();
+
+                fn parse_line(
+                    line: Result<String, io::Error>,
+                ) -> Result<Option<OpenAIResponseStreamEvent>> {
+                    if let Some(data) = line?.strip_prefix("data: ") {
+                        let event = serde_json::from_str(&data)?;
+                        Ok(Some(event))
+                    } else {
+                        Ok(None)
+                    }
+                }
+
+                while let Some(line) = lines.next().await {
+                    if let Some(event) = parse_line(line).transpose() {
+                        let done = event.as_ref().map_or(false, |event| {
+                            event
+                                .choices
+                                .last()
+                                .map_or(false, |choice| choice.finish_reason.is_some())
+                        });
+                        if tx.unbounded_send(event).is_err() {
+                            break;
+                        }
+
+                        if done {
+                            break;
+                        }
+                    }
+                }
+
+                anyhow::Ok(())
+            })
+            .detach();
+
+        Ok(rx)
+    } else {
+        let mut body = String::new();
+        response.body_mut().read_to_string(&mut body).await?;
+
+        #[derive(Deserialize)]
+        struct OpenAIResponse {
+            error: OpenAIError,
+        }
+
+        #[derive(Deserialize)]
+        struct OpenAIError {
+            message: String,
+        }
+
+        match serde_json::from_str::<OpenAIResponse>(&body) {
+            Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
+                "Failed to connect to OpenAI API: {}",
+                response.error.message,
+            )),
+
+            _ => Err(anyhow!(
+                "Failed to connect to OpenAI API: {} {}",
+                response.status(),
+                body,
+            )),
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct OpenAICompletionProvider {
+    model: OpenAILanguageModel,
+    credential: Arc<RwLock<ProviderCredential>>,
+    executor: Arc<Executor>,
+}
+
+impl OpenAICompletionProvider {
+    pub fn new(model_name: &str, executor: Arc<Executor>) -> Self {
+        let model = OpenAILanguageModel::load(model_name);
+        let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
+        Self {
+            model,
+            credential,
+            executor,
+        }
+    }
+}
+
+#[async_trait]
+impl CredentialProvider for OpenAICompletionProvider {
+    fn has_credentials(&self) -> bool {
+        match *self.credential.read() {
+            ProviderCredential::Credentials { .. } => true,
+            _ => false,
+        }
+    }
+    async fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential {
+        let existing_credential = self.credential.read().clone();
+
+        let retrieved_credential = cx
+            .run_on_main(move |cx| match existing_credential {
+                ProviderCredential::Credentials { .. } => {
+                    return existing_credential.clone();
+                }
+                _ => {
+                    if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() {
+                        return ProviderCredential::Credentials { api_key };
+                    }
+
+                    if let Some(Some((_, api_key))) = cx.read_credentials(OPENAI_API_URL).log_err()
+                    {
+                        if let Some(api_key) = String::from_utf8(api_key).log_err() {
+                            return ProviderCredential::Credentials { api_key };
+                        } else {
+                            return ProviderCredential::NoCredentials;
+                        }
+                    } else {
+                        return ProviderCredential::NoCredentials;
+                    }
+                }
+            })
+            .await;
+
+        *self.credential.write() = retrieved_credential.clone();
+        retrieved_credential
+    }
+
+    async fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) {
+        *self.credential.write() = credential.clone();
+        let credential = credential.clone();
+        cx.run_on_main(move |cx| match credential {
+            ProviderCredential::Credentials { api_key } => {
+                cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
+                    .log_err();
+            }
+            _ => {}
+        })
+        .await;
+    }
+    async fn delete_credentials(&self, cx: &mut AppContext) {
+        cx.run_on_main(move |cx| cx.delete_credentials(OPENAI_API_URL).log_err())
+            .await;
+        *self.credential.write() = ProviderCredential::NoCredentials;
+    }
+}
+
+impl CompletionProvider for OpenAICompletionProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel> {
+        let model: Box<dyn LanguageModel> = Box::new(self.model.clone());
+        model
+    }
+    fn complete(
+        &self,
+        prompt: Box<dyn CompletionRequest>,
+    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
+        // Currently the CompletionRequest for OpenAI, includes a 'model' parameter
+        // This means that the model is determined by the CompletionRequest and not the CompletionProvider,
+        // which is currently model based, due to the langauge model.
+        // At some point in the future we should rectify this.
+        let credential = self.credential.read().clone();
+        let request = stream_completion(credential, self.executor.clone(), prompt);
+        async move {
+            let response = request.await?;
+            let stream = response
+                .filter_map(|response| async move {
+                    match response {
+                        Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
+                        Err(error) => Some(Err(error)),
+                    }
+                })
+                .boxed();
+            Ok(stream)
+        }
+        .boxed()
+    }
+    fn box_clone(&self) -> Box<dyn CompletionProvider> {
+        Box::new((*self).clone())
+    }
+}

crates/ai2/src/providers/open_ai/embedding.rs 🔗

@@ -0,0 +1,313 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::AsyncReadExt;
+use gpui2::Executor;
+use gpui2::{serde_json, AppContext};
+use isahc::http::StatusCode;
+use isahc::prelude::Configurable;
+use isahc::{AsyncBody, Response};
+use lazy_static::lazy_static;
+use parking_lot::{Mutex, RwLock};
+use parse_duration::parse;
+use postage::watch;
+use serde::{Deserialize, Serialize};
+use std::env;
+use std::ops::Add;
+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::auth::{CredentialProvider, ProviderCredential};
+use crate::embedding::{Embedding, EmbeddingProvider};
+use crate::models::LanguageModel;
+use crate::providers::open_ai::OpenAILanguageModel;
+
+use crate::providers::open_ai::OPENAI_API_URL;
+
+lazy_static! {
+    static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap();
+}
+
+#[derive(Clone)]
+pub struct OpenAIEmbeddingProvider {
+    model: OpenAILanguageModel,
+    credential: Arc<RwLock<ProviderCredential>>,
+    pub client: Arc<dyn HttpClient>,
+    pub executor: Arc<Executor>,
+    rate_limit_count_rx: watch::Receiver<Option<Instant>>,
+    rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
+}
+
+#[derive(Serialize)]
+struct OpenAIEmbeddingRequest<'a> {
+    model: &'static str,
+    input: Vec<&'a str>,
+}
+
+#[derive(Deserialize)]
+struct OpenAIEmbeddingResponse {
+    data: Vec<OpenAIEmbedding>,
+    usage: OpenAIEmbeddingUsage,
+}
+
+#[derive(Debug, Deserialize)]
+struct OpenAIEmbedding {
+    embedding: Vec<f32>,
+    index: usize,
+    object: String,
+}
+
+#[derive(Deserialize)]
+struct OpenAIEmbeddingUsage {
+    prompt_tokens: usize,
+    total_tokens: usize,
+}
+
+impl OpenAIEmbeddingProvider {
+    pub fn new(client: Arc<dyn HttpClient>, executor: Arc<Executor>) -> Self {
+        let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
+        let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
+
+        let model = OpenAILanguageModel::load("text-embedding-ada-002");
+        let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
+
+        OpenAIEmbeddingProvider {
+            model,
+            credential,
+            client,
+            executor,
+            rate_limit_count_rx,
+            rate_limit_count_tx,
+        }
+    }
+
+    fn get_api_key(&self) -> Result<String> {
+        match self.credential.read().clone() {
+            ProviderCredential::Credentials { api_key } => Ok(api_key),
+            _ => Err(anyhow!("api credentials not provided")),
+        }
+    }
+
+    fn resolve_rate_limit(&self) {
+        let reset_time = *self.rate_limit_count_tx.lock().borrow();
+
+        if let Some(reset_time) = reset_time {
+            if Instant::now() >= reset_time {
+                *self.rate_limit_count_tx.lock().borrow_mut() = None
+            }
+        }
+
+        log::trace!(
+            "resolving reset time: {:?}",
+            *self.rate_limit_count_tx.lock().borrow()
+        );
+    }
+
+    fn update_reset_time(&self, reset_time: Instant) {
+        let original_time = *self.rate_limit_count_tx.lock().borrow();
+
+        let updated_time = if let Some(original_time) = original_time {
+            if reset_time < original_time {
+                Some(reset_time)
+            } else {
+                Some(original_time)
+            }
+        } else {
+            Some(reset_time)
+        };
+
+        log::trace!("updating rate limit time: {:?}", updated_time);
+
+        *self.rate_limit_count_tx.lock().borrow_mut() = updated_time;
+    }
+    async fn send_request(
+        &self,
+        api_key: &str,
+        spans: Vec<&str>,
+        request_timeout: u64,
+    ) -> Result<Response<AsyncBody>> {
+        let request = Request::post("https://api.openai.com/v1/embeddings")
+            .redirect_policy(isahc::config::RedirectPolicy::Follow)
+            .timeout(Duration::from_secs(request_timeout))
+            .header("Content-Type", "application/json")
+            .header("Authorization", format!("Bearer {}", api_key))
+            .body(
+                serde_json::to_string(&OpenAIEmbeddingRequest {
+                    input: spans.clone(),
+                    model: "text-embedding-ada-002",
+                })
+                .unwrap()
+                .into(),
+            )?;
+
+        Ok(self.client.send(request).await?)
+    }
+}
+
+#[async_trait]
+impl CredentialProvider for OpenAIEmbeddingProvider {
+    fn has_credentials(&self) -> bool {
+        match *self.credential.read() {
+            ProviderCredential::Credentials { .. } => true,
+            _ => false,
+        }
+    }
+    async fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential {
+        let existing_credential = self.credential.read().clone();
+
+        let retrieved_credential = cx
+            .run_on_main(move |cx| match existing_credential {
+                ProviderCredential::Credentials { .. } => {
+                    return existing_credential.clone();
+                }
+                _ => {
+                    if let Some(api_key) = env::var("OPENAI_API_KEY").log_err() {
+                        return ProviderCredential::Credentials { api_key };
+                    }
+
+                    if let Some(Some((_, api_key))) = cx.read_credentials(OPENAI_API_URL).log_err()
+                    {
+                        if let Some(api_key) = String::from_utf8(api_key).log_err() {
+                            return ProviderCredential::Credentials { api_key };
+                        } else {
+                            return ProviderCredential::NoCredentials;
+                        }
+                    } else {
+                        return ProviderCredential::NoCredentials;
+                    }
+                }
+            })
+            .await;
+
+        *self.credential.write() = retrieved_credential.clone();
+        retrieved_credential
+    }
+
+    async fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential) {
+        *self.credential.write() = credential.clone();
+        let credential = credential.clone();
+        cx.run_on_main(move |cx| match credential {
+            ProviderCredential::Credentials { api_key } => {
+                cx.write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
+                    .log_err();
+            }
+            _ => {}
+        })
+        .await;
+    }
+    async fn delete_credentials(&self, cx: &mut AppContext) {
+        cx.run_on_main(move |cx| cx.delete_credentials(OPENAI_API_URL).log_err())
+            .await;
+        *self.credential.write() = ProviderCredential::NoCredentials;
+    }
+}
+
+#[async_trait]
+impl EmbeddingProvider for OpenAIEmbeddingProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel> {
+        let model: Box<dyn LanguageModel> = Box::new(self.model.clone());
+        model
+    }
+
+    fn max_tokens_per_batch(&self) -> usize {
+        50000
+    }
+
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        *self.rate_limit_count_rx.borrow()
+    }
+
+    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
+        const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
+        const MAX_RETRIES: usize = 4;
+
+        let api_key = self.get_api_key()?;
+
+        let mut request_number = 0;
+        let mut rate_limiting = false;
+        let mut request_timeout: u64 = 15;
+        let mut response: Response<AsyncBody>;
+        while request_number < MAX_RETRIES {
+            response = self
+                .send_request(
+                    &api_key,
+                    spans.iter().map(|x| &**x).collect(),
+                    request_timeout,
+                )
+                .await?;
+
+            request_number += 1;
+
+            match response.status() {
+                StatusCode::REQUEST_TIMEOUT => {
+                    request_timeout += 5;
+                }
+                StatusCode::OK => {
+                    let mut body = String::new();
+                    response.body_mut().read_to_string(&mut body).await?;
+                    let response: OpenAIEmbeddingResponse = serde_json::from_str(&body)?;
+
+                    log::trace!(
+                        "openai embedding completed. tokens: {:?}",
+                        response.usage.total_tokens
+                    );
+
+                    // If we complete a request successfully that was previously rate_limited
+                    // resolve the rate limit
+                    if rate_limiting {
+                        self.resolve_rate_limit()
+                    }
+
+                    return Ok(response
+                        .data
+                        .into_iter()
+                        .map(|embedding| Embedding::from(embedding.embedding))
+                        .collect());
+                }
+                StatusCode::TOO_MANY_REQUESTS => {
+                    rate_limiting = true;
+                    let mut body = String::new();
+                    response.body_mut().read_to_string(&mut body).await?;
+
+                    let delay_duration = {
+                        let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64);
+                        if let Some(time_to_reset) =
+                            response.headers().get("x-ratelimit-reset-tokens")
+                        {
+                            if let Ok(time_str) = time_to_reset.to_str() {
+                                parse(time_str).unwrap_or(delay)
+                            } else {
+                                delay
+                            }
+                        } else {
+                            delay
+                        }
+                    };
+
+                    // If we've previously rate limited, increment the duration but not the count
+                    let reset_time = Instant::now().add(delay_duration);
+                    self.update_reset_time(reset_time);
+
+                    log::trace!(
+                        "openai rate limiting: waiting {:?} until lifted",
+                        &delay_duration
+                    );
+
+                    self.executor.timer(delay_duration).await;
+                }
+                _ => {
+                    let mut body = String::new();
+                    response.body_mut().read_to_string(&mut body).await?;
+                    return Err(anyhow!(
+                        "open ai bad request: {:?} {:?}",
+                        &response.status(),
+                        body
+                    ));
+                }
+            }
+        }
+        Err(anyhow!("openai max retries"))
+    }
+}

crates/ai2/src/providers/open_ai/mod.rs 🔗

@@ -0,0 +1,9 @@
+pub mod completion;
+pub mod embedding;
+pub mod model;
+
+pub use completion::*;
+pub use embedding::*;
+pub use model::OpenAILanguageModel;
+
+pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";

crates/ai2/src/providers/open_ai/model.rs 🔗

@@ -0,0 +1,57 @@
+use anyhow::anyhow;
+use tiktoken_rs::CoreBPE;
+use util::ResultExt;
+
+use crate::models::{LanguageModel, TruncationDirection};
+
+#[derive(Clone)]
+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,
+        direction: TruncationDirection,
+    ) -> anyhow::Result<String> {
+        if let Some(bpe) = &self.bpe {
+            let tokens = bpe.encode_with_special_tokens(content);
+            if tokens.len() > length {
+                match direction {
+                    TruncationDirection::End => bpe.decode(tokens[..length].to_vec()),
+                    TruncationDirection::Start => 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/ai2/src/providers/open_ai/new.rs 🔗

@@ -0,0 +1,11 @@
+pub trait LanguageModel {
+    fn name(&self) -> String;
+    fn count_tokens(&self, content: &str) -> anyhow::Result<usize>;
+    fn truncate(
+        &self,
+        content: &str,
+        length: usize,
+        direction: TruncationDirection,
+    ) -> anyhow::Result<String>;
+    fn capacity(&self) -> anyhow::Result<usize>;
+}

crates/ai2/src/test.rs 🔗

@@ -0,0 +1,193 @@
+use std::{
+    sync::atomic::{self, AtomicUsize, Ordering},
+    time::Instant,
+};
+
+use async_trait::async_trait;
+use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
+use gpui2::AppContext;
+use parking_lot::Mutex;
+
+use crate::{
+    auth::{CredentialProvider, ProviderCredential},
+    completion::{CompletionProvider, CompletionRequest},
+    embedding::{Embedding, EmbeddingProvider},
+    models::{LanguageModel, TruncationDirection},
+};
+
+#[derive(Clone)]
+pub struct FakeLanguageModel {
+    pub capacity: usize,
+}
+
+impl LanguageModel for FakeLanguageModel {
+    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,
+        direction: TruncationDirection,
+    ) -> anyhow::Result<String> {
+        println!("TRYING TO TRUNCATE: {:?}", length.clone());
+
+        if length > self.count_tokens(content)? {
+            println!("NOT TRUNCATING");
+            return anyhow::Ok(content.to_string());
+        }
+
+        anyhow::Ok(match direction {
+            TruncationDirection::End => content.chars().collect::<Vec<char>>()[..length]
+                .into_iter()
+                .collect::<String>(),
+            TruncationDirection::Start => content.chars().collect::<Vec<char>>()[length..]
+                .into_iter()
+                .collect::<String>(),
+        })
+    }
+    fn capacity(&self) -> anyhow::Result<usize> {
+        anyhow::Ok(self.capacity)
+    }
+}
+
+pub struct FakeEmbeddingProvider {
+    pub embedding_count: AtomicUsize,
+}
+
+impl Clone for FakeEmbeddingProvider {
+    fn clone(&self) -> Self {
+        FakeEmbeddingProvider {
+            embedding_count: AtomicUsize::new(self.embedding_count.load(Ordering::SeqCst)),
+        }
+    }
+}
+
+impl Default for FakeEmbeddingProvider {
+    fn default() -> Self {
+        FakeEmbeddingProvider {
+            embedding_count: AtomicUsize::default(),
+        }
+    }
+}
+
+impl FakeEmbeddingProvider {
+    pub fn embedding_count(&self) -> usize {
+        self.embedding_count.load(atomic::Ordering::SeqCst)
+    }
+
+    pub fn embed_sync(&self, span: &str) -> Embedding {
+        let mut result = vec![1.0; 26];
+        for letter in span.chars() {
+            let letter = letter.to_ascii_lowercase();
+            if letter as u32 >= 'a' as u32 {
+                let ix = (letter as u32) - ('a' as u32);
+                if ix < 26 {
+                    result[ix as usize] += 1.0;
+                }
+            }
+        }
+
+        let norm = result.iter().map(|x| x * x).sum::<f32>().sqrt();
+        for x in &mut result {
+            *x /= norm;
+        }
+
+        result.into()
+    }
+}
+
+#[async_trait]
+impl CredentialProvider for FakeEmbeddingProvider {
+    fn has_credentials(&self) -> bool {
+        true
+    }
+    async fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential {
+        ProviderCredential::NotNeeded
+    }
+    async fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {}
+    async fn delete_credentials(&self, _cx: &mut AppContext) {}
+}
+
+#[async_trait]
+impl EmbeddingProvider for FakeEmbeddingProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel> {
+        Box::new(FakeLanguageModel { capacity: 1000 })
+    }
+    fn max_tokens_per_batch(&self) -> usize {
+        1000
+    }
+
+    fn rate_limit_expiration(&self) -> Option<Instant> {
+        None
+    }
+
+    async fn embed_batch(&self, spans: Vec<String>) -> anyhow::Result<Vec<Embedding>> {
+        self.embedding_count
+            .fetch_add(spans.len(), atomic::Ordering::SeqCst);
+
+        anyhow::Ok(spans.iter().map(|span| self.embed_sync(span)).collect())
+    }
+}
+
+pub struct FakeCompletionProvider {
+    last_completion_tx: Mutex<Option<mpsc::Sender<String>>>,
+}
+
+impl Clone for FakeCompletionProvider {
+    fn clone(&self) -> Self {
+        Self {
+            last_completion_tx: Mutex::new(None),
+        }
+    }
+}
+
+impl FakeCompletionProvider {
+    pub fn new() -> Self {
+        Self {
+            last_completion_tx: Mutex::new(None),
+        }
+    }
+
+    pub fn send_completion(&self, completion: impl Into<String>) {
+        let mut tx = self.last_completion_tx.lock();
+        tx.as_mut().unwrap().try_send(completion.into()).unwrap();
+    }
+
+    pub fn finish_completion(&self) {
+        self.last_completion_tx.lock().take().unwrap();
+    }
+}
+
+#[async_trait]
+impl CredentialProvider for FakeCompletionProvider {
+    fn has_credentials(&self) -> bool {
+        true
+    }
+    async fn retrieve_credentials(&self, _cx: &mut AppContext) -> ProviderCredential {
+        ProviderCredential::NotNeeded
+    }
+    async fn save_credentials(&self, _cx: &mut AppContext, _credential: ProviderCredential) {}
+    async fn delete_credentials(&self, _cx: &mut AppContext) {}
+}
+
+impl CompletionProvider for FakeCompletionProvider {
+    fn base_model(&self) -> Box<dyn LanguageModel> {
+        let model: Box<dyn LanguageModel> = Box::new(FakeLanguageModel { capacity: 8190 });
+        model
+    }
+    fn complete(
+        &self,
+        _prompt: Box<dyn CompletionRequest>,
+    ) -> BoxFuture<'static, anyhow::Result<BoxStream<'static, anyhow::Result<String>>>> {
+        let (tx, rx) = mpsc::channel(1);
+        *self.last_completion_tx.lock() = Some(tx);
+        async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed()
+    }
+    fn box_clone(&self) -> Box<dyn CompletionProvider> {
+        Box::new((*self).clone())
+    }
+}

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,11 +40,12 @@ 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"] }
 project = { path = "../project", features = ["test-support"] }
+ai = { path = "../ai", features = ["test-support"]}
 
 ctor.workspace = true
 env_logger.workspace = true

crates/assistant/src/assistant.rs 🔗

@@ -4,7 +4,7 @@ mod codegen;
 mod prompts;
 mod streaming_diff;
 
-use ai::completion::Role;
+use ai::providers::open_ai::Role;
 use anyhow::Result;
 pub use assistant_panel::AssistantPanel;
 use assistant_settings::OpenAIModel;

crates/assistant/src/assistant_panel.rs 🔗

@@ -5,9 +5,14 @@ use crate::{
     MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
     SavedMessage,
 };
-use ai::completion::{
-    stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
+
+use ai::{
+    auth::ProviderCredential,
+    completion::{CompletionProvider, CompletionRequest},
+    providers::open_ai::{OpenAICompletionProvider, OpenAIRequest, RequestMessage},
 };
+
+use ai::prompts::repository_context::PromptCodeSnippet;
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
 use client::{telemetry::AssistantKind, ClickhouseEvent, TelemetrySettings};
@@ -29,24 +34,26 @@ 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},
-    cmp, env,
+    cell::Cell,
+    cmp,
     fmt::Write,
     iter,
     ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
-    time::Duration,
+    time::{Duration, Instant},
 };
 use theme::{
     components::{action_button::Button, ComponentExt},
@@ -72,6 +79,7 @@ actions!(
         ResetKey,
         InlineAssist,
         ToggleIncludeConversation,
+        ToggleRetrieveContext,
     ]
 );
 
@@ -91,8 +99,8 @@ pub fn init(cx: &mut AppContext) {
     cx.capture_action(ConversationEditor::copy);
     cx.add_action(ConversationEditor::split);
     cx.capture_action(ConversationEditor::cycle_message_role);
-    cx.add_action(AssistantPanel::save_api_key);
-    cx.add_action(AssistantPanel::reset_api_key);
+    cx.add_action(AssistantPanel::save_credentials);
+    cx.add_action(AssistantPanel::reset_credentials);
     cx.add_action(AssistantPanel::toggle_zoom);
     cx.add_action(AssistantPanel::deploy);
     cx.add_action(AssistantPanel::select_next_match);
@@ -108,6 +116,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);
 }
@@ -133,9 +142,8 @@ pub struct AssistantPanel {
     zoomed: bool,
     has_focus: bool,
     toolbar: ViewHandle<Toolbar>,
-    api_key: Rc<RefCell<Option<String>>>,
+    completion_provider: Box<dyn CompletionProvider>,
     api_key_editor: Option<ViewHandle<Editor>>,
-    has_read_credentials: bool,
     languages: Arc<LanguageRegistry>,
     fs: Arc<dyn Fs>,
     subscriptions: Vec<Subscription>,
@@ -145,6 +153,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 +201,14 @@ impl AssistantPanel {
                         toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
                         toolbar
                     });
+
+                    let semantic_index = SemanticIndex::global(cx);
+                    // Defaulting currently to GPT4, allow for this to be set via config.
+                    let completion_provider = Box::new(OpenAICompletionProvider::new(
+                        "gpt-4",
+                        cx.background().clone(),
+                    ));
+
                     let mut this = Self {
                         workspace: workspace_handle,
                         active_editor_index: Default::default(),
@@ -201,9 +219,8 @@ impl AssistantPanel {
                         zoomed: false,
                         has_focus: false,
                         toolbar,
-                        api_key: Rc::new(RefCell::new(None)),
+                        completion_provider,
                         api_key_editor: None,
-                        has_read_credentials: false,
                         languages: workspace.app_state().languages.clone(),
                         fs: workspace.app_state().fs.clone(),
                         width: None,
@@ -215,6 +232,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);
@@ -240,10 +259,7 @@ impl AssistantPanel {
         cx: &mut ViewContext<Workspace>,
     ) {
         let this = if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
-            if this
-                .update(cx, |assistant, cx| assistant.load_api_key(cx))
-                .is_some()
-            {
+            if this.update(cx, |assistant, _| assistant.has_credentials()) {
                 this
             } else {
                 workspace.focus_panel::<AssistantPanel>(cx);
@@ -262,20 +278,21 @@ 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>) {
-        let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
-            api_key
-        } else {
-            return;
-        };
-
+    fn new_inline_assist(
+        &mut self,
+        editor: &ViewHandle<Editor>,
+        cx: &mut ViewContext<Self>,
+        project: &ModelHandle<Project>,
+    ) {
         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);
@@ -304,14 +321,38 @@ impl AssistantPanel {
 
         let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
         let provider = Arc::new(OpenAICompletionProvider::new(
-            api_key,
+            "gpt-4",
             cx.background().clone(),
         ));
 
+        // Retrieve Credentials Authenticates the Provider
+        // provider.retrieve_credentials(cx);
+
         let codegen = cx.add_model(|cx| {
             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 +363,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 +406,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 +485,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 +506,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 +587,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 +609,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 +660,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 +735,25 @@ 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 {
+
+            let request = Box::new(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();
     }
@@ -683,7 +811,7 @@ impl AssistantPanel {
     fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<ConversationEditor> {
         let editor = cx.add_view(|cx| {
             ConversationEditor::new(
-                self.api_key.clone(),
+                self.completion_provider.clone(),
                 self.languages.clone(),
                 self.fs.clone(),
                 self.workspace.clone(),
@@ -742,17 +870,19 @@ impl AssistantPanel {
         }
     }
 
-    fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+    fn save_credentials(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
         if let Some(api_key) = self
             .api_key_editor
             .as_ref()
             .map(|editor| editor.read(cx).text(cx))
         {
             if !api_key.is_empty() {
-                cx.platform()
-                    .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
-                    .log_err();
-                *self.api_key.borrow_mut() = Some(api_key);
+                let credential = ProviderCredential::Credentials {
+                    api_key: api_key.clone(),
+                };
+
+                self.completion_provider.save_credentials(cx, credential);
+
                 self.api_key_editor.take();
                 cx.focus_self();
                 cx.notify();
@@ -762,9 +892,8 @@ impl AssistantPanel {
         }
     }
 
-    fn reset_api_key(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
-        cx.platform().delete_credentials(OPENAI_API_URL).log_err();
-        self.api_key.take();
+    fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
+        self.completion_provider.delete_credentials(cx);
         self.api_key_editor = Some(build_api_key_editor(cx));
         cx.focus_self();
         cx.notify();
@@ -1023,13 +1152,12 @@ impl AssistantPanel {
 
         let fs = self.fs.clone();
         let workspace = self.workspace.clone();
-        let api_key = self.api_key.clone();
         let languages = self.languages.clone();
         cx.spawn(|this, mut cx| async move {
             let saved_conversation = fs.load(&path).await?;
             let saved_conversation = serde_json::from_str(&saved_conversation)?;
             let conversation = cx.add_model(|cx| {
-                Conversation::deserialize(saved_conversation, path.clone(), api_key, languages, cx)
+                Conversation::deserialize(saved_conversation, path.clone(), languages, cx)
             });
             this.update(&mut cx, |this, cx| {
                 // If, by the time we've loaded the conversation, the user has already opened
@@ -1053,30 +1181,12 @@ impl AssistantPanel {
             .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path))
     }
 
-    fn load_api_key(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
-        if self.api_key.borrow().is_none() && !self.has_read_credentials {
-            self.has_read_credentials = true;
-            let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
-                Some(api_key)
-            } else if let Some((_, api_key)) = cx
-                .platform()
-                .read_credentials(OPENAI_API_URL)
-                .log_err()
-                .flatten()
-            {
-                String::from_utf8(api_key).log_err()
-            } else {
-                None
-            };
-            if let Some(api_key) = api_key {
-                *self.api_key.borrow_mut() = Some(api_key);
-            } else if self.api_key_editor.is_none() {
-                self.api_key_editor = Some(build_api_key_editor(cx));
-                cx.notify();
-            }
-        }
+    fn has_credentials(&mut self) -> bool {
+        self.completion_provider.has_credentials()
+    }
 
-        self.api_key.borrow().clone()
+    fn load_credentials(&mut self, cx: &mut ViewContext<Self>) {
+        self.completion_provider.retrieve_credentials(cx);
     }
 }
 
@@ -1261,7 +1371,7 @@ impl Panel for AssistantPanel {
 
     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         if active {
-            self.load_api_key(cx);
+            self.load_credentials(cx);
 
             if self.editors.is_empty() {
                 self.new_conversation(cx);
@@ -1326,10 +1436,10 @@ struct Conversation {
     token_count: Option<usize>,
     max_token_count: usize,
     pending_token_count: Task<Option<()>>,
-    api_key: Rc<RefCell<Option<String>>>,
     pending_save: Task<Result<()>>,
     path: Option<PathBuf>,
     _subscriptions: Vec<Subscription>,
+    completion_provider: Box<dyn CompletionProvider>,
 }
 
 impl Entity for Conversation {
@@ -1338,9 +1448,9 @@ impl Entity for Conversation {
 
 impl Conversation {
     fn new(
-        api_key: Rc<RefCell<Option<String>>>,
         language_registry: Arc<LanguageRegistry>,
         cx: &mut ModelContext<Self>,
+        completion_provider: Box<dyn CompletionProvider>,
     ) -> Self {
         let markdown = language_registry.language_for_name("Markdown");
         let buffer = cx.add_model(|cx| {
@@ -1379,8 +1489,8 @@ impl Conversation {
             _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
             pending_save: Task::ready(Ok(())),
             path: None,
-            api_key,
             buffer,
+            completion_provider,
         };
         let message = MessageAnchor {
             id: MessageId(post_inc(&mut this.next_message_id.0)),
@@ -1426,7 +1536,6 @@ impl Conversation {
     fn deserialize(
         saved_conversation: SavedConversation,
         path: PathBuf,
-        api_key: Rc<RefCell<Option<String>>>,
         language_registry: Arc<LanguageRegistry>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
@@ -1435,6 +1544,10 @@ impl Conversation {
             None => Some(Uuid::new_v4().to_string()),
         };
         let model = saved_conversation.model;
+        let completion_provider: Box<dyn CompletionProvider> = Box::new(
+            OpenAICompletionProvider::new(model.full_name(), cx.background().clone()),
+        );
+        completion_provider.retrieve_credentials(cx);
         let markdown = language_registry.language_for_name("Markdown");
         let mut message_anchors = Vec::new();
         let mut next_message_id = MessageId(0);
@@ -1481,8 +1594,8 @@ impl Conversation {
             _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
             pending_save: Task::ready(Ok(())),
             path: Some(path),
-            api_key,
             buffer,
+            completion_provider,
         };
         this.count_remaining_tokens(cx);
         this
@@ -1514,12 +1627,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<_>>();
@@ -1601,11 +1716,11 @@ impl Conversation {
         }
 
         if should_assist {
-            let Some(api_key) = self.api_key.borrow().clone() else {
+            if !self.completion_provider.has_credentials() {
                 return Default::default();
-            };
+            }
 
-            let request = OpenAIRequest {
+            let request: Box<dyn CompletionRequest> = Box::new(OpenAIRequest {
                 model: self.model.full_name().to_string(),
                 messages: self
                     .messages(cx)
@@ -1613,9 +1728,11 @@ 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);
+            let stream = self.completion_provider.complete(request);
             let assistant_message = self
                 .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
                 .unwrap();
@@ -1633,33 +1750,28 @@ impl Conversation {
                         let mut messages = stream.await?;
 
                         while let Some(message) = messages.next().await {
-                            let mut message = message?;
-                            if let Some(choice) = message.choices.pop() {
-                                this.upgrade(&cx)
-                                    .ok_or_else(|| anyhow!("conversation was dropped"))?
-                                    .update(&mut cx, |this, cx| {
-                                        let text: Arc<str> = choice.delta.content?.into();
-                                        let message_ix =
-                                            this.message_anchors.iter().position(|message| {
-                                                message.id == assistant_message_id
-                                            })?;
-                                        this.buffer.update(cx, |buffer, cx| {
-                                            let offset = this.message_anchors[message_ix + 1..]
-                                                .iter()
-                                                .find(|message| message.start.is_valid(buffer))
-                                                .map_or(buffer.len(), |message| {
-                                                    message
-                                                        .start
-                                                        .to_offset(buffer)
-                                                        .saturating_sub(1)
-                                                });
-                                            buffer.edit([(offset..offset, text)], None, cx);
-                                        });
-                                        cx.emit(ConversationEvent::StreamedCompletion);
-
-                                        Some(())
+                            let text = message?;
+
+                            this.upgrade(&cx)
+                                .ok_or_else(|| anyhow!("conversation was dropped"))?
+                                .update(&mut cx, |this, cx| {
+                                    let message_ix = this
+                                        .message_anchors
+                                        .iter()
+                                        .position(|message| message.id == assistant_message_id)?;
+                                    this.buffer.update(cx, |buffer, cx| {
+                                        let offset = this.message_anchors[message_ix + 1..]
+                                            .iter()
+                                            .find(|message| message.start.is_valid(buffer))
+                                            .map_or(buffer.len(), |message| {
+                                                message.start.to_offset(buffer).saturating_sub(1)
+                                            });
+                                        buffer.edit([(offset..offset, text)], None, cx);
                                     });
-                            }
+                                    cx.emit(ConversationEvent::StreamedCompletion);
+
+                                    Some(())
+                                });
                             smol::future::yield_now().await;
                         }
 
@@ -1881,55 +1993,54 @@ impl Conversation {
 
     fn summarize(&mut self, cx: &mut ModelContext<Self>) {
         if self.message_anchors.len() >= 2 && self.summary.is_none() {
-            let api_key = self.api_key.borrow().clone();
-            if let Some(api_key) = api_key {
-                let messages = self
-                    .messages(cx)
-                    .take(2)
-                    .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
-                    .chain(Some(RequestMessage {
-                        role: Role::User,
-                        content:
-                            "Summarize the conversation into a short title without punctuation"
-                                .into(),
-                    }));
-                let request = OpenAIRequest {
-                    model: self.model.full_name().to_string(),
-                    messages: messages.collect(),
-                    stream: true,
-                };
+            if !self.completion_provider.has_credentials() {
+                return;
+            }
 
-                let stream = stream_completion(api_key, cx.background().clone(), request);
-                self.pending_summary = cx.spawn(|this, mut cx| {
-                    async move {
-                        let mut messages = stream.await?;
+            let messages = self
+                .messages(cx)
+                .take(2)
+                .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
+                .chain(Some(RequestMessage {
+                    role: Role::User,
+                    content: "Summarize the conversation into a short title without punctuation"
+                        .into(),
+                }));
+            let request: Box<dyn CompletionRequest> = Box::new(OpenAIRequest {
+                model: self.model.full_name().to_string(),
+                messages: messages.collect(),
+                stream: true,
+                stop: vec![],
+                temperature: 1.0,
+            });
 
-                        while let Some(message) = messages.next().await {
-                            let mut message = message?;
-                            if let Some(choice) = message.choices.pop() {
-                                let text = choice.delta.content.unwrap_or_default();
-                                this.update(&mut cx, |this, cx| {
-                                    this.summary
-                                        .get_or_insert(Default::default())
-                                        .text
-                                        .push_str(&text);
-                                    cx.emit(ConversationEvent::SummaryChanged);
-                                });
-                            }
-                        }
+            let stream = self.completion_provider.complete(request);
+            self.pending_summary = cx.spawn(|this, mut cx| {
+                async move {
+                    let mut messages = stream.await?;
 
+                    while let Some(message) = messages.next().await {
+                        let text = message?;
                         this.update(&mut cx, |this, cx| {
-                            if let Some(summary) = this.summary.as_mut() {
-                                summary.done = true;
-                                cx.emit(ConversationEvent::SummaryChanged);
-                            }
+                            this.summary
+                                .get_or_insert(Default::default())
+                                .text
+                                .push_str(&text);
+                            cx.emit(ConversationEvent::SummaryChanged);
                         });
-
-                        anyhow::Ok(())
                     }
-                    .log_err()
-                });
-            }
+
+                    this.update(&mut cx, |this, cx| {
+                        if let Some(summary) = this.summary.as_mut() {
+                            summary.done = true;
+                            cx.emit(ConversationEvent::SummaryChanged);
+                        }
+                    });
+
+                    anyhow::Ok(())
+                }
+                .log_err()
+            });
         }
     }
 
@@ -2090,13 +2201,14 @@ struct ConversationEditor {
 
 impl ConversationEditor {
     fn new(
-        api_key: Rc<RefCell<Option<String>>>,
+        completion_provider: Box<dyn CompletionProvider>,
         language_registry: Arc<LanguageRegistry>,
         fs: Arc<dyn Fs>,
         workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let conversation = cx.add_model(|cx| Conversation::new(api_key, language_registry, cx));
+        let conversation =
+            cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider));
         Self::for_conversation(conversation, fs, workspace, cx)
     }
 
@@ -2638,12 +2750,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 +2775,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 +2796,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 +2877,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 +2912,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 +2928,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 +2950,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 +2991,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| {

crates/assistant/src/codegen.rs 🔗

@@ -1,10 +1,11 @@
 use crate::streaming_diff::{Hunk, StreamingDiff};
-use ai::completion::{CompletionProvider, OpenAIRequest};
+use ai::completion::{CompletionProvider, CompletionRequest};
 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 {
@@ -95,7 +96,7 @@ impl Codegen {
         self.error.as_ref()
     }
 
-    pub fn start(&mut self, prompt: OpenAIRequest, cx: &mut ModelContext<Self>) {
+    pub fn start(&mut self, prompt: Box<dyn CompletionRequest>, cx: &mut ModelContext<Self>) {
         let range = self.range();
         let snapshot = self.snapshot.clone();
         let selected_text = snapshot
@@ -335,17 +336,25 @@ fn strip_markdown_codeblock(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use futures::{
-        future::BoxFuture,
-        stream::{self, BoxStream},
-    };
+    use ai::test::FakeCompletionProvider;
+    use futures::stream::{self};
     use gpui::{executor::Deterministic, TestAppContext};
     use indoc::indoc;
     use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
-    use parking_lot::Mutex;
     use rand::prelude::*;
+    use serde::Serialize;
     use settings::SettingsStore;
-    use smol::future::FutureExt;
+
+    #[derive(Serialize)]
+    pub struct DummyCompletionRequest {
+        pub name: String,
+    }
+
+    impl CompletionRequest for DummyCompletionRequest {
+        fn data(&self) -> serde_json::Result<String> {
+            serde_json::to_string(self)
+        }
+    }
 
     #[gpui::test(iterations = 10)]
     async fn test_transform_autoindent(
@@ -371,7 +380,7 @@ mod tests {
             let snapshot = buffer.snapshot(cx);
             snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
         });
-        let provider = Arc::new(TestCompletionProvider::new());
+        let provider = Arc::new(FakeCompletionProvider::new());
         let codegen = cx.add_model(|cx| {
             Codegen::new(
                 buffer.clone(),
@@ -380,7 +389,11 @@ mod tests {
                 cx,
             )
         });
-        codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx));
+
+        let request = Box::new(DummyCompletionRequest {
+            name: "test".to_string(),
+        });
+        codegen.update(cx, |codegen, cx| codegen.start(request, cx));
 
         let mut new_text = concat!(
             "       let mut x = 0;\n",
@@ -433,7 +446,7 @@ mod tests {
             let snapshot = buffer.snapshot(cx);
             snapshot.anchor_before(Point::new(1, 6))
         });
-        let provider = Arc::new(TestCompletionProvider::new());
+        let provider = Arc::new(FakeCompletionProvider::new());
         let codegen = cx.add_model(|cx| {
             Codegen::new(
                 buffer.clone(),
@@ -442,7 +455,11 @@ mod tests {
                 cx,
             )
         });
-        codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx));
+
+        let request = Box::new(DummyCompletionRequest {
+            name: "test".to_string(),
+        });
+        codegen.update(cx, |codegen, cx| codegen.start(request, cx));
 
         let mut new_text = concat!(
             "t mut x = 0;\n",
@@ -495,7 +512,7 @@ mod tests {
             let snapshot = buffer.snapshot(cx);
             snapshot.anchor_before(Point::new(1, 2))
         });
-        let provider = Arc::new(TestCompletionProvider::new());
+        let provider = Arc::new(FakeCompletionProvider::new());
         let codegen = cx.add_model(|cx| {
             Codegen::new(
                 buffer.clone(),
@@ -504,7 +521,11 @@ mod tests {
                 cx,
             )
         });
-        codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx));
+
+        let request = Box::new(DummyCompletionRequest {
+            name: "test".to_string(),
+        });
+        codegen.update(cx, |codegen, cx| codegen.start(request, cx));
 
         let mut new_text = concat!(
             "let mut x = 0;\n",
@@ -592,38 +613,6 @@ mod tests {
         }
     }
 
-    struct TestCompletionProvider {
-        last_completion_tx: Mutex<Option<mpsc::Sender<String>>>,
-    }
-
-    impl TestCompletionProvider {
-        fn new() -> Self {
-            Self {
-                last_completion_tx: Mutex::new(None),
-            }
-        }
-
-        fn send_completion(&self, completion: impl Into<String>) {
-            let mut tx = self.last_completion_tx.lock();
-            tx.as_mut().unwrap().try_send(completion.into()).unwrap();
-        }
-
-        fn finish_completion(&self) {
-            self.last_completion_tx.lock().take().unwrap();
-        }
-    }
-
-    impl CompletionProvider for TestCompletionProvider {
-        fn complete(
-            &self,
-            _prompt: OpenAIRequest,
-        ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
-            let (tx, rx) = mpsc::channel(1);
-            *self.last_completion_tx.lock() = Some(tx);
-            async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed()
-        }
-    }
-
     fn rust_lang() -> Language {
         Language::new(
             LanguageConfig {

crates/assistant/src/prompts.rs 🔗

@@ -1,8 +1,14 @@
-use crate::codegen::CodegenKind;
+use ai::models::LanguageModel;
+use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
+use ai::prompts::file_context::FileContext;
+use ai::prompts::generate::GenerateInlineContent;
+use ai::prompts::preamble::EngineerPreamble;
+use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext};
+use ai::providers::open_ai::OpenAILanguageModel;
 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 +124,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/call2.rs 🔗

@@ -12,8 +12,8 @@ use client2::{
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
 use gpui2::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Handle, ModelContext, Subscription, Task,
-    WeakHandle,
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
+    WeakModel,
 };
 use postage::watch;
 use project2::Project;
@@ -23,10 +23,10 @@ use std::sync::Arc;
 pub use participant::ParticipantLocation;
 pub use room::Room;
 
-pub fn init(client: Arc<Client>, user_store: Handle<UserStore>, cx: &mut AppContext) {
+pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
     CallSettings::register(cx);
 
-    let active_call = cx.entity(|cx| ActiveCall::new(client, user_store, cx));
+    let active_call = cx.build_model(|cx| ActiveCall::new(client, user_store, cx));
     cx.set_global(active_call);
 }
 
@@ -40,16 +40,16 @@ pub struct IncomingCall {
 
 /// Singleton global maintaining the user's participation in a room across workspaces.
 pub struct ActiveCall {
-    room: Option<(Handle<Room>, Vec<Subscription>)>,
-    pending_room_creation: Option<Shared<Task<Result<Handle<Room>, Arc<anyhow::Error>>>>>,
-    location: Option<WeakHandle<Project>>,
+    room: Option<(Model<Room>, Vec<Subscription>)>,
+    pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
+    location: Option<WeakModel<Project>>,
     pending_invites: HashSet<u64>,
     incoming_call: (
         watch::Sender<Option<IncomingCall>>,
         watch::Receiver<Option<IncomingCall>>,
     ),
     client: Arc<Client>,
-    user_store: Handle<UserStore>,
+    user_store: Model<UserStore>,
     _subscriptions: Vec<client2::Subscription>,
 }
 
@@ -58,11 +58,7 @@ impl EventEmitter for ActiveCall {
 }
 
 impl ActiveCall {
-    fn new(
-        client: Arc<Client>,
-        user_store: Handle<UserStore>,
-        cx: &mut ModelContext<Self>,
-    ) -> Self {
+    fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
         Self {
             room: None,
             pending_room_creation: None,
@@ -71,8 +67,8 @@ impl ActiveCall {
             incoming_call: watch::channel(),
 
             _subscriptions: vec![
-                client.add_request_handler(cx.weak_handle(), Self::handle_incoming_call),
-                client.add_message_handler(cx.weak_handle(), Self::handle_call_canceled),
+                client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
+                client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
             ],
             client,
             user_store,
@@ -84,7 +80,7 @@ impl ActiveCall {
     }
 
     async fn handle_incoming_call(
-        this: Handle<Self>,
+        this: Model<Self>,
         envelope: TypedEnvelope<proto::IncomingCall>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
@@ -112,7 +108,7 @@ impl ActiveCall {
     }
 
     async fn handle_call_canceled(
-        this: Handle<Self>,
+        this: Model<Self>,
         envelope: TypedEnvelope<proto::CallCanceled>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
@@ -129,14 +125,14 @@ impl ActiveCall {
         Ok(())
     }
 
-    pub fn global(cx: &AppContext) -> Handle<Self> {
-        cx.global::<Handle<Self>>().clone()
+    pub fn global(cx: &AppContext) -> Model<Self> {
+        cx.global::<Model<Self>>().clone()
     }
 
     pub fn invite(
         &mut self,
         called_user_id: u64,
-        initial_project: Option<Handle<Project>>,
+        initial_project: Option<Model<Project>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         if !self.pending_invites.insert(called_user_id) {
@@ -291,7 +287,7 @@ impl ActiveCall {
         &mut self,
         channel_id: u64,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Room>>> {
+    ) -> Task<Result<Model<Room>>> {
         if let Some(room) = self.room().cloned() {
             if room.read(cx).channel_id() == Some(channel_id) {
                 return Task::ready(Ok(room));
@@ -327,7 +323,7 @@ impl ActiveCall {
 
     pub fn share_project(
         &mut self,
-        project: Handle<Project>,
+        project: Model<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<u64>> {
         if let Some((room, _)) = self.room.as_ref() {
@@ -340,7 +336,7 @@ impl ActiveCall {
 
     pub fn unshare_project(
         &mut self,
-        project: Handle<Project>,
+        project: Model<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
         if let Some((room, _)) = self.room.as_ref() {
@@ -351,13 +347,13 @@ impl ActiveCall {
         }
     }
 
-    pub fn location(&self) -> Option<&WeakHandle<Project>> {
+    pub fn location(&self) -> Option<&WeakModel<Project>> {
         self.location.as_ref()
     }
 
     pub fn set_location(
         &mut self,
-        project: Option<&Handle<Project>>,
+        project: Option<&Model<Project>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         if project.is_some() || !*ZED_ALWAYS_ACTIVE {
@@ -371,7 +367,7 @@ impl ActiveCall {
 
     fn set_room(
         &mut self,
-        room: Option<Handle<Room>>,
+        room: Option<Model<Room>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
@@ -407,7 +403,7 @@ impl ActiveCall {
         }
     }
 
-    pub fn room(&self) -> Option<&Handle<Room>> {
+    pub fn room(&self) -> Option<&Model<Room>> {
         self.room.as_ref().map(|(room, _)| room)
     }
 

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;
+use gpui2::WeakModel;
 pub use live_kit_client::Frame;
-use live_kit_client::RemoteAudioTrack;
 use project2::Project;
 use std::{fmt, sync::Arc};
 
@@ -35,7 +33,7 @@ impl ParticipantLocation {
 #[derive(Clone, Default)]
 pub struct LocalParticipant {
     pub projects: Vec<proto::ParticipantProject>,
-    pub active_project: Option<WeakHandle<Project>>,
+    pub active_project: Option<WeakModel<Project>>,
 }
 
 #[derive(Clone, Debug)]
@@ -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)]

crates/call2/src/room.rs 🔗

@@ -1,3 +1,6 @@
+#![allow(dead_code, unused)]
+// todo!()
+
 use crate::{
     call_settings::CallSettings,
     participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
@@ -13,18 +16,15 @@ use collections::{BTreeMap, HashMap, HashSet};
 use fs2::Fs;
 use futures::{FutureExt, StreamExt};
 use gpui2::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Handle, ModelContext, Task, WeakHandle,
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
 };
 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,10 +59,10 @@ 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>>,
+    shared_projects: HashSet<WeakModel<Project>>,
+    joined_projects: HashSet<WeakModel<Project>>,
     local_participant: LocalParticipant,
     remote_participants: BTreeMap<u64, RemoteParticipant>,
     pending_participants: Vec<Arc<User>>,
@@ -70,7 +70,7 @@ pub struct Room {
     pending_call_count: usize,
     leave_when_empty: bool,
     client: Arc<Client>,
-    user_store: Handle<UserStore>,
+    user_store: Model<UserStore>,
     follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
     client_subscriptions: Vec<client2::Subscription>,
     _subscriptions: Vec<gpui2::Subscription>,
@@ -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(
@@ -110,140 +111,141 @@ impl Room {
         channel_id: Option<u64>,
         live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
         client: Arc<Client>,
-        user_store: Handle<UserStore>,
+        user_store: Model<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(
         called_user_id: u64,
-        initial_project: Option<Handle<Project>>,
+        initial_project: Option<Model<Project>>,
         client: Arc<Client>,
-        user_store: Handle<UserStore>,
+        user_store: Model<UserStore>,
         cx: &mut AppContext,
-    ) -> Task<Result<Handle<Self>>> {
+    ) -> Task<Result<Model<Self>>> {
         cx.spawn(move |mut cx| async move {
             let response = client.request(proto::CreateRoom {}).await?;
             let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
-            let room = cx.entity(|cx| {
+            let room = cx.build_model(|cx| {
                 Self::new(
                     room_proto.id,
                     None,
@@ -281,9 +283,9 @@ impl Room {
     pub(crate) fn join_channel(
         channel_id: u64,
         client: Arc<Client>,
-        user_store: Handle<UserStore>,
+        user_store: Model<UserStore>,
         cx: &mut AppContext,
-    ) -> Task<Result<Handle<Self>>> {
+    ) -> Task<Result<Model<Self>>> {
         cx.spawn(move |cx| async move {
             Self::from_join_response(
                 client.request(proto::JoinChannel { channel_id }).await?,
@@ -297,9 +299,9 @@ impl Room {
     pub(crate) fn join(
         call: &IncomingCall,
         client: Arc<Client>,
-        user_store: Handle<UserStore>,
+        user_store: Model<UserStore>,
         cx: &mut AppContext,
-    ) -> Task<Result<Handle<Self>>> {
+    ) -> Task<Result<Model<Self>>> {
         let id = call.room_id;
         cx.spawn(move |cx| async move {
             Self::from_join_response(
@@ -341,11 +343,11 @@ impl Room {
     fn from_join_response(
         response: proto::JoinRoomResponse,
         client: Arc<Client>,
-        user_store: Handle<UserStore>,
+        user_store: Model<UserStore>,
         mut cx: AsyncAppContext,
-    ) -> Result<Handle<Self>> {
+    ) -> Result<Model<Self>> {
         let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
-        let room = cx.entity(|cx| {
+        let room = cx.build_model(|cx| {
             Self::new(
                 room_proto.id,
                 response.channel_id,
@@ -416,13 +418,13 @@ 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();
     }
 
     async fn maintain_connection(
-        this: WeakHandle<Self>,
+        this: WeakModel<Self>,
         client: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<()> {
@@ -659,7 +661,7 @@ impl Room {
     }
 
     async fn handle_room_updated(
-        this: Handle<Self>,
+        this: Model<Self>,
         envelope: TypedEnvelope<proto::RoomUpdated>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
@@ -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,
+                // });
             }
         }
 
@@ -1095,7 +1101,7 @@ impl Room {
         language_registry: Arc<LanguageRegistry>,
         fs: Arc<dyn Fs>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Project>>> {
+    ) -> Task<Result<Model<Project>>> {
         let client = self.client.clone();
         let user_store = self.user_store.clone();
         cx.emit(Event::RemoteProjectJoined { project_id: id });
@@ -1119,7 +1125,7 @@ impl Room {
 
     pub(crate) fn share_project(
         &mut self,
-        project: Handle<Project>,
+        project: Model<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<u64>> {
         if let Some(project_id) = project.read(cx).remote_id() {
@@ -1155,7 +1161,7 @@ impl Room {
 
     pub(crate) fn unshare_project(
         &mut self,
-        project: Handle<Project>,
+        project: Model<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
         let project_id = match project.read(cx).remote_id() {
@@ -1169,7 +1175,7 @@ impl Room {
 
     pub(crate) fn set_location(
         &mut self,
-        project: Option<&Handle<Project>>,
+        project: Option<&Model<Project>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         if self.status.is_offline() {
@@ -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 🔗

@@ -14,8 +14,8 @@ use futures::{
     future::BoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _, TryStreamExt,
 };
 use gpui2::{
-    serde_json, AnyHandle, AnyWeakHandle, AppContext, AsyncAppContext, Handle, SemanticVersion,
-    Task, WeakHandle,
+    serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, SemanticVersion, Task,
+    WeakModel,
 };
 use lazy_static::lazy_static;
 use parking_lot::RwLock;
@@ -227,7 +227,7 @@ struct ClientState {
     _reconnect_task: Option<Task<()>>,
     reconnect_interval: Duration,
     entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>,
-    models_by_message_type: HashMap<TypeId, AnyWeakHandle>,
+    models_by_message_type: HashMap<TypeId, AnyWeakModel>,
     entity_types_by_message_type: HashMap<TypeId, TypeId>,
     #[allow(clippy::type_complexity)]
     message_handlers: HashMap<
@@ -236,7 +236,7 @@ struct ClientState {
             dyn Send
                 + Sync
                 + Fn(
-                    AnyHandle,
+                    AnyModel,
                     Box<dyn AnyTypedEnvelope>,
                     &Arc<Client>,
                     AsyncAppContext,
@@ -246,7 +246,7 @@ struct ClientState {
 }
 
 enum WeakSubscriber {
-    Entity { handle: AnyWeakHandle },
+    Entity { handle: AnyWeakModel },
     Pending(Vec<Box<dyn AnyTypedEnvelope>>),
 }
 
@@ -312,9 +312,9 @@ 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 {
+    pub fn set_model(mut self, model: &Model<T>, cx: &mut AsyncAppContext) -> Subscription {
         self.consumed = true;
         let mut state = self.client.state.write();
         let id = (TypeId::of::<T>(), self.remote_id);
@@ -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);
 
@@ -552,13 +552,13 @@ impl Client {
     #[track_caller]
     pub fn add_message_handler<M, E, H, F>(
         self: &Arc<Self>,
-        entity: WeakHandle<E>,
+        entity: WeakModel<E>,
         handler: H,
     ) -> Subscription
     where
         M: EnvelopedMessage,
-        E: 'static + Send + Sync,
-        H: 'static + Send + Sync + Fn(Handle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        E: 'static + Send,
+        H: 'static + Send + Sync + Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<()>> + Send,
     {
         let message_type_id = TypeId::of::<M>();
@@ -594,13 +594,13 @@ impl Client {
 
     pub fn add_request_handler<M, E, H, F>(
         self: &Arc<Self>,
-        model: WeakHandle<E>,
+        model: WeakModel<E>,
         handler: H,
     ) -> Subscription
     where
         M: RequestMessage,
-        E: 'static + Send + Sync,
-        H: 'static + Send + Sync + Fn(Handle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        E: 'static + Send,
+        H: 'static + Send + Sync + Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<M::Response>> + Send,
     {
         self.add_message_handler(model, move |handle, envelope, this, cx| {
@@ -615,8 +615,8 @@ impl Client {
     pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage,
-        E: 'static + Send + Sync,
-        H: 'static + Send + Sync + Fn(Handle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        E: 'static + Send,
+        H: 'static + Send + Sync + Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<()>> + Send,
     {
         self.add_entity_message_handler::<M, E, _, _>(move |subscriber, message, client, cx| {
@@ -627,8 +627,8 @@ impl Client {
     fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage,
-        E: 'static + Send + Sync,
-        H: 'static + Send + Sync + Fn(AnyHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        E: 'static + Send,
+        H: 'static + Send + Sync + Fn(AnyModel, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<()>> + Send,
     {
         let model_type_id = TypeId::of::<E>();
@@ -666,8 +666,8 @@ 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,
-        H: 'static + Send + Sync + Fn(Handle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        E: 'static + Send,
+        H: 'static + Send + Sync + Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<M::Response>> + Send,
     {
         self.add_model_message_handler(move |entity, envelope, client, cx| {
@@ -1546,7 +1546,7 @@ mod tests {
         let (done_tx1, mut done_rx1) = smol::channel::unbounded();
         let (done_tx2, mut done_rx2) = smol::channel::unbounded();
         client.add_model_message_handler(
-            move |model: Handle<Model>, _: TypedEnvelope<proto::JoinProject>, _, mut cx| {
+            move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, _, mut cx| {
                 match model.update(&mut cx, |model, _| model.id).unwrap() {
                     1 => done_tx1.try_send(()).unwrap(),
                     2 => done_tx2.try_send(()).unwrap(),
@@ -1555,15 +1555,15 @@ mod tests {
                 async { Ok(()) }
             },
         );
-        let model1 = cx.entity(|_| Model {
+        let model1 = cx.build_model(|_| TestModel {
             id: 1,
             subscription: None,
         });
-        let model2 = cx.entity(|_| Model {
+        let model2 = cx.build_model(|_| TestModel {
             id: 2,
             subscription: None,
         });
-        let model3 = cx.entity(|_| Model {
+        let model3 = cx.build_model(|_| TestModel {
             id: 3,
             subscription: None,
         });
@@ -1596,7 +1596,7 @@ mod tests {
         let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
         let server = FakeServer::for_client(user_id, &client, cx).await;
 
-        let model = cx.entity(|_| Model::default());
+        let model = cx.build_model(|_| TestModel::default());
         let (done_tx1, _done_rx1) = smol::channel::unbounded();
         let (done_tx2, mut done_rx2) = smol::channel::unbounded();
         let subscription1 = client.add_message_handler(
@@ -1624,11 +1624,11 @@ mod tests {
         let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
         let server = FakeServer::for_client(user_id, &client, cx).await;
 
-        let model = cx.entity(|_| Model::default());
+        let model = cx.build_model(|_| TestModel::default());
         let (done_tx, mut done_rx) = smol::channel::unbounded();
         let subscription = client.add_message_handler(
             model.clone().downgrade(),
-            move |model: Handle<Model>, _: TypedEnvelope<proto::Ping>, _, mut cx| {
+            move |model: Model<TestModel>, _: TypedEnvelope<proto::Ping>, _, mut cx| {
                 model
                     .update(&mut cx, |model, _| model.subscription.take())
                     .unwrap();
@@ -1644,7 +1644,7 @@ mod tests {
     }
 
     #[derive(Default)]
-    struct Model {
+    struct TestModel {
         id: usize,
         subscription: Option<Subscription>,
     }

crates/client2/src/telemetry.rs 🔗

@@ -5,7 +5,9 @@ use parking_lot::Mutex;
 use serde::Serialize;
 use settings2::Settings;
 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};
@@ -161,8 +163,16 @@ impl Telemetry {
 
         let this = self.clone();
         cx.spawn(|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
@@ -170,8 +180,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/client2/src/test.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
 use anyhow::{anyhow, Result};
 use futures::{stream::BoxStream, StreamExt};
-use gpui2::{Context, Executor, Handle, TestAppContext};
+use gpui2::{Context, Executor, Model, TestAppContext};
 use parking_lot::Mutex;
 use rpc2::{
     proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
@@ -194,9 +194,9 @@ impl FakeServer {
         &self,
         client: Arc<Client>,
         cx: &mut TestAppContext,
-    ) -> Handle<UserStore> {
+    ) -> Model<UserStore> {
         let http_client = FakeHttpClient::with_404_response();
-        let user_store = cx.entity(|cx| UserStore::new(client, http_client, cx));
+        let user_store = cx.build_model(|cx| UserStore::new(client, http_client, cx));
         assert_eq!(
             self.receive::<proto::GetUsers>()
                 .await

crates/client2/src/user.rs 🔗

@@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result};
 use collections::{hash_map::Entry, HashMap, HashSet};
 use feature_flags2::FeatureFlagAppExt;
 use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
-use gpui2::{AsyncAppContext, EventEmitter, Handle, ImageData, ModelContext, Task};
+use gpui2::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, Task};
 use postage::{sink::Sink, watch};
 use rpc2::proto::{RequestMessage, UsersResponse};
 use std::sync::{Arc, Weak};
@@ -122,9 +122,9 @@ impl UserStore {
         let (mut current_user_tx, current_user_rx) = watch::channel();
         let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
         let rpc_subscriptions = vec![
-            client.add_message_handler(cx.weak_handle(), Self::handle_update_contacts),
-            client.add_message_handler(cx.weak_handle(), Self::handle_update_invite_info),
-            client.add_message_handler(cx.weak_handle(), Self::handle_show_contacts),
+            client.add_message_handler(cx.weak_model(), Self::handle_update_contacts),
+            client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info),
+            client.add_message_handler(cx.weak_model(), Self::handle_show_contacts),
         ];
         Self {
             users: Default::default(),
@@ -213,7 +213,7 @@ impl UserStore {
     }
 
     async fn handle_update_invite_info(
-        this: Handle<Self>,
+        this: Model<Self>,
         message: TypedEnvelope<proto::UpdateInviteInfo>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
@@ -229,7 +229,7 @@ impl UserStore {
     }
 
     async fn handle_show_contacts(
-        this: Handle<Self>,
+        this: Model<Self>,
         _: TypedEnvelope<proto::ShowContacts>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
@@ -243,7 +243,7 @@ impl UserStore {
     }
 
     async fn handle_update_contacts(
-        this: Handle<Self>,
+        this: Model<Self>,
         message: TypedEnvelope<proto::UpdateContacts>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
@@ -690,7 +690,7 @@ impl User {
 impl Contact {
     async fn from_proto(
         contact: proto::Contact,
-        user_store: &Handle<UserStore>,
+        user_store: &Model<UserStore>,
         cx: &mut AsyncAppContext,
     ) -> Result<Self> {
         let user = user_store

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

@@ -7,8 +7,8 @@ use async_tar::Archive;
 use collections::{HashMap, HashSet};
 use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
 use gpui2::{
-    AppContext, AsyncAppContext, Context, EntityId, EventEmitter, Handle, ModelContext, Task,
-    WeakHandle,
+    AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Model, ModelContext,
+    Task, WeakModel,
 };
 use language2::{
     language_settings::{all_language_settings, language_settings},
@@ -49,7 +49,7 @@ pub fn init(
     node_runtime: Arc<dyn NodeRuntime>,
     cx: &mut AppContext,
 ) {
-    let copilot = cx.entity({
+    let copilot = cx.build_model({
         let node_runtime = node_runtime.clone();
         move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
     });
@@ -183,7 +183,7 @@ struct RegisteredBuffer {
 impl RegisteredBuffer {
     fn report_changes(
         &mut self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         cx: &mut ModelContext<Copilot>,
     ) -> oneshot::Receiver<(i32, BufferSnapshot)> {
         let (done_tx, done_rx) = oneshot::channel();
@@ -278,7 +278,7 @@ pub struct Copilot {
     http: Arc<dyn HttpClient>,
     node_runtime: Arc<dyn NodeRuntime>,
     server: CopilotServer,
-    buffers: HashSet<WeakHandle<Buffer>>,
+    buffers: HashSet<WeakModel<Buffer>>,
     server_id: LanguageServerId,
     _subscription: gpui2::Subscription,
 }
@@ -292,9 +292,9 @@ impl EventEmitter for Copilot {
 }
 
 impl Copilot {
-    pub fn global(cx: &AppContext) -> Option<Handle<Self>> {
-        if cx.has_global::<Handle<Self>>() {
-            Some(cx.global::<Handle<Self>>().clone())
+    pub fn global(cx: &AppContext) -> Option<Model<Self>> {
+        if cx.has_global::<Model<Self>>() {
+            Some(cx.global::<Model<Self>>().clone())
         } else {
             None
         }
@@ -383,7 +383,7 @@ impl Copilot {
         new_server_id: LanguageServerId,
         http: Arc<dyn HttpClient>,
         node_runtime: Arc<dyn NodeRuntime>,
-        this: WeakHandle<Self>,
+        this: WeakModel<Self>,
         mut cx: AsyncAppContext,
     ) -> impl Future<Output = ()> {
         async move {
@@ -590,7 +590,7 @@ impl Copilot {
         }
     }
 
-    pub fn register_buffer(&mut self, buffer: &Handle<Buffer>, cx: &mut ModelContext<Self>) {
+    pub fn register_buffer(&mut self, buffer: &Model<Buffer>, cx: &mut ModelContext<Self>) {
         let weak_buffer = buffer.downgrade();
         self.buffers.insert(weak_buffer.clone());
 
@@ -646,7 +646,7 @@ impl Copilot {
 
     fn handle_buffer_event(
         &mut self,
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         event: &language2::Event,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
@@ -706,7 +706,7 @@ impl Copilot {
         Ok(())
     }
 
-    fn unregister_buffer(&mut self, buffer: &WeakHandle<Buffer>) {
+    fn unregister_buffer(&mut self, buffer: &WeakModel<Buffer>) {
         if let Ok(server) = self.server.as_running() {
             if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) {
                 server
@@ -723,7 +723,7 @@ impl Copilot {
 
     pub fn completions<T>(
         &mut self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>>
@@ -735,7 +735,7 @@ impl Copilot {
 
     pub fn completions_cycling<T>(
         &mut self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>>
@@ -792,7 +792,7 @@ impl Copilot {
 
     fn request_completions<R, T>(
         &mut self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>>
@@ -926,7 +926,7 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
     }
 }
 
-fn uri_for_buffer(buffer: &Handle<Buffer>, cx: &AppContext) -> lsp2::Url {
+fn uri_for_buffer(buffer: &Model<Buffer>, cx: &AppContext) -> lsp2::Url {
     if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
         lsp2::Url::from_file_path(file.abs_path(cx)).unwrap()
     } else {

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
@@ -79,6 +80,7 @@ util = { path = "../util", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
+multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
 
 ctor.workspace = true
 env_logger.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,354 @@ 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;
-            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        } else {
+            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 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 +1281,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 +1327,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 +1358,7 @@ impl CompletionsMenu {
                                                         .with_style(
                                                             style
                                                                 .autocomplete
-                                                                .server_name_container,
+                                                                .inline_docs_container,
                                                         )
                                                         .into_any(),
                                                 )
@@ -1060,15 +1397,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 +1476,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 +1492,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 +1535,21 @@ impl CodeActionsMenu {
     fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
         if self.selected_item > 0 {
             self.selected_item -= 1;
-            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
-            cx.notify()
+        } else {
+            self.selected_item = self.actions.len() - 1;
         }
+        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 +1952,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 +2247,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 +2264,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 +3294,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 +3331,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 +3484,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 +3578,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 +3596,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 +3605,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 +3617,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 +3671,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 +3785,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 🔗

@@ -15,14 +15,14 @@ pub use test_context::*;
 use crate::{
     current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AppMetadata, AssetSource,
     ClipboardItem, Context, DispatchPhase, DisplayId, Executor, FocusEvent, FocusHandle, FocusId,
-    KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly, Pixels, Platform, Point,
+    KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly, Pixels, Platform, Point, Render,
     SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement,
     TextSystem, View, Window, WindowContext, WindowHandle, WindowId,
 };
 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},
@@ -38,7 +38,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(),
@@ -47,6 +50,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>),
@@ -60,6 +65,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),
@@ -109,11 +116,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>>,
@@ -130,12 +136,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>,
@@ -162,8 +167,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(),
@@ -185,10 +190,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(),
@@ -206,6 +210,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();
 
@@ -233,6 +239,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);
     }
@@ -305,6 +313,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();
@@ -351,6 +362,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();
@@ -368,6 +382,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) {
         let window_ids = self.windows.keys().collect::<SmallVec<[_; 8]>>();
         for window_id in window_ids {
@@ -447,10 +464,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()) },
@@ -458,10 +477,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,
@@ -482,6 +505,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,
@@ -494,6 +522,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,
@@ -506,20 +536,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 {
@@ -528,10 +563,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>())
@@ -540,12 +577,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 });
@@ -556,7 +595,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
@@ -566,12 +607,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);
@@ -579,9 +623,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>(),
@@ -592,6 +637,11 @@ impl AppContext {
         )
     }
 
+    pub fn all_action_names<'a>(&'a self) -> impl Iterator<Item = SharedString> + 'a {
+        self.action_builders.keys().cloned()
+    }
+
+    /// 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
@@ -601,6 +651,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 });
@@ -615,15 +666,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()
@@ -635,10 +685,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,
@@ -651,36 +703,43 @@ 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 ModelContext<'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 `Model` will be returned
+    /// which can be used to access the entity in a context.
+    fn build_model<T: 'static + Send>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
-    ) -> Handle<T> {
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Model<T> {
         self.update(|cx| {
             let slot = cx.entities.reserve();
-            let entity = build_entity(&mut ModelContext::mutable(cx, slot.downgrade()));
+            let entity = build_model(&mut ModelContext::mutable(cx, slot.downgrade()));
             cx.entities.insert(slot, entity)
         })
     }
 
-    fn update_entity<T: 'static, R>(
+    /// Update the entity referenced by the given model. The function is passed a mutable reference to the
+    /// entity along with a `ModelContext` for the entity.
+    fn update_model<T: 'static, R>(
         &mut self,
-        handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        model: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
     ) -> R {
         self.update(|cx| {
-            let mut entity = cx.entities.lease(handle);
+            let mut entity = cx.entities.lease(model);
             let result = update(
                 &mut entity,
-                &mut ModelContext::mutable(cx, handle.downgrade()),
+                &mut ModelContext::mutable(cx, model.downgrade()),
             );
             cx.entities.end_lease(entity);
             result
@@ -696,30 +755,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);
     }
@@ -750,7 +816,10 @@ impl MainThread<AppContext> {
         })
     }
 
-    pub fn open_window<V: 'static>(
+    /// 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: Render>(
         &mut self,
         options: crate::WindowOptions,
         build_root_view: impl FnOnce(&mut WindowContext) -> View<V> + Send + 'static,
@@ -760,13 +829,15 @@ impl MainThread<AppContext> {
             let handle = WindowHandle::new(id);
             let mut window = Window::new(handle.into(), options, cx);
             let root_view = build_root_view(&mut WindowContext::mutable(cx, &mut window));
-            window.root_view.replace(root_view.into_any());
+            window.root_view.replace(root_view.into());
             cx.windows.get_mut(id).unwrap().replace(window);
             handle
         })
     }
 
-    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 {
@@ -777,13 +848,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_id: WindowId,
@@ -794,10 +866,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>,
@@ -826,11 +899,11 @@ 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 view: AnyView,
     pub cursor_offset: Point<Pixels>,
-    pub state: AnyBox,
-    pub state_type: TypeId,
 }
 
 #[cfg(test)]

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

@@ -1,11 +1,11 @@
 use crate::{
-    AnyWindowHandle, AppContext, Context, Executor, Handle, MainThread, ModelContext, Result, Task,
-    ViewContext, WindowContext,
+    AnyWindowHandle, AppContext, Context, Executor, MainThread, Model, ModelContext, Result, Task,
+    WindowContext,
 };
 use anyhow::anyhow;
 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,35 +14,35 @@ pub struct AsyncAppContext {
 }
 
 impl Context for AsyncAppContext {
-    type EntityContext<'a, 'w, T> = ModelContext<'a, T>;
+    type ModelContext<'a, T> = ModelContext<'a, T>;
     type Result<T> = Result<T>;
 
-    fn entity<T: 'static>(
+    fn build_model<T: 'static>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
-    ) -> Self::Result<Handle<T>>
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Self::Result<Model<T>>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let app = self
             .app
             .upgrade()
             .ok_or_else(|| anyhow!("app was released"))?;
         let mut lock = app.lock(); // Need this to compile
-        Ok(lock.entity(build_entity))
+        Ok(lock.build_model(build_model))
     }
 
-    fn update_entity<T: 'static, R>(
+    fn update_model<T: 'static, R>(
         &mut self,
-        handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        handle: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
     ) -> Self::Result<R> {
         let app = self
             .app
             .upgrade()
             .ok_or_else(|| anyhow!("app was released"))?;
         let mut lock = app.lock(); // Need this to compile
-        Ok(lock.update_entity(handle, update))
+        Ok(lock.update_model(handle, update))
     }
 }
 
@@ -216,27 +216,27 @@ impl AsyncWindowContext {
 }
 
 impl Context for AsyncWindowContext {
-    type EntityContext<'a, 'w, T> = ViewContext<'a, 'w, T>;
+    type ModelContext<'a, T> = ModelContext<'a, T>;
     type Result<T> = Result<T>;
 
-    fn entity<T>(
+    fn build_model<T>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
-    ) -> Result<Handle<T>>
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Result<Model<T>>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         self.app
-            .update_window(self.window, |cx| cx.entity(build_entity))
+            .update_window(self.window, |cx| cx.build_model(build_model))
     }
 
-    fn update_entity<T: 'static, R>(
+    fn update_model<T: 'static, R>(
         &mut self,
-        handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        handle: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
     ) -> Result<R> {
         self.app
-            .update_window(self.window, |cx| cx.update_entity(handle, update))
+            .update_window(self.window, |cx| cx.update_model(handle, update))
     }
 }
 

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

@@ -1,10 +1,10 @@
-use crate::{AnyBox, AppContext, Context};
+use crate::{private::Sealed, AnyBox, AppContext, Context, Entity};
 use anyhow::{anyhow, Result};
 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,
@@ -53,29 +53,29 @@ impl EntityMap {
     /// Reserve a slot for an entity, which you can subsequently use with `insert`.
     pub fn reserve<T: 'static>(&self) -> Slot<T> {
         let id = self.ref_counts.write().counts.insert(1.into());
-        Slot(Handle::new(id, Arc::downgrade(&self.ref_counts)))
+        Slot(Model::new(id, Arc::downgrade(&self.ref_counts)))
     }
 
     /// Insert an entity into a slot obtained by calling `reserve`.
-    pub fn insert<T>(&mut self, slot: Slot<T>, entity: T) -> Handle<T>
+    pub fn insert<T>(&mut self, slot: Slot<T>, entity: T) -> Model<T>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
-        let handle = slot.0;
-        self.entities.insert(handle.entity_id, Box::new(entity));
-        handle
+        let model = slot.0;
+        self.entities.insert(model.entity_id, Box::new(entity));
+        model
     }
 
     /// Move an entity to the stack.
-    pub fn lease<'a, T>(&mut self, handle: &'a Handle<T>) -> Lease<'a, T> {
-        self.assert_valid_context(handle);
+    pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
+        self.assert_valid_context(model);
         let entity = Some(
             self.entities
-                .remove(handle.entity_id)
+                .remove(model.entity_id)
                 .expect("Circular entity lease. Is the entity already being updated?"),
         );
         Lease {
-            handle,
+            model,
             entity,
             entity_type: PhantomData,
         }
@@ -84,18 +84,18 @@ impl EntityMap {
     /// Return an entity after moving it to the stack.
     pub fn end_lease<T>(&mut self, mut lease: Lease<T>) {
         self.entities
-            .insert(lease.handle.entity_id, lease.entity.take().unwrap());
+            .insert(lease.model.entity_id, lease.entity.take().unwrap());
     }
 
-    pub fn read<T: 'static>(&self, handle: &Handle<T>) -> &T {
-        self.assert_valid_context(handle);
-        self.entities[handle.entity_id].downcast_ref().unwrap()
+    pub fn read<T: 'static>(&self, model: &Model<T>) -> &T {
+        self.assert_valid_context(model);
+        self.entities[model.entity_id].downcast_ref().unwrap()
     }
 
-    fn assert_valid_context(&self, handle: &AnyHandle) {
+    fn assert_valid_context(&self, model: &AnyModel) {
         debug_assert!(
-            Weak::ptr_eq(&handle.entity_map, &Arc::downgrade(&self.ref_counts)),
-            "used a handle with the wrong context"
+            Weak::ptr_eq(&model.entity_map, &Arc::downgrade(&self.ref_counts)),
+            "used a model with the wrong context"
         );
     }
 
@@ -120,7 +120,7 @@ impl EntityMap {
 
 pub struct Lease<'a, T> {
     entity: Option<AnyBox>,
-    pub handle: &'a Handle<T>,
+    pub model: &'a Model<T>,
     entity_type: PhantomData<T>,
 }
 
@@ -148,15 +148,15 @@ impl<'a, T> Drop for Lease<'a, T> {
 }
 
 #[derive(Deref, DerefMut)]
-pub struct Slot<T>(Handle<T>);
+pub struct Slot<T>(Model<T>);
 
-pub struct AnyHandle {
+pub struct AnyModel {
     pub(crate) entity_id: EntityId,
-    entity_type: TypeId,
+    pub(crate) entity_type: TypeId,
     entity_map: Weak<RwLock<EntityRefCounts>>,
 }
 
-impl AnyHandle {
+impl AnyModel {
     fn new(id: EntityId, entity_type: TypeId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self {
         Self {
             entity_id: id,
@@ -169,36 +169,36 @@ impl AnyHandle {
         self.entity_id
     }
 
-    pub fn downgrade(&self) -> AnyWeakHandle {
-        AnyWeakHandle {
+    pub fn downgrade(&self) -> AnyWeakModel {
+        AnyWeakModel {
             entity_id: self.entity_id,
             entity_type: self.entity_type,
             entity_ref_counts: self.entity_map.clone(),
         }
     }
 
-    pub fn downcast<T: 'static>(&self) -> Option<Handle<T>> {
+    pub fn downcast<T: 'static>(self) -> Result<Model<T>, AnyModel> {
         if TypeId::of::<T>() == self.entity_type {
-            Some(Handle {
-                any_handle: self.clone(),
+            Ok(Model {
+                any_model: self,
                 entity_type: PhantomData,
             })
         } else {
-            None
+            Err(self)
         }
     }
 }
 
-impl Clone for AnyHandle {
+impl Clone for AnyModel {
     fn clone(&self) -> Self {
         if let Some(entity_map) = self.entity_map.upgrade() {
             let entity_map = entity_map.read();
             let count = entity_map
                 .counts
                 .get(self.entity_id)
-                .expect("detected over-release of a handle");
+                .expect("detected over-release of a model");
             let prev_count = count.fetch_add(1, SeqCst);
-            assert_ne!(prev_count, 0, "Detected over-release of a handle.");
+            assert_ne!(prev_count, 0, "Detected over-release of a model.");
         }
 
         Self {
@@ -209,7 +209,7 @@ impl Clone for AnyHandle {
     }
 }
 
-impl Drop for AnyHandle {
+impl Drop for AnyModel {
     fn drop(&mut self) {
         if let Some(entity_map) = self.entity_map.upgrade() {
             let entity_map = entity_map.upgradable_read();
@@ -218,7 +218,7 @@ impl Drop for AnyHandle {
                 .get(self.entity_id)
                 .expect("detected over-release of a handle.");
             let prev_count = count.fetch_sub(1, SeqCst);
-            assert_ne!(prev_count, 0, "Detected over-release of a handle.");
+            assert_ne!(prev_count, 0, "Detected over-release of a model.");
             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);
@@ -228,60 +228,100 @@ impl Drop for AnyHandle {
     }
 }
 
-impl<T> From<Handle<T>> for AnyHandle {
-    fn from(handle: Handle<T>) -> Self {
-        handle.any_handle
+impl<T> From<Model<T>> for AnyModel {
+    fn from(model: Model<T>) -> Self {
+        model.any_model
     }
 }
 
-impl Hash for AnyHandle {
+impl Hash for AnyModel {
     fn hash<H: Hasher>(&self, state: &mut H) {
         self.entity_id.hash(state);
     }
 }
 
-impl PartialEq for AnyHandle {
+impl PartialEq for AnyModel {
     fn eq(&self, other: &Self) -> bool {
         self.entity_id == other.entity_id
     }
 }
 
-impl Eq for AnyHandle {}
+impl Eq for AnyModel {}
+
+impl std::fmt::Debug for AnyModel {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("AnyModel")
+            .field("entity_id", &self.entity_id.as_u64())
+            .finish()
+    }
+}
 
 #[derive(Deref, DerefMut)]
-pub struct Handle<T> {
+pub struct Model<T> {
     #[deref]
     #[deref_mut]
-    any_handle: AnyHandle,
-    entity_type: PhantomData<T>,
+    pub(crate) any_model: AnyModel,
+    pub(crate) entity_type: PhantomData<T>,
 }
 
-unsafe impl<T> Send for Handle<T> {}
-unsafe impl<T> Sync for Handle<T> {}
+unsafe impl<T> Send for Model<T> {}
+unsafe impl<T> Sync for Model<T> {}
+impl<T> Sealed for Model<T> {}
+
+impl<T: 'static> Entity<T> for Model<T> {
+    type Weak = WeakModel<T>;
+
+    fn entity_id(&self) -> EntityId {
+        self.any_model.entity_id
+    }
+
+    fn downgrade(&self) -> Self::Weak {
+        WeakModel {
+            any_model: self.any_model.downgrade(),
+            entity_type: self.entity_type,
+        }
+    }
 
-impl<T: 'static> Handle<T> {
+    fn upgrade_from(weak: &Self::Weak) -> Option<Self>
+    where
+        Self: Sized,
+    {
+        Some(Model {
+            any_model: weak.any_model.upgrade()?,
+            entity_type: weak.entity_type,
+        })
+    }
+}
+
+impl<T: 'static> Model<T> {
     fn new(id: EntityId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self
     where
         T: 'static,
     {
         Self {
-            any_handle: AnyHandle::new(id, TypeId::of::<T>(), entity_map),
+            any_model: AnyModel::new(id, TypeId::of::<T>(), entity_map),
             entity_type: PhantomData,
         }
     }
 
-    pub fn downgrade(&self) -> WeakHandle<T> {
-        WeakHandle {
-            any_handle: self.any_handle.downgrade(),
-            entity_type: self.entity_type,
-        }
+    /// Downgrade the this to a weak model reference
+    pub fn downgrade(&self) -> WeakModel<T> {
+        // Delegate to the trait implementation to keep behavior in one place.
+        // This method was included to improve method resolution in the presence of
+        // the Model's deref
+        Entity::downgrade(self)
+    }
+
+    /// Convert this into a dynamically typed model.
+    pub fn into_any(self) -> AnyModel {
+        self.any_model
     }
 
     pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
         cx.entities.read(self)
     }
 
-    /// Update the entity referenced by this handle with the given function.
+    /// Update the entity referenced by this model with the given function.
     ///
     /// The update function receives a context appropriate for its environment.
     /// When updating in an `AppContext`, it receives a `ModelContext`.
@@ -289,63 +329,63 @@ 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::ModelContext<'_, T>) -> R,
     ) -> C::Result<R>
     where
         C: Context,
     {
-        cx.update_entity(self, update)
+        cx.update_model(self, update)
     }
 }
 
-impl<T> Clone for Handle<T> {
+impl<T> Clone for Model<T> {
     fn clone(&self) -> Self {
         Self {
-            any_handle: self.any_handle.clone(),
+            any_model: self.any_model.clone(),
             entity_type: self.entity_type,
         }
     }
 }
 
-impl<T> std::fmt::Debug for Handle<T> {
+impl<T> std::fmt::Debug for Model<T> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(
             f,
-            "Handle {{ entity_id: {:?}, entity_type: {:?} }}",
-            self.any_handle.entity_id,
+            "Model {{ entity_id: {:?}, entity_type: {:?} }}",
+            self.any_model.entity_id,
             type_name::<T>()
         )
     }
 }
 
-impl<T> Hash for Handle<T> {
+impl<T> Hash for Model<T> {
     fn hash<H: Hasher>(&self, state: &mut H) {
-        self.any_handle.hash(state);
+        self.any_model.hash(state);
     }
 }
 
-impl<T> PartialEq for Handle<T> {
+impl<T> PartialEq for Model<T> {
     fn eq(&self, other: &Self) -> bool {
-        self.any_handle == other.any_handle
+        self.any_model == other.any_model
     }
 }
 
-impl<T> Eq for Handle<T> {}
+impl<T> Eq for Model<T> {}
 
-impl<T> PartialEq<WeakHandle<T>> for Handle<T> {
-    fn eq(&self, other: &WeakHandle<T>) -> bool {
-        self.entity_id() == other.entity_id()
+impl<T> PartialEq<WeakModel<T>> for Model<T> {
+    fn eq(&self, other: &WeakModel<T>) -> bool {
+        self.any_model.entity_id() == other.entity_id()
     }
 }
 
 #[derive(Clone)]
-pub struct AnyWeakHandle {
+pub struct AnyWeakModel {
     pub(crate) entity_id: EntityId,
     entity_type: TypeId,
     entity_ref_counts: Weak<RwLock<EntityRefCounts>>,
 }
 
-impl AnyWeakHandle {
+impl AnyWeakModel {
     pub fn entity_id(&self) -> EntityId {
         self.entity_id
     }
@@ -359,10 +399,10 @@ impl AnyWeakHandle {
         ref_count > 0
     }
 
-    pub fn upgrade(&self) -> Option<AnyHandle> {
-        let entity_map = self.entity_ref_counts.upgrade()?;
-        let entity_map = entity_map.read();
-        let ref_count = entity_map.counts.get(self.entity_id)?;
+    pub fn upgrade(&self) -> Option<AnyModel> {
+        let ref_counts = &self.entity_ref_counts.upgrade()?;
+        let ref_counts = ref_counts.read();
+        let ref_count = ref_counts.counts.get(self.entity_id)?;
 
         // entity_id is in dropped_entity_ids
         if ref_count.load(SeqCst) == 0 {
@@ -370,7 +410,7 @@ impl AnyWeakHandle {
         }
         ref_count.fetch_add(1, SeqCst);
 
-        Some(AnyHandle {
+        Some(AnyModel {
             entity_id: self.entity_id,
             entity_type: self.entity_type,
             entity_map: self.entity_ref_counts.clone(),
@@ -378,55 +418,54 @@ impl AnyWeakHandle {
     }
 }
 
-impl<T> From<WeakHandle<T>> for AnyWeakHandle {
-    fn from(handle: WeakHandle<T>) -> Self {
-        handle.any_handle
+impl<T> From<WeakModel<T>> for AnyWeakModel {
+    fn from(model: WeakModel<T>) -> Self {
+        model.any_model
     }
 }
 
-impl Hash for AnyWeakHandle {
+impl Hash for AnyWeakModel {
     fn hash<H: Hasher>(&self, state: &mut H) {
         self.entity_id.hash(state);
     }
 }
 
-impl PartialEq for AnyWeakHandle {
+impl PartialEq for AnyWeakModel {
     fn eq(&self, other: &Self) -> bool {
         self.entity_id == other.entity_id
     }
 }
 
-impl Eq for AnyWeakHandle {}
+impl Eq for AnyWeakModel {}
 
 #[derive(Deref, DerefMut)]
-pub struct WeakHandle<T> {
+pub struct WeakModel<T> {
     #[deref]
     #[deref_mut]
-    any_handle: AnyWeakHandle,
+    any_model: AnyWeakModel,
     entity_type: PhantomData<T>,
 }
 
-unsafe impl<T> Send for WeakHandle<T> {}
-unsafe impl<T> Sync for WeakHandle<T> {}
+unsafe impl<T> Send for WeakModel<T> {}
+unsafe impl<T> Sync for WeakModel<T> {}
 
-impl<T> Clone for WeakHandle<T> {
+impl<T> Clone for WeakModel<T> {
     fn clone(&self) -> Self {
         Self {
-            any_handle: self.any_handle.clone(),
+            any_model: self.any_model.clone(),
             entity_type: self.entity_type,
         }
     }
 }
 
-impl<T: 'static> WeakHandle<T> {
-    pub fn upgrade(&self) -> Option<Handle<T>> {
-        Some(Handle {
-            any_handle: self.any_handle.upgrade()?,
-            entity_type: self.entity_type,
-        })
+impl<T: 'static> WeakModel<T> {
+    /// Upgrade this weak model reference into a strong model reference
+    pub fn upgrade(&self) -> Option<Model<T>> {
+        // Delegate to the trait implementation to keep behavior in one place.
+        Model::upgrade_from(self)
     }
 
-    /// Update the entity referenced by this handle with the given function if
+    /// Update the entity referenced by this model with the given function if
     /// the referenced entity still exists. Returns an error if the entity has
     /// been released.
     ///
@@ -436,7 +475,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::ModelContext<'_, T>) -> R,
     ) -> Result<R>
     where
         C: Context,
@@ -445,28 +484,28 @@ impl<T: 'static> WeakHandle<T> {
         crate::Flatten::flatten(
             self.upgrade()
                 .ok_or_else(|| anyhow!("entity release"))
-                .map(|this| cx.update_entity(&this, update)),
+                .map(|this| cx.update_model(&this, update)),
         )
     }
 }
 
-impl<T> Hash for WeakHandle<T> {
+impl<T> Hash for WeakModel<T> {
     fn hash<H: Hasher>(&self, state: &mut H) {
-        self.any_handle.hash(state);
+        self.any_model.hash(state);
     }
 }
 
-impl<T> PartialEq for WeakHandle<T> {
+impl<T> PartialEq for WeakModel<T> {
     fn eq(&self, other: &Self) -> bool {
-        self.any_handle == other.any_handle
+        self.any_model == other.any_model
     }
 }
 
-impl<T> Eq for WeakHandle<T> {}
+impl<T> Eq for WeakModel<T> {}
 
-impl<T> PartialEq<Handle<T>> for WeakHandle<T> {
-    fn eq(&self, other: &Handle<T>) -> bool {
-        self.entity_id() == other.entity_id()
+impl<T> PartialEq<Model<T>> for WeakModel<T> {
+    fn eq(&self, other: &Model<T>) -> bool {
+        self.entity_id() == other.any_model.entity_id()
     }
 }
 

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

@@ -1,6 +1,6 @@
 use crate::{
-    AppContext, AsyncAppContext, Context, Effect, EntityId, EventEmitter, Handle, MainThread,
-    Reference, Subscription, Task, WeakHandle,
+    AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, EventEmitter, MainThread,
+    Model, Reference, Subscription, Task, WeakModel,
 };
 use derive_more::{Deref, DerefMut};
 use futures::FutureExt;
@@ -15,11 +15,11 @@ pub struct ModelContext<'a, T> {
     #[deref]
     #[deref_mut]
     app: Reference<'a, AppContext>,
-    model_state: WeakHandle<T>,
+    model_state: WeakModel<T>,
 }
 
 impl<'a, T: 'static> ModelContext<'a, T> {
-    pub(crate) fn mutable(app: &'a mut AppContext, model_state: WeakHandle<T>) -> Self {
+    pub(crate) fn mutable(app: &'a mut AppContext, model_state: WeakModel<T>) -> Self {
         Self {
             app: Reference::Mutable(app),
             model_state,
@@ -30,30 +30,33 @@ impl<'a, T: 'static> ModelContext<'a, T> {
         self.model_state.entity_id
     }
 
-    pub fn handle(&self) -> Handle<T> {
-        self.weak_handle()
+    pub fn handle(&self) -> Model<T> {
+        self.weak_model()
             .upgrade()
             .expect("The entity must be alive if we have a model context")
     }
 
-    pub fn weak_handle(&self) -> WeakHandle<T> {
+    pub fn weak_model(&self) -> WeakModel<T> {
         self.model_state.clone()
     }
 
-    pub fn observe<T2: 'static>(
+    pub fn observe<T2, E>(
         &mut self,
-        handle: &Handle<T2>,
-        mut on_notify: impl FnMut(&mut T, Handle<T2>, &mut ModelContext<'_, T>) + Send + Sync + 'static,
+        entity: &E,
+        mut on_notify: impl FnMut(&mut T, E, &mut ModelContext<'_, T>) + Send + 'static,
     ) -> Subscription
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
+        T2: 'static,
+        E: Entity<T2>,
     {
-        let this = self.weak_handle();
-        let handle = handle.downgrade();
+        let this = self.weak_model();
+        let entity_id = entity.entity_id();
+        let handle = entity.downgrade();
         self.app.observers.insert(
-            handle.entity_id,
+            entity_id,
             Box::new(move |cx| {
-                if let Some((this, handle)) = this.upgrade().zip(handle.upgrade()) {
+                if let Some((this, handle)) = this.upgrade().zip(E::upgrade_from(&handle)) {
                     this.update(cx, |this, cx| on_notify(this, handle, cx));
                     true
                 } else {
@@ -63,24 +66,24 @@ impl<'a, T: 'static> ModelContext<'a, T> {
         )
     }
 
-    pub fn subscribe<E: 'static + EventEmitter>(
+    pub fn subscribe<T2, E>(
         &mut self,
-        handle: &Handle<E>,
-        mut on_event: impl FnMut(&mut T, Handle<E>, &E::Event, &mut ModelContext<'_, T>)
-            + Send
-            + Sync
-            + 'static,
+        entity: &E,
+        mut on_event: impl FnMut(&mut T, E, &T2::Event, &mut ModelContext<'_, T>) + Send + 'static,
     ) -> Subscription
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
+        T2: 'static + EventEmitter,
+        E: Entity<T2>,
     {
-        let this = self.weak_handle();
-        let handle = handle.downgrade();
+        let this = self.weak_model();
+        let entity_id = entity.entity_id();
+        let entity = entity.downgrade();
         self.app.event_listeners.insert(
-            handle.entity_id,
+            entity_id,
             Box::new(move |event, cx| {
-                let event: &E::Event = event.downcast_ref().expect("invalid event type");
-                if let Some((this, handle)) = this.upgrade().zip(handle.upgrade()) {
+                let event: &T2::Event = event.downcast_ref().expect("invalid event type");
+                if let Some((this, handle)) = this.upgrade().zip(E::upgrade_from(&entity)) {
                     this.update(cx, |this, cx| on_event(this, handle, event, cx));
                     true
                 } else {
@@ -92,7 +95,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,
@@ -106,17 +109,20 @@ impl<'a, T: 'static> ModelContext<'a, T> {
         )
     }
 
-    pub fn observe_release<E: 'static>(
+    pub fn observe_release<T2, E>(
         &mut self,
-        handle: &Handle<E>,
-        mut on_release: impl FnMut(&mut T, &mut E, &mut ModelContext<'_, T>) + Send + Sync + 'static,
+        entity: &E,
+        mut on_release: impl FnMut(&mut T, &mut T2, &mut ModelContext<'_, T>) + Send + 'static,
     ) -> Subscription
     where
-        T: Any + Send + Sync,
+        T: Any + Send,
+        T2: 'static,
+        E: Entity<T2>,
     {
-        let this = self.weak_handle();
+        let entity_id = entity.entity_id();
+        let this = self.weak_model();
         self.app.release_listeners.insert(
-            handle.entity_id,
+            entity_id,
             Box::new(move |entity, cx| {
                 let entity = entity.downcast_mut().expect("invalid entity type");
                 if let Some(this) = this.upgrade() {
@@ -128,12 +134,12 @@ 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();
+        let handle = self.weak_model();
         self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()),
@@ -142,13 +148,13 @@ 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();
+        let handle = self.weak_model();
         self.app.quit_observers.insert(
             (),
             Box::new(move |cx| {
@@ -177,7 +183,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);
@@ -187,26 +193,26 @@ impl<'a, T: 'static> ModelContext<'a, T> {
 
     pub fn spawn<Fut, R>(
         &self,
-        f: impl FnOnce(WeakHandle<T>, AsyncAppContext) -> Fut + Send + 'static,
+        f: impl FnOnce(WeakModel<T>, AsyncAppContext) -> Fut + Send + 'static,
     ) -> Task<R>
     where
         T: 'static,
         Fut: Future<Output = R> + Send + 'static,
         R: Send + 'static,
     {
-        let this = self.weak_handle();
+        let this = self.weak_model();
         self.app.spawn(|cx| f(this, cx))
     }
 
     pub fn spawn_on_main<Fut, R>(
         &self,
-        f: impl FnOnce(WeakHandle<T>, MainThread<AsyncAppContext>) -> Fut + Send + 'static,
+        f: impl FnOnce(WeakModel<T>, MainThread<AsyncAppContext>) -> Fut + Send + 'static,
     ) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,
         R: Send + 'static,
     {
-        let this = self.weak_handle();
+        let this = self.weak_model();
         self.app.spawn_on_main(|cx| f(this, cx))
     }
 }
@@ -214,7 +220,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 {
@@ -225,25 +231,25 @@ where
 }
 
 impl<'a, T> Context for ModelContext<'a, T> {
-    type EntityContext<'b, 'c, U> = ModelContext<'b, U>;
+    type ModelContext<'b, U> = ModelContext<'b, U>;
     type Result<U> = U;
 
-    fn entity<U>(
+    fn build_model<U>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, U>) -> U,
-    ) -> Handle<U>
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, U>) -> U,
+    ) -> Model<U>
     where
-        U: 'static + Send + Sync,
+        U: 'static + Send,
     {
-        self.app.entity(build_entity)
+        self.app.build_model(build_model)
     }
 
-    fn update_entity<U: 'static, R>(
+    fn update_model<U: 'static, R>(
         &mut self,
-        handle: &Handle<U>,
-        update: impl FnOnce(&mut U, &mut Self::EntityContext<'_, '_, U>) -> R,
+        handle: &Model<U>,
+        update: impl FnOnce(&mut U, &mut Self::ModelContext<'_, U>) -> R,
     ) -> R {
-        self.app.update_entity(handle, update)
+        self.app.update_model(handle, update)
     }
 }
 

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

@@ -1,10 +1,10 @@
 use crate::{
-    AnyWindowHandle, AppContext, AsyncAppContext, Context, EventEmitter, Executor, Handle,
-    MainThread, ModelContext, Result, Task, TestDispatcher, TestPlatform, WindowContext,
+    AnyWindowHandle, AppContext, AsyncAppContext, Context, EventEmitter, Executor, MainThread,
+    Model, ModelContext, Result, Task, TestDispatcher, TestPlatform, WindowContext,
 };
 use futures::SinkExt;
 use parking_lot::Mutex;
-use std::{any::Any, future::Future, sync::Arc};
+use std::{future::Future, sync::Arc};
 
 #[derive(Clone)]
 pub struct TestAppContext {
@@ -13,27 +13,27 @@ pub struct TestAppContext {
 }
 
 impl Context for TestAppContext {
-    type EntityContext<'a, 'w, T> = ModelContext<'a, T>;
+    type ModelContext<'a, T> = ModelContext<'a, T>;
     type Result<T> = T;
 
-    fn entity<T: 'static>(
+    fn build_model<T: 'static>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
-    ) -> Self::Result<Handle<T>>
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Self::Result<Model<T>>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
         let mut lock = self.app.lock();
-        lock.entity(build_entity)
+        lock.build_model(build_model)
     }
 
-    fn update_entity<T: 'static, R>(
+    fn update_model<T: 'static, R>(
         &mut self,
-        handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        handle: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
     ) -> Self::Result<R> {
         let mut lock = self.app.lock();
-        lock.update_entity(handle, update)
+        lock.update_model(handle, update)
     }
 }
 
@@ -151,9 +151,9 @@ impl TestAppContext {
         }
     }
 
-    pub fn subscribe<T: 'static + EventEmitter + Send + Sync>(
+    pub fn subscribe<T: 'static + EventEmitter + Send>(
         &mut self,
-        entity: &Handle<T>,
+        entity: &Model<T>,
     ) -> futures::channel::mpsc::UnboundedReceiver<T::Event>
     where
         T::Event: 'static + Send + Clone,
@@ -161,7 +161,7 @@ impl TestAppContext {
         let (mut tx, rx) = futures::channel::mpsc::unbounded();
         entity
             .update(self, |_, cx: &mut ModelContext<T>| {
-                cx.subscribe(&entity, move |_, _, event, cx| {
+                cx.subscribe(entity, move |_, _, event, cx| {
                     cx.executor().block(tx.send(event.clone())).unwrap();
                 })
             })
@@ -169,20 +169,3 @@ impl TestAppContext {
         rx
     }
 }
-
-// pub fn subscribe<T: Entity>(
-//     entity: &impl Handle<T>,
-//     cx: &mut TestAppContext,
-// ) -> Observation<T::Event>
-// where
-//     T::Event: Clone,
-// {
-//     let (tx, rx) = smol::channel::unbounded();
-//     let _subscription = cx.update(|cx| {
-//         cx.subscribe(entity, move |_, event, _| {
-//             let _ = smol::block_on(tx.send(event.clone()));
-//         })
-//     });
-
-//     Observation { rx, _subscription }
-// }

crates/gpui2/src/element.rs 🔗

@@ -4,7 +4,7 @@ pub(crate) use smallvec::SmallVec;
 use std::{any::Any, mem};
 
 pub trait Element<V: 'static> {
-    type ElementState: 'static;
+    type ElementState: 'static + Send;
 
     fn id(&self) -> Option<ElementId>;
 
@@ -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,8 +1,7 @@
 use crate::{AppContext, PlatformDispatcher};
-use futures::{channel::mpsc, pin_mut};
+use futures::{channel::mpsc, pin_mut, FutureExt};
 use smol::prelude::*;
 use std::{
-    borrow::BorrowMut,
     fmt::Debug,
     marker::PhantomData,
     mem,
@@ -181,16 +180,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) {

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 🔗

@@ -24,6 +24,12 @@ mod util;
 mod view;
 mod window;
 
+mod private {
+    /// A mechanism for restricting implementations of a trait to only those in GPUI.
+    /// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/
+    pub trait Sealed {}
+}
+
 pub use action::*;
 pub use anyhow::Result;
 pub use app::*;
@@ -39,6 +45,7 @@ pub use image_cache::*;
 pub use interactive::*;
 pub use keymap::*;
 pub use platform::*;
+use private::Sealed;
 pub use refineable::*;
 pub use scene::*;
 pub use serde;
@@ -67,26 +74,53 @@ 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 ModelContext<'a, T>;
     type Result<T>;
 
-    fn entity<T>(
+    fn build_model<T>(
+        &mut self,
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Self::Result<Model<T>>
+    where
+        T: 'static + Send;
+
+    fn update_model<T: 'static, R>(
+        &mut self,
+        handle: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
+    ) -> Self::Result<R>;
+}
+
+pub trait VisualContext: Context {
+    type ViewContext<'a, 'w, V>;
+
+    fn build_view<V>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
-    ) -> Self::Result<Handle<T>>
+        build_view_state: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V,
+    ) -> Self::Result<View<V>>
     where
-        T: 'static + Send + Sync;
+        V: 'static + Send;
 
-    fn update_entity<T: 'static, R>(
+    fn update_view<V: 'static, R>(
         &mut self,
-        handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        view: &View<V>,
+        update: impl FnOnce(&mut V, &mut Self::ViewContext<'_, '_, V>) -> R,
     ) -> Self::Result<R>;
 }
 
+pub trait Entity<T>: Sealed {
+    type Weak: 'static + Send;
+
+    fn entity_id(&self) -> EntityId;
+    fn downgrade(&self) -> Self::Weak;
+    fn upgrade_from(weak: &Self::Weak) -> Option<Self>
+    where
+        Self: Sized;
+}
+
 pub enum GlobalKey {
     Numeric(usize),
     View(EntityId),
@@ -111,37 +145,37 @@ 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 ModelContext<'a, T> = MainThread<C::ModelContext<'a, T>>;
     type Result<T> = C::Result<T>;
 
-    fn entity<T>(
+    fn build_model<T>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
-    ) -> Self::Result<Handle<T>>
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Self::Result<Model<T>>
     where
-        T: Any + Send + Sync,
+        T: 'static + Send,
     {
-        self.0.entity(|cx| {
+        self.0.build_model(|cx| {
             let cx = unsafe {
                 mem::transmute::<
-                    &mut C::EntityContext<'_, '_, T>,
-                    &mut MainThread<C::EntityContext<'_, '_, T>>,
+                    &mut C::ModelContext<'_, T>,
+                    &mut MainThread<C::ModelContext<'_, T>>,
                 >(cx)
             };
-            build_entity(cx)
+            build_model(cx)
         })
     }
 
-    fn update_entity<T: 'static, R>(
+    fn update_model<T: 'static, R>(
         &mut self,
-        handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        handle: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
     ) -> Self::Result<R> {
-        self.0.update_entity(handle, |entity, cx| {
+        self.0.update_model(handle, |entity, cx| {
             let cx = unsafe {
                 mem::transmute::<
-                    &mut C::EntityContext<'_, '_, T>,
-                    &mut MainThread<C::EntityContext<'_, '_, T>>,
+                    &mut C::ModelContext<'_, T>,
+                    &mut MainThread<C::ModelContext<'_, T>>,
                 >(cx)
             };
             update(entity, cx)
@@ -149,12 +183,50 @@ 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<V>(
+        &mut self,
+        build_view_state: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V,
+    ) -> Self::Result<View<V>>
+    where
+        V: 'static + Send,
+    {
+        self.0.build_view(|cx| {
+            let cx = unsafe {
+                mem::transmute::<
+                    &mut C::ViewContext<'_, '_, V>,
+                    &mut MainThread<C::ViewContext<'_, '_, V>>,
+                >(cx)
+            };
+            build_view_state(cx)
+        })
+    }
+
+    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 +243,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,
-    DispatchContext, DispatchPhase, Element, ElementId, FocusHandle, KeyMatch, Keystroke,
-    Modifiers, Overflow, Pixels, Point, SharedString, Size, Style, StyleRefinement, ViewContext,
+    div, point, px, Action, AnyDrag, AnyView, AppContext, BorrowWindow, Bounds, Component,
+    DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch, Keystroke,
+    Modifiers, Overflow, Pixels, Point, Render, 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
@@ -262,17 +258,17 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
         self
     }
 
-    fn on_drop<S: 'static>(
+    fn on_drop<W: 'static + Send>(
         mut self,
-        listener: impl Fn(&mut V, S, &mut ViewContext<V>) + Send + Sync + 'static,
+        listener: impl Fn(&mut V, View<W>, &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| {
-                listener(view, *drag_state.downcast().unwrap(), cx);
+            TypeId::of::<W>(),
+            Box::new(move |view, dragged_view, cx| {
+                listener(view, dragged_view.downcast().unwrap(), cx);
             }),
         ));
         self
@@ -307,54 +303,39 @@ 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>(
+    fn on_drag<W>(
         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>) -> View<W> + Send + 'static,
     ) -> Self
     where
         Self: Sized,
-        S: Any + Send + Sync,
-        R: Fn(&mut V, &mut ViewContext<V>) -> E,
-        R: 'static + Send + Sync,
-        E: Component<V>,
+        W: 'static + Send + Render,
     {
         debug_assert!(
             self.stateful_interaction().drag_listener.is_none(),
             "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| {
-                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| {
-                        (drag.render_drag_handle)(view_state, cx)
-                    })
-                    .into_any(),
-                );
-                AnyDrag {
-                    drag_handle_view,
-                    cursor_offset,
-                    state: Box::new(drag.state),
-                    state_type: TypeId::of::<S>(),
-                }
+            Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag {
+                view: listener(view_state, cx).into(),
+                cursor_offset,
             }));
         self
     }
 }
 
-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 +350,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 +368,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)
             })
         }
     }
@@ -417,7 +398,7 @@ pub trait ElementInteraction<V: 'static>: 'static + Send + Sync {
         if let Some(drag) = cx.active_drag.take() {
             for (state_type, group_drag_style) in &self.as_stateless().group_drag_over_styles {
                 if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
-                    if *state_type == drag.state_type
+                    if *state_type == drag.view.entity_type()
                         && group_bounds.contains_point(&mouse_position)
                     {
                         style.refine(&group_drag_style.style);
@@ -426,7 +407,8 @@ pub trait ElementInteraction<V: 'static>: 'static + Send + Sync {
             }
 
             for (state_type, drag_over_style) in &self.as_stateless().drag_over_styles {
-                if *state_type == drag.state_type && bounds.contains_point(&mouse_position) {
+                if *state_type == drag.view.entity_type() && bounds.contains_point(&mouse_position)
+                {
                     style.refine(drag_over_style);
                 }
             }
@@ -455,26 +437,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,11 +492,11 @@ 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) =
-                        cx.active_drag.as_ref().map(|drag| drag.state_type)
+                        cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
                     {
                         for (drop_state_type, listener) in &drop_listeners {
                             if *drop_state_type == drag_state_type {
@@ -522,7 +504,7 @@ pub trait ElementInteraction<V: 'static>: 'static + Send + Sync {
                                     .active_drag
                                     .take()
                                     .expect("checked for type drag state type above");
-                                listener(view, drag.state, cx);
+                                listener(view, drag.view.clone(), cx);
                                 cx.notify();
                                 cx.stop_propagation();
                             }
@@ -532,9 +514,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 +672,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, AnyView, &mut ViewContext<V>) + 'static + Send;
 
 pub struct StatelessInteraction<V> {
     pub dispatch_context: DispatchContext,
@@ -703,7 +685,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> {
@@ -871,7 +853,7 @@ pub struct Drag<S, R, V, E>
 where
     R: Fn(&mut V, &mut ViewContext<V>) -> E,
     V: 'static,
-    E: Component<V>,
+    E: Component<()>,
 {
     pub state: S,
     pub render_drag_handle: R,
@@ -882,7 +864,7 @@ impl<S, R, V, E> Drag<S, R, V, E>
 where
     R: Fn(&mut V, &mut ViewContext<V>) -> E,
     V: 'static,
-    E: Component<V>,
+    E: Component<()>,
 {
     pub fn new(state: S, render_drag_handle: R) -> Self {
         Drag {
@@ -893,6 +875,10 @@ where
     }
 }
 
+// impl<S, R, V, E> Render for Drag<S, R, V, E> {
+//     // fn render(&mut self, cx: ViewContext<Self>) ->
+// }
+
 #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
 pub enum MouseButton {
     Left,
@@ -1000,6 +986,14 @@ impl Deref for MouseExitEvent {
 #[derive(Debug, Clone, Default)]
 pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
 
+impl Render for ExternalPaths {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
+        div() // Intentionally left empty because the platform will render icons for the dragged files
+    }
+}
+
 #[derive(Debug, Clone)]
 pub enum FileDropEvent {
     Entered {
@@ -1082,40 +1076,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 +1113,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/view.rs 🔗

@@ -1,47 +1,76 @@
-use parking_lot::Mutex;
-
 use crate::{
-    AnyBox, AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, EntityId, Handle,
-    LayoutId, Pixels, ViewContext, WindowContext,
+    private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
+    BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, LayoutId, Model, Pixels,
+    Size, ViewContext, VisualContext, WeakModel, WindowContext,
 };
-use std::{marker::PhantomData, sync::Arc};
+use anyhow::{Context, Result};
+use std::{any::TypeId, marker::PhantomData};
+
+pub trait Render: 'static + Sized {
+    type Element: Element<Self> + 'static + Send;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
+}
 
 pub struct View<V> {
-    state: Handle<V>,
-    render: Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyElement<V> + Send + Sync + 'static>,
+    pub(crate) model: Model<V>,
 }
 
-impl<V: 'static> View<V> {
-    pub fn into_any(self) -> AnyView {
-        AnyView {
-            view: Arc::new(Mutex::new(self)),
+impl<V> Sealed for View<V> {}
+
+impl<V: 'static> Entity<V> for View<V> {
+    type Weak = WeakView<V>;
+
+    fn entity_id(&self) -> EntityId {
+        self.model.entity_id
+    }
+
+    fn downgrade(&self) -> Self::Weak {
+        WeakView {
+            model: self.model.downgrade(),
         }
     }
+
+    fn upgrade_from(weak: &Self::Weak) -> Option<Self>
+    where
+        Self: Sized,
+    {
+        let model = weak.model.upgrade()?;
+        Some(View { model })
+    }
+}
+
+impl<V: 'static> View<V> {
+    /// Convert this strong view reference into a weak view reference.
+    pub fn downgrade(&self) -> WeakView<V> {
+        Entity::downgrade(self)
+    }
+
+    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)
+    }
+
+    pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
+        self.model.read(cx)
+    }
 }
 
 impl<V> Clone for View<V> {
     fn clone(&self) -> Self {
         Self {
-            state: self.state.clone(),
-            render: self.render.clone(),
+            model: self.model.clone(),
         }
     }
 }
 
-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> {
+impl<V: Render, ParentViewState: 'static> Component<ParentViewState> for View<V> {
     fn render(self) -> AnyElement<ParentViewState> {
         AnyElement::new(EraseViewState {
             view: self,
@@ -50,11 +79,14 @@ impl<V: 'static, ParentViewState: 'static> Component<ParentViewState> for View<V
     }
 }
 
-impl<V: 'static> Element<()> for View<V> {
+impl<V> Element<()> for View<V>
+where
+    V: Render,
+{
     type ElementState = AnyElement<V>;
 
     fn id(&self) -> Option<crate::ElementId> {
-        Some(ElementId::View(self.state.entity_id))
+        Some(ElementId::View(self.model.entity_id))
     }
 
     fn initialize(
@@ -63,8 +95,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 = AnyElement::new(state.render(cx));
             any_element.initialize(state, cx);
             any_element
         })
@@ -76,7 +108,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 +118,34 @@ 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) model: WeakModel<V>,
+}
+
+impl<V: 'static> WeakView<V> {
+    pub fn upgrade(&self) -> Option<View<V>> {
+        Entity::upgrade_from(self)
+    }
+
+    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 {
+            model: self.model.clone(),
+        }
     }
 }
 
@@ -96,15 +155,14 @@ 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> {
+impl<V: Render, ParentV: 'static> Component<ParentV> for EraseViewState<V, ParentV> {
     fn render(self) -> AnyElement<ParentV> {
         AnyElement::new(self)
     }
 }
 
-impl<V: 'static, ParentV: 'static> Element<ParentV> for EraseViewState<V, ParentV> {
+impl<V: Render, ParentV: 'static> Element<ParentV> for EraseViewState<V, ParentV> {
     type ElementState = AnyBox;
 
     fn id(&self) -> Option<crate::ElementId> {
@@ -141,149 +199,212 @@ impl<V: 'static, ParentV: 'static> Element<ParentV> for EraseViewState<V, Parent
 }
 
 trait ViewObject: Send + Sync {
+    fn entity_type(&self) -> TypeId;
     fn entity_id(&self) -> EntityId;
-    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 model(&self) -> AnyModel;
+    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);
+    fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
 }
 
-impl<V: 'static> ViewObject for View<V> {
+impl<V> ViewObject for View<V>
+where
+    V: Render,
+{
+    fn entity_type(&self) -> TypeId {
+        TypeId::of::<V>()
+    }
+
     fn entity_id(&self) -> EntityId {
-        self.state.entity_id
+        Entity::entity_id(self)
     }
 
-    fn initialize(&mut self, cx: &mut WindowContext) -> AnyBox {
-        cx.with_element_id(self.entity_id(), |_global_id, cx| {
-            self.state.update(cx, |state, cx| {
-                let mut any_element = Box::new((self.render)(state, cx));
+    fn model(&self) -> AnyModel {
+        self.model.clone().into_any()
+    }
+
+    fn initialize(&self, cx: &mut WindowContext) -> AnyBox {
+        cx.with_element_id(ViewObject::entity_id(self), |_global_id, cx| {
+            self.update(cx, |state, cx| {
+                let mut any_element = Box::new(AnyElement::new(state.render(cx)));
                 any_element.initialize(state, cx);
-                any_element as AnyBox
+                any_element
             })
         })
     }
 
-    fn layout(&mut self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId {
-        cx.with_element_id(self.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(ViewObject::entity_id(self), |_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.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(ViewObject::entity_id(self), |_global_id, cx| {
+            self.update(cx, |state, cx| {
                 let element = element.downcast_mut::<AnyElement<V>>().unwrap();
                 element.paint(state, cx);
             });
         });
     }
-}
 
-pub struct AnyView {
-    view: Arc<Mutex<dyn ViewObject>>,
-}
-
-impl<ParentV: 'static> Component<ParentV> for AnyView {
-    fn render(self) -> AnyElement<ParentV> {
-        AnyElement::new(EraseAnyViewState {
-            view: self,
-            parent_view_state_type: PhantomData,
-        })
+    fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct(&format!("AnyView<{}>", std::any::type_name::<V>()))
+            .field("entity_id", &ViewObject::entity_id(self).as_u64())
+            .finish()
     }
 }
 
-impl Element<()> for AnyView {
-    type ElementState = AnyBox;
+#[derive(Clone, Debug)]
+pub struct AnyView {
+    model: AnyModel,
+    initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
+    layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
+    paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
+}
 
-    fn id(&self) -> Option<crate::ElementId> {
-        Some(ElementId::View(self.view.lock().entity_id()))
+impl AnyView {
+    pub fn downgrade(&self) -> AnyWeakView {
+        AnyWeakView {
+            model: self.model.downgrade(),
+            initialize: self.initialize,
+            layout: self.layout,
+            paint: self.paint,
+        }
     }
 
-    fn initialize(
-        &mut self,
-        _: &mut (),
-        _: Option<Self::ElementState>,
-        cx: &mut ViewContext<()>,
-    ) -> Self::ElementState {
-        self.view.lock().initialize(cx)
+    pub fn downcast<T: 'static>(self) -> Result<View<T>, Self> {
+        match self.model.downcast() {
+            Ok(model) => Ok(View { model }),
+            Err(model) => Err(Self {
+                model,
+                initialize: self.initialize,
+                layout: self.layout,
+                paint: self.paint,
+            }),
+        }
     }
 
-    fn layout(
-        &mut self,
-        _: &mut (),
-        element: &mut Self::ElementState,
-        cx: &mut ViewContext<()>,
-    ) -> LayoutId {
-        self.view.lock().layout(element, cx)
+    pub(crate) fn entity_type(&self) -> TypeId {
+        self.model.entity_type
     }
 
-    fn paint(
-        &mut self,
-        bounds: Bounds<Pixels>,
-        _: &mut (),
-        element: &mut AnyBox,
-        cx: &mut ViewContext<()>,
-    ) {
-        self.view.lock().paint(bounds, element, cx)
+    pub(crate) fn draw(&self, available_space: Size<AvailableSpace>, cx: &mut WindowContext) {
+        let mut rendered_element = (self.initialize)(self, cx);
+        let layout_id = (self.layout)(self, &mut rendered_element, cx);
+        cx.window
+            .layout_engine
+            .compute_layout(layout_id, available_space);
+        (self.paint)(self, &mut rendered_element, cx);
     }
 }
 
-struct EraseAnyViewState<ParentViewState> {
-    view: AnyView,
-    parent_view_state_type: PhantomData<ParentViewState>,
+impl<V: 'static> Component<V> for AnyView {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
 }
 
-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> {
-        AnyElement::new(self)
+impl<V: Render> From<View<V>> for AnyView {
+    fn from(value: View<V>) -> Self {
+        AnyView {
+            model: value.model.into_any(),
+            initialize: |view, cx| {
+                cx.with_element_id(view.model.entity_id, |_, cx| {
+                    let view = view.clone().downcast::<V>().unwrap();
+                    let element = view.update(cx, |view, cx| {
+                        let mut element = AnyElement::new(view.render(cx));
+                        element.initialize(view, cx);
+                        element
+                    });
+                    Box::new(element)
+                })
+            },
+            layout: |view, element, cx| {
+                cx.with_element_id(view.model.entity_id, |_, cx| {
+                    let view = view.clone().downcast::<V>().unwrap();
+                    let element = element.downcast_mut::<AnyElement<V>>().unwrap();
+                    view.update(cx, |view, cx| element.layout(view, cx))
+                })
+            },
+            paint: |view, element, cx| {
+                cx.with_element_id(view.model.entity_id, |_, cx| {
+                    let view = view.clone().downcast::<V>().unwrap();
+                    let element = element.downcast_mut::<AnyElement<V>>().unwrap();
+                    view.update(cx, |view, cx| element.paint(view, cx))
+                })
+            },
+        }
     }
 }
 
-impl<ParentV: 'static> Element<ParentV> for EraseAnyViewState<ParentV> {
+impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
     type ElementState = AnyBox;
 
-    fn id(&self) -> Option<crate::ElementId> {
-        Element::id(&self.view)
+    fn id(&self) -> Option<ElementId> {
+        Some(self.model.entity_id.into())
     }
 
     fn initialize(
         &mut self,
-        _: &mut ParentV,
-        _: Option<Self::ElementState>,
-        cx: &mut ViewContext<ParentV>,
+        _view_state: &mut ParentViewState,
+        _element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<ParentViewState>,
     ) -> Self::ElementState {
-        self.view.view.lock().initialize(cx)
+        (self.initialize)(self, cx)
     }
 
     fn layout(
         &mut self,
-        _: &mut ParentV,
-        element: &mut Self::ElementState,
-        cx: &mut ViewContext<ParentV>,
+        _view_state: &mut ParentViewState,
+        rendered_element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentViewState>,
     ) -> LayoutId {
-        self.view.view.lock().layout(element, cx)
+        (self.layout)(self, rendered_element, cx)
     }
 
     fn paint(
         &mut self,
-        bounds: Bounds<Pixels>,
-        _: &mut ParentV,
-        element: &mut Self::ElementState,
-        cx: &mut ViewContext<ParentV>,
+        _bounds: Bounds<Pixels>,
+        _view_state: &mut ParentViewState,
+        rendered_element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentViewState>,
     ) {
-        self.view.view.lock().paint(bounds, element, cx)
+        (self.paint)(self, rendered_element, cx)
     }
 }
 
-impl Clone for AnyView {
-    fn clone(&self) -> Self {
-        Self {
-            view: self.view.clone(),
-        }
+pub struct AnyWeakView {
+    model: AnyWeakModel,
+    initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
+    layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
+    paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
+}
+
+impl AnyWeakView {
+    pub fn upgrade(&self) -> Option<AnyView> {
+        let model = self.model.upgrade()?;
+        Some(AnyView {
+            model,
+            initialize: self.initialize,
+            layout: self.layout,
+            paint: self.paint,
+        })
+    }
+}
+
+impl<T, E> Render for T
+where
+    T: 'static + FnMut(&mut WindowContext) -> E,
+    E: 'static + Send + Element<T>,
+{
+    type Element = E;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        (self)(cx)
     }
 }

crates/gpui2/src/window.rs 🔗

@@ -1,14 +1,14 @@
 use crate::{
     px, size, Action, AnyBox, AnyDrag, 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,
+    Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId,
+    Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId,
+    MainThread, MainThreadOnly, Model, 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, WeakHandle, WindowOptions,
-    SUBPIXEL_VARIANTS,
+    TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakModel, WeakView,
+    WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::Result;
 use collections::HashMap;
@@ -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 {
     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,24 +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
     }
 
+    /// 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);
@@ -333,6 +363,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);
@@ -347,6 +378,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,
@@ -364,10 +398,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;
@@ -411,6 +448,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,
@@ -427,6 +467,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,
@@ -437,6 +479,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,
@@ -451,6 +496,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,
     >(
@@ -464,6 +515,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
@@ -474,14 +528,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();
@@ -490,17 +550,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
@@ -509,23 +578,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>,
@@ -553,6 +626,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>,
@@ -579,6 +654,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();
@@ -590,6 +666,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>,
@@ -622,6 +699,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>,
@@ -674,6 +752,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>,
@@ -724,6 +803,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>,
@@ -764,6 +844,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>,
@@ -799,61 +880,40 @@ 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();
-
-            cx.stack(0, |cx| {
-                let available_space = cx.window.content_size.map(Into::into);
-                draw_any_view(&mut root_view, available_space, cx);
-            });
+        self.start_frame();
 
-            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(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);
+                    active_drag.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) {
@@ -891,21 +951,26 @@ 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;
-                    self.active_drag.get_or_insert_with(|| AnyDrag {
-                        drag_handle_view: None,
-                        cursor_offset: position,
-                        state: Box::new(files),
-                        state_type: TypeId::of::<ExternalPaths>(),
-                    });
+                    if self.active_drag.is_none() {
+                        self.active_drag = Some(AnyDrag {
+                            view: self.build_view(|_| files).into(),
+                            cursor_offset: position,
+                        });
+                    }
                     InputEvent::MouseDown(MouseDownEvent {
                         position,
                         button: MouseButton::Left,
@@ -1057,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,
@@ -1079,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_id = self.window.handle.id;
         self.global_observers.insert(
@@ -1170,40 +1238,70 @@ impl<'a, 'w> WindowContext<'a, 'w> {
 }
 
 impl Context for WindowContext<'_, '_> {
-    type EntityContext<'a, 'w, T> = ViewContext<'a, 'w, T>;
+    type ModelContext<'a, T> = ModelContext<'a, T>;
     type Result<T> = T;
 
-    fn entity<T>(
+    fn build_model<T>(
         &mut self,
-        build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
-    ) -> Handle<T>
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Model<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(),
-        ));
-        self.entities.insert(slot, entity)
+        let model = build_model(&mut ModelContext::mutable(&mut *self.app, slot.downgrade()));
+        self.entities.insert(slot, model)
     }
 
-    fn update_entity<T: 'static, R>(
+    fn update_model<T: 'static, R>(
         &mut self,
-        handle: &Handle<T>,
-        update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+        model: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
     ) -> R {
-        let mut entity = self.entities.lease(handle);
+        let mut entity = self.entities.lease(model);
         let result = update(
             &mut *entity,
-            &mut ViewContext::mutable(&mut *self.app, &mut *self.window, handle.downgrade()),
+            &mut ModelContext::mutable(&mut *self.app, model.downgrade()),
         );
         self.entities.end_lease(entity);
         result
     }
 }
 
+impl VisualContext for WindowContext<'_, '_> {
+    type ViewContext<'a, 'w, V> = ViewContext<'a, 'w, V>;
+
+    fn build_view<V>(
+        &mut self,
+        build_view_state: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V,
+    ) -> Self::Result<View<V>>
+    where
+        V: 'static + Send,
+    {
+        let slot = self.app.entities.reserve();
+        let view = View {
+            model: slot.clone(),
+        };
+        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.model);
+        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;
 
@@ -1243,6 +1341,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>,
@@ -1269,6 +1371,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>,
@@ -1281,6 +1385,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>>,
@@ -1297,6 +1403,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
@@ -1305,13 +1412,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
@@ -1341,13 +1452,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)
@@ -1356,6 +1469,7 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         }
     }
 
+    /// Obtain the current content mask.
     fn content_mask(&self) -> ContentMask<Pixels> {
         self.window()
             .content_mask_stack
@@ -1369,6 +1483,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
     }
@@ -1390,7 +1506,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> {
@@ -1418,15 +1534,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 handle(&self) -> WeakHandle<V> {
-        self.view_state.clone()
+    pub fn view(&self) -> WeakView<V> {
+        self.view.clone()
+    }
+
+    pub fn model(&self) -> WeakModel<V> {
+        self.view.model.clone()
     }
 
     pub fn stack<R>(&mut self, order: u32, f: impl FnOnce(&mut Self) -> R) -> R {
@@ -1438,35 +1562,32 @@ 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>(
+    pub fn observe<V2, E>(
         &mut self,
-        handle: &Handle<E>,
-        mut on_notify: impl FnMut(&mut V, Handle<E>, &mut ViewContext<'_, '_, V>)
-            + Send
-            + Sync
-            + 'static,
+        entity: &E,
+        mut on_notify: impl FnMut(&mut V, E, &mut ViewContext<'_, '_, V>) + Send + 'static,
     ) -> Subscription
     where
-        E: 'static,
-        V: Any + Send + Sync,
+        V2: 'static,
+        V: Any + Send,
+        E: Entity<V2>,
     {
-        let this = self.handle();
-        let handle = handle.downgrade();
+        let view = self.view();
+        let entity_id = entity.entity_id();
+        let entity = entity.downgrade();
         let window_handle = self.window.handle;
         self.app.observers.insert(
-            handle.entity_id,
+            entity_id,
             Box::new(move |cx| {
                 cx.update_window(window_handle.id, |cx| {
-                    if let Some(handle) = handle.upgrade() {
-                        this.update(cx, |this, cx| on_notify(this, handle, cx))
+                    if let Some(handle) = E::upgrade_from(&entity) {
+                        view.update(cx, |this, cx| on_notify(this, handle, cx))
                             .is_ok()
                     } else {
                         false
@@ -1477,24 +1598,26 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
         )
     }
 
-    pub fn subscribe<E: EventEmitter>(
+    pub fn subscribe<V2, E>(
         &mut self,
-        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 handle = handle.downgrade();
+        entity: &E,
+        mut on_event: impl FnMut(&mut V, E, &V2::Event, &mut ViewContext<'_, '_, V>) + Send + 'static,
+    ) -> Subscription
+    where
+        V2: EventEmitter,
+        E: Entity<V2>,
+    {
+        let view = self.view();
+        let entity_id = entity.entity_id();
+        let handle = entity.downgrade();
         let window_handle = self.window.handle;
         self.app.event_listeners.insert(
-            handle.entity_id,
+            entity_id,
             Box::new(move |event, cx| {
                 cx.update_window(window_handle.id, |cx| {
-                    if let Some(handle) = handle.upgrade() {
+                    if let Some(handle) = E::upgrade_from(&handle) {
                         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
@@ -1507,11 +1630,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.model.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?")
@@ -1520,23 +1643,25 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
         )
     }
 
-    pub fn observe_release<T: 'static>(
+    pub fn observe_release<V2, E>(
         &mut self,
-        handle: &Handle<T>,
-        mut on_release: impl FnMut(&mut V, &mut T, &mut ViewContext<'_, '_, V>) + Send + Sync + 'static,
+        entity: &E,
+        mut on_release: impl FnMut(&mut V, &mut V2, &mut ViewContext<'_, '_, V>) + Send + 'static,
     ) -> Subscription
     where
-        V: Any + Send + Sync,
+        V: Any + Send,
+        V2: 'static,
+        E: Entity<V2>,
     {
-        let this = self.handle();
+        let view = self.view();
+        let entity_id = entity.entity_id();
         let window_handle = self.window.handle;
         self.app.release_listeners.insert(
-            handle.entity_id,
+            entity_id,
             Box::new(move |entity, cx| {
                 let entity = entity.downcast_mut().expect("invalid entity type");
-                // todo!("are we okay with silently swallowing the error?")
                 let _ = cx.update_window(window_handle.id, |cx| {
-                    this.update(cx, |this, cx| on_release(this, entity, cx))
+                    view.update(cx, |this, cx| on_release(this, entity, cx))
                 });
             }),
         )
@@ -1545,16 +1670,16 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
     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.model.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();
@@ -1563,13 +1688,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,
@@ -1594,8 +1720,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
@@ -1659,29 +1784,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);
@@ -1691,10 +1816,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_id = self.window.handle.id;
-        let handle = self.handle();
+        let handle = self.view();
         self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| {

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

@@ -5,7 +5,7 @@ use crate::language_settings::{
 use crate::Buffer;
 use clock::ReplicaId;
 use collections::BTreeMap;
-use gpui2::{AppContext, Handle};
+use gpui2::{AppContext, Model};
 use gpui2::{Context, TestAppContext};
 use indoc::indoc;
 use proto::deserialize_operation;
@@ -42,7 +42,7 @@ fn init_logger() {
 fn test_line_endings(cx: &mut gpui2::AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|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");
@@ -138,8 +138,8 @@ fn test_edit_events(cx: &mut gpui2::AppContext) {
     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 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcdef"));
+    let buffer2 = cx.build_model(|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();
@@ -218,7 +218,7 @@ fn test_edit_events(cx: &mut gpui2::AppContext) {
 #[gpui2::test]
 async fn test_apply_diff(cx: &mut TestAppContext) {
     let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n";
-    let buffer = cx.entity(|cx| Buffer::new(0, cx.entity_id().as_u64(), text));
+    let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text));
     let anchor = buffer.update(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3)));
 
     let text = "a\nccc\ndddd\nffffff\n";
@@ -250,7 +250,7 @@ async fn test_normalize_whitespace(cx: &mut gpui2::TestAppContext) {
     ]
     .join("\n");
 
-    let buffer = cx.entity(|cx| Buffer::new(0, cx.entity_id().as_u64(), text));
+    let buffer = cx.build_model(|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.
@@ -311,138 +311,138 @@ async fn test_normalize_whitespace(cx: &mut gpui2::TestAppContext) {
     });
 }
 
-// #[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_reparse(cx: &mut gpui2::TestAppContext) {
+    let text = "fn a() {}";
+    let buffer = cx.build_model(|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 buffer = cx.build_model(|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);
@@ -492,7 +492,7 @@ async fn test_outline(cx: &mut gpui2::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.entity(|cx| {
+    let buffer = cx.build_model(|cx| {
         Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx)
     });
     let outline = buffer
@@ -578,7 +578,7 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui2::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.entity(|cx| {
+    let buffer = cx.build_model(|cx| {
         Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx)
     });
     let outline = buffer
@@ -616,7 +616,7 @@ async fn test_outline_with_extra_context(cx: &mut gpui2::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.entity(|cx| {
+    let buffer = cx.build_model(|cx| {
         Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(language), cx)
     });
     let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
@@ -660,7 +660,7 @@ async fn test_symbols_containing(cx: &mut gpui2::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.entity(|cx| {
+    let buffer = cx.build_model(|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());
@@ -881,7 +881,7 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: &
 
 #[gpui2::test]
 fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
-    cx.entity(|cx| {
+    cx.build_model(|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);
@@ -922,7 +922,7 @@ fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
 fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let text = "fn a() {}";
         let mut buffer =
             Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
@@ -965,7 +965,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
         settings.defaults.hard_tabs = Some(true);
     });
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let text = "fn a() {}";
         let mut buffer =
             Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
@@ -1006,7 +1006,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
 fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let entity_id = cx.entity_id();
         let mut buffer = Buffer::new(
             0,
@@ -1080,7 +1080,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
         buffer
     });
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         eprintln!("second buffer: {:?}", cx.entity_id());
 
         let mut buffer = Buffer::new(
@@ -1147,7 +1147,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
 fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let mut buffer = Buffer::new(
             0,
             cx.entity_id().as_u64(),
@@ -1209,7 +1209,7 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap
 fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let mut buffer = Buffer::new(
             0,
             cx.entity_id().as_u64(),
@@ -1266,7 +1266,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
 fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let text = "a\nb";
         let mut buffer =
             Buffer::new(0, cx.entity_id().as_u64(), text).with_language(Arc::new(rust_lang()), cx);
@@ -1284,7 +1284,7 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
 fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let text = "
             const a: usize = 1;
             fn b() {
@@ -1326,7 +1326,7 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
 fn test_autoindent_block_mode(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let text = r#"
             fn a() {
                 b();
@@ -1410,7 +1410,7 @@ fn test_autoindent_block_mode(cx: &mut AppContext) {
 fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let text = r#"
             fn a() {
                 if b() {
@@ -1490,7 +1490,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
 fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let text = "
             * one
                 - a
@@ -1559,7 +1559,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
     language_registry.add(html_language.clone());
     language_registry.add(javascript_language.clone());
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let (text, ranges) = marked_text_ranges(
             &"
                 <div>ˇ
@@ -1610,7 +1610,7 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
         settings.defaults.tab_size = Some(2.try_into().unwrap());
     });
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let mut buffer =
             Buffer::new(0, cx.entity_id().as_u64(), "").with_language(Arc::new(ruby_lang()), cx);
 
@@ -1653,7 +1653,7 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
 fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let language = Language::new(
             LanguageConfig {
                 name: "JavaScript".into(),
@@ -1742,7 +1742,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
 fn test_language_scope_at_with_rust(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let language = Language::new(
             LanguageConfig {
                 name: "Rust".into(),
@@ -1810,7 +1810,7 @@ fn test_language_scope_at_with_rust(cx: &mut AppContext) {
 fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
     init_settings(cx, |_| {});
 
-    cx.entity(|cx| {
+    cx.build_model(|cx| {
         let text = r#"
             <ol>
             <% people.each do |person| %>
@@ -1858,7 +1858,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
 fn test_serialization(cx: &mut gpui2::AppContext) {
     let mut now = Instant::now();
 
-    let buffer1 = cx.entity(|cx| {
+    let buffer1 = cx.build_model(|cx| {
         let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), "abc");
         buffer.edit([(3..3, "D")], None, cx);
 
@@ -1881,7 +1881,7 @@ fn test_serialization(cx: &mut gpui2::AppContext) {
     let ops = cx
         .executor()
         .block(buffer1.read(cx).serialize_ops(None, cx));
-    let buffer2 = cx.entity(|cx| {
+    let buffer2 = cx.build_model(|cx| {
         let mut buffer = Buffer::from_proto(1, state, None).unwrap();
         buffer
             .apply_ops(
@@ -1914,10 +1914,11 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
     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()));
+    let base_buffer =
+        cx.build_model(|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 buffer = cx.build_model(|cx| {
             let state = base_buffer.read(cx).to_proto();
             let ops = cx
                 .executor()
@@ -2034,7 +2035,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
                     new_replica_id,
                     replica_id
                 );
-                new_buffer = Some(cx.entity(|cx| {
+                new_buffer = Some(cx.build_model(|cx| {
                     let mut new_buffer =
                         Buffer::from_proto(new_replica_id, old_buffer_state, None).unwrap();
                     new_buffer
@@ -2396,7 +2397,7 @@ fn javascript_lang() -> Language {
     .unwrap()
 }
 
-fn get_tree_sexp(buffer: &Handle<Buffer>, cx: &mut gpui2::TestAppContext) -> String {
+fn get_tree_sexp(buffer: &Model<Buffer>, cx: &mut gpui2::TestAppContext) -> String {
     buffer.update(cx, |buffer, _| {
         let snapshot = buffer.snapshot();
         let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
@@ -2412,7 +2413,7 @@ fn assert_bracket_pairs(
     cx: &mut AppContext,
 ) {
     let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
-    let buffer = cx.entity(|cx| {
+    let buffer = cx.build_model(|cx| {
         Buffer::new(0, cx.entity_id().as_u64(), expected_text.clone())
             .with_language(Arc::new(language), cx)
     });

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),
         })
     }
 
@@ -228,8 +230,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 +340,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 +379,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 +454,7 @@ impl Default for LanguageConfig {
             overrides: Default::default(),
             collapsed_placeholder: Default::default(),
             word_characters: Default::default(),
+            prettier_parser_name: None,
         }
     }
 }
@@ -498,7 +480,7 @@ pub struct FakeLspAdapter {
     pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp2::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)]
@@ -1602,6 +1584,10 @@ impl Language {
             override_id: None,
         }
     }
+
+    pub fn prettier_parser_name(&self) -> Option<&str> {
+        self.config.prettier_parser_name.as_deref()
+    }
 }
 
 impl LanguageScope {
@@ -1764,7 +1750,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(),
         }
     }
 }
@@ -1822,8 +1808,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/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) {
@@ -622,8 +622,6 @@ impl Drop for RoomDelegate {
 
 pub struct LocalAudioTrack(*const c_void);
 unsafe impl Send for LocalAudioTrack {}
-// todo!(Sync is not ok here. We need to remove it)
-unsafe impl Sync for LocalAudioTrack {}
 
 impl LocalAudioTrack {
     pub fn create() -> Self {
@@ -639,8 +637,6 @@ impl Drop for LocalAudioTrack {
 
 pub struct LocalVideoTrack(*const c_void);
 unsafe impl Send for LocalVideoTrack {}
-// todo!(Sync is not ok here. We need to remove it)
-unsafe impl Sync for LocalVideoTrack {}
 
 impl LocalVideoTrack {
     pub fn screen_share_for_display(display: &MacOSDisplay) -> Self {
@@ -656,8 +652,6 @@ impl Drop for LocalVideoTrack {
 
 pub struct LocalTrackPublication(*const c_void);
 unsafe impl Send for LocalTrackPublication {}
-// todo!(Sync is not ok here. We need to remove it)
-unsafe impl Sync for LocalTrackPublication {}
 
 impl LocalTrackPublication {
     pub fn new(native_track_publication: *const c_void) -> Self {
@@ -702,8 +696,6 @@ impl Drop for LocalTrackPublication {
 pub struct RemoteTrackPublication(*const c_void);
 
 unsafe impl Send for RemoteTrackPublication {}
-// todo!(Sync is not ok here. We need to remove it)
-unsafe impl Sync for RemoteTrackPublication {}
 
 impl RemoteTrackPublication {
     pub fn new(native_track_publication: *const c_void) -> Self {
@@ -761,8 +753,6 @@ pub struct RemoteAudioTrack {
 }
 
 unsafe impl Send for RemoteAudioTrack {}
-// todo!(Sync is not ok here. We need to remove it)
-unsafe impl Sync for RemoteAudioTrack {}
 
 impl RemoteAudioTrack {
     fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
@@ -801,8 +791,6 @@ pub struct RemoteVideoTrack {
 }
 
 unsafe impl Send for RemoteVideoTrack {}
-// todo!(Sync is not ok here. We need to remove it)
-unsafe impl Sync for RemoteVideoTrack {}
 
 impl RemoteVideoTrack {
     fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
@@ -886,8 +874,6 @@ pub enum RemoteAudioTrackUpdate {
 pub struct MacOSDisplay(*const c_void);
 
 unsafe impl Send for MacOSDisplay {}
-// todo!(Sync is not ok here. We need to remove it)
-unsafe impl Sync for MacOSDisplay {}
 
 impl MacOSDisplay {
     fn new(ptr: *const c_void) -> Self {

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/menu2/Cargo.toml 🔗

@@ -0,0 +1,12 @@
+[package]
+name = "menu2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/menu2.rs"
+doctest = false
+
+[dependencies]
+gpui2 = { path = "../gpui2" }

crates/menu2/src/menu2.rs 🔗

@@ -0,0 +1,25 @@
+// todo!(use actions! macro)
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct Cancel;
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct Confirm;
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct SecondaryConfirm;
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct SelectPrev;
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct SelectNext;
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct SelectFirst;
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct SelectLast;
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct ShowContextMenu;

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/prettier/src/prettier_server.js 🔗

@@ -55,8 +55,11 @@ async function handleBuffer(prettier) {
         }
         // allow concurrent request handling by not `await`ing the message handling promise (async function)
         handleMessage(message, prettier).catch(e => {
-            sendResponse({ id: message.id, ...makeError(`error during message handling: ${e}`) });
-        });
+            const errorMessage = message;
+            if ((errorMessage.params || {}).text !== undefined) {
+                errorMessage.params.text = "..snip..";
+            }
+            sendResponse({ id: message.id, ...makeError(`error during message '${JSON.stringify(errorMessage)}' handling: ${e}`) }); });
     }
 }
 
@@ -172,7 +175,7 @@ async function handleMessage(message, prettier) {
         sendResponse({ id, result: null });
     } else if (method === 'initialize') {
         sendResponse({
-            id,
+            id: id || 0,
             result: {
                 "capabilities": {}
             }

crates/prettier2/src/prettier2.rs 🔗

@@ -1,8 +1,8 @@
 use anyhow::Context;
-use collections::{HashMap, HashSet};
+use collections::HashMap;
 use fs2::Fs;
-use gpui2::{AsyncAppContext, Handle};
-use language2::{language_settings::language_settings, Buffer, BundledFormatter, Diff};
+use gpui2::{AsyncAppContext, Model};
+use language2::{language_settings::language_settings, Buffer, Diff};
 use lsp2::{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)
                     }
                 }
             }
@@ -235,134 +183,140 @@ impl Prettier {
 
     pub async fn format(
         &self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         buffer_path: Option<PathBuf>,
         cx: &mut AsyncAppContext,
     ) -> anyhow::Result<Diff> {
         match self {
             Self::Real(local) => {
-                let params = buffer.update(cx, |buffer, cx| {
-                    let buffer_language = buffer.language();
-                    let parsers_with_plugins = buffer_language
-                        .into_iter()
-                        .flat_map(|language| {
-                            language
+                let params = buffer
+                    .update(cx, |buffer, cx| {
+                        let buffer_language = buffer.language();
+                        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.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
-                            },
+                                .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:?}"
                         );
-
-                    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 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:?}");
-                    let plugin_name_into_path = |plugin_name: &str| {
-                        let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
-                        for possible_plugin_path in [
-                            prettier_plugin_dir.join("dist").join("index.mjs"),
-                            prettier_plugin_dir.join("dist").join("index.js"),
-                            prettier_plugin_dir.join("dist").join("plugin.js"),
-                            prettier_plugin_dir.join("index.mjs"),
-                            prettier_plugin_dir.join("index.js"),
-                            prettier_plugin_dir.join("plugin.js"),
-                            prettier_plugin_dir,
-                        ] {
-                            if possible_plugin_path.is_file() {
-                                return Some(possible_plugin_path);
+                        let plugin_name_into_path = |plugin_name: &str| {
+                            let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
+                            for possible_plugin_path in [
+                                prettier_plugin_dir.join("dist").join("index.mjs"),
+                                prettier_plugin_dir.join("dist").join("index.js"),
+                                prettier_plugin_dir.join("dist").join("plugin.js"),
+                                prettier_plugin_dir.join("index.mjs"),
+                                prettier_plugin_dir.join("index.js"),
+                                prettier_plugin_dir.join("plugin.js"),
+                                prettier_plugin_dir,
+                            ] {
+                                if possible_plugin_path.is_file() {
+                                    return Some(possible_plugin_path);
+                                }
                             }
-                        }
-                        None
-                    };
-                    let (parser, located_plugins) = match selected_parser_with_plugins {
-                        Some((parser, plugins)) => {
-                            // Tailwind plugin requires being added last
-                            // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
-                            let mut add_tailwind_back = false;
-
-                            let mut plugins = plugins.into_iter().filter(|&&plugin_name| {
-                                if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
-                                    add_tailwind_back = true;
-                                    false
-                                } else {
-                                    true
+                            None
+                        };
+                        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
+                                let mut add_tailwind_back = false;
+
+                                let mut plugins = plugins
+                                    .into_iter()
+                                    .filter(|&&plugin_name| {
+                                        if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
+                                            add_tailwind_back = true;
+                                            false
+                                        } else {
+                                            true
+                                        }
+                                    })
+                                    .map(|plugin_name| {
+                                        (plugin_name, plugin_name_into_path(plugin_name))
+                                    })
+                                    .collect::<Vec<_>>();
+                                if add_tailwind_back {
+                                    plugins.push((
+                                        &TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME,
+                                        plugin_name_into_path(
+                                            TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME,
+                                        ),
+                                    ));
                                 }
-                            }).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::<Vec<_>>();
-                            if add_tailwind_back {
-                                plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME)));
+                                (Some(parser.to_string()), plugins)
                             }
-                            (Some(parser.to_string()), plugins)
-                        },
-                        None => (None, Vec::new()),
-                    };
-
-                    let prettier_options = if self.is_default() {
-                        let language_settings = language_settings(buffer_language, buffer.file(), cx);
-                        let mut options = language_settings.prettier.clone();
-                        if !options.contains_key("tabWidth") {
-                            options.insert(
-                                "tabWidth".to_string(),
-                                serde_json::Value::Number(serde_json::Number::from(
-                                    language_settings.tab_size.get(),
-                                )),
-                            );
-                        }
-                        if !options.contains_key("printWidth") {
-                            options.insert(
-                                "printWidth".to_string(),
-                                serde_json::Value::Number(serde_json::Number::from(
-                                    language_settings.preferred_line_length,
-                                )),
-                            );
-                        }
-                        Some(options)
-                    } else {
-                        None
-                    };
-
-                    let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| {
-                        match located_plugin_path {
-                            Some(path) => Some(path),
-                            None => {
-                                log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
-                                None},
-                        }
-                    }).collect();
-                    log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
-
-                    anyhow::Ok(FormatParams {
-                        text: buffer.text(),
-                        options: FormatOptions {
-                            parser,
+                            None => (None, Vec::new()),
+                        };
+
+                        let prettier_options = if self.is_default() {
+                            let language_settings =
+                                language_settings(buffer_language, buffer.file(), cx);
+                            let mut options = language_settings.prettier.clone();
+                            if !options.contains_key("tabWidth") {
+                                options.insert(
+                                    "tabWidth".to_string(),
+                                    serde_json::Value::Number(serde_json::Number::from(
+                                        language_settings.tab_size.get(),
+                                    )),
+                                );
+                            }
+                            if !options.contains_key("printWidth") {
+                                options.insert(
+                                    "printWidth".to_string(),
+                                    serde_json::Value::Number(serde_json::Number::from(
+                                        language_settings.preferred_line_length,
+                                    )),
+                                );
+                            }
+                            Some(options)
+                        } else {
+                            None
+                        };
+
+                        let plugins = located_plugins
+                            .into_iter()
+                            .filter_map(|(plugin_name, located_plugin_path)| {
+                                match located_plugin_path {
+                                    Some(path) => Some(path),
+                                    None => {
+                                        log::error!(
+                                            "Have not found plugin path for {:?} inside {:?}",
+                                            plugin_name,
+                                            prettier_node_modules
+                                        );
+                                        None
+                                    }
+                                }
+                            })
+                            .collect();
+                        log::debug!(
+                            "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
                             plugins,
-                            path: buffer_path,
                             prettier_options,
-                        },
-                    })
-                })?.context("prettier params calculation")?;
+                            buffer.file().map(|f| f.full_path(cx))
+                        );
+
+                        anyhow::Ok(FormatParams {
+                            text: buffer.text(),
+                            options: FormatOptions {
+                                parser,
+                                plugins,
+                                path: buffer_path,
+                                prettier_options,
+                            },
+                        })
+                    })?
+                    .context("prettier params calculation")?;
                 let response = local
                     .server
                     .request::<Format>(params)
@@ -374,7 +328,7 @@ impl Prettier {
             #[cfg(any(test, feature = "test-support"))]
             Self::Test(_) => Ok(buffer
                 .update(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/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>;
@@ -160,12 +162,20 @@ pub struct Project {
     copilot_log_subscription: Option<lsp::Subscription>,
     current_lsp_settings: HashMap<Arc<str>, LspSettings>,
     node: Option<Arc<dyn NodeRuntime>>,
+    #[cfg(not(any(test, feature = "test-support")))]
+    default_prettier: Option<DefaultPrettier>,
     prettier_instances: HashMap<
         (Option<WorktreeId>, PathBuf),
         Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
     >,
 }
 
+#[cfg(not(any(test, feature = "test-support")))]
+struct DefaultPrettier {
+    installation_process: Option<Shared<Task<()>>>,
+    installed_plugins: HashSet<&'static str>,
+}
+
 struct DelayedDebounced {
     task: Option<Task<()>>,
     cancel_channel: Option<oneshot::Sender<()>>,
@@ -592,6 +602,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);
@@ -674,6 +685,8 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
                 node: Some(node),
+                #[cfg(not(any(test, feature = "test-support")))]
+                default_prettier: None,
                 prettier_instances: HashMap::default(),
             }
         })
@@ -773,6 +786,8 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
                 node: None,
+                #[cfg(not(any(test, feature = "test-support")))]
+                default_prettier: None,
                 prettier_instances: HashMap::default(),
             };
             for worktree in worktrees {
@@ -835,16 +850,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 +2736,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 +2762,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 +2773,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 +2784,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 +2888,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 +2922,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 +2942,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 +3005,7 @@ impl Project {
                 },
             )
             .detach();
+
         language_server
             .on_request::<lsp::request::RegisterCapability, _, _>({
                 move |params, mut cx| async move {
@@ -3063,6 +3072,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 +3091,7 @@ impl Project {
             )
             .ok();
 
-        Ok(Some(language_server))
+        Ok(language_server)
     }
 
     fn insert_newly_running_language_server(
@@ -7353,6 +7371,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 +8370,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,8 +8507,20 @@ impl Project {
         }
     }
 
+    #[cfg(any(test, feature = "test-support"))]
     fn install_default_formatters(
-        &self,
+        &mut 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(
+        &mut self,
         worktree: Option<WorktreeId>,
         new_language: &Language,
         language_settings: &LanguageSettings,
@@ -8476,66 +8535,122 @@ 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 mut plugins_to_install = prettier_plugins;
+        let (mut install_success_tx, mut install_success_rx) =
+            futures::channel::mpsc::channel::<HashSet<&'static str>>(1);
+        let new_installation_process = cx
+            .spawn(|this, mut cx| async move {
+                if let Some(installed_plugins) = install_success_rx.next().await {
+                    this.update(&mut cx, |this, _| {
+                        let default_prettier =
+                            this.default_prettier
+                                .get_or_insert_with(|| DefaultPrettier {
+                                    installation_process: None,
+                                    installed_plugins: HashSet::default(),
+                                });
+                        if !installed_plugins.is_empty() {
+                            log::info!("Installed new prettier plugins: {installed_plugins:?}");
+                            default_prettier.installed_plugins.extend(installed_plugins);
+                        }
+                    })
+                }
+            })
+            .shared();
+        let previous_installation_process =
+            if let Some(default_prettier) = &mut self.default_prettier {
+                plugins_to_install
+                    .retain(|plugin| !default_prettier.installed_plugins.contains(plugin));
+                if plugins_to_install.is_empty() {
+                    return Task::ready(Ok(()));
+                }
+                std::mem::replace(
+                    &mut default_prettier.installation_process,
+                    Some(new_installation_process.clone()),
+                )
+            } else {
+                None
+            };
+
+        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()))
             .cloned();
-
         let fs = Arc::clone(&self.fs);
-        cx.background()
-            .spawn(async move {
-                let prettier_wrapper_path = default_prettier_dir.join(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:?}"))?;
-
-                let packages_to_versions = future::try_join_all(
-                    prettier_plugins
-                        .iter()
-                        .chain(Some(&"prettier"))
-                        .map(|package_name| async {
-                            let returned_package_name = package_name.to_string();
-                            let latest_version = node.npm_package_latest_version(package_name)
-                                .await
-                                .with_context(|| {
-                                    format!("fetching latest npm version for package {returned_package_name}")
-                                })?;
-                            anyhow::Ok((returned_package_name, latest_version))
-                        }),
-                )
-                .await
-                .context("fetching latest npm versions")?;
+        cx.spawn(|this, mut cx| async move {
+            if let Some(previous_installation_process) = previous_installation_process {
+                previous_installation_process.await;
+            }
+            let mut everything_was_installed = false;
+            this.update(&mut cx, |this, _| {
+                match &mut this.default_prettier {
+                    Some(default_prettier) => {
+                        plugins_to_install
+                            .retain(|plugin| !default_prettier.installed_plugins.contains(plugin));
+                        everything_was_installed = plugins_to_install.is_empty();
+                    },
+                    None => this.default_prettier = Some(DefaultPrettier { installation_process: Some(new_installation_process), installed_plugins: HashSet::default() }),
+                }
+            });
+            if everything_was_installed {
+                return Ok(());
+            }
 
-                log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
-                let borrowed_packages = packages_to_versions.iter().map(|(package, version)| {
-                    (package.as_str(), version.as_str())
-                }).collect::<Vec<_>>();
-                node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?;
+            cx.background()
+                .spawn(async move {
+                    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, &text::Rope::from(prettier::PRETTIER_SERVER_JS), text::LineEnding::Unix).await
+                    .with_context(|| format!("writing {} file at {prettier_wrapper_path:?}", prettier::PRETTIER_SERVER_FILE))?;
 
-                if !prettier_plugins.is_empty() {
-                    if let Some(prettier) = already_running_prettier {
-                        prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?;
+                    let packages_to_versions = future::try_join_all(
+                        plugins_to_install
+                            .iter()
+                            .chain(Some(&"prettier"))
+                            .map(|package_name| async {
+                                let returned_package_name = package_name.to_string();
+                                let latest_version = node.npm_package_latest_version(package_name)
+                                    .await
+                                    .with_context(|| {
+                                        format!("fetching latest npm version for package {returned_package_name}")
+                                    })?;
+                                anyhow::Ok((returned_package_name, latest_version))
+                            }),
+                    )
+                    .await
+                    .context("fetching latest npm versions")?;
+
+                    log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
+                    let borrowed_packages = packages_to_versions.iter().map(|(package, version)| {
+                        (package.as_str(), version.as_str())
+                    }).collect::<Vec<_>>();
+                    node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?;
+                    let installed_packages = !plugins_to_install.is_empty();
+                    install_success_tx.try_send(plugins_to_install).ok();
+
+                    if !installed_packages {
+                        if let Some(prettier) = already_running_prettier {
+                            prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?;
+                        }
                     }
-                }
 
-                anyhow::Ok(())
-            })
+                    anyhow::Ok(())
+                }).await
+        })
     }
 }
 

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
     }
 
@@ -2658,12 +2662,12 @@ impl language::File for File {
 
 impl language::LocalFile for File {
     fn abs_path(&self, cx: &AppContext) -> PathBuf {
-        self.worktree
-            .read(cx)
-            .as_local()
-            .unwrap()
-            .abs_path
-            .join(&self.path)
+        let worktree_path = &self.worktree.read(cx).as_local().unwrap().abs_path;
+        if self.path.as_ref() == Path::new("") {
+            worktree_path.to_path_buf()
+        } else {
+            worktree_path.join(&self.path)
+        }
     }
 
     fn load(&self, cx: &AppContext) -> Task<Result<String>> {
@@ -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/Cargo.toml 🔗

@@ -16,6 +16,7 @@ test-support = [
     "settings2/test-support",
     "text/test-support",
     "prettier2/test-support",
+    "gpui2/test-support",
 ]
 
 [dependencies]

crates/project2/src/lsp_command.rs 🔗

@@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use client2::proto::{self, PeerId};
 use futures::future;
-use gpui2::{AppContext, AsyncAppContext, Handle};
+use gpui2::{AppContext, AsyncAppContext, Model};
 use language2::{
     language_settings::{language_settings, InlayHintKind},
     point_from_lsp, point_to_lsp,
@@ -53,8 +53,8 @@ pub(crate) trait LspCommand: 'static + Sized + Send {
     async fn response_from_lsp(
         self,
         message: <Self::LspRequest as lsp2::request::Request>::Result,
-        project: Handle<Project>,
-        buffer: Handle<Buffer>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
         server_id: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Self::Response>;
@@ -63,8 +63,8 @@ pub(crate) trait LspCommand: 'static + Sized + Send {
 
     async fn from_proto(
         message: Self::ProtoRequest,
-        project: Handle<Project>,
-        buffer: Handle<Buffer>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
         cx: AsyncAppContext,
     ) -> Result<Self>;
 
@@ -79,8 +79,8 @@ pub(crate) trait LspCommand: 'static + Sized + Send {
     async fn response_from_proto(
         self,
         message: <Self::ProtoRequest as proto::RequestMessage>::Response,
-        project: Handle<Project>,
-        buffer: Handle<Buffer>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
         cx: AsyncAppContext,
     ) -> Result<Self::Response>;
 
@@ -180,8 +180,8 @@ impl LspCommand for PrepareRename {
     async fn response_from_lsp(
         self,
         message: Option<lsp2::PrepareRenameResponse>,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         _: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<Option<Range<Anchor>>> {
@@ -215,8 +215,8 @@ impl LspCommand for PrepareRename {
 
     async fn from_proto(
         message: proto::PrepareRename,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let position = message
@@ -256,8 +256,8 @@ impl LspCommand for PrepareRename {
     async fn response_from_proto(
         self,
         message: proto::PrepareRenameResponse,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Option<Range<Anchor>>> {
         if message.can_rename {
@@ -307,8 +307,8 @@ impl LspCommand for PerformRename {
     async fn response_from_lsp(
         self,
         message: Option<lsp2::WorkspaceEdit>,
-        project: Handle<Project>,
-        buffer: Handle<Buffer>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
         server_id: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<ProjectTransaction> {
@@ -343,8 +343,8 @@ impl LspCommand for PerformRename {
 
     async fn from_proto(
         message: proto::PerformRename,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let position = message
@@ -379,8 +379,8 @@ impl LspCommand for PerformRename {
     async fn response_from_proto(
         self,
         message: proto::PerformRenameResponse,
-        project: Handle<Project>,
-        _: Handle<Buffer>,
+        project: Model<Project>,
+        _: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<ProjectTransaction> {
         let message = message
@@ -426,8 +426,8 @@ impl LspCommand for GetDefinition {
     async fn response_from_lsp(
         self,
         message: Option<lsp2::GotoDefinitionResponse>,
-        project: Handle<Project>,
-        buffer: Handle<Buffer>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
         server_id: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Vec<LocationLink>> {
@@ -447,8 +447,8 @@ impl LspCommand for GetDefinition {
 
     async fn from_proto(
         message: proto::GetDefinition,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let position = message
@@ -479,8 +479,8 @@ impl LspCommand for GetDefinition {
     async fn response_from_proto(
         self,
         message: proto::GetDefinitionResponse,
-        project: Handle<Project>,
-        _: Handle<Buffer>,
+        project: Model<Project>,
+        _: Model<Buffer>,
         cx: AsyncAppContext,
     ) -> Result<Vec<LocationLink>> {
         location_links_from_proto(message.links, project, cx).await
@@ -527,8 +527,8 @@ impl LspCommand for GetTypeDefinition {
     async fn response_from_lsp(
         self,
         message: Option<lsp2::GotoTypeDefinitionResponse>,
-        project: Handle<Project>,
-        buffer: Handle<Buffer>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
         server_id: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Vec<LocationLink>> {
@@ -548,8 +548,8 @@ impl LspCommand for GetTypeDefinition {
 
     async fn from_proto(
         message: proto::GetTypeDefinition,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let position = message
@@ -580,8 +580,8 @@ impl LspCommand for GetTypeDefinition {
     async fn response_from_proto(
         self,
         message: proto::GetTypeDefinitionResponse,
-        project: Handle<Project>,
-        _: Handle<Buffer>,
+        project: Model<Project>,
+        _: Model<Buffer>,
         cx: AsyncAppContext,
     ) -> Result<Vec<LocationLink>> {
         location_links_from_proto(message.links, project, cx).await
@@ -593,8 +593,8 @@ impl LspCommand for GetTypeDefinition {
 }
 
 fn language_server_for_buffer(
-    project: &Handle<Project>,
-    buffer: &Handle<Buffer>,
+    project: &Model<Project>,
+    buffer: &Model<Buffer>,
     server_id: LanguageServerId,
     cx: &mut AsyncAppContext,
 ) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
@@ -609,7 +609,7 @@ fn language_server_for_buffer(
 
 async fn location_links_from_proto(
     proto_links: Vec<proto::LocationLink>,
-    project: Handle<Project>,
+    project: Model<Project>,
     mut cx: AsyncAppContext,
 ) -> Result<Vec<LocationLink>> {
     let mut links = Vec::new();
@@ -671,8 +671,8 @@ async fn location_links_from_proto(
 
 async fn location_links_from_lsp(
     message: Option<lsp2::GotoDefinitionResponse>,
-    project: Handle<Project>,
-    buffer: Handle<Buffer>,
+    project: Model<Project>,
+    buffer: Model<Buffer>,
     server_id: LanguageServerId,
     mut cx: AsyncAppContext,
 ) -> Result<Vec<LocationLink>> {
@@ -814,8 +814,8 @@ impl LspCommand for GetReferences {
     async fn response_from_lsp(
         self,
         locations: Option<Vec<lsp2::Location>>,
-        project: Handle<Project>,
-        buffer: Handle<Buffer>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
         server_id: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<Location>> {
@@ -868,8 +868,8 @@ impl LspCommand for GetReferences {
 
     async fn from_proto(
         message: proto::GetReferences,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let position = message
@@ -910,8 +910,8 @@ impl LspCommand for GetReferences {
     async fn response_from_proto(
         self,
         message: proto::GetReferencesResponse,
-        project: Handle<Project>,
-        _: Handle<Buffer>,
+        project: Model<Project>,
+        _: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<Location>> {
         let mut locations = Vec::new();
@@ -977,8 +977,8 @@ impl LspCommand for GetDocumentHighlights {
     async fn response_from_lsp(
         self,
         lsp_highlights: Option<Vec<lsp2::DocumentHighlight>>,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         _: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<DocumentHighlight>> {
@@ -1016,8 +1016,8 @@ impl LspCommand for GetDocumentHighlights {
 
     async fn from_proto(
         message: proto::GetDocumentHighlights,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let position = message
@@ -1060,8 +1060,8 @@ impl LspCommand for GetDocumentHighlights {
     async fn response_from_proto(
         self,
         message: proto::GetDocumentHighlightsResponse,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<DocumentHighlight>> {
         let mut highlights = Vec::new();
@@ -1123,8 +1123,8 @@ impl LspCommand for GetHover {
     async fn response_from_lsp(
         self,
         message: Option<lsp2::Hover>,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         _: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<Self::Response> {
@@ -1206,8 +1206,8 @@ impl LspCommand for GetHover {
 
     async fn from_proto(
         message: Self::ProtoRequest,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let position = message
@@ -1272,8 +1272,8 @@ impl LspCommand for GetHover {
     async fn response_from_proto(
         self,
         message: proto::GetHoverResponse,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self::Response> {
         let contents: Vec<_> = message
@@ -1341,8 +1341,8 @@ impl LspCommand for GetCompletions {
     async fn response_from_lsp(
         self,
         completions: Option<lsp2::CompletionResponse>,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         server_id: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<Completion>> {
@@ -1484,8 +1484,8 @@ impl LspCommand for GetCompletions {
 
     async fn from_proto(
         message: proto::GetCompletions,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let version = deserialize_version(&message.version);
@@ -1523,8 +1523,8 @@ impl LspCommand for GetCompletions {
     async fn response_from_proto(
         self,
         message: proto::GetCompletionsResponse,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<Completion>> {
         buffer
@@ -1589,8 +1589,8 @@ impl LspCommand for GetCodeActions {
     async fn response_from_lsp(
         self,
         actions: Option<lsp2::CodeActionResponse>,
-        _: Handle<Project>,
-        _: Handle<Buffer>,
+        _: Model<Project>,
+        _: Model<Buffer>,
         server_id: LanguageServerId,
         _: AsyncAppContext,
     ) -> Result<Vec<CodeAction>> {
@@ -1623,8 +1623,8 @@ impl LspCommand for GetCodeActions {
 
     async fn from_proto(
         message: proto::GetCodeActions,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let start = message
@@ -1663,8 +1663,8 @@ impl LspCommand for GetCodeActions {
     async fn response_from_proto(
         self,
         message: proto::GetCodeActionsResponse,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<CodeAction>> {
         buffer
@@ -1726,8 +1726,8 @@ impl LspCommand for OnTypeFormatting {
     async fn response_from_lsp(
         self,
         message: Option<Vec<lsp2::TextEdit>>,
-        project: Handle<Project>,
-        buffer: Handle<Buffer>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
         server_id: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<Option<Transaction>> {
@@ -1763,8 +1763,8 @@ impl LspCommand for OnTypeFormatting {
 
     async fn from_proto(
         message: proto::OnTypeFormatting,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let position = message
@@ -1805,8 +1805,8 @@ impl LspCommand for OnTypeFormatting {
     async fn response_from_proto(
         self,
         message: proto::OnTypeFormattingResponse,
-        _: Handle<Project>,
-        _: Handle<Buffer>,
+        _: Model<Project>,
+        _: Model<Buffer>,
         _: AsyncAppContext,
     ) -> Result<Option<Transaction>> {
         let Some(transaction) = message.transaction else {
@@ -1825,7 +1825,7 @@ impl LspCommand for OnTypeFormatting {
 impl InlayHints {
     pub async fn lsp_to_project_hint(
         lsp_hint: lsp2::InlayHint,
-        buffer_handle: &Handle<Buffer>,
+        buffer_handle: &Model<Buffer>,
         server_id: LanguageServerId,
         resolve_state: ResolveState,
         force_no_type_left_padding: bool,
@@ -2230,8 +2230,8 @@ impl LspCommand for InlayHints {
     async fn response_from_lsp(
         self,
         message: Option<Vec<lsp2::InlayHint>>,
-        project: Handle<Project>,
-        buffer: Handle<Buffer>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
         server_id: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> anyhow::Result<Vec<InlayHint>> {
@@ -2286,8 +2286,8 @@ impl LspCommand for InlayHints {
 
     async fn from_proto(
         message: proto::InlayHints,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self> {
         let start = message
@@ -2326,8 +2326,8 @@ impl LspCommand for InlayHints {
     async fn response_from_proto(
         self,
         message: proto::InlayHintsResponse,
-        _: Handle<Project>,
-        buffer: Handle<Buffer>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
         mut cx: AsyncAppContext,
     ) -> anyhow::Result<Vec<InlayHint>> {
         buffer

crates/project2/src/project2.rs 🔗

@@ -26,8 +26,8 @@ use futures::{
 };
 use globset::{Glob, GlobSet, GlobSetBuilder};
 use gpui2::{
-    AnyHandle, AppContext, AsyncAppContext, Context, EventEmitter, Executor, Handle, ModelContext,
-    Task, WeakHandle,
+    AnyModel, AppContext, AsyncAppContext, Context, Entity, EventEmitter, Executor, Model,
+    ModelContext, Task, WeakModel,
 };
 use itertools::Itertools;
 use language2::{
@@ -39,11 +39,11 @@ use language2::{
         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 lsp2::{
@@ -54,7 +54,7 @@ use lsp_command::*;
 use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
 use postage::watch;
-use prettier2::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS};
+use prettier2::{LocateStart, Prettier};
 use project_settings::{LspSettings, ProjectSettings};
 use rand::prelude::*;
 use search::SearchQuery;
@@ -80,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 fs2::*;
+#[cfg(any(test, feature = "test-support"))]
+pub use prettier2::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>;
@@ -126,7 +127,7 @@ pub struct Project {
     next_entry_id: Arc<AtomicUsize>,
     join_project_response_message_id: u32,
     next_diagnostic_group_id: usize,
-    user_store: Handle<UserStore>,
+    user_store: Model<UserStore>,
     fs: Arc<dyn Fs>,
     client_state: Option<ProjectClientState>,
     collaborators: HashMap<proto::PeerId, Collaborator>,
@@ -138,20 +139,20 @@ pub struct Project {
     #[allow(clippy::type_complexity)]
     loading_buffers_by_path: HashMap<
         ProjectPath,
-        postage::watch::Receiver<Option<Result<Handle<Buffer>, Arc<anyhow::Error>>>>,
+        postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
     >,
     #[allow(clippy::type_complexity)]
     loading_local_worktrees:
-        HashMap<Arc<Path>, Shared<Task<Result<Handle<Worktree>, Arc<anyhow::Error>>>>>,
+        HashMap<Arc<Path>, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
     opened_buffers: HashMap<u64, OpenBuffer>,
     local_buffer_ids_by_path: HashMap<ProjectPath, u64>,
     local_buffer_ids_by_entry_id: HashMap<ProjectEntryId, u64>,
     /// A mapping from a buffer ID to None means that we've started waiting for an ID but haven't finished loading it.
     /// Used for re-issuing buffer requests when peers temporarily disconnect
-    incomplete_remote_buffers: HashMap<u64, Option<Handle<Buffer>>>,
+    incomplete_remote_buffers: HashMap<u64, Option<Model<Buffer>>>,
     buffer_snapshots: HashMap<u64, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
     buffers_being_formatted: HashSet<u64>,
-    buffers_needing_diff: HashSet<WeakHandle<Buffer>>,
+    buffers_needing_diff: HashSet<WeakModel<Buffer>>,
     git_diff_debouncer: DelayedDebounced,
     nonce: u128,
     _maintain_buffer_languages: Task<()>,
@@ -161,12 +162,20 @@ pub struct Project {
     copilot_log_subscription: Option<lsp2::Subscription>,
     current_lsp_settings: HashMap<Arc<str>, LspSettings>,
     node: Option<Arc<dyn NodeRuntime>>,
+    #[cfg(not(any(test, feature = "test-support")))]
+    default_prettier: Option<DefaultPrettier>,
     prettier_instances: HashMap<
         (Option<WorktreeId>, PathBuf),
         Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
     >,
 }
 
+#[cfg(not(any(test, feature = "test-support")))]
+struct DefaultPrettier {
+    installation_process: Option<Shared<Task<()>>>,
+    installed_plugins: HashSet<&'static str>,
+}
+
 struct DelayedDebounced {
     task: Option<Task<()>>,
     cancel_channel: Option<oneshot::Sender<()>>,
@@ -242,15 +251,15 @@ enum LocalProjectUpdate {
 }
 
 enum OpenBuffer {
-    Strong(Handle<Buffer>),
-    Weak(WeakHandle<Buffer>),
+    Strong(Model<Buffer>),
+    Weak(WeakModel<Buffer>),
     Operations(Vec<Operation>),
 }
 
 #[derive(Clone)]
 enum WorktreeHandle {
-    Strong(Handle<Worktree>),
-    Weak(WeakHandle<Worktree>),
+    Strong(Model<Worktree>),
+    Weak(WeakModel<Worktree>),
 }
 
 enum ProjectClientState {
@@ -342,7 +351,7 @@ pub struct DiagnosticSummary {
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Location {
-    pub buffer: Handle<Buffer>,
+    pub buffer: Model<Buffer>,
     pub range: Range<language2::Anchor>,
 }
 
@@ -455,7 +464,7 @@ impl Hover {
 }
 
 #[derive(Default)]
-pub struct ProjectTransaction(pub HashMap<Handle<Buffer>, language2::Transaction>);
+pub struct ProjectTransaction(pub HashMap<Model<Buffer>, language2::Transaction>);
 
 impl DiagnosticSummary {
     fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> Self {
@@ -525,7 +534,7 @@ pub enum FormatTrigger {
 }
 
 struct ProjectLspAdapterDelegate {
-    project: Handle<Project>,
+    project: Model<Project>,
     http_client: Arc<dyn HttpClient>,
 }
 
@@ -541,7 +550,7 @@ impl FormatTrigger {
 #[derive(Clone, Debug, PartialEq)]
 enum SearchMatchCandidate {
     OpenBuffer {
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         // This might be an unnamed file without representation on filesystem
         path: Option<Arc<Path>>,
     },
@@ -619,12 +628,12 @@ impl Project {
     pub fn local(
         client: Arc<Client>,
         node: Arc<dyn NodeRuntime>,
-        user_store: Handle<UserStore>,
+        user_store: Model<UserStore>,
         languages: Arc<LanguageRegistry>,
         fs: Arc<dyn Fs>,
         cx: &mut AppContext,
-    ) -> Handle<Self> {
-        cx.entity(|cx: &mut ModelContext<Self>| {
+    ) -> Model<Self> {
+        cx.build_model(|cx: &mut ModelContext<Self>| {
             let (tx, rx) = mpsc::unbounded();
             cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
                 .detach();
@@ -677,6 +686,8 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
                 node: Some(node),
+                #[cfg(not(any(test, feature = "test-support")))]
+                default_prettier: None,
                 prettier_instances: HashMap::default(),
             }
         })
@@ -685,11 +696,11 @@ impl Project {
     pub async fn remote(
         remote_id: u64,
         client: Arc<Client>,
-        user_store: Handle<UserStore>,
+        user_store: Model<UserStore>,
         languages: Arc<LanguageRegistry>,
         fs: Arc<dyn Fs>,
         mut cx: AsyncAppContext,
-    ) -> Result<Handle<Self>> {
+    ) -> Result<Model<Self>> {
         client.authenticate_and_connect(true, &cx).await?;
 
         let subscription = client.subscribe_to_entity(remote_id)?;
@@ -698,7 +709,7 @@ impl Project {
                 project_id: remote_id,
             })
             .await?;
-        let this = cx.entity(|cx| {
+        let this = cx.build_model(|cx| {
             let replica_id = response.payload.replica_id as ReplicaId;
 
             let mut worktrees = Vec::new();
@@ -778,6 +789,8 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
                 node: None,
+                #[cfg(not(any(test, feature = "test-support")))]
+                default_prettier: None,
                 prettier_instances: HashMap::default(),
             };
             for worktree in worktrees {
@@ -847,12 +860,12 @@ impl Project {
         fs: Arc<dyn Fs>,
         root_paths: impl IntoIterator<Item = &Path>,
         cx: &mut gpui2::TestAppContext,
-    ) -> Handle<Project> {
+    ) -> Model<Project> {
         let mut languages = LanguageRegistry::test();
         languages.set_executor(cx.executor().clone());
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = cx.update(|cx| client2::Client::new(http_client.clone(), cx));
-        let user_store = cx.entity(|cx| UserStore::new(client.clone(), http_client, cx));
+        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let project = cx.update(|cx| {
             Project::local(
                 client,
@@ -876,16 +889,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();
@@ -989,7 +992,7 @@ impl Project {
         cx.notify();
     }
 
-    pub fn buffer_for_id(&self, remote_id: u64) -> Option<Handle<Buffer>> {
+    pub fn buffer_for_id(&self, remote_id: u64) -> Option<Model<Buffer>> {
         self.opened_buffers
             .get(&remote_id)
             .and_then(|buffer| buffer.upgrade())
@@ -1003,11 +1006,11 @@ impl Project {
         self.client.clone()
     }
 
-    pub fn user_store(&self) -> Handle<UserStore> {
+    pub fn user_store(&self) -> Model<UserStore> {
         self.user_store.clone()
     }
 
-    pub fn opened_buffers(&self) -> Vec<Handle<Buffer>> {
+    pub fn opened_buffers(&self) -> Vec<Model<Buffer>> {
         self.opened_buffers
             .values()
             .filter_map(|b| b.upgrade())
@@ -1069,7 +1072,7 @@ impl Project {
     }
 
     /// Collect all worktrees, including ones that don't appear in the project panel
-    pub fn worktrees<'a>(&'a self) -> impl 'a + DoubleEndedIterator<Item = Handle<Worktree>> {
+    pub fn worktrees<'a>(&'a self) -> impl 'a + DoubleEndedIterator<Item = Model<Worktree>> {
         self.worktrees
             .iter()
             .filter_map(move |worktree| worktree.upgrade())
@@ -1079,7 +1082,7 @@ impl Project {
     pub fn visible_worktrees<'a>(
         &'a self,
         cx: &'a AppContext,
-    ) -> impl 'a + DoubleEndedIterator<Item = Handle<Worktree>> {
+    ) -> impl 'a + DoubleEndedIterator<Item = Model<Worktree>> {
         self.worktrees.iter().filter_map(|worktree| {
             worktree.upgrade().and_then(|worktree| {
                 if worktree.read(cx).is_visible() {
@@ -1096,7 +1099,7 @@ impl Project {
             .map(|tree| tree.read(cx).root_name())
     }
 
-    pub fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Handle<Worktree>> {
+    pub fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Model<Worktree>> {
         self.worktrees()
             .find(|worktree| worktree.read(cx).id() == id)
     }
@@ -1105,7 +1108,7 @@ impl Project {
         &self,
         entry_id: ProjectEntryId,
         cx: &AppContext,
-    ) -> Option<Handle<Worktree>> {
+    ) -> Option<Model<Worktree>> {
         self.worktrees()
             .find(|worktree| worktree.read(cx).contains_entry(entry_id))
     }
@@ -1660,12 +1663,12 @@ impl Project {
         text: &str,
         language: Option<Arc<Language>>,
         cx: &mut ModelContext<Self>,
-    ) -> Result<Handle<Buffer>> {
+    ) -> Result<Model<Buffer>> {
         if self.is_remote() {
             return Err(anyhow!("creating buffers as a guest is not supported yet"));
         }
         let id = post_inc(&mut self.next_buffer_id);
-        let buffer = cx.entity(|cx| {
+        let buffer = cx.build_model(|cx| {
             Buffer::new(self.replica_id(), id, text).with_language(
                 language.unwrap_or_else(|| language2::PLAIN_TEXT.clone()),
                 cx,
@@ -1679,7 +1682,7 @@ impl Project {
         &mut self,
         path: impl Into<ProjectPath>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<(ProjectEntryId, AnyHandle)>> {
+    ) -> Task<Result<(ProjectEntryId, AnyModel)>> {
         let task = self.open_buffer(path, cx);
         cx.spawn(move |_, mut cx| async move {
             let buffer = task.await?;
@@ -1689,7 +1692,7 @@ impl Project {
                 })?
                 .ok_or_else(|| anyhow!("no project entry"))?;
 
-            let buffer: &AnyHandle = &buffer;
+            let buffer: &AnyModel = &buffer;
             Ok((project_entry_id, buffer.clone()))
         })
     }
@@ -1698,7 +1701,7 @@ impl Project {
         &mut self,
         abs_path: impl AsRef<Path>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Buffer>>> {
+    ) -> Task<Result<Model<Buffer>>> {
         if let Some((worktree, relative_path)) = self.find_local_worktree(abs_path.as_ref(), cx) {
             self.open_buffer((worktree.read(cx).id(), relative_path), cx)
         } else {
@@ -1710,7 +1713,7 @@ impl Project {
         &mut self,
         path: impl Into<ProjectPath>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Buffer>>> {
+    ) -> Task<Result<Model<Buffer>>> {
         let project_path = path.into();
         let worktree = if let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) {
             worktree
@@ -1765,9 +1768,9 @@ impl Project {
     fn open_local_buffer_internal(
         &mut self,
         path: &Arc<Path>,
-        worktree: &Handle<Worktree>,
+        worktree: &Model<Worktree>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Buffer>>> {
+    ) -> Task<Result<Model<Buffer>>> {
         let buffer_id = post_inc(&mut self.next_buffer_id);
         let load_buffer = worktree.update(cx, |worktree, cx| {
             let worktree = worktree.as_local_mut().unwrap();
@@ -1783,9 +1786,9 @@ impl Project {
     fn open_remote_buffer_internal(
         &mut self,
         path: &Arc<Path>,
-        worktree: &Handle<Worktree>,
+        worktree: &Model<Worktree>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Buffer>>> {
+    ) -> Task<Result<Model<Buffer>>> {
         let rpc = self.client.clone();
         let project_id = self.remote_id().unwrap();
         let remote_worktree_id = worktree.read(cx).id();
@@ -1813,7 +1816,7 @@ impl Project {
         language_server_id: LanguageServerId,
         language_server_name: LanguageServerName,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Buffer>>> {
+    ) -> Task<Result<Model<Buffer>>> {
         cx.spawn(move |this, mut cx| async move {
             let abs_path = abs_path
                 .to_file_path()
@@ -1851,7 +1854,7 @@ impl Project {
         &mut self,
         id: u64,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Buffer>>> {
+    ) -> Task<Result<Model<Buffer>>> {
         if let Some(buffer) = self.buffer_for_id(id) {
             Task::ready(Ok(buffer))
         } else if self.is_local() {
@@ -1874,7 +1877,7 @@ impl Project {
 
     pub fn save_buffers(
         &self,
-        buffers: HashSet<Handle<Buffer>>,
+        buffers: HashSet<Model<Buffer>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         cx.spawn(move |this, mut cx| async move {
@@ -1889,7 +1892,7 @@ impl Project {
 
     pub fn save_buffer(
         &self,
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
         let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
@@ -1905,7 +1908,7 @@ impl Project {
 
     pub fn save_buffer_as(
         &mut self,
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         abs_path: PathBuf,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
@@ -1941,7 +1944,7 @@ impl Project {
         &mut self,
         path: &ProjectPath,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Handle<Buffer>> {
+    ) -> Option<Model<Buffer>> {
         let worktree = self.worktree_for_id(path.worktree_id, cx)?;
         self.opened_buffers.values().find_map(|buffer| {
             let buffer = buffer.upgrade()?;
@@ -1956,7 +1959,7 @@ impl Project {
 
     fn register_buffer(
         &mut self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
         self.request_buffer_diff_recalculation(buffer, cx);
@@ -2038,7 +2041,7 @@ impl Project {
 
     fn register_buffer_with_language_servers(
         &mut self,
-        buffer_handle: &Handle<Buffer>,
+        buffer_handle: &Model<Buffer>,
         cx: &mut ModelContext<Self>,
     ) {
         let buffer = buffer_handle.read(cx);
@@ -2122,7 +2125,7 @@ impl Project {
 
     fn unregister_buffer_from_language_servers(
         &mut self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         old_file: &File,
         cx: &mut ModelContext<Self>,
     ) {
@@ -2157,7 +2160,7 @@ impl Project {
 
     fn register_buffer_with_copilot(
         &self,
-        buffer_handle: &Handle<Buffer>,
+        buffer_handle: &Model<Buffer>,
         cx: &mut ModelContext<Self>,
     ) {
         if let Some(copilot) = Copilot::global(cx) {
@@ -2166,7 +2169,7 @@ impl Project {
     }
 
     async fn send_buffer_ordered_messages(
-        this: WeakHandle<Self>,
+        this: WeakModel<Self>,
         rx: UnboundedReceiver<BufferOrderedMessage>,
         mut cx: AsyncAppContext,
     ) -> Result<()> {
@@ -2174,7 +2177,7 @@ impl Project {
 
         let mut operations_by_buffer_id = HashMap::default();
         async fn flush_operations(
-            this: &WeakHandle<Project>,
+            this: &WeakModel<Project>,
             operations_by_buffer_id: &mut HashMap<u64, Vec<proto::Operation>>,
             needs_resync_with_host: &mut bool,
             is_local: bool,
@@ -2275,7 +2278,7 @@ impl Project {
 
     fn on_buffer_event(
         &mut self,
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         event: &BufferEvent,
         cx: &mut ModelContext<Self>,
     ) -> Option<()> {
@@ -2488,7 +2491,7 @@ impl Project {
 
     fn request_buffer_diff_recalculation(
         &mut self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         cx: &mut ModelContext<Self>,
     ) {
         self.buffers_needing_diff.insert(buffer.downgrade());
@@ -2499,7 +2502,7 @@ impl Project {
             delay
         } else {
             if first_insertion {
-                let this = cx.weak_handle();
+                let this = cx.weak_model();
                 cx.defer(move |cx| {
                     if let Some(this) = this.upgrade() {
                         this.update(cx, |this, cx| {
@@ -2684,7 +2687,7 @@ impl Project {
 
     fn detect_language_for_buffer(
         &mut self,
-        buffer_handle: &Handle<Buffer>,
+        buffer_handle: &Model<Buffer>,
         cx: &mut ModelContext<Self>,
     ) -> Option<()> {
         // If the buffer has a language, set it and start the language server if we haven't already.
@@ -2702,7 +2705,7 @@ impl Project {
 
     pub fn set_language_for_buffer(
         &mut self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         new_language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
@@ -2743,7 +2746,7 @@ impl Project {
 
     fn start_language_servers(
         &mut self,
-        worktree: &Handle<Worktree>,
+        worktree: &Model<Worktree>,
         worktree_path: Arc<Path>,
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
@@ -2774,6 +2777,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 +2840,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
                     }
                 }
@@ -2929,7 +2942,7 @@ impl Project {
     }
 
     async fn setup_and_insert_language_server(
-        this: WeakHandle<Self>,
+        this: WeakModel<Self>,
         initialization_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
@@ -2968,7 +2981,7 @@ impl Project {
     }
 
     async fn setup_pending_language_server(
-        this: WeakHandle<Self>,
+        this: WeakModel<Self>,
         initialization_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
@@ -2984,8 +2997,8 @@ impl Project {
                 let this = this.clone();
                 move |mut params, mut cx| {
                     let adapter = adapter.clone();
-                    adapter.process_diagnostics(&mut params);
                     if let Some(this) = this.upgrade() {
+                        adapter.process_diagnostics(&mut params);
                         this.update(&mut cx, |this, cx| {
                             this.update_diagnostics(
                                 server_id,
@@ -3348,10 +3361,10 @@ impl Project {
 
     pub fn restart_language_servers_for_buffers(
         &mut self,
-        buffers: impl IntoIterator<Item = Handle<Buffer>>,
+        buffers: impl IntoIterator<Item = Model<Buffer>>,
         cx: &mut ModelContext<Self>,
     ) -> Option<()> {
-        let language_server_lookup_info: HashSet<(Handle<Worktree>, Arc<Language>)> = buffers
+        let language_server_lookup_info: HashSet<(Model<Worktree>, Arc<Language>)> = buffers
             .into_iter()
             .filter_map(|buffer| {
                 let buffer = buffer.read(cx);
@@ -3375,7 +3388,7 @@ impl Project {
     // TODO This will break in the case where the adapter's root paths and worktrees are not equal
     fn restart_language_servers(
         &mut self,
-        worktree: Handle<Worktree>,
+        worktree: Model<Worktree>,
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
@@ -3746,7 +3759,7 @@ impl Project {
     }
 
     async fn on_lsp_workspace_edit(
-        this: WeakHandle<Self>,
+        this: WeakModel<Self>,
         params: lsp2::ApplyWorkspaceEditParams,
         server_id: LanguageServerId,
         adapter: Arc<CachedLspAdapter>,
@@ -3947,7 +3960,7 @@ impl Project {
 
     fn update_buffer_diagnostics(
         &mut self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         server_id: LanguageServerId,
         version: Option<i32>,
         mut diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
@@ -4020,7 +4033,7 @@ impl Project {
 
     pub fn reload_buffers(
         &self,
-        buffers: HashSet<Handle<Buffer>>,
+        buffers: HashSet<Model<Buffer>>,
         push_to_history: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ProjectTransaction>> {
@@ -4086,7 +4099,7 @@ impl Project {
 
     pub fn format(
         &self,
-        buffers: HashSet<Handle<Buffer>>,
+        buffers: HashSet<Model<Buffer>>,
         push_to_history: bool,
         trigger: FormatTrigger,
         cx: &mut ModelContext<Project>,
@@ -4358,8 +4371,8 @@ impl Project {
     }
 
     async fn format_via_lsp(
-        this: &WeakHandle<Self>,
-        buffer: &Handle<Buffer>,
+        this: &WeakModel<Self>,
+        buffer: &Model<Buffer>,
         abs_path: &Path,
         language_server: &Arc<LanguageServer>,
         tab_size: NonZeroU32,
@@ -4408,7 +4421,7 @@ impl Project {
     }
 
     async fn format_via_external_command(
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         buffer_abs_path: &Path,
         command: &str,
         arguments: &[String],
@@ -4468,7 +4481,7 @@ impl Project {
 
     pub fn definition<T: ToPointUtf16>(
         &self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<LocationLink>>> {
@@ -4483,7 +4496,7 @@ impl Project {
 
     pub fn type_definition<T: ToPointUtf16>(
         &self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<LocationLink>>> {
@@ -4498,7 +4511,7 @@ impl Project {
 
     pub fn references<T: ToPointUtf16>(
         &self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Location>>> {
@@ -4513,7 +4526,7 @@ impl Project {
 
     pub fn document_highlights<T: ToPointUtf16>(
         &self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<DocumentHighlight>>> {
@@ -4692,7 +4705,7 @@ impl Project {
         &mut self,
         symbol: &Symbol,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Buffer>>> {
+    ) -> Task<Result<Model<Buffer>>> {
         if self.is_local() {
             let language_server_id = if let Some(id) = self.language_server_ids.get(&(
                 symbol.source_worktree_id,
@@ -4746,7 +4759,7 @@ impl Project {
 
     pub fn hover<T: ToPointUtf16>(
         &self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Hover>>> {
@@ -4761,7 +4774,7 @@ impl Project {
 
     pub fn completions<T: ToOffset + ToPointUtf16>(
         &self,
-        buffer: &Handle<Buffer>,
+        buffer: &Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>> {
@@ -4815,7 +4828,7 @@ impl Project {
 
     pub fn apply_additional_edits_for_completion(
         &self,
-        buffer_handle: Handle<Buffer>,
+        buffer_handle: Model<Buffer>,
         completion: Completion,
         push_to_history: bool,
         cx: &mut ModelContext<Self>,
@@ -4926,7 +4939,7 @@ impl Project {
 
     pub fn code_actions<T: Clone + ToOffset>(
         &self,
-        buffer_handle: &Handle<Buffer>,
+        buffer_handle: &Model<Buffer>,
         range: Range<T>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<CodeAction>>> {
@@ -4942,7 +4955,7 @@ impl Project {
 
     pub fn apply_code_action(
         &self,
-        buffer_handle: Handle<Buffer>,
+        buffer_handle: Model<Buffer>,
         mut action: CodeAction,
         push_to_history: bool,
         cx: &mut ModelContext<Self>,
@@ -5050,7 +5063,7 @@ impl Project {
 
     fn apply_on_type_formatting(
         &self,
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         position: Anchor,
         trigger: String,
         cx: &mut ModelContext<Self>,
@@ -5111,8 +5124,8 @@ impl Project {
     }
 
     async fn deserialize_edits(
-        this: Handle<Self>,
-        buffer_to_edit: Handle<Buffer>,
+        this: Model<Self>,
+        buffer_to_edit: Model<Buffer>,
         edits: Vec<lsp2::TextEdit>,
         push_to_history: bool,
         _: Arc<CachedLspAdapter>,
@@ -5153,7 +5166,7 @@ impl Project {
     }
 
     async fn deserialize_workspace_edit(
-        this: Handle<Self>,
+        this: Model<Self>,
         edit: lsp2::WorkspaceEdit,
         push_to_history: bool,
         lsp_adapter: Arc<CachedLspAdapter>,
@@ -5308,7 +5321,7 @@ impl Project {
 
     pub fn prepare_rename<T: ToPointUtf16>(
         &self,
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Range<Anchor>>>> {
@@ -5323,7 +5336,7 @@ impl Project {
 
     pub fn perform_rename<T: ToPointUtf16>(
         &self,
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         position: T,
         new_name: String,
         push_to_history: bool,
@@ -5344,7 +5357,7 @@ impl Project {
 
     pub fn on_type_format<T: ToPointUtf16>(
         &self,
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         position: T,
         trigger: String,
         push_to_history: bool,
@@ -5373,7 +5386,7 @@ impl Project {
 
     pub fn inlay_hints<T: ToOffset>(
         &self,
-        buffer_handle: Handle<Buffer>,
+        buffer_handle: Model<Buffer>,
         range: Range<T>,
         cx: &mut ModelContext<Self>,
     ) -> Task<anyhow::Result<Vec<InlayHint>>> {
@@ -5434,7 +5447,7 @@ impl Project {
     pub fn resolve_inlay_hint(
         &self,
         hint: InlayHint,
-        buffer_handle: Handle<Buffer>,
+        buffer_handle: Model<Buffer>,
         server_id: LanguageServerId,
         cx: &mut ModelContext<Self>,
     ) -> Task<anyhow::Result<InlayHint>> {
@@ -5499,7 +5512,7 @@ impl Project {
         &self,
         query: SearchQuery,
         cx: &mut ModelContext<Self>,
-    ) -> Receiver<(Handle<Buffer>, Vec<Range<Anchor>>)> {
+    ) -> Receiver<(Model<Buffer>, Vec<Range<Anchor>>)> {
         if self.is_local() {
             self.search_local(query, cx)
         } else if let Some(project_id) = self.remote_id() {
@@ -5543,7 +5556,7 @@ impl Project {
         &self,
         query: SearchQuery,
         cx: &mut ModelContext<Self>,
-    ) -> Receiver<(Handle<Buffer>, Vec<Range<Anchor>>)> {
+    ) -> Receiver<(Model<Buffer>, Vec<Range<Anchor>>)> {
         // Local search is split into several phases.
         // TL;DR is that we do 2 passes; initial pass to pick files which contain at least one match
         // and the second phase that finds positions of all the matches found in the candidate files.
@@ -5636,7 +5649,7 @@ impl Project {
                     .scoped(|scope| {
                         #[derive(Clone)]
                         struct FinishedStatus {
-                            entry: Option<(Handle<Buffer>, Vec<Range<Anchor>>)>,
+                            entry: Option<(Model<Buffer>, Vec<Range<Anchor>>)>,
                             buffer_index: SearchMatchCandidateIndex,
                         }
 
@@ -5726,8 +5739,8 @@ impl Project {
 
     /// Pick paths that might potentially contain a match of a given search query.
     async fn background_search(
-        unnamed_buffers: Vec<Handle<Buffer>>,
-        opened_buffers: HashMap<Arc<Path>, (Handle<Buffer>, BufferSnapshot)>,
+        unnamed_buffers: Vec<Model<Buffer>>,
+        opened_buffers: HashMap<Arc<Path>, (Model<Buffer>, BufferSnapshot)>,
         executor: Executor,
         fs: Arc<dyn Fs>,
         workers: usize,
@@ -5827,7 +5840,7 @@ impl Project {
 
     fn request_lsp<R: LspCommand>(
         &self,
-        buffer_handle: Handle<Buffer>,
+        buffer_handle: Model<Buffer>,
         server: LanguageServerToQuery,
         request: R,
         cx: &mut ModelContext<Self>,
@@ -5891,7 +5904,7 @@ impl Project {
 
     fn send_lsp_proto_request<R: LspCommand>(
         &self,
-        buffer: Handle<Buffer>,
+        buffer: Model<Buffer>,
         project_id: u64,
         request: R,
         cx: &mut ModelContext<'_, Project>,
@@ -5920,7 +5933,7 @@ impl Project {
     ) -> (
         futures::channel::oneshot::Receiver<Vec<SearchMatchCandidate>>,
         Receiver<(
-            Option<(Handle<Buffer>, BufferSnapshot)>,
+            Option<(Model<Buffer>, BufferSnapshot)>,
             SearchMatchCandidateIndex,
         )>,
     ) {
@@ -5974,7 +5987,7 @@ impl Project {
         abs_path: impl AsRef<Path>,
         visible: bool,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<(Handle<Worktree>, PathBuf)>> {
+    ) -> Task<Result<(Model<Worktree>, PathBuf)>> {
         let abs_path = abs_path.as_ref();
         if let Some((tree, relative_path)) = self.find_local_worktree(abs_path, cx) {
             Task::ready(Ok((tree, relative_path)))
@@ -5989,7 +6002,7 @@ impl Project {
         &self,
         abs_path: &Path,
         cx: &AppContext,
-    ) -> Option<(Handle<Worktree>, PathBuf)> {
+    ) -> Option<(Model<Worktree>, PathBuf)> {
         for tree in &self.worktrees {
             if let Some(tree) = tree.upgrade() {
                 if let Some(relative_path) = tree
@@ -6016,7 +6029,7 @@ impl Project {
         abs_path: impl AsRef<Path>,
         visible: bool,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Handle<Worktree>>> {
+    ) -> Task<Result<Model<Worktree>>> {
         let fs = self.fs.clone();
         let client = self.client.clone();
         let next_entry_id = self.next_entry_id.clone();
@@ -6076,7 +6089,7 @@ impl Project {
         self.metadata_changed(cx);
     }
 
-    fn add_worktree(&mut self, worktree: &Handle<Worktree>, cx: &mut ModelContext<Self>) {
+    fn add_worktree(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
         cx.observe(worktree, |_, _, cx| cx.notify()).detach();
         if worktree.read(cx).is_local() {
             cx.subscribe(worktree, |this, worktree, event, cx| match event {
@@ -6126,7 +6139,7 @@ impl Project {
 
     fn update_local_worktree_buffers(
         &mut self,
-        worktree_handle: &Handle<Worktree>,
+        worktree_handle: &Model<Worktree>,
         changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
         cx: &mut ModelContext<Self>,
     ) {

crates/project2/src/project_tests.rs 🔗

@@ -2275,7 +2275,7 @@ async fn test_definition(cx: &mut gpui2::TestAppContext) {
     });
 
     fn list_worktrees<'a>(
-        project: &'a Handle<Project>,
+        project: &'a Model<Project>,
         cx: &'a AppContext,
     ) -> Vec<(&'a Path, bool)> {
         project
@@ -4035,7 +4035,7 @@ fn test_glob_literal_prefix() {
 }
 
 async fn search(
-    project: &Handle<Project>,
+    project: &Model<Project>,
     query: SearchQuery,
     cx: &mut gpui2::TestAppContext,
 ) -> Result<HashMap<String, Vec<Range<usize>>>> {

crates/project2/src/terminals.rs 🔗

@@ -1,5 +1,5 @@
 use crate::Project;
-use gpui2::{AnyWindowHandle, Context, Handle, ModelContext, WeakHandle};
+use gpui2::{AnyWindowHandle, Context, Entity, Model, ModelContext, WeakModel};
 use settings2::Settings;
 use std::path::{Path, PathBuf};
 use terminal2::{
@@ -11,7 +11,7 @@ use terminal2::{
 use std::os::unix::ffi::OsStrExt;
 
 pub struct Terminals {
-    pub(crate) local_handles: Vec<WeakHandle<terminal2::Terminal>>,
+    pub(crate) local_handles: Vec<WeakModel<terminal2::Terminal>>,
 }
 
 impl Project {
@@ -20,7 +20,7 @@ impl Project {
         working_directory: Option<PathBuf>,
         window: AnyWindowHandle,
         cx: &mut ModelContext<Self>,
-    ) -> anyhow::Result<Handle<Terminal>> {
+    ) -> anyhow::Result<Model<Terminal>> {
         if self.is_remote() {
             return Err(anyhow::anyhow!(
                 "creating terminals as a guest is not supported yet"
@@ -40,7 +40,7 @@ impl Project {
                 |_, _| todo!("color_for_index"),
             )
             .map(|builder| {
-                let terminal_handle = cx.entity(|cx| builder.subscribe(cx));
+                let terminal_handle = cx.build_model(|cx| builder.subscribe(cx));
 
                 self.terminals
                     .local_handles
@@ -108,7 +108,7 @@ impl Project {
     fn activate_python_virtual_environment(
         &mut self,
         activate_script: Option<PathBuf>,
-        terminal_handle: &Handle<Terminal>,
+        terminal_handle: &Model<Terminal>,
         cx: &mut ModelContext<Project>,
     ) {
         if let Some(activate_script) = activate_script {
@@ -121,7 +121,7 @@ impl Project {
         }
     }
 
-    pub fn local_terminal_handles(&self) -> &Vec<WeakHandle<terminal2::Terminal>> {
+    pub fn local_terminal_handles(&self) -> &Vec<WeakModel<terminal2::Terminal>> {
         &self.terminals.local_handles
     }
 }

crates/project2/src/worktree.rs 🔗

@@ -22,7 +22,7 @@ use futures::{
 use fuzzy2::CharBag;
 use git::{DOT_GIT, GITIGNORE};
 use gpui2::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Executor, Handle, ModelContext, Task,
+    AppContext, AsyncAppContext, Context, EventEmitter, Executor, Model, ModelContext, Task,
 };
 use language2::{
     proto::{
@@ -292,7 +292,7 @@ impl Worktree {
         fs: Arc<dyn Fs>,
         next_entry_id: Arc<AtomicUsize>,
         cx: &mut AsyncAppContext,
-    ) -> Result<Handle<Self>> {
+    ) -> Result<Model<Self>> {
         // After determining whether the root entry is a file or a directory, populate the
         // snapshot's "root name", which will be used for the purpose of fuzzy matching.
         let abs_path = path.into();
@@ -301,7 +301,7 @@ impl Worktree {
             .await
             .context("failed to stat worktree path")?;
 
-        cx.entity(move |cx: &mut ModelContext<Worktree>| {
+        cx.build_model(move |cx: &mut ModelContext<Worktree>| {
             let root_name = abs_path
                 .file_name()
                 .map_or(String::new(), |f| f.to_string_lossy().to_string());
@@ -406,8 +406,8 @@ impl Worktree {
         worktree: proto::WorktreeMetadata,
         client: Arc<Client>,
         cx: &mut AppContext,
-    ) -> Handle<Self> {
-        cx.entity(|cx: &mut ModelContext<Self>| {
+    ) -> Model<Self> {
+        cx.build_model(|cx: &mut ModelContext<Self>| {
             let snapshot = Snapshot {
                 id: WorktreeId(worktree.id as usize),
                 abs_path: Arc::from(PathBuf::from(worktree.abs_path)),
@@ -593,7 +593,7 @@ impl LocalWorktree {
         id: u64,
         path: &Path,
         cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<Handle<Buffer>>> {
+    ) -> Task<Result<Model<Buffer>>> {
         let path = Arc::from(path);
         cx.spawn(move |this, mut cx| async move {
             let (file, contents, diff_base) = this
@@ -603,7 +603,7 @@ impl LocalWorktree {
                 .executor()
                 .spawn(async move { text::Buffer::new(0, id, contents) })
                 .await;
-            cx.entity(|_| Buffer::build(text_buffer, diff_base, Some(Arc::new(file))))
+            cx.build_model(|_| Buffer::build(text_buffer, diff_base, Some(Arc::new(file))))
         })
     }
 
@@ -920,7 +920,7 @@ impl LocalWorktree {
 
     pub fn save_buffer(
         &self,
-        buffer_handle: Handle<Buffer>,
+        buffer_handle: Model<Buffer>,
         path: Arc<Path>,
         has_changed_file: bool,
         cx: &mut ModelContext<Worktree>,
@@ -1331,7 +1331,7 @@ impl RemoteWorktree {
 
     pub fn save_buffer(
         &self,
-        buffer_handle: Handle<Buffer>,
+        buffer_handle: Model<Buffer>,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<()>> {
         let buffer = buffer_handle.read(cx);
@@ -2577,7 +2577,7 @@ impl fmt::Debug for Snapshot {
 
 #[derive(Clone, PartialEq)]
 pub struct File {
-    pub worktree: Handle<Worktree>,
+    pub worktree: Model<Worktree>,
     pub path: Arc<Path>,
     pub mtime: SystemTime,
     pub(crate) entry_id: ProjectEntryId,
@@ -2659,12 +2659,12 @@ impl language2::File for File {
 
 impl language2::LocalFile for File {
     fn abs_path(&self, cx: &AppContext) -> PathBuf {
-        self.worktree
-            .read(cx)
-            .as_local()
-            .unwrap()
-            .abs_path
-            .join(&self.path)
+        let worktree_path = &self.worktree.read(cx).as_local().unwrap().abs_path;
+        if self.path.as_ref() == Path::new("") {
+            worktree_path.to_path_buf()
+        } else {
+            worktree_path.join(&self.path)
+        }
     }
 
     fn load(&self, cx: &AppContext) -> Task<Result<String>> {
@@ -2701,7 +2701,7 @@ impl language2::LocalFile for File {
 }
 
 impl File {
-    pub fn for_entry(entry: Entry, worktree: Handle<Worktree>) -> Arc<Self> {
+    pub fn for_entry(entry: Entry, worktree: Model<Worktree>) -> Arc<Self> {
         Arc::new(Self {
             worktree,
             path: entry.path.clone(),
@@ -2714,7 +2714,7 @@ impl File {
 
     pub fn from_proto(
         proto: rpc2::proto::File,
-        worktree: Handle<Worktree>,
+        worktree: Model<Worktree>,
         cx: &AppContext,
     ) -> Result<Self> {
         let worktree_id = worktree
@@ -4038,7 +4038,7 @@ pub trait WorktreeModelHandle {
     ) -> futures::future::LocalBoxFuture<'a, ()>;
 }
 
-impl WorktreeModelHandle for Handle<Worktree> {
+impl WorktreeModelHandle for Model<Worktree> {
     // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
     // occurred before the worktree was constructed. These events can cause the worktree to perform
     // extra directory scans, and emit extra scan-state notifications.

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

@@ -42,6 +42,7 @@ sha1 = "0.10.5"
 ndarray = { version = "0.15.0" }
 
 [dev-dependencies]
+ai = { path = "../ai", features = ["test-support"] }
 collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
@@ -51,7 +52,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 +70,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/parsing.rs 🔗

@@ -1,4 +1,7 @@
-use ai::embedding::{Embedding, EmbeddingProvider};
+use ai::{
+    embedding::{Embedding, EmbeddingProvider},
+    models::TruncationDirection,
+};
 use anyhow::{anyhow, Result};
 use language::{Grammar, Language};
 use rusqlite::{
@@ -108,7 +111,14 @@ impl CodeContextRetriever {
             .replace("<language>", language_name.as_ref())
             .replace("<item>", &content);
         let digest = SpanDigest::from(document_span.as_str());
-        let (document_span, token_count) = self.embedding_provider.truncate(&document_span);
+        let model = self.embedding_provider.base_model();
+        let document_span = model.truncate(
+            &document_span,
+            model.capacity()?,
+            ai::models::TruncationDirection::End,
+        )?;
+        let token_count = model.count_tokens(&document_span)?;
+
         Ok(vec![Span {
             range: 0..content.len(),
             content: document_span,
@@ -131,7 +141,15 @@ impl CodeContextRetriever {
             )
             .replace("<item>", &content);
         let digest = SpanDigest::from(document_span.as_str());
-        let (document_span, token_count) = self.embedding_provider.truncate(&document_span);
+
+        let model = self.embedding_provider.base_model();
+        let document_span = model.truncate(
+            &document_span,
+            model.capacity()?,
+            ai::models::TruncationDirection::End,
+        )?;
+        let token_count = model.count_tokens(&document_span)?;
+
         Ok(vec![Span {
             range: 0..content.len(),
             content: document_span,
@@ -222,8 +240,13 @@ impl CodeContextRetriever {
                 .replace("<language>", language_name.as_ref())
                 .replace("item", &span.content);
 
-            let (document_content, token_count) =
-                self.embedding_provider.truncate(&document_content);
+            let model = self.embedding_provider.base_model();
+            let document_content = model.truncate(
+                &document_content,
+                model.capacity()?,
+                TruncationDirection::End,
+            )?;
+            let token_count = model.count_tokens(&document_content)?;
 
             span.content = document_content;
             span.token_count = token_count;

crates/semantic_index/src/semantic_index.rs 🔗

@@ -7,7 +7,8 @@ pub mod semantic_index_settings;
 mod semantic_index_tests;
 
 use crate::semantic_index_settings::SemanticIndexSettings;
-use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
+use ai::embedding::{Embedding, EmbeddingProvider};
+use ai::providers::open_ai::OpenAIEmbeddingProvider;
 use anyhow::{anyhow, Result};
 use collections::{BTreeMap, HashMap, HashSet};
 use db::VectorDatabase;
@@ -88,7 +89,7 @@ pub fn init(
         let semantic_index = SemanticIndex::new(
             fs,
             db_file_path,
-            Arc::new(OpenAIEmbeddings::new(http_client, cx.background())),
+            Arc::new(OpenAIEmbeddingProvider::new(http_client, cx.background())),
             language_registry,
             cx.clone(),
         )
@@ -268,7 +269,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 +277,26 @@ impl SemanticIndex {
         }
     }
 
+    pub fn authenticate(&mut self, cx: &AppContext) -> bool {
+        if !self.embedding_provider.has_credentials() {
+            self.embedding_provider.retrieve_credentials(cx);
+        } else {
+            return true;
+        }
+
+        self.embedding_provider.has_credentials()
+    }
+
+    pub fn is_authenticated(&self) -> bool {
+        self.embedding_provider.has_credentials()
+    }
+
     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;
         }
 
@@ -706,6 +721,7 @@ impl SemanticIndex {
         cx.spawn(|this, mut cx| async move {
             index.await?;
             let t0 = Instant::now();
+
             let query = embedding_provider
                 .embed_batch(vec![query])
                 .await?
@@ -982,8 +998,10 @@ 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.is_authenticated() {
+            if !self.authenticate(cx) {
+                return Task::ready(Err(anyhow!("user is not authenticated")));
+            }
         }
 
         if !self.projects.contains_key(&project.downgrade()) {

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -4,9 +4,8 @@ use crate::{
     semantic_index_settings::SemanticIndexSettings,
     FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
 };
-use ai::embedding::{DummyEmbeddings, Embedding, EmbeddingProvider};
-use anyhow::Result;
-use async_trait::async_trait;
+use ai::test::FakeEmbeddingProvider;
+
 use gpui::{executor::Deterministic, Task, TestAppContext};
 use language::{Language, LanguageConfig, LanguageRegistry, ToOffset};
 use parking_lot::Mutex;
@@ -15,14 +14,7 @@ use project::{project_settings::ProjectSettings, search::PathMatcher, FakeFs, Fs
 use rand::{rngs::StdRng, Rng};
 use serde_json::json;
 use settings::SettingsStore;
-use std::{
-    path::Path,
-    sync::{
-        atomic::{self, AtomicUsize},
-        Arc,
-    },
-    time::{Instant, SystemTime},
-};
+use std::{path::Path, sync::Arc, time::SystemTime};
 use unindent::Unindent;
 use util::RandomCharIter;
 
@@ -280,7 +272,7 @@ fn assert_search_results(
 #[gpui::test]
 async fn test_code_context_retrieval_rust() {
     let language = rust_lang();
-    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
     let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = "
@@ -382,7 +374,7 @@ async fn test_code_context_retrieval_rust() {
 #[gpui::test]
 async fn test_code_context_retrieval_json() {
     let language = json_lang();
-    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
     let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
@@ -466,7 +458,7 @@ fn assert_documents_eq(
 #[gpui::test]
 async fn test_code_context_retrieval_javascript() {
     let language = js_lang();
-    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
     let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = "
@@ -565,7 +557,7 @@ async fn test_code_context_retrieval_javascript() {
 #[gpui::test]
 async fn test_code_context_retrieval_lua() {
     let language = lua_lang();
-    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
     let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
@@ -639,7 +631,7 @@ async fn test_code_context_retrieval_lua() {
 #[gpui::test]
 async fn test_code_context_retrieval_elixir() {
     let language = elixir_lang();
-    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
     let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
@@ -756,7 +748,7 @@ async fn test_code_context_retrieval_elixir() {
 #[gpui::test]
 async fn test_code_context_retrieval_cpp() {
     let language = cpp_lang();
-    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
     let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = "
@@ -909,7 +901,7 @@ async fn test_code_context_retrieval_cpp() {
 #[gpui::test]
 async fn test_code_context_retrieval_ruby() {
     let language = ruby_lang();
-    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
     let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
@@ -1100,7 +1092,7 @@ async fn test_code_context_retrieval_ruby() {
 #[gpui::test]
 async fn test_code_context_retrieval_php() {
     let language = php_lang();
-    let embedding_provider = Arc::new(DummyEmbeddings {});
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
     let mut retriever = CodeContextRetriever::new(embedding_provider);
 
     let text = r#"
@@ -1248,61 +1240,6 @@ async fn test_code_context_retrieval_php() {
     );
 }
 
-#[derive(Default)]
-struct FakeEmbeddingProvider {
-    embedding_count: AtomicUsize,
-}
-
-impl FakeEmbeddingProvider {
-    fn embedding_count(&self) -> usize {
-        self.embedding_count.load(atomic::Ordering::SeqCst)
-    }
-
-    fn embed_sync(&self, span: &str) -> Embedding {
-        let mut result = vec![1.0; 26];
-        for letter in span.chars() {
-            let letter = letter.to_ascii_lowercase();
-            if letter as u32 >= 'a' as u32 {
-                let ix = (letter as u32) - ('a' as u32);
-                if ix < 26 {
-                    result[ix as usize] += 1.0;
-                }
-            }
-        }
-
-        let norm = result.iter().map(|x| x * x).sum::<f32>().sqrt();
-        for x in &mut result {
-            *x /= norm;
-        }
-
-        result.into()
-    }
-}
-
-#[async_trait]
-impl EmbeddingProvider for FakeEmbeddingProvider {
-    fn is_authenticated(&self) -> bool {
-        true
-    }
-    fn truncate(&self, span: &str) -> (String, usize) {
-        (span.to_string(), 1)
-    }
-
-    fn max_tokens_per_batch(&self) -> usize {
-        200
-    }
-
-    fn rate_limit_expiration(&self) -> Option<Instant> {
-        None
-    }
-
-    async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>> {
-        self.embedding_count
-            .fetch_add(spans.len(), atomic::Ordering::SeqCst);
-        Ok(spans.iter().map(|span| self.embed_sync(span)).collect())
-    }
-}
-
 fn js_lang() -> Arc<Language> {
     Arc::new(
         Language::new(

crates/settings2/src/keymap_file.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{settings_store::parse_json_with_comments, SettingsAssets};
 use anyhow::{anyhow, Context, Result};
 use collections::BTreeMap;
-use gpui2::{AppContext, KeyBinding};
+use gpui2::{AppContext, KeyBinding, SharedString};
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
     schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
@@ -96,7 +96,7 @@ impl KeymapFile {
         Ok(())
     }
 
-    pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
+    pub fn generate_json_schema(action_names: &[SharedString]) -> serde_json::Value {
         let mut root_schema = SchemaSettings::draft07()
             .with(|settings| settings.option_add_null_type = false)
             .into_generator()

crates/storybook2/src/stories.rs 🔗

@@ -1,9 +1,11 @@
+mod colors;
 mod focus;
 mod kitchen_sink;
 mod scroll;
 mod text;
 mod z_index;
 
+pub use colors::*;
 pub use focus::*;
 pub use kitchen_sink::*;
 pub use scroll::*;

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

@@ -0,0 +1,38 @@
+use crate::story::Story;
+use gpui2::{px, Div, Render};
+use ui::prelude::*;
+
+pub struct ColorsStory;
+
+impl Render for ColorsStory {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let color_scales = theme2::default_color_scales();
+
+        Story::container(cx)
+            .child(Story::title(cx, "Colors"))
+            .child(
+                div()
+                    .id("colors")
+                    .flex()
+                    .flex_col()
+                    .gap_1()
+                    .overflow_y_scroll()
+                    .text_color(gpui2::white())
+                    .children(color_scales.into_iter().map(|(name, scale)| {
+                        div()
+                            .flex()
+                            .child(
+                                div()
+                                    .w(px(75.))
+                                    .line_height(px(24.))
+                                    .child(name.to_string()),
+                            )
+                            .child(div().flex().gap_1().children(
+                                (1..=12).map(|step| div().flex().size_6().bg(scale.step(cx, step))),
+                            ))
+                    })),
+            )
+    }
+}

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

@@ -1,9 +1,9 @@
-use crate::themes::rose_pine;
 use gpui2::{
-    div, view, Context, Focusable, KeyBinding, ParentElement, StatelessInteractive, Styled, View,
-    WindowContext,
+    div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, StatefulInteraction,
+    StatelessInteractive, Styled, View, VisualContext, WindowContext,
 };
 use serde::Deserialize;
+use theme2::theme;
 
 #[derive(Clone, Default, PartialEq, Deserialize)]
 struct ActionA;
@@ -14,12 +14,10 @@ struct ActionB;
 #[derive(Clone, Default, PartialEq, Deserialize)]
 struct ActionC;
 
-pub struct FocusStory {
-    text: View<()>,
-}
+pub struct FocusStory {}
 
 impl FocusStory {
-    pub fn view(cx: &mut WindowContext) -> View<()> {
+    pub fn view(cx: &mut WindowContext) -> View<Self> {
         cx.bind_keys([
             KeyBinding::new("cmd-a", ActionA, Some("parent")),
             KeyBinding::new("cmd-a", ActionB, Some("child-1")),
@@ -27,88 +25,92 @@ impl FocusStory {
         ]);
         cx.register_action_type::<ActionA>();
         cx.register_action_type::<ActionB>();
-        let theme = rose_pine();
 
-        let color_1 = theme.lowest.negative.default.foreground;
-        let color_2 = theme.lowest.positive.default.foreground;
-        let color_3 = theme.lowest.warning.default.foreground;
-        let color_4 = theme.lowest.accent.default.foreground;
-        let color_5 = theme.lowest.variant.default.foreground;
-        let color_6 = theme.highest.negative.default.foreground;
+        cx.build_view(move |cx| Self {})
+    }
+}
 
+impl Render for FocusStory {
+    type Element = Div<Self, StatefulInteraction<Self>, FocusEnabled<Self>>;
+
+    fn render(&mut self, cx: &mut gpui2::ViewContext<Self>) -> Self::Element {
+        let theme = theme(cx);
+        let color_1 = theme.git_created;
+        let color_2 = theme.git_modified;
+        let color_3 = theme.git_deleted;
+        let color_4 = theme.git_conflict;
+        let color_5 = theme.git_ignored;
+        let color_6 = theme.git_renamed;
         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"),
-                )
-        })
+
+        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,22 +1,23 @@
-use gpui2::{view, Context, View};
+use crate::{
+    story::Story,
+    story_selector::{ComponentStory, ElementStory},
+};
+use gpui2::{Div, Render, StatefulInteraction, View, VisualContext};
 use strum::IntoEnumIterator;
 use ui::prelude::*;
 
-use crate::story::Story;
-use crate::story_selector::{ComponentStory, ElementStory};
-
-pub struct KitchenSinkStory {}
+pub struct KitchenSinkStory;
 
 impl KitchenSinkStory {
-    pub fn new() -> Self {
-        Self {}
-    }
-
     pub fn view(cx: &mut WindowContext) -> View<Self> {
-        view(cx.entity(|cx| Self::new()), Self::render)
+        cx.build_view(|cx| Self)
     }
+}
+
+impl Render for KitchenSinkStory {
+    type Element = Div<Self, StatefulInteraction<Self>>;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let element_stories = ElementStory::iter()
             .map(|selector| selector.story(cx))
             .collect::<Vec<_>>();

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

@@ -1,56 +1,54 @@
-use crate::themes::rose_pine;
 use gpui2::{
-    div, px, view, Component, Context, ParentElement, SharedString, Styled, View, WindowContext,
+    div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteraction, Styled,
+    View, VisualContext, WindowContext,
 };
+use theme2::theme;
 
-pub struct ScrollStory {
-    text: View<()>,
-}
+pub struct ScrollStory;
 
 impl ScrollStory {
-    pub fn view(cx: &mut WindowContext) -> View<()> {
-        let theme = rose_pine();
-
-        view(cx.entity(|cx| ()), move |_, cx| checkerboard(1))
+    pub fn view(cx: &mut WindowContext) -> View<ScrollStory> {
+        cx.build_view(|cx| ScrollStory)
     }
 }
 
-fn checkerboard<S>(depth: usize) -> impl Component<S>
-where
-    S: 'static + Send + Sync,
-{
-    let theme = rose_pine();
-    let color_1 = theme.lowest.positive.default.background;
-    let color_2 = theme.lowest.warning.default.background;
+impl Render for ScrollStory {
+    type Element = Div<Self, StatefulInteraction<Self>>;
 
-    div()
-        .id("parent")
-        .bg(theme.lowest.base.default.background)
-        .size_full()
-        .overflow_scroll()
-        .children((0..10).map(|row| {
-            div()
-                .w(px(1000.))
-                .h(px(100.))
-                .flex()
-                .flex_row()
-                .children((0..10).map(|column| {
-                    let id = SharedString::from(format!("{}, {}", row, column));
-                    let bg = if row % 2 == column % 2 {
-                        color_1
-                    } else {
-                        color_2
-                    };
-                    div().id(id).bg(bg).size(px(100. / depth as f32)).when(
-                        row >= 5 && column >= 5,
-                        |d| {
-                            d.overflow_scroll()
-                                .child(div().size(px(50.)).bg(color_1))
-                                .child(div().size(px(50.)).bg(color_2))
-                                .child(div().size(px(50.)).bg(color_1))
-                                .child(div().size(px(50.)).bg(color_2))
-                        },
-                    )
-                }))
-        }))
+    fn render(&mut self, cx: &mut gpui2::ViewContext<Self>) -> Self::Element {
+        let theme = theme(cx);
+        let color_1 = theme.git_created;
+        let color_2 = theme.git_modified;
+
+        div()
+            .id("parent")
+            .bg(theme.background)
+            .size_full()
+            .overflow_scroll()
+            .children((0..10).map(|row| {
+                div()
+                    .w(px(1000.))
+                    .h(px(100.))
+                    .flex()
+                    .flex_row()
+                    .children((0..10).map(|column| {
+                        let id = SharedString::from(format!("{}, {}", row, column));
+                        let bg = if row % 2 == column % 2 {
+                            color_1
+                        } else {
+                            color_2
+                        };
+                        div().id(id).bg(bg).size(px(100. as f32)).when(
+                            row >= 5 && column >= 5,
+                            |d| {
+                                d.overflow_scroll()
+                                    .child(div().size(px(50.)).bg(color_1))
+                                    .child(div().size(px(50.)).bg(color_2))
+                                    .child(div().size(px(50.)).bg(color_1))
+                                    .child(div().size(px(50.)).bg(color_2))
+                            },
+                        )
+                    }))
+            }))
+    }
 }

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

@@ -1,20 +1,21 @@
-use gpui2::{div, view, white, Context, ParentElement, Styled, View, WindowContext};
+use gpui2::{div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext};
 
-pub struct TextStory {
-    text: View<()>,
-}
+pub struct TextStory;
 
 impl TextStory {
-    pub fn view(cx: &mut WindowContext) -> View<()> {
-        view(cx.entity(|cx| ()), |_, cx| {
-            div()
-                .size_full()
-                .bg(white())
-                .child(concat!(
-                    "The quick brown fox jumps over the lazy dog. ",
-                    "Meanwhile, the lazy dog decided it was time for a change. ",
-                    "He started daily workout routines, ate healthier and became the fastest dog in town.",
-                ))
-        })
+    pub fn view(cx: &mut WindowContext) -> View<Self> {
+        cx.build_view(|cx| Self)
+    }
+}
+
+impl Render for TextStory {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut gpui2::ViewContext<Self>) -> Self::Element {
+        div().size_full().bg(white()).child(concat!(
+            "The quick brown fox jumps over the lazy dog. ",
+            "Meanwhile, the lazy dog decided it was time for a change. ",
+            "He started daily workout routines, ate healthier and became the fastest dog in town.",
+        ))
     }
 }

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

@@ -1,20 +1,16 @@
-
-use gpui2::{px, rgb, Div, Hsla};
+use gpui2::{px, rgb, Div, Hsla, Render};
 use ui::prelude::*;
 
 use crate::story::Story;
 
 /// A reimplementation of the MDN `z-index` example, found here:
 /// [https://developer.mozilla.org/en-US/docs/Web/CSS/z-index](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index).
-#[derive(Component)]
 pub struct ZIndexStory;
 
-impl ZIndexStory {
-    pub fn new() -> Self {
-        Self
-    }
+impl Render for ZIndexStory {
+    type Element = Div<Self>;
 
-    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container(cx)
             .child(Story::title(cx, "z-index"))
             .child(

crates/storybook2/src/story_selector.rs 🔗

@@ -5,15 +5,16 @@ 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::*;
+use ui::{prelude::*, AvatarStory, ButtonStory, DetailsStory, IconStory, InputStory, LabelStory};
 
 #[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
 #[strum(serialize_all = "snake_case")]
 pub enum ElementStory {
     Avatar,
     Button,
+    Colors,
     Details,
     Focus,
     Icon,
@@ -27,31 +28,17 @@ 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::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::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::Colors => cx.build_view(|_| ColorsStory).into(),
+            Self::Avatar => cx.build_view(|_| AvatarStory).into(),
+            Self::Button => cx.build_view(|_| ButtonStory).into(),
+            Self::Details => cx.build_view(|_| DetailsStory).into(),
+            Self::Focus => FocusStory::view(cx).into(),
+            Self::Icon => cx.build_view(|_| IconStory).into(),
+            Self::Input => cx.build_view(|_| InputStory).into(),
+            Self::Label => cx.build_view(|_| LabelStory).into(),
+            Self::Scroll => ScrollStory::view(cx).into(),
+            Self::Text => TextStory::view(cx).into(),
+            Self::ZIndex => cx.build_view(|_| ZIndexStory).into(),
         }
     }
 }
@@ -90,97 +77,32 @@ 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::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::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::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::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(),
+            Self::AssistantPanel => cx.build_view(|_| ui::AssistantPanelStory).into(),
+            Self::Buffer => cx.build_view(|_| ui::BufferStory).into(),
+            Self::Breadcrumb => cx.build_view(|_| ui::BreadcrumbStory).into(),
+            Self::ChatPanel => cx.build_view(|_| ui::ChatPanelStory).into(),
+            Self::CollabPanel => cx.build_view(|_| ui::CollabPanelStory).into(),
+            Self::CommandPalette => cx.build_view(|_| ui::CommandPaletteStory).into(),
+            Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
+            Self::Facepile => cx.build_view(|_| ui::FacepileStory).into(),
+            Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
+            Self::LanguageSelector => cx.build_view(|_| ui::LanguageSelectorStory).into(),
+            Self::MultiBuffer => cx.build_view(|_| ui::MultiBufferStory).into(),
+            Self::NotificationsPanel => cx.build_view(|cx| ui::NotificationsPanelStory).into(),
+            Self::Palette => cx.build_view(|cx| ui::PaletteStory).into(),
+            Self::Panel => cx.build_view(|cx| ui::PanelStory).into(),
+            Self::ProjectPanel => cx.build_view(|_| ui::ProjectPanelStory).into(),
+            Self::RecentProjects => cx.build_view(|_| ui::RecentProjectsStory).into(),
+            Self::Tab => cx.build_view(|_| ui::TabStory).into(),
+            Self::TabBar => cx.build_view(|_| ui::TabBarStory).into(),
+            Self::Terminal => cx.build_view(|_| ui::TerminalStory).into(),
+            Self::ThemeSelector => cx.build_view(|_| ui::ThemeSelectorStory).into(),
+            Self::Toast => cx.build_view(|_| ui::ToastStory).into(),
+            Self::Toolbar => cx.build_view(|_| ui::ToolbarStory).into(),
+            Self::TrafficLights => cx.build_view(|_| ui::TrafficLightsStory).into(),
+            Self::Copilot => cx.build_view(|_| ui::CopilotModalStory).into(),
+            Self::TitleBar => ui::TitleBarStory::view(cx).into(),
+            Self::Workspace => ui::WorkspaceStory::view(cx).into(),
         }
     }
 }
@@ -227,7 +149,7 @@ impl StorySelector {
         match self {
             Self::Element(element_story) => element_story.story(cx),
             Self::Component(component_story) => component_story.story(cx),
-            Self::KitchenSink => KitchenSinkStory::view(cx).into_any(),
+            Self::KitchenSink => KitchenSinkStory::view(cx).into(),
         }
     }
 }

crates/storybook2/src/storybook2.rs 🔗

@@ -4,21 +4,20 @@ mod assets;
 mod stories;
 mod story;
 mod story_selector;
-mod themes;
 
 use std::sync::Arc;
 
 use clap::Parser;
 use gpui2::{
-    div, px, size, view, AnyView, AppContext, Bounds, Context, ViewContext, WindowBounds,
-    WindowOptions,
+    div, px, size, AnyView, AppContext, Bounds, Div, Render, ViewContext, VisualContext,
+    WindowBounds, WindowOptions,
 };
 use log::LevelFilter;
 use settings2::{default_settings, Settings, SettingsStore};
 use simplelog::SimpleLogger;
 use story_selector::ComponentStory;
 use theme2::{ThemeRegistry, ThemeSettings};
-use ui::{prelude::*, themed};
+use ui::prelude::*;
 
 use crate::assets::Assets;
 use crate::story_selector::StorySelector;
@@ -50,7 +49,6 @@ fn main() {
 
     let story_selector = args.story.clone();
     let theme_name = args.theme.unwrap_or("One Dark".to_string());
-    let theme = themes::load_theme(theme_name.clone()).unwrap();
 
     let asset_source = Arc::new(Assets);
     gpui2::App::production(asset_source).run(move |cx| {
@@ -84,12 +82,7 @@ fn main() {
                 }),
                 ..Default::default()
             },
-            move |cx| {
-                view(
-                    cx.entity(|cx| StoryWrapper::new(selector.story(cx), theme)),
-                    StoryWrapper::render,
-                )
-            },
+            move |cx| cx.build_view(|cx| StoryWrapper::new(selector.story(cx))),
         );
 
         cx.activate(true);
@@ -99,22 +92,23 @@ fn main() {
 #[derive(Clone)]
 pub struct StoryWrapper {
     story: AnyView,
-    theme: Theme,
 }
 
 impl StoryWrapper {
-    pub(crate) fn new(story: AnyView, theme: Theme) -> Self {
-        Self { story, theme }
+    pub(crate) fn new(story: AnyView) -> Self {
+        Self { story }
     }
+}
+
+impl Render for StoryWrapper {
+    type Element = Div<Self>;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
-        themed(self.theme.clone(), cx, |cx| {
-            div()
-                .flex()
-                .flex_col()
-                .size_full()
-                .child(self.story.clone())
-        })
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .flex()
+            .flex_col()
+            .size_full()
+            .child(self.story.clone())
     }
 }
 

crates/storybook2/src/themes.rs 🔗

@@ -1,30 +0,0 @@
-mod rose_pine;
-
-pub use rose_pine::*;
-
-use anyhow::{Context, Result};
-use gpui2::serde_json;
-use serde::Deserialize;
-use ui::Theme;
-
-use crate::assets::Assets;
-
-#[derive(Deserialize)]
-struct LegacyTheme {
-    pub base_theme: serde_json::Value,
-}
-
-/// Loads the [`Theme`] with the given name.
-pub fn load_theme(name: String) -> Result<Theme> {
-    let theme_contents = Assets::get(&format!("themes/{name}.json"))
-        .with_context(|| format!("theme file not found: '{name}'"))?;
-
-    let legacy_theme: LegacyTheme =
-        serde_json::from_str(std::str::from_utf8(&theme_contents.data)?)
-            .context("failed to parse legacy theme")?;
-
-    let theme: Theme = serde_json::from_value(legacy_theme.base_theme.clone())
-        .context("failed to parse `base_theme`")?;
-
-    Ok(theme)
-}

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

@@ -1,1686 +0,0 @@
-use gpui2::serde_json::{self, json};
-use ui::Theme;
-
-pub fn rose_pine() -> Theme {
-    serde_json::from_value(json! {
-        {
-          "name": "Rosé Pine",
-          "is_light": false,
-          "ramps": {},
-          "lowest": {
-            "base": {
-              "default": {
-                "background": "#292739",
-                "border": "#423f55",
-                "foreground": "#e0def4"
-              },
-              "hovered": {
-                "background": "#423f55",
-                "border": "#423f55",
-                "foreground": "#e0def4"
-              },
-              "pressed": {
-                "background": "#4e4b63",
-                "border": "#423f55",
-                "foreground": "#e0def4"
-              },
-              "active": {
-                "background": "#47445b",
-                "border": "#36334a",
-                "foreground": "#e0def4"
-              },
-              "disabled": {
-                "background": "#292739",
-                "border": "#353347",
-                "foreground": "#2f2b43"
-              },
-              "inverted": {
-                "background": "#e0def4",
-                "border": "#191724",
-                "foreground": "#4b4860"
-              }
-            },
-            "variant": {
-              "default": {
-                "background": "#292739",
-                "border": "#423f55",
-                "foreground": "#75718e"
-              },
-              "hovered": {
-                "background": "#423f55",
-                "border": "#423f55",
-                "foreground": "#75718e"
-              },
-              "pressed": {
-                "background": "#4e4b63",
-                "border": "#423f55",
-                "foreground": "#75718e"
-              },
-              "active": {
-                "background": "#47445b",
-                "border": "#36334a",
-                "foreground": "#e0def4"
-              },
-              "disabled": {
-                "background": "#292739",
-                "border": "#353347",
-                "foreground": "#2f2b43"
-              },
-              "inverted": {
-                "background": "#e0def4",
-                "border": "#191724",
-                "foreground": "#4b4860"
-              }
-            },
-            "on": {
-              "default": {
-                "background": "#1d1b2a",
-                "border": "#232132",
-                "foreground": "#e0def4"
-              },
-              "hovered": {
-                "background": "#232132",
-                "border": "#232132",
-                "foreground": "#e0def4"
-              },
-              "pressed": {
-                "background": "#2f2d40",
-                "border": "#232132",
-                "foreground": "#e0def4"
-              },
-              "active": {
-                "background": "#403e53",
-                "border": "#504d65",
-                "foreground": "#e0def4"
-              },
-              "disabled": {
-                "background": "#1d1b2a",
-                "border": "#1e1c2c",
-                "foreground": "#3b384f"
-              },
-              "inverted": {
-                "background": "#e0def4",
-                "border": "#191724",
-                "foreground": "#3b394e"
-              }
-            },
-            "accent": {
-              "default": {
-                "background": "#2f3739",
-                "border": "#435255",
-                "foreground": "#9cced7"
-              },
-              "hovered": {
-                "background": "#435255",
-                "border": "#435255",
-                "foreground": "#9cced7"
-              },
-              "pressed": {
-                "background": "#4e6164",
-                "border": "#435255",
-                "foreground": "#9cced7"
-              },
-              "active": {
-                "background": "#5d757a",
-                "border": "#6e8f94",
-                "foreground": "#fbfdfd"
-              },
-              "disabled": {
-                "background": "#2f3739",
-                "border": "#3a4446",
-                "foreground": "#85aeb5"
-              },
-              "inverted": {
-                "background": "#fbfdfd",
-                "border": "#171717",
-                "foreground": "#587074"
-              }
-            },
-            "positive": {
-              "default": {
-                "background": "#182e23",
-                "border": "#254839",
-                "foreground": "#5dc2a3"
-              },
-              "hovered": {
-                "background": "#254839",
-                "border": "#254839",
-                "foreground": "#5dc2a3"
-              },
-              "pressed": {
-                "background": "#2c5645",
-                "border": "#254839",
-                "foreground": "#5dc2a3"
-              },
-              "active": {
-                "background": "#356b57",
-                "border": "#40836c",
-                "foreground": "#f9fdfb"
-              },
-              "disabled": {
-                "background": "#182e23",
-                "border": "#1e3b2e",
-                "foreground": "#4ea287"
-              },
-              "inverted": {
-                "background": "#f9fdfb",
-                "border": "#000e00",
-                "foreground": "#326552"
-              }
-            },
-            "warning": {
-              "default": {
-                "background": "#50341a",
-                "border": "#6d4d2b",
-                "foreground": "#f5c177"
-              },
-              "hovered": {
-                "background": "#6d4d2b",
-                "border": "#6d4d2b",
-                "foreground": "#f5c177"
-              },
-              "pressed": {
-                "background": "#7e5a34",
-                "border": "#6d4d2b",
-                "foreground": "#f5c177"
-              },
-              "active": {
-                "background": "#946e41",
-                "border": "#b0854f",
-                "foreground": "#fffcf9"
-              },
-              "disabled": {
-                "background": "#50341a",
-                "border": "#5e4023",
-                "foreground": "#d2a263"
-              },
-              "inverted": {
-                "background": "#fffcf9",
-                "border": "#2c1600",
-                "foreground": "#8e683c"
-              }
-            },
-            "negative": {
-              "default": {
-                "background": "#431820",
-                "border": "#612834",
-                "foreground": "#ea6f92"
-              },
-              "hovered": {
-                "background": "#612834",
-                "border": "#612834",
-                "foreground": "#ea6f92"
-              },
-              "pressed": {
-                "background": "#71303f",
-                "border": "#612834",
-                "foreground": "#ea6f92"
-              },
-              "active": {
-                "background": "#883c4f",
-                "border": "#a44961",
-                "foreground": "#fff9fa"
-              },
-              "disabled": {
-                "background": "#431820",
-                "border": "#52202a",
-                "foreground": "#c75c79"
-              },
-              "inverted": {
-                "background": "#fff9fa",
-                "border": "#230000",
-                "foreground": "#82384a"
-              }
-            }
-          },
-          "middle": {
-            "base": {
-              "default": {
-                "background": "#1d1b2a",
-                "border": "#232132",
-                "foreground": "#e0def4"
-              },
-              "hovered": {
-                "background": "#232132",
-                "border": "#232132",
-                "foreground": "#e0def4"
-              },
-              "pressed": {
-                "background": "#2f2d40",
-                "border": "#232132",
-                "foreground": "#e0def4"
-              },
-              "active": {
-                "background": "#403e53",
-                "border": "#504d65",
-                "foreground": "#e0def4"
-              },
-              "disabled": {
-                "background": "#1d1b2a",
-                "border": "#1e1c2c",
-                "foreground": "#3b384f"
-              },
-              "inverted": {
-                "background": "#e0def4",
-                "border": "#191724",
-                "foreground": "#3b394e"
-              }
-            },
-            "variant": {
-              "default": {
-                "background": "#1d1b2a",
-                "border": "#232132",
-                "foreground": "#75718e"
-              },
-              "hovered": {
-                "background": "#232132",
-                "border": "#232132",
-                "foreground": "#75718e"
-              },
-              "pressed": {
-                "background": "#2f2d40",
-                "border": "#232132",
-                "foreground": "#75718e"
-              },
-              "active": {
-                "background": "#403e53",
-                "border": "#504d65",
-                "foreground": "#e0def4"
-              },
-              "disabled": {
-                "background": "#1d1b2a",
-                "border": "#1e1c2c",
-                "foreground": "#3b384f"
-              },
-              "inverted": {
-                "background": "#e0def4",
-                "border": "#191724",
-                "foreground": "#3b394e"
-              }
-            },
-            "on": {
-              "default": {
-                "background": "#191724",
-                "border": "#1c1a29",
-                "foreground": "#e0def4"
-              },
-              "hovered": {
-                "background": "#1c1a29",
-                "border": "#1c1a29",
-                "foreground": "#e0def4"
-              },
-              "pressed": {
-                "background": "#1d1b2b",
-                "border": "#1c1a29",
-                "foreground": "#e0def4"
-              },
-              "active": {
-                "background": "#222031",
-                "border": "#353347",
-                "foreground": "#e0def4"
-              },
-              "disabled": {
-                "background": "#191724",
-                "border": "#1a1826",
-                "foreground": "#4e4b63"
-              },
-              "inverted": {
-                "background": "#e0def4",
-                "border": "#191724",
-                "foreground": "#1f1d2e"
-              }
-            },
-            "accent": {
-              "default": {
-                "background": "#2f3739",
-                "border": "#435255",
-                "foreground": "#9cced7"
-              },
-              "hovered": {
-                "background": "#435255",
-                "border": "#435255",
-                "foreground": "#9cced7"
-              },
-              "pressed": {
-                "background": "#4e6164",
-                "border": "#435255",
-                "foreground": "#9cced7"
-              },
-              "active": {
-                "background": "#5d757a",
-                "border": "#6e8f94",
-                "foreground": "#fbfdfd"
-              },
-              "disabled": {
-                "background": "#2f3739",
-                "border": "#3a4446",
-                "foreground": "#85aeb5"
-              },
-              "inverted": {
-                "background": "#fbfdfd",
-                "border": "#171717",
-                "foreground": "#587074"
-              }
-            },
-            "positive": {
-              "default": {
-                "background": "#182e23",
-                "border": "#254839",
-                "foreground": "#5dc2a3"
-              },
-              "hovered": {
-                "background": "#254839",
-                "border": "#254839",
-                "foreground": "#5dc2a3"
-              },
-              "pressed": {
-                "background": "#2c5645",
-                "border": "#254839",
-                "foreground": "#5dc2a3"
-              },
-              "active": {
-                "background": "#356b57",
-                "border": "#40836c",
-                "foreground": "#f9fdfb"
-              },
-              "disabled": {
-                "background": "#182e23",
-                "border": "#1e3b2e",
-                "foreground": "#4ea287"
-              },
-              "inverted": {
-                "background": "#f9fdfb",
-                "border": "#000e00",
-                "foreground": "#326552"
-              }
-            },
-            "warning": {
-              "default": {
-                "background": "#50341a",
-                "border": "#6d4d2b",
-                "foreground": "#f5c177"
-              },
-              "hovered": {
-                "background": "#6d4d2b",
-                "border": "#6d4d2b",
-                "foreground": "#f5c177"
-              },
-              "pressed": {
-                "background": "#7e5a34",
-                "border": "#6d4d2b",
-                "foreground": "#f5c177"
-              },
-              "active": {
-                "background": "#946e41",
-                "border": "#b0854f",
-                "foreground": "#fffcf9"
-              },
-              "disabled": {
-                "background": "#50341a",
-                "border": "#5e4023",
-                "foreground": "#d2a263"
-              },
-              "inverted": {
-                "background": "#fffcf9",
-                "border": "#2c1600",
-                "foreground": "#8e683c"
-              }
-            },
-            "negative": {
-              "default": {
-                "background": "#431820",
-                "border": "#612834",
-                "foreground": "#ea6f92"
-              },
-              "hovered": {
-                "background": "#612834",
-                "border": "#612834",
-                "foreground": "#ea6f92"
-              },
-              "pressed": {
-                "background": "#71303f",
-                "border": "#612834",
-                "foreground": "#ea6f92"
-              },
-              "active": {
-                "background": "#883c4f",
-                "border": "#a44961",
-                "foreground": "#fff9fa"
-              },
-              "disabled": {
-                "background": "#431820",
-                "border": "#52202a",
-                "foreground": "#c75c79"
-              },
-              "inverted": {
-                "background": "#fff9fa",
-                "border": "#230000",
-                "foreground": "#82384a"
-              }
-            }
-          },
-          "highest": {
-            "base": {
-              "default": {
-                "background": "#191724",
-                "border": "#1c1a29",
-                "foreground": "#e0def4"
-              },
-              "hovered": {
-                "background": "#1c1a29",
-                "border": "#1c1a29",
-                "foreground": "#e0def4"
-              },
-              "pressed": {
-                "background": "#1d1b2b",
-                "border": "#1c1a29",
-                "foreground": "#e0def4"
-              },
-              "active": {
-                "background": "#222031",
-                "border": "#353347",
-                "foreground": "#e0def4"
-              },
-              "disabled": {
-                "background": "#191724",
-                "border": "#1a1826",
-                "foreground": "#4e4b63"
-              },
-              "inverted": {
-                "background": "#e0def4",
-                "border": "#191724",
-                "foreground": "#1f1d2e"
-              }
-            },
-            "variant": {
-              "default": {
-                "background": "#191724",
-                "border": "#1c1a29",
-                "foreground": "#75718e"
-              },
-              "hovered": {
-                "background": "#1c1a29",
-                "border": "#1c1a29",
-                "foreground": "#75718e"
-              },
-              "pressed": {
-                "background": "#1d1b2b",
-                "border": "#1c1a29",
-                "foreground": "#75718e"
-              },
-              "active": {
-                "background": "#222031",
-                "border": "#353347",
-                "foreground": "#e0def4"
-              },
-              "disabled": {
-                "background": "#191724",
-                "border": "#1a1826",
-                "foreground": "#4e4b63"
-              },
-              "inverted": {
-                "background": "#e0def4",
-                "border": "#191724",
-                "foreground": "#1f1d2e"
-              }
-            },
-            "on": {
-              "default": {
-                "background": "#1d1b2a",
-                "border": "#232132",
-                "foreground": "#e0def4"
-              },
-              "hovered": {
-                "background": "#232132",
-                "border": "#232132",
-                "foreground": "#e0def4"
-              },
-              "pressed": {
-                "background": "#2f2d40",
-                "border": "#232132",
-                "foreground": "#e0def4"
-              },
-              "active": {
-                "background": "#403e53",
-                "border": "#504d65",
-                "foreground": "#e0def4"
-              },
-              "disabled": {
-                "background": "#1d1b2a",
-                "border": "#1e1c2c",
-                "foreground": "#3b384f"
-              },
-              "inverted": {
-                "background": "#e0def4",
-                "border": "#191724",
-                "foreground": "#3b394e"
-              }
-            },
-            "accent": {
-              "default": {
-                "background": "#2f3739",
-                "border": "#435255",
-                "foreground": "#9cced7"
-              },
-              "hovered": {
-                "background": "#435255",
-                "border": "#435255",
-                "foreground": "#9cced7"
-              },
-              "pressed": {
-                "background": "#4e6164",
-                "border": "#435255",
-                "foreground": "#9cced7"
-              },
-              "active": {
-                "background": "#5d757a",
-                "border": "#6e8f94",
-                "foreground": "#fbfdfd"
-              },
-              "disabled": {
-                "background": "#2f3739",
-                "border": "#3a4446",
-                "foreground": "#85aeb5"
-              },
-              "inverted": {
-                "background": "#fbfdfd",
-                "border": "#171717",
-                "foreground": "#587074"
-              }
-            },
-            "positive": {
-              "default": {
-                "background": "#182e23",
-                "border": "#254839",
-                "foreground": "#5dc2a3"
-              },
-              "hovered": {
-                "background": "#254839",
-                "border": "#254839",
-                "foreground": "#5dc2a3"
-              },
-              "pressed": {
-                "background": "#2c5645",
-                "border": "#254839",
-                "foreground": "#5dc2a3"
-              },
-              "active": {
-                "background": "#356b57",
-                "border": "#40836c",
-                "foreground": "#f9fdfb"
-              },
-              "disabled": {
-                "background": "#182e23",
-                "border": "#1e3b2e",
-                "foreground": "#4ea287"
-              },
-              "inverted": {
-                "background": "#f9fdfb",
-                "border": "#000e00",
-                "foreground": "#326552"
-              }
-            },
-            "warning": {
-              "default": {
-                "background": "#50341a",
-                "border": "#6d4d2b",
-                "foreground": "#f5c177"
-              },
-              "hovered": {
-                "background": "#6d4d2b",
-                "border": "#6d4d2b",
-                "foreground": "#f5c177"
-              },
-              "pressed": {
-                "background": "#7e5a34",
-                "border": "#6d4d2b",
-                "foreground": "#f5c177"
-              },
-              "active": {
-                "background": "#946e41",
-                "border": "#b0854f",
-                "foreground": "#fffcf9"
-              },
-              "disabled": {
-                "background": "#50341a",
-                "border": "#5e4023",
-                "foreground": "#d2a263"
-              },
-              "inverted": {
-                "background": "#fffcf9",
-                "border": "#2c1600",
-                "foreground": "#8e683c"
-              }
-            },
-            "negative": {
-              "default": {
-                "background": "#431820",
-                "border": "#612834",
-                "foreground": "#ea6f92"
-              },
-              "hovered": {
-                "background": "#612834",
-                "border": "#612834",
-                "foreground": "#ea6f92"
-              },
-              "pressed": {
-                "background": "#71303f",
-                "border": "#612834",
-                "foreground": "#ea6f92"
-              },
-              "active": {
-                "background": "#883c4f",
-                "border": "#a44961",
-                "foreground": "#fff9fa"
-              },
-              "disabled": {
-                "background": "#431820",
-                "border": "#52202a",
-                "foreground": "#c75c79"
-              },
-              "inverted": {
-                "background": "#fff9fa",
-                "border": "#230000",
-                "foreground": "#82384a"
-              }
-            }
-          },
-          "popover_shadow": {
-            "blur": 4,
-            "color": "#00000033",
-            "offset": [
-              1,
-              2
-            ]
-          },
-          "modal_shadow": {
-            "blur": 16,
-            "color": "#00000033",
-            "offset": [
-              0,
-              2
-            ]
-          },
-          "players": {
-            "0": {
-              "selection": "#9cced73d",
-              "cursor": "#9cced7"
-            },
-            "1": {
-              "selection": "#5dc2a33d",
-              "cursor": "#5dc2a3"
-            },
-            "2": {
-              "selection": "#9d76913d",
-              "cursor": "#9d7691"
-            },
-            "3": {
-              "selection": "#c4a7e63d",
-              "cursor": "#c4a7e6"
-            },
-            "4": {
-              "selection": "#c4a7e63d",
-              "cursor": "#c4a7e6"
-            },
-            "5": {
-              "selection": "#32748f3d",
-              "cursor": "#32748f"
-            },
-            "6": {
-              "selection": "#ea6f923d",
-              "cursor": "#ea6f92"
-            },
-            "7": {
-              "selection": "#f5c1773d",
-              "cursor": "#f5c177"
-            }
-          },
-          "syntax": {
-            "comment": {
-              "color": "#6e6a86"
-            },
-            "operator": {
-              "color": "#31748f"
-            },
-            "punctuation": {
-              "color": "#908caa"
-            },
-            "variable": {
-              "color": "#e0def4"
-            },
-            "string": {
-              "color": "#f6c177"
-            },
-            "type": {
-              "color": "#9ccfd8"
-            },
-            "type.builtin": {
-              "color": "#9ccfd8"
-            },
-            "boolean": {
-              "color": "#ebbcba"
-            },
-            "function": {
-              "color": "#ebbcba"
-            },
-            "keyword": {
-              "color": "#31748f"
-            },
-            "tag": {
-              "color": "#9ccfd8"
-            },
-            "function.method": {
-              "color": "#ebbcba"
-            },
-            "title": {
-              "color": "#f6c177"
-            },
-            "link_text": {
-              "color": "#9ccfd8",
-              "italic": false
-            },
-            "link_uri": {
-              "color": "#ebbcba"
-            }
-          },
-          "color_family": {
-            "neutral": {
-              "low": 11.568627450980392,
-              "high": 91.37254901960785,
-              "range": 79.80392156862746,
-              "scaling_value": 1.2530712530712529
-            },
-            "red": {
-              "low": 6.862745098039216,
-              "high": 100,
-              "range": 93.13725490196079,
-              "scaling_value": 1.0736842105263158
-            },
-            "orange": {
-              "low": 5.490196078431373,
-              "high": 100,
-              "range": 94.50980392156863,
-              "scaling_value": 1.058091286307054
-            },
-            "yellow": {
-              "low": 8.627450980392156,
-              "high": 100,
-              "range": 91.37254901960785,
-              "scaling_value": 1.094420600858369
-            },
-            "green": {
-              "low": 2.7450980392156863,
-              "high": 100,
-              "range": 97.25490196078431,
-              "scaling_value": 1.028225806451613
-            },
-            "cyan": {
-              "low": 0,
-              "high": 100,
-              "range": 100,
-              "scaling_value": 1
-            },
-            "blue": {
-              "low": 9.019607843137255,
-              "high": 100,
-              "range": 90.98039215686275,
-              "scaling_value": 1.0991379310344827
-            },
-            "violet": {
-              "low": 5.490196078431373,
-              "high": 100,
-              "range": 94.50980392156863,
-              "scaling_value": 1.058091286307054
-            },
-            "magenta": {
-              "low": 0,
-              "high": 100,
-              "range": 100,
-              "scaling_value": 1
-            }
-          }
-        }
-    })
-    .unwrap()
-}
-
-pub fn rose_pine_dawn() -> Theme {
-    serde_json::from_value(json!({
-      "name": "Rosé Pine Dawn",
-      "is_light": true,
-      "ramps": {},
-      "lowest": {
-        "base": {
-          "default": {
-            "background": "#dcd8d8",
-            "border": "#dcd6d5",
-            "foreground": "#575279"
-          },
-          "hovered": {
-            "background": "#dcd6d5",
-            "border": "#dcd6d5",
-            "foreground": "#575279"
-          },
-          "pressed": {
-            "background": "#efe6df",
-            "border": "#dcd6d5",
-            "foreground": "#575279"
-          },
-          "active": {
-            "background": "#c1bac1",
-            "border": "#a9a3b0",
-            "foreground": "#575279"
-          },
-          "disabled": {
-            "background": "#dcd8d8",
-            "border": "#d0cccf",
-            "foreground": "#938fa3"
-          },
-          "inverted": {
-            "background": "#575279",
-            "border": "#faf4ed",
-            "foreground": "#c7c0c5"
-          }
-        },
-        "variant": {
-          "default": {
-            "background": "#dcd8d8",
-            "border": "#dcd6d5",
-            "foreground": "#706c8c"
-          },
-          "hovered": {
-            "background": "#dcd6d5",
-            "border": "#dcd6d5",
-            "foreground": "#706c8c"
-          },
-          "pressed": {
-            "background": "#efe6df",
-            "border": "#dcd6d5",
-            "foreground": "#706c8c"
-          },
-          "active": {
-            "background": "#c1bac1",
-            "border": "#a9a3b0",
-            "foreground": "#575279"
-          },
-          "disabled": {
-            "background": "#dcd8d8",
-            "border": "#d0cccf",
-            "foreground": "#938fa3"
-          },
-          "inverted": {
-            "background": "#575279",
-            "border": "#faf4ed",
-            "foreground": "#c7c0c5"
-          }
-        },
-        "on": {
-          "default": {
-            "background": "#fef9f2",
-            "border": "#e5e0df",
-            "foreground": "#575279"
-          },
-          "hovered": {
-            "background": "#e5e0df",
-            "border": "#e5e0df",
-            "foreground": "#575279"
-          },
-          "pressed": {
-            "background": "#d4d0d2",
-            "border": "#e5e0df",
-            "foreground": "#575279"
-          },
-          "active": {
-            "background": "#dbd5d4",
-            "border": "#dbd3d1",
-            "foreground": "#575279"
-          },
-          "disabled": {
-            "background": "#fef9f2",
-            "border": "#f6f1eb",
-            "foreground": "#b1abb5"
-          },
-          "inverted": {
-            "background": "#575279",
-            "border": "#faf4ed",
-            "foreground": "#d6d1d1"
-          }
-        },
-        "accent": {
-          "default": {
-            "background": "#dde9eb",
-            "border": "#c3d7db",
-            "foreground": "#57949f"
-          },
-          "hovered": {
-            "background": "#c3d7db",
-            "border": "#c3d7db",
-            "foreground": "#57949f"
-          },
-          "pressed": {
-            "background": "#b6cfd3",
-            "border": "#c3d7db",
-            "foreground": "#57949f"
-          },
-          "active": {
-            "background": "#a3c3c9",
-            "border": "#8db6bd",
-            "foreground": "#06090a"
-          },
-          "disabled": {
-            "background": "#dde9eb",
-            "border": "#d0e0e3",
-            "foreground": "#72a5ae"
-          },
-          "inverted": {
-            "background": "#06090a",
-            "border": "#ffffff",
-            "foreground": "#a8c7cd"
-          }
-        },
-        "positive": {
-          "default": {
-            "background": "#dbeee7",
-            "border": "#bee0d5",
-            "foreground": "#3eaa8e"
-          },
-          "hovered": {
-            "background": "#bee0d5",
-            "border": "#bee0d5",
-            "foreground": "#3eaa8e"
-          },
-          "pressed": {
-            "background": "#b0dacb",
-            "border": "#bee0d5",
-            "foreground": "#3eaa8e"
-          },
-          "active": {
-            "background": "#9bd0bf",
-            "border": "#82c6b1",
-            "foreground": "#060a09"
-          },
-          "disabled": {
-            "background": "#dbeee7",
-            "border": "#cde7de",
-            "foreground": "#63b89f"
-          },
-          "inverted": {
-            "background": "#060a09",
-            "border": "#ffffff",
-            "foreground": "#a1d4c3"
-          }
-        },
-        "warning": {
-          "default": {
-            "background": "#ffebd6",
-            "border": "#ffdab7",
-            "foreground": "#e99d35"
-          },
-          "hovered": {
-            "background": "#ffdab7",
-            "border": "#ffdab7",
-            "foreground": "#e99d35"
-          },
-          "pressed": {
-            "background": "#fed2a6",
-            "border": "#ffdab7",
-            "foreground": "#e99d35"
-          },
-          "active": {
-            "background": "#fbc891",
-            "border": "#f7bc77",
-            "foreground": "#330704"
-          },
-          "disabled": {
-            "background": "#ffebd6",
-            "border": "#ffe2c7",
-            "foreground": "#f1ac57"
-          },
-          "inverted": {
-            "background": "#330704",
-            "border": "#ffffff",
-            "foreground": "#fccb97"
-          }
-        },
-        "negative": {
-          "default": {
-            "background": "#f1dfe3",
-            "border": "#e6c6cd",
-            "foreground": "#b4647a"
-          },
-          "hovered": {
-            "background": "#e6c6cd",
-            "border": "#e6c6cd",
-            "foreground": "#b4647a"
-          },
-          "pressed": {
-            "background": "#e0bac2",
-            "border": "#e6c6cd",
-            "foreground": "#b4647a"
-          },
-          "active": {
-            "background": "#d8a8b3",
-            "border": "#ce94a3",
-            "foreground": "#0b0708"
-          },
-          "disabled": {
-            "background": "#f1dfe3",
-            "border": "#ecd2d8",
-            "foreground": "#c17b8e"
-          },
-          "inverted": {
-            "background": "#0b0708",
-            "border": "#ffffff",
-            "foreground": "#dbadb8"
-          }
-        }
-      },
-      "middle": {
-        "base": {
-          "default": {
-            "background": "#fef9f2",
-            "border": "#e5e0df",
-            "foreground": "#575279"
-          },
-          "hovered": {
-            "background": "#e5e0df",
-            "border": "#e5e0df",
-            "foreground": "#575279"
-          },
-          "pressed": {
-            "background": "#d4d0d2",
-            "border": "#e5e0df",
-            "foreground": "#575279"
-          },
-          "active": {
-            "background": "#dbd5d4",
-            "border": "#dbd3d1",
-            "foreground": "#575279"
-          },
-          "disabled": {
-            "background": "#fef9f2",
-            "border": "#f6f1eb",
-            "foreground": "#b1abb5"
-          },
-          "inverted": {
-            "background": "#575279",
-            "border": "#faf4ed",
-            "foreground": "#d6d1d1"
-          }
-        },
-        "variant": {
-          "default": {
-            "background": "#fef9f2",
-            "border": "#e5e0df",
-            "foreground": "#706c8c"
-          },
-          "hovered": {
-            "background": "#e5e0df",
-            "border": "#e5e0df",
-            "foreground": "#706c8c"
-          },
-          "pressed": {
-            "background": "#d4d0d2",
-            "border": "#e5e0df",
-            "foreground": "#706c8c"
-          },
-          "active": {
-            "background": "#dbd5d4",
-            "border": "#dbd3d1",
-            "foreground": "#575279"
-          },
-          "disabled": {
-            "background": "#fef9f2",
-            "border": "#f6f1eb",
-            "foreground": "#b1abb5"
-          },
-          "inverted": {
-            "background": "#575279",
-            "border": "#faf4ed",
-            "foreground": "#d6d1d1"
-          }
-        },
-        "on": {
-          "default": {
-            "background": "#faf4ed",
-            "border": "#fdf8f1",
-            "foreground": "#575279"
-          },
-          "hovered": {
-            "background": "#fdf8f1",
-            "border": "#fdf8f1",
-            "foreground": "#575279"
-          },
-          "pressed": {
-            "background": "#fdf8f2",
-            "border": "#fdf8f1",
-            "foreground": "#575279"
-          },
-          "active": {
-            "background": "#e6e1e0",
-            "border": "#d0cccf",
-            "foreground": "#575279"
-          },
-          "disabled": {
-            "background": "#faf4ed",
-            "border": "#fcf6ef",
-            "foreground": "#efe6df"
-          },
-          "inverted": {
-            "background": "#575279",
-            "border": "#faf4ed",
-            "foreground": "#ede9e5"
-          }
-        },
-        "accent": {
-          "default": {
-            "background": "#dde9eb",
-            "border": "#c3d7db",
-            "foreground": "#57949f"
-          },
-          "hovered": {
-            "background": "#c3d7db",
-            "border": "#c3d7db",
-            "foreground": "#57949f"
-          },
-          "pressed": {
-            "background": "#b6cfd3",
-            "border": "#c3d7db",
-            "foreground": "#57949f"
-          },
-          "active": {
-            "background": "#a3c3c9",
-            "border": "#8db6bd",
-            "foreground": "#06090a"
-          },
-          "disabled": {
-            "background": "#dde9eb",
-            "border": "#d0e0e3",
-            "foreground": "#72a5ae"
-          },
-          "inverted": {
-            "background": "#06090a",
-            "border": "#ffffff",
-            "foreground": "#a8c7cd"
-          }
-        },
-        "positive": {
-          "default": {
-            "background": "#dbeee7",
-            "border": "#bee0d5",
-            "foreground": "#3eaa8e"
-          },
-          "hovered": {
-            "background": "#bee0d5",
-            "border": "#bee0d5",
-            "foreground": "#3eaa8e"
-          },
-          "pressed": {
-            "background": "#b0dacb",
-            "border": "#bee0d5",
-            "foreground": "#3eaa8e"
-          },
-          "active": {
-            "background": "#9bd0bf",
-            "border": "#82c6b1",
-            "foreground": "#060a09"
-          },
-          "disabled": {
-            "background": "#dbeee7",
-            "border": "#cde7de",
-            "foreground": "#63b89f"
-          },
-          "inverted": {
-            "background": "#060a09",
-            "border": "#ffffff",
-            "foreground": "#a1d4c3"
-          }
-        },
-        "warning": {
-          "default": {
-            "background": "#ffebd6",
-            "border": "#ffdab7",
-            "foreground": "#e99d35"
-          },
-          "hovered": {
-            "background": "#ffdab7",
-            "border": "#ffdab7",
-            "foreground": "#e99d35"
-          },
-          "pressed": {
-            "background": "#fed2a6",
-            "border": "#ffdab7",
-            "foreground": "#e99d35"
-          },
-          "active": {
-            "background": "#fbc891",
-            "border": "#f7bc77",
-            "foreground": "#330704"
-          },
-          "disabled": {
-            "background": "#ffebd6",
-            "border": "#ffe2c7",
-            "foreground": "#f1ac57"
-          },
-          "inverted": {
-            "background": "#330704",
-            "border": "#ffffff",
-            "foreground": "#fccb97"
-          }
-        },
-        "negative": {
-          "default": {
-            "background": "#f1dfe3",
-            "border": "#e6c6cd",
-            "foreground": "#b4647a"
-          },
-          "hovered": {
-            "background": "#e6c6cd",
-            "border": "#e6c6cd",
-            "foreground": "#b4647a"
-          },
-          "pressed": {
-            "background": "#e0bac2",
-            "border": "#e6c6cd",
-            "foreground": "#b4647a"
-          },
-          "active": {
-            "background": "#d8a8b3",
-            "border": "#ce94a3",
-            "foreground": "#0b0708"
-          },
-          "disabled": {
-            "background": "#f1dfe3",
-            "border": "#ecd2d8",
-            "foreground": "#c17b8e"
-          },
-          "inverted": {
-            "background": "#0b0708",
-            "border": "#ffffff",
-            "foreground": "#dbadb8"
-          }
-        }
-      },
-      "highest": {
-        "base": {
-          "default": {
-            "background": "#faf4ed",
-            "border": "#fdf8f1",
-            "foreground": "#575279"
-          },
-          "hovered": {
-            "background": "#fdf8f1",
-            "border": "#fdf8f1",
-            "foreground": "#575279"
-          },
-          "pressed": {
-            "background": "#fdf8f2",
-            "border": "#fdf8f1",
-            "foreground": "#575279"
-          },
-          "active": {
-            "background": "#e6e1e0",
-            "border": "#d0cccf",
-            "foreground": "#575279"
-          },
-          "disabled": {
-            "background": "#faf4ed",
-            "border": "#fcf6ef",
-            "foreground": "#efe6df"
-          },
-          "inverted": {
-            "background": "#575279",
-            "border": "#faf4ed",
-            "foreground": "#ede9e5"
-          }
-        },
-        "variant": {
-          "default": {
-            "background": "#faf4ed",
-            "border": "#fdf8f1",
-            "foreground": "#706c8c"
-          },
-          "hovered": {
-            "background": "#fdf8f1",
-            "border": "#fdf8f1",
-            "foreground": "#706c8c"
-          },
-          "pressed": {
-            "background": "#fdf8f2",
-            "border": "#fdf8f1",
-            "foreground": "#706c8c"
-          },
-          "active": {
-            "background": "#e6e1e0",
-            "border": "#d0cccf",
-            "foreground": "#575279"
-          },
-          "disabled": {
-            "background": "#faf4ed",
-            "border": "#fcf6ef",
-            "foreground": "#efe6df"
-          },
-          "inverted": {
-            "background": "#575279",
-            "border": "#faf4ed",
-            "foreground": "#ede9e5"
-          }
-        },
-        "on": {
-          "default": {
-            "background": "#fef9f2",
-            "border": "#e5e0df",
-            "foreground": "#575279"
-          },
-          "hovered": {
-            "background": "#e5e0df",
-            "border": "#e5e0df",
-            "foreground": "#575279"
-          },
-          "pressed": {
-            "background": "#d4d0d2",
-            "border": "#e5e0df",
-            "foreground": "#575279"
-          },
-          "active": {
-            "background": "#dbd5d4",
-            "border": "#dbd3d1",
-            "foreground": "#575279"
-          },
-          "disabled": {
-            "background": "#fef9f2",
-            "border": "#f6f1eb",
-            "foreground": "#b1abb5"
-          },
-          "inverted": {
-            "background": "#575279",
-            "border": "#faf4ed",
-            "foreground": "#d6d1d1"
-          }
-        },
-        "accent": {
-          "default": {
-            "background": "#dde9eb",
-            "border": "#c3d7db",
-            "foreground": "#57949f"
-          },
-          "hovered": {
-            "background": "#c3d7db",
-            "border": "#c3d7db",
-            "foreground": "#57949f"
-          },
-          "pressed": {
-            "background": "#b6cfd3",
-            "border": "#c3d7db",
-            "foreground": "#57949f"
-          },
-          "active": {
-            "background": "#a3c3c9",
-            "border": "#8db6bd",
-            "foreground": "#06090a"
-          },
-          "disabled": {
-            "background": "#dde9eb",
-            "border": "#d0e0e3",
-            "foreground": "#72a5ae"
-          },
-          "inverted": {
-            "background": "#06090a",
-            "border": "#ffffff",
-            "foreground": "#a8c7cd"
-          }
-        },
-        "positive": {
-          "default": {
-            "background": "#dbeee7",
-            "border": "#bee0d5",
-            "foreground": "#3eaa8e"
-          },
-          "hovered": {
-            "background": "#bee0d5",
-            "border": "#bee0d5",
-            "foreground": "#3eaa8e"
-          },
-          "pressed": {
-            "background": "#b0dacb",
-            "border": "#bee0d5",
-            "foreground": "#3eaa8e"
-          },
-          "active": {
-            "background": "#9bd0bf",
-            "border": "#82c6b1",
-            "foreground": "#060a09"
-          },
-          "disabled": {
-            "background": "#dbeee7",
-            "border": "#cde7de",
-            "foreground": "#63b89f"
-          },
-          "inverted": {
-            "background": "#060a09",
-            "border": "#ffffff",
-            "foreground": "#a1d4c3"
-          }
-        },
-        "warning": {
-          "default": {
-            "background": "#ffebd6",
-            "border": "#ffdab7",
-            "foreground": "#e99d35"
-          },
-          "hovered": {
-            "background": "#ffdab7",
-            "border": "#ffdab7",
-            "foreground": "#e99d35"
-          },
-          "pressed": {
-            "background": "#fed2a6",
-            "border": "#ffdab7",
-            "foreground": "#e99d35"
-          },
-          "active": {
-            "background": "#fbc891",
-            "border": "#f7bc77",
-            "foreground": "#330704"
-          },
-          "disabled": {
-            "background": "#ffebd6",
-            "border": "#ffe2c7",
-            "foreground": "#f1ac57"
-          },
-          "inverted": {
-            "background": "#330704",
-            "border": "#ffffff",
-            "foreground": "#fccb97"
-          }
-        },
-        "negative": {
-          "default": {
-            "background": "#f1dfe3",
-            "border": "#e6c6cd",
-            "foreground": "#b4647a"
-          },
-          "hovered": {
-            "background": "#e6c6cd",
-            "border": "#e6c6cd",
-            "foreground": "#b4647a"
-          },
-          "pressed": {
-            "background": "#e0bac2",
-            "border": "#e6c6cd",
-            "foreground": "#b4647a"
-          },
-          "active": {
-            "background": "#d8a8b3",
-            "border": "#ce94a3",
-            "foreground": "#0b0708"
-          },
-          "disabled": {
-            "background": "#f1dfe3",
-            "border": "#ecd2d8",
-            "foreground": "#c17b8e"
-          },
-          "inverted": {
-            "background": "#0b0708",
-            "border": "#ffffff",
-            "foreground": "#dbadb8"
-          }
-        }
-      },
-      "popover_shadow": {
-        "blur": 4,
-        "color": "#2c2a4d33",
-        "offset": [
-          1,
-          2
-        ]
-      },
-      "modal_shadow": {
-        "blur": 16,
-        "color": "#2c2a4d33",
-        "offset": [
-          0,
-          2
-        ]
-      },
-      "players": {
-        "0": {
-          "selection": "#57949f3d",
-          "cursor": "#57949f"
-        },
-        "1": {
-          "selection": "#3eaa8e3d",
-          "cursor": "#3eaa8e"
-        },
-        "2": {
-          "selection": "#7c697f3d",
-          "cursor": "#7c697f"
-        },
-        "3": {
-          "selection": "#907aa93d",
-          "cursor": "#907aa9"
-        },
-        "4": {
-          "selection": "#907aa93d",
-          "cursor": "#907aa9"
-        },
-        "5": {
-          "selection": "#2a69833d",
-          "cursor": "#2a6983"
-        },
-        "6": {
-          "selection": "#b4647a3d",
-          "cursor": "#b4647a"
-        },
-        "7": {
-          "selection": "#e99d353d",
-          "cursor": "#e99d35"
-        }
-      },
-      "syntax": {
-        "comment": {
-          "color": "#9893a5"
-        },
-        "operator": {
-          "color": "#286983"
-        },
-        "punctuation": {
-          "color": "#797593"
-        },
-        "variable": {
-          "color": "#575279"
-        },
-        "string": {
-          "color": "#ea9d34"
-        },
-        "type": {
-          "color": "#56949f"
-        },
-        "type.builtin": {
-          "color": "#56949f"
-        },
-        "boolean": {
-          "color": "#d7827e"
-        },
-        "function": {
-          "color": "#d7827e"
-        },
-        "keyword": {
-          "color": "#286983"
-        },
-        "tag": {
-          "color": "#56949f"
-        },
-        "function.method": {
-          "color": "#d7827e"
-        },
-        "title": {
-          "color": "#ea9d34"
-        },
-        "link_text": {
-          "color": "#56949f",
-          "italic": false
-        },
-        "link_uri": {
-          "color": "#d7827e"
-        }
-      },
-      "color_family": {
-        "neutral": {
-          "low": 39.80392156862745,
-          "high": 95.49019607843137,
-          "range": 55.686274509803916,
-          "scaling_value": 1.7957746478873242
-        },
-        "red": {
-          "low": 0,
-          "high": 100,
-          "range": 100,
-          "scaling_value": 1
-        },
-        "orange": {
-          "low": 0,
-          "high": 100,
-          "range": 100,
-          "scaling_value": 1
-        },
-        "yellow": {
-          "low": 8.823529411764707,
-          "high": 100,
-          "range": 91.17647058823529,
-          "scaling_value": 1.0967741935483872
-        },
-        "green": {
-          "low": 0,
-          "high": 100,
-          "range": 100,
-          "scaling_value": 1
-        },
-        "cyan": {
-          "low": 0,
-          "high": 100,
-          "range": 100,
-          "scaling_value": 1
-        },
-        "blue": {
-          "low": 0,
-          "high": 100,
-          "range": 100,
-          "scaling_value": 1
-        },
-        "violet": {
-          "low": 0,
-          "high": 100,
-          "range": 100,
-          "scaling_value": 1
-        },
-        "magenta": {
-          "low": 0,
-          "high": 100,
-          "range": 100,
-          "scaling_value": 1
-        }
-      }
-    }))
-    .unwrap()
-}

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

@@ -0,0 +1,2118 @@
+use gpui2::Rgba;
+use indexmap::IndexMap;
+
+use crate::scale::{ColorScaleName, ColorScaleSet, ColorScales};
+
+struct DefaultColorScaleSet {
+    scale: ColorScaleName,
+    light: [&'static str; 12],
+    light_alpha: [&'static str; 12],
+    dark: [&'static str; 12],
+    dark_alpha: [&'static str; 12],
+}
+
+impl From<DefaultColorScaleSet> for ColorScaleSet {
+    fn from(default: DefaultColorScaleSet) -> Self {
+        Self::new(
+            default.scale,
+            default
+                .light
+                .map(|color| Rgba::try_from(color).unwrap().into()),
+            default
+                .light_alpha
+                .map(|color| Rgba::try_from(color).unwrap().into()),
+            default
+                .dark
+                .map(|color| Rgba::try_from(color).unwrap().into()),
+            default
+                .dark_alpha
+                .map(|color| Rgba::try_from(color).unwrap().into()),
+        )
+    }
+}
+
+pub fn default_color_scales() -> ColorScales {
+    use ColorScaleName::*;
+
+    IndexMap::from_iter([
+        (Gray, gray().into()),
+        (Mauve, mauve().into()),
+        (Slate, slate().into()),
+        (Sage, sage().into()),
+        (Olive, olive().into()),
+        (Sand, sand().into()),
+        (Gold, gold().into()),
+        (Bronze, bronze().into()),
+        (Brown, brown().into()),
+        (Yellow, yellow().into()),
+        (Amber, amber().into()),
+        (Orange, orange().into()),
+        (Tomato, tomato().into()),
+        (Red, red().into()),
+        (Ruby, ruby().into()),
+        (Crimson, crimson().into()),
+        (Pink, pink().into()),
+        (Plum, plum().into()),
+        (Purple, purple().into()),
+        (Violet, violet().into()),
+        (Iris, iris().into()),
+        (Indigo, indigo().into()),
+        (Blue, blue().into()),
+        (Cyan, cyan().into()),
+        (Teal, teal().into()),
+        (Jade, jade().into()),
+        (Green, green().into()),
+        (Grass, grass().into()),
+        (Lime, lime().into()),
+        (Mint, mint().into()),
+        (Sky, sky().into()),
+        (Black, black().into()),
+        (White, white().into()),
+    ])
+}
+
+fn gray() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Gray,
+        light: [
+            "#fcfcfcff",
+            "#f9f9f9ff",
+            "#f0f0f0ff",
+            "#e8e8e8ff",
+            "#e0e0e0ff",
+            "#d9d9d9ff",
+            "#cececeff",
+            "#bbbbbbff",
+            "#8d8d8dff",
+            "#838383ff",
+            "#646464ff",
+            "#202020ff",
+        ],
+        light_alpha: [
+            "#00000003",
+            "#00000006",
+            "#0000000f",
+            "#00000017",
+            "#0000001f",
+            "#00000026",
+            "#00000031",
+            "#00000044",
+            "#00000072",
+            "#0000007c",
+            "#0000009b",
+            "#000000df",
+        ],
+        dark: [
+            "#111111ff",
+            "#191919ff",
+            "#222222ff",
+            "#2a2a2aff",
+            "#313131ff",
+            "#3a3a3aff",
+            "#484848ff",
+            "#606060ff",
+            "#6e6e6eff",
+            "#7b7b7bff",
+            "#b4b4b4ff",
+            "#eeeeeeff",
+        ],
+        dark_alpha: [
+            "#00000000",
+            "#ffffff09",
+            "#ffffff12",
+            "#ffffff1b",
+            "#ffffff22",
+            "#ffffff2c",
+            "#ffffff3b",
+            "#ffffff55",
+            "#ffffff64",
+            "#ffffff72",
+            "#ffffffaf",
+            "#ffffffed",
+        ],
+    }
+}
+
+fn mauve() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Mauve,
+        light: [
+            "#fdfcfdff",
+            "#faf9fbff",
+            "#f2eff3ff",
+            "#eae7ecff",
+            "#e3dfe6ff",
+            "#dbd8e0ff",
+            "#d0cdd7ff",
+            "#bcbac7ff",
+            "#8e8c99ff",
+            "#84828eff",
+            "#65636dff",
+            "#211f26ff",
+        ],
+        light_alpha: [
+            "#55005503",
+            "#2b005506",
+            "#30004010",
+            "#20003618",
+            "#20003820",
+            "#14003527",
+            "#10003332",
+            "#08003145",
+            "#05001d73",
+            "#0500197d",
+            "#0400119c",
+            "#020008e0",
+        ],
+        dark: [
+            "#121113ff",
+            "#1a191bff",
+            "#232225ff",
+            "#2b292dff",
+            "#323035ff",
+            "#3c393fff",
+            "#49474eff",
+            "#625f69ff",
+            "#6f6d78ff",
+            "#7c7a85ff",
+            "#b5b2bcff",
+            "#eeeef0ff",
+        ],
+        dark_alpha: [
+            "#00000000",
+            "#f5f4f609",
+            "#ebeaf814",
+            "#eee5f81d",
+            "#efe6fe25",
+            "#f1e6fd30",
+            "#eee9ff40",
+            "#eee7ff5d",
+            "#eae6fd6e",
+            "#ece9fd7c",
+            "#f5f1ffb7",
+            "#fdfdffef",
+        ],
+    }
+}
+
+fn slate() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Slate,
+        light: [
+            "#fcfcfdff",
+            "#f9f9fbff",
+            "#f0f0f3ff",
+            "#e8e8ecff",
+            "#e0e1e6ff",
+            "#d9d9e0ff",
+            "#cdced6ff",
+            "#b9bbc6ff",
+            "#8b8d98ff",
+            "#80838dff",
+            "#60646cff",
+            "#1c2024ff",
+        ],
+        light_alpha: [
+            "#00005503",
+            "#00005506",
+            "#0000330f",
+            "#00002d17",
+            "#0009321f",
+            "#00002f26",
+            "#00062e32",
+            "#00083046",
+            "#00051d74",
+            "#00071b7f",
+            "#0007149f",
+            "#000509e3",
+        ],
+        dark: [
+            "#111113ff",
+            "#18191bff",
+            "#212225ff",
+            "#272a2dff",
+            "#2e3135ff",
+            "#363a3fff",
+            "#43484eff",
+            "#5a6169ff",
+            "#696e77ff",
+            "#777b84ff",
+            "#b0b4baff",
+            "#edeef0ff",
+        ],
+        dark_alpha: [
+            "#00000000",
+            "#d8f4f609",
+            "#ddeaf814",
+            "#d3edf81d",
+            "#d9edfe25",
+            "#d6ebfd30",
+            "#d9edff40",
+            "#d9edff5d",
+            "#dfebfd6d",
+            "#e5edfd7b",
+            "#f1f7feb5",
+            "#fcfdffef",
+        ],
+    }
+}
+
+fn sage() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Sage,
+        light: [
+            "#fbfdfcff",
+            "#f7f9f8ff",
+            "#eef1f0ff",
+            "#e6e9e8ff",
+            "#dfe2e0ff",
+            "#d7dad9ff",
+            "#cbcfcdff",
+            "#b8bcbaff",
+            "#868e8bff",
+            "#7c8481ff",
+            "#5f6563ff",
+            "#1a211eff",
+        ],
+        light_alpha: [
+            "#00804004",
+            "#00402008",
+            "#002d1e11",
+            "#001f1519",
+            "#00180820",
+            "#00140d28",
+            "#00140a34",
+            "#000f0847",
+            "#00110b79",
+            "#00100a83",
+            "#000a07a0",
+            "#000805e5",
+        ],
+        dark: [
+            "#101211ff",
+            "#171918ff",
+            "#202221ff",
+            "#272a29ff",
+            "#2e3130ff",
+            "#373b39ff",
+            "#444947ff",
+            "#5b625fff",
+            "#63706bff",
+            "#717d79ff",
+            "#adb5b2ff",
+            "#eceeedff",
+        ],
+        dark_alpha: [
+            "#00000000",
+            "#f0f2f108",
+            "#f3f5f412",
+            "#f2fefd1a",
+            "#f1fbfa22",
+            "#edfbf42d",
+            "#edfcf73c",
+            "#ebfdf657",
+            "#dffdf266",
+            "#e5fdf674",
+            "#f4fefbb0",
+            "#fdfffeed",
+        ],
+    }
+}
+
+fn olive() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Olive,
+        light: [
+            "#fcfdfcff",
+            "#f8faf8ff",
+            "#eff1efff",
+            "#e7e9e7ff",
+            "#dfe2dfff",
+            "#d7dad7ff",
+            "#cccfccff",
+            "#b9bcb8ff",
+            "#898e87ff",
+            "#7f847dff",
+            "#60655fff",
+            "#1d211cff",
+        ],
+        light_alpha: [
+            "#00550003",
+            "#00490007",
+            "#00200010",
+            "#00160018",
+            "#00180020",
+            "#00140028",
+            "#000f0033",
+            "#040f0047",
+            "#050f0078",
+            "#040e0082",
+            "#020a00a0",
+            "#010600e3",
+        ],
+        dark: [
+            "#111210ff",
+            "#181917ff",
+            "#212220ff",
+            "#282a27ff",
+            "#2f312eff",
+            "#383a36ff",
+            "#454843ff",
+            "#5c625bff",
+            "#687066ff",
+            "#767d74ff",
+            "#afb5adff",
+            "#eceeecff",
+        ],
+        dark_alpha: [
+            "#00000000",
+            "#f1f2f008",
+            "#f4f5f312",
+            "#f3fef21a",
+            "#f2fbf122",
+            "#f4faed2c",
+            "#f2fced3b",
+            "#edfdeb57",
+            "#ebfde766",
+            "#f0fdec74",
+            "#f6fef4b0",
+            "#fdfffded",
+        ],
+    }
+}
+
+fn sand() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Sand,
+        light: [
+            "#fdfdfcff",
+            "#f9f9f8ff",
+            "#f1f0efff",
+            "#e9e8e6ff",
+            "#e2e1deff",
+            "#dad9d6ff",
+            "#cfcecaff",
+            "#bcbbb5ff",
+            "#8d8d86ff",
+            "#82827cff",
+            "#63635eff",
+            "#21201cff",
+        ],
+        light_alpha: [
+            "#55550003",
+            "#25250007",
+            "#20100010",
+            "#1f150019",
+            "#1f180021",
+            "#19130029",
+            "#19140035",
+            "#1915014a",
+            "#0f0f0079",
+            "#0c0c0083",
+            "#080800a1",
+            "#060500e3",
+        ],
+        dark: [
+            "#111110ff",
+            "#191918ff",
+            "#222221ff",
+            "#2a2a28ff",
+            "#31312eff",
+            "#3b3a37ff",
+            "#494844ff",
+            "#62605bff",
+            "#6f6d66ff",
+            "#7c7b74ff",
+            "#b5b3adff",
+            "#eeeeecff",
+        ],
+        dark_alpha: [
+            "#00000000",
+            "#f4f4f309",
+            "#f6f6f513",
+            "#fefef31b",
+            "#fbfbeb23",
+            "#fffaed2d",
+            "#fffbed3c",
+            "#fff9eb57",
+            "#fffae965",
+            "#fffdee73",
+            "#fffcf4b0",
+            "#fffffded",
+        ],
+    }
+}
+
+fn gold() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Gold,
+        light: [
+            "#fdfdfcff",
+            "#faf9f2ff",
+            "#f2f0e7ff",
+            "#eae6dbff",
+            "#e1dccfff",
+            "#d8d0bfff",
+            "#cbc0aaff",
+            "#b9a88dff",
+            "#978365ff",
+            "#8c7a5eff",
+            "#71624bff",
+            "#3b352bff",
+        ],
+        light_alpha: [
+            "#55550003",
+            "#9d8a000d",
+            "#75600018",
+            "#6b4e0024",
+            "#60460030",
+            "#64440040",
+            "#63420055",
+            "#633d0072",
+            "#5332009a",
+            "#492d00a1",
+            "#362100b4",
+            "#130c00d4",
+        ],
+        dark: [
+            "#121211ff",
+            "#1b1a17ff",
+            "#24231fff",
+            "#2d2b26ff",
+            "#38352eff",
+            "#444039ff",
+            "#544f46ff",
+            "#696256ff",
+            "#978365ff",
+            "#a39073ff",
+            "#cbb99fff",
+            "#e8e2d9ff",
+        ],
+        dark_alpha: [
+            "#91911102",
+            "#f9e29d0b",
+            "#f8ecbb15",
+            "#ffeec41e",
+            "#feecc22a",
+            "#feebcb37",
+            "#ffedcd48",
+            "#fdeaca5f",
+            "#ffdba690",
+            "#fedfb09d",
+            "#fee7c6c8",
+            "#fef7ede7",
+        ],
+    }
+}
+
+fn bronze() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Bronze,
+        light: [
+            "#fdfcfcff",
+            "#fdf7f5ff",
+            "#f6edeaff",
+            "#efe4dfff",
+            "#e7d9d3ff",
+            "#dfcdc5ff",
+            "#d3bcb3ff",
+            "#c2a499ff",
+            "#a18072ff",
+            "#957468ff",
+            "#7d5e54ff",
+            "#43302bff",
+        ],
+        light_alpha: [
+            "#55000003",
+            "#cc33000a",
+            "#92250015",
+            "#80280020",
+            "#7423002c",
+            "#7324003a",
+            "#6c1f004c",
+            "#671c0066",
+            "#551a008d",
+            "#4c150097",
+            "#3d0f00ab",
+            "#1d0600d4",
+        ],
+        dark: [
+            "#141110ff",
+            "#1c1917ff",
+            "#262220ff",
+            "#302a27ff",
+            "#3b3330ff",
+            "#493e3aff",
+            "#5a4c47ff",
+            "#6f5f58ff",
+            "#a18072ff",
+            "#ae8c7eff",
+            "#d4b3a5ff",
+            "#ede0d9ff",
+        ],
+        dark_alpha: [
+            "#d1110004",
+            "#fbbc910c",
+            "#faceb817",
+            "#facdb622",
+            "#ffd2c12d",
+            "#ffd1c03c",
+            "#fdd0c04f",
+            "#ffd6c565",
+            "#fec7b09b",
+            "#fecab5a9",
+            "#ffd7c6d1",
+            "#fff1e9ec",
+        ],
+    }
+}
+
+fn brown() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Brown,
+        light: [
+            "#fefdfcff",
+            "#fcf9f6ff",
+            "#f6eee7ff",
+            "#f0e4d9ff",
+            "#ebdacaff",
+            "#e4cdb7ff",
+            "#dcbc9fff",
+            "#cea37eff",
+            "#ad7f58ff",
+            "#a07553ff",
+            "#815e46ff",
+            "#3e332eff",
+        ],
+        light_alpha: [
+            "#aa550003",
+            "#aa550009",
+            "#a04b0018",
+            "#9b4a0026",
+            "#9f4d0035",
+            "#a04e0048",
+            "#a34e0060",
+            "#9f4a0081",
+            "#823c00a7",
+            "#723300ac",
+            "#522100b9",
+            "#140600d1",
+        ],
+        dark: [
+            "#12110fff",
+            "#1c1816ff",
+            "#28211dff",
+            "#322922ff",
+            "#3e3128ff",
+            "#4d3c2fff",
+            "#614a39ff",
+            "#7c5f46ff",
+            "#ad7f58ff",
+            "#b88c67ff",
+            "#dbb594ff",
+            "#f2e1caff",
+        ],
+        dark_alpha: [
+            "#91110002",
+            "#fba67c0c",
+            "#fcb58c19",
+            "#fbbb8a24",
+            "#fcb88931",
+            "#fdba8741",
+            "#ffbb8856",
+            "#ffbe8773",
+            "#feb87da8",
+            "#ffc18cb3",
+            "#fed1aad9",
+            "#feecd4f2",
+        ],
+    }
+}
+
+fn yellow() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Yellow,
+        light: [
+            "#fdfdf9ff",
+            "#fefce9ff",
+            "#fffab8ff",
+            "#fff394ff",
+            "#ffe770ff",
+            "#f3d768ff",
+            "#e4c767ff",
+            "#d5ae39ff",
+            "#ffe629ff",
+            "#ffdc00ff",
+            "#9e6c00ff",
+            "#473b1fff",
+        ],
+        light_alpha: [
+            "#aaaa0006",
+            "#f4dd0016",
+            "#ffee0047",
+            "#ffe3016b",
+            "#ffd5008f",
+            "#ebbc0097",
+            "#d2a10098",
+            "#c99700c6",
+            "#ffe100d6",
+            "#ffdc00ff",
+            "#9e6c00ff",
+            "#2e2000e0",
+        ],
+        dark: [
+            "#14120bff",
+            "#1b180fff",
+            "#2d2305ff",
+            "#362b00ff",
+            "#433500ff",
+            "#524202ff",
+            "#665417ff",
+            "#836a21ff",
+            "#ffe629ff",
+            "#ffff57ff",
+            "#f5e147ff",
+            "#f6eeb4ff",
+        ],
+        dark_alpha: [
+            "#d1510004",
+            "#f9b4000b",
+            "#ffaa001e",
+            "#fdb70028",
+            "#febb0036",
+            "#fec40046",
+            "#fdcb225c",
+            "#fdca327b",
+            "#ffe629ff",
+            "#ffff57ff",
+            "#fee949f5",
+            "#fef6baf6",
+        ],
+    }
+}
+
+fn amber() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Amber,
+        light: [
+            "#fefdfbff",
+            "#fefbe9ff",
+            "#fff7c2ff",
+            "#ffee9cff",
+            "#fbe577ff",
+            "#f3d673ff",
+            "#e9c162ff",
+            "#e2a336ff",
+            "#ffc53dff",
+            "#ffba18ff",
+            "#ab6400ff",
+            "#4f3422ff",
+        ],
+        light_alpha: [
+            "#c0800004",
+            "#f4d10016",
+            "#ffde003d",
+            "#ffd40063",
+            "#f8cf0088",
+            "#eab5008c",
+            "#dc9b009d",
+            "#da8a00c9",
+            "#ffb300c2",
+            "#ffb300e7",
+            "#ab6400ff",
+            "#341500dd",
+        ],
+        dark: [
+            "#16120cff",
+            "#1d180fff",
+            "#302008ff",
+            "#3f2700ff",
+            "#4d3000ff",
+            "#5c3d05ff",
+            "#714f19ff",
+            "#8f6424ff",
+            "#ffc53dff",
+            "#ffd60aff",
+            "#ffca16ff",
+            "#ffe7b3ff",
+        ],
+        dark_alpha: [
+            "#e63c0006",
+            "#fd9b000d",
+            "#fa820022",
+            "#fc820032",
+            "#fd8b0041",
+            "#fd9b0051",
+            "#ffab2567",
+            "#ffae3587",
+            "#ffc53dff",
+            "#ffd60aff",
+            "#ffca16ff",
+            "#ffe7b3ff",
+        ],
+    }
+}
+
+fn orange() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Orange,
+        light: [
+            "#fefcfbff",
+            "#fff7edff",
+            "#ffefd6ff",
+            "#ffdfb5ff",
+            "#ffd19aff",
+            "#ffc182ff",
+            "#f5ae73ff",
+            "#ec9455ff",
+            "#f76b15ff",
+            "#ef5f00ff",
+            "#cc4e00ff",
+            "#582d1dff",
+        ],
+        light_alpha: [
+            "#c0400004",
+            "#ff8e0012",
+            "#ff9c0029",
+            "#ff91014a",
+            "#ff8b0065",
+            "#ff81007d",
+            "#ed6c008c",
+            "#e35f00aa",
+            "#f65e00ea",
+            "#ef5f00ff",
+            "#cc4e00ff",
+            "#431200e2",
+        ],
+        dark: [
+            "#17120eff",
+            "#1e160fff",
+            "#331e0bff",
+            "#462100ff",
+            "#562800ff",
+            "#66350cff",
+            "#7e451dff",
+            "#a35829ff",
+            "#f76b15ff",
+            "#ff801fff",
+            "#ffa057ff",
+            "#ffe0c2ff",
+        ],
+        dark_alpha: [
+            "#ec360007",
+            "#fe6d000e",
+            "#fb6a0025",
+            "#ff590039",
+            "#ff61004a",
+            "#fd75045c",
+            "#ff832c75",
+            "#fe84389d",
+            "#fe6d15f7",
+            "#ff801fff",
+            "#ffa057ff",
+            "#ffe0c2ff",
+        ],
+    }
+}
+
+fn tomato() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Tomato,
+        light: [
+            "#fffcfcff",
+            "#fff8f7ff",
+            "#feebe7ff",
+            "#ffdcd3ff",
+            "#ffcdc2ff",
+            "#fdbdafff",
+            "#f5a898ff",
+            "#ec8e7bff",
+            "#e54d2eff",
+            "#dd4425ff",
+            "#d13415ff",
+            "#5c271fff",
+        ],
+        light_alpha: [
+            "#ff000003",
+            "#ff200008",
+            "#f52b0018",
+            "#ff35002c",
+            "#ff2e003d",
+            "#f92d0050",
+            "#e7280067",
+            "#db250084",
+            "#df2600d1",
+            "#d72400da",
+            "#cd2200ea",
+            "#460900e0",
+        ],
+        dark: [
+            "#181111ff",
+            "#1f1513ff",
+            "#391714ff",
+            "#4e1511ff",
+            "#5e1c16ff",
+            "#6e2920ff",
+            "#853a2dff",
+            "#ac4d39ff",
+            "#e54d2eff",
+            "#ec6142ff",
+            "#ff977dff",
+            "#fbd3cbff",
+        ],
+        dark_alpha: [
+            "#f1121208",
+            "#ff55330f",
+            "#ff35232b",
+            "#fd201142",
+            "#fe332153",
+            "#ff4f3864",
+            "#fd644a7d",
+            "#fe6d4ea7",
+            "#fe5431e4",
+            "#ff6847eb",
+            "#ff977dff",
+            "#ffd6cefb",
+        ],
+    }
+}
+
+fn red() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Red,
+        light: [
+            "#fffcfcff",
+            "#fff7f7ff",
+            "#feebecff",
+            "#ffdbdcff",
+            "#ffcdceff",
+            "#fdbdbeff",
+            "#f4a9aaff",
+            "#eb8e90ff",
+            "#e5484dff",
+            "#dc3e42ff",
+            "#ce2c31ff",
+            "#641723ff",
+        ],
+        light_alpha: [
+            "#ff000003",
+            "#ff000008",
+            "#f3000d14",
+            "#ff000824",
+            "#ff000632",
+            "#f8000442",
+            "#df000356",
+            "#d2000571",
+            "#db0007b7",
+            "#d10005c1",
+            "#c40006d3",
+            "#55000de8",
+        ],
+        dark: [
+            "#191111ff",
+            "#201314ff",
+            "#3b1219ff",
+            "#500f1cff",
+            "#611623ff",
+            "#72232dff",
+            "#8c333aff",
+            "#b54548ff",
+            "#e5484dff",
+            "#ec5d5eff",
+            "#ff9592ff",
+            "#ffd1d9ff",
+        ],
+        dark_alpha: [
+            "#f4121209",
+            "#f22f3e11",
+            "#ff173f2d",
+            "#fe0a3b44",
+            "#ff204756",
+            "#ff3e5668",
+            "#ff536184",
+            "#ff5d61b0",
+            "#fe4e54e4",
+            "#ff6465eb",
+            "#ff9592ff",
+            "#ffd1d9ff",
+        ],
+    }
+}
+
+fn ruby() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Ruby,
+        light: [
+            "#fffcfdff",
+            "#fff7f8ff",
+            "#feeaedff",
+            "#ffdce1ff",
+            "#ffced6ff",
+            "#f8bfc8ff",
+            "#efacb8ff",
+            "#e592a3ff",
+            "#e54666ff",
+            "#dc3b5dff",
+            "#ca244dff",
+            "#64172bff",
+        ],
+        light_alpha: [
+            "#ff005503",
+            "#ff002008",
+            "#f3002515",
+            "#ff002523",
+            "#ff002a31",
+            "#e4002440",
+            "#ce002553",
+            "#c300286d",
+            "#db002cb9",
+            "#d2002cc4",
+            "#c10030db",
+            "#550016e8",
+        ],
+        dark: [
+            "#191113ff",
+            "#1e1517ff",
+            "#3a141eff",
+            "#4e1325ff",
+            "#5e1a2eff",
+            "#6f2539ff",
+            "#883447ff",
+            "#b3445aff",
+            "#e54666ff",
+            "#ec5a72ff",
+            "#ff949dff",
+            "#fed2e1ff",
+        ],
+        dark_alpha: [
+            "#f4124a09",
+            "#fe5a7f0e",
+            "#ff235d2c",
+            "#fd195e42",
+            "#fe2d6b53",
+            "#ff447665",
+            "#ff577d80",
+            "#ff5c7cae",
+            "#fe4c70e4",
+            "#ff617beb",
+            "#ff949dff",
+            "#ffd3e2fe",
+        ],
+    }
+}
+
+fn crimson() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Crimson,
+        light: [
+            "#fffcfdff",
+            "#fef7f9ff",
+            "#ffe9f0ff",
+            "#fedce7ff",
+            "#faceddff",
+            "#f3bed1ff",
+            "#eaacc3ff",
+            "#e093b2ff",
+            "#e93d82ff",
+            "#df3478ff",
+            "#cb1d63ff",
+            "#621639ff",
+        ],
+        light_alpha: [
+            "#ff005503",
+            "#e0004008",
+            "#ff005216",
+            "#f8005123",
+            "#e5004f31",
+            "#d0004b41",
+            "#bf004753",
+            "#b6004a6c",
+            "#e2005bc2",
+            "#d70056cb",
+            "#c4004fe2",
+            "#530026e9",
+        ],
+        dark: [
+            "#191114ff",
+            "#201318ff",
+            "#381525ff",
+            "#4d122fff",
+            "#5c1839ff",
+            "#6d2545ff",
+            "#873356ff",
+            "#b0436eff",
+            "#e93d82ff",
+            "#ee518aff",
+            "#ff92adff",
+            "#fdd3e8ff",
+        ],
+        dark_alpha: [
+            "#f4126709",
+            "#f22f7a11",
+            "#fe2a8b2a",
+            "#fd158741",
+            "#fd278f51",
+            "#fe459763",
+            "#fd559b7f",
+            "#fe5b9bab",
+            "#fe418de8",
+            "#ff5693ed",
+            "#ff92adff",
+            "#ffd5eafd",
+        ],
+    }
+}
+
+fn pink() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Pink,
+        light: [
+            "#fffcfeff",
+            "#fef7fbff",
+            "#fee9f5ff",
+            "#fbdcefff",
+            "#f6cee7ff",
+            "#efbfddff",
+            "#e7acd0ff",
+            "#dd93c2ff",
+            "#d6409fff",
+            "#cf3897ff",
+            "#c2298aff",
+            "#651249ff",
+        ],
+        light_alpha: [
+            "#ff00aa03",
+            "#e0008008",
+            "#f4008c16",
+            "#e2008b23",
+            "#d1008331",
+            "#c0007840",
+            "#b6006f53",
+            "#af006f6c",
+            "#c8007fbf",
+            "#c2007ac7",
+            "#b60074d6",
+            "#59003bed",
+        ],
+        dark: [
+            "#191117ff",
+            "#21121dff",
+            "#37172fff",
+            "#4b143dff",
+            "#591c47ff",
+            "#692955ff",
+            "#833869ff",
+            "#a84885ff",
+            "#d6409fff",
+            "#de51a8ff",
+            "#ff8dccff",
+            "#fdd1eaff",
+        ],
+        dark_alpha: [
+            "#f412bc09",
+            "#f420bb12",
+            "#fe37cc29",
+            "#fc1ec43f",
+            "#fd35c24e",
+            "#fd51c75f",
+            "#fd62c87b",
+            "#ff68c8a2",
+            "#fe49bcd4",
+            "#ff5cc0dc",
+            "#ff8dccff",
+            "#ffd3ecfd",
+        ],
+    }
+}
+
+fn plum() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Plum,
+        light: [
+            "#fefcffff",
+            "#fdf7fdff",
+            "#fbebfbff",
+            "#f7def8ff",
+            "#f2d1f3ff",
+            "#e9c2ecff",
+            "#deade3ff",
+            "#cf91d8ff",
+            "#ab4abaff",
+            "#a144afff",
+            "#953ea3ff",
+            "#53195dff",
+        ],
+        light_alpha: [
+            "#aa00ff03",
+            "#c000c008",
+            "#cc00cc14",
+            "#c200c921",
+            "#b700bd2e",
+            "#a400b03d",
+            "#9900a852",
+            "#9000a56e",
+            "#89009eb5",
+            "#7f0092bb",
+            "#730086c1",
+            "#40004be6",
+        ],
+        dark: [
+            "#181118ff",
+            "#201320ff",
+            "#351a35ff",
+            "#451d47ff",
+            "#512454ff",
+            "#5e3061ff",
+            "#734079ff",
+            "#92549cff",
+            "#ab4abaff",
+            "#b658c4ff",
+            "#e796f3ff",
+            "#f4d4f4ff",
+        ],
+        dark_alpha: [
+            "#f112f108",
+            "#f22ff211",
+            "#fd4cfd27",
+            "#f646ff3a",
+            "#f455ff48",
+            "#f66dff56",
+            "#f07cfd70",
+            "#ee84ff95",
+            "#e961feb6",
+            "#ed70ffc0",
+            "#f19cfef3",
+            "#feddfef4",
+        ],
+    }
+}
+
+fn purple() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Purple,
+        light: [
+            "#fefcfeff",
+            "#fbf7feff",
+            "#f7edfeff",
+            "#f2e2fcff",
+            "#ead5f9ff",
+            "#e0c4f4ff",
+            "#d1afecff",
+            "#be93e4ff",
+            "#8e4ec6ff",
+            "#8347b9ff",
+            "#8145b5ff",
+            "#402060ff",
+        ],
+        light_alpha: [
+            "#aa00aa03",
+            "#8000e008",
+            "#8e00f112",
+            "#8d00e51d",
+            "#8000db2a",
+            "#7a01d03b",
+            "#6d00c350",
+            "#6600c06c",
+            "#5c00adb1",
+            "#53009eb8",
+            "#52009aba",
+            "#250049df",
+        ],
+        dark: [
+            "#18111bff",
+            "#1e1523ff",
+            "#301c3bff",
+            "#3d224eff",
+            "#48295cff",
+            "#54346bff",
+            "#664282ff",
+            "#8457aaff",
+            "#8e4ec6ff",
+            "#9a5cd0ff",
+            "#d19dffff",
+            "#ecd9faff",
+        ],
+        dark_alpha: [
+            "#b412f90b",
+            "#b744f714",
+            "#c150ff2d",
+            "#bb53fd42",
+            "#be5cfd51",
+            "#c16dfd61",
+            "#c378fd7a",
+            "#c47effa4",
+            "#b661ffc2",
+            "#bc6fffcd",
+            "#d19dffff",
+            "#f1ddfffa",
+        ],
+    }
+}
+
+fn violet() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Violet,
+        light: [
+            "#fdfcfeff",
+            "#faf8ffff",
+            "#f4f0feff",
+            "#ebe4ffff",
+            "#e1d9ffff",
+            "#d4cafeff",
+            "#c2b5f5ff",
+            "#aa99ecff",
+            "#6e56cfff",
+            "#654dc4ff",
+            "#6550b9ff",
+            "#2f265fff",
+        ],
+        light_alpha: [
+            "#5500aa03",
+            "#4900ff07",
+            "#4400ee0f",
+            "#4300ff1b",
+            "#3600ff26",
+            "#3100fb35",
+            "#2d01dd4a",
+            "#2b00d066",
+            "#2400b7a9",
+            "#2300abb2",
+            "#1f0099af",
+            "#0b0043d9",
+        ],
+        dark: [
+            "#14121fff",
+            "#1b1525ff",
+            "#291f43ff",
+            "#33255bff",
+            "#3c2e69ff",
+            "#473876ff",
+            "#56468bff",
+            "#6958adff",
+            "#6e56cfff",
+            "#7d66d9ff",
+            "#baa7ffff",
+            "#e2ddfeff",
+        ],
+        dark_alpha: [
+            "#4422ff0f",
+            "#853ff916",
+            "#8354fe36",
+            "#7d51fd50",
+            "#845ffd5f",
+            "#8f6cfd6d",
+            "#9879ff83",
+            "#977dfea8",
+            "#8668ffcc",
+            "#9176fed7",
+            "#baa7ffff",
+            "#e3defffe",
+        ],
+    }
+}
+
+fn iris() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Iris,
+        light: [
+            "#fdfdffff",
+            "#f8f8ffff",
+            "#f0f1feff",
+            "#e6e7ffff",
+            "#dadcffff",
+            "#cbcdffff",
+            "#b8baf8ff",
+            "#9b9ef0ff",
+            "#5b5bd6ff",
+            "#5151cdff",
+            "#5753c6ff",
+            "#272962ff",
+        ],
+        light_alpha: [
+            "#0000ff02",
+            "#0000ff07",
+            "#0011ee0f",
+            "#000bff19",
+            "#000eff25",
+            "#000aff34",
+            "#0008e647",
+            "#0008d964",
+            "#0000c0a4",
+            "#0000b6ae",
+            "#0600abac",
+            "#000246d8",
+        ],
+        dark: [
+            "#13131eff",
+            "#171625ff",
+            "#202248ff",
+            "#262a65ff",
+            "#303374ff",
+            "#3d3e82ff",
+            "#4a4a95ff",
+            "#5958b1ff",
+            "#5b5bd6ff",
+            "#6e6adeff",
+            "#b1a9ffff",
+            "#e0dffeff",
+        ],
+        dark_alpha: [
+            "#3636fe0e",
+            "#564bf916",
+            "#525bff3b",
+            "#4d58ff5a",
+            "#5b62fd6b",
+            "#6d6ffd7a",
+            "#7777fe8e",
+            "#7b7afeac",
+            "#6a6afed4",
+            "#7d79ffdc",
+            "#b1a9ffff",
+            "#e1e0fffe",
+        ],
+    }
+}
+
+fn indigo() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Indigo,
+        light: [
+            "#fdfdfeff",
+            "#f7f9ffff",
+            "#edf2feff",
+            "#e1e9ffff",
+            "#d2deffff",
+            "#c1d0ffff",
+            "#abbdf9ff",
+            "#8da4efff",
+            "#3e63ddff",
+            "#3358d4ff",
+            "#3a5bc7ff",
+            "#1f2d5cff",
+        ],
+        light_alpha: [
+            "#00008002",
+            "#0040ff08",
+            "#0047f112",
+            "#0044ff1e",
+            "#0044ff2d",
+            "#003eff3e",
+            "#0037ed54",
+            "#0034dc72",
+            "#0031d2c1",
+            "#002ec9cc",
+            "#002bb7c5",
+            "#001046e0",
+        ],
+        dark: [
+            "#11131fff",
+            "#141726ff",
+            "#182449ff",
+            "#1d2e62ff",
+            "#253974ff",
+            "#304384ff",
+            "#3a4f97ff",
+            "#435db1ff",
+            "#3e63ddff",
+            "#5472e4ff",
+            "#9eb1ffff",
+            "#d6e1ffff",
+        ],
+        dark_alpha: [
+            "#1133ff0f",
+            "#3354fa17",
+            "#2f62ff3c",
+            "#3566ff57",
+            "#4171fd6b",
+            "#5178fd7c",
+            "#5a7fff90",
+            "#5b81feac",
+            "#4671ffdb",
+            "#5c7efee3",
+            "#9eb1ffff",
+            "#d6e1ffff",
+        ],
+    }
+}
+
+fn blue() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Blue,
+        light: [
+            "#fbfdffff",
+            "#f4faffff",
+            "#e6f4feff",
+            "#d5efffff",
+            "#c2e5ffff",
+            "#acd8fcff",
+            "#8ec8f6ff",
+            "#5eb1efff",
+            "#0090ffff",
+            "#0588f0ff",
+            "#0d74ceff",
+            "#113264ff",
+        ],
+        light_alpha: [
+            "#0080ff04",
+            "#008cff0b",
+            "#008ff519",
+            "#009eff2a",
+            "#0093ff3d",
+            "#0088f653",
+            "#0083eb71",
+            "#0084e6a1",
+            "#0090ffff",
+            "#0086f0fa",
+            "#006dcbf2",
+            "#002359ee",
+        ],
+        dark: [
+            "#0d1520ff",
+            "#111927ff",
+            "#0d2847ff",
+            "#003362ff",
+            "#004074ff",
+            "#104d87ff",
+            "#205d9eff",
+            "#2870bdff",
+            "#0090ffff",
+            "#3b9effff",
+            "#70b8ffff",
+            "#c2e6ffff",
+        ],
+        dark_alpha: [
+            "#004df211",
+            "#1166fb18",
+            "#0077ff3a",
+            "#0075ff57",
+            "#0081fd6b",
+            "#0f89fd7f",
+            "#2a91fe98",
+            "#3094feb9",
+            "#0090ffff",
+            "#3b9effff",
+            "#70b8ffff",
+            "#c2e6ffff",
+        ],
+    }
+}
+
+fn cyan() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Cyan,
+        light: [
+            "#fafdfeff",
+            "#f2fafbff",
+            "#def7f9ff",
+            "#caf1f6ff",
+            "#b5e9f0ff",
+            "#9ddde7ff",
+            "#7dcedcff",
+            "#3db9cfff",
+            "#00a2c7ff",
+            "#0797b9ff",
+            "#107d98ff",
+            "#0d3c48ff",
+        ],
+        light_alpha: [
+            "#0099cc05",
+            "#009db10d",
+            "#00c2d121",
+            "#00bcd435",
+            "#01b4cc4a",
+            "#00a7c162",
+            "#009fbb82",
+            "#00a3c0c2",
+            "#00a2c7ff",
+            "#0094b7f8",
+            "#007491ef",
+            "#00323ef2",
+        ],
+        dark: [
+            "#0b161aff",
+            "#101b20ff",
+            "#082c36ff",
+            "#003848ff",
+            "#004558ff",
+            "#045468ff",
+            "#12677eff",
+            "#11809cff",
+            "#00a2c7ff",
+            "#23afd0ff",
+            "#4ccce6ff",
+            "#b6ecf7ff",
+        ],
+        dark_alpha: [
+            "#0091f70a",
+            "#02a7f211",
+            "#00befd28",
+            "#00baff3b",
+            "#00befd4d",
+            "#00c7fd5e",
+            "#14cdff75",
+            "#11cfff95",
+            "#00cfffc3",
+            "#28d6ffcd",
+            "#52e1fee5",
+            "#bbf3fef7",
+        ],
+    }
+}
+
+fn teal() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Teal,
+        light: [
+            "#fafefdff",
+            "#f3fbf9ff",
+            "#e0f8f3ff",
+            "#ccf3eaff",
+            "#b8eae0ff",
+            "#a1ded2ff",
+            "#83cdc1ff",
+            "#53b9abff",
+            "#12a594ff",
+            "#0d9b8aff",
+            "#008573ff",
+            "#0d3d38ff",
+        ],
+        light_alpha: [
+            "#00cc9905",
+            "#00aa800c",
+            "#00c69d1f",
+            "#00c39633",
+            "#00b49047",
+            "#00a6855e",
+            "#0099807c",
+            "#009783ac",
+            "#009e8ced",
+            "#009684f2",
+            "#008573ff",
+            "#00332df2",
+        ],
+        dark: [
+            "#0d1514ff",
+            "#111c1bff",
+            "#0d2d2aff",
+            "#023b37ff",
+            "#084843ff",
+            "#145750ff",
+            "#1c6961ff",
+            "#207e73ff",
+            "#12a594ff",
+            "#0eb39eff",
+            "#0bd8b6ff",
+            "#adf0ddff",
+        ],
+        dark_alpha: [
+            "#00deab05",
+            "#12fbe60c",
+            "#00ffe61e",
+            "#00ffe92d",
+            "#00ffea3b",
+            "#1cffe84b",
+            "#2efde85f",
+            "#32ffe775",
+            "#13ffe49f",
+            "#0dffe0ae",
+            "#0afed5d6",
+            "#b8ffebef",
+        ],
+    }
+}
+
+fn jade() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Jade,
+        light: [
+            "#fbfefdff",
+            "#f4fbf7ff",
+            "#e6f7edff",
+            "#d6f1e3ff",
+            "#c3e9d7ff",
+            "#acdec8ff",
+            "#8bceb6ff",
+            "#56ba9fff",
+            "#29a383ff",
+            "#26997bff",
+            "#208368ff",
+            "#1d3b31ff",
+        ],
+        light_alpha: [
+            "#00c08004",
+            "#00a3460b",
+            "#00ae4819",
+            "#00a85129",
+            "#00a2553c",
+            "#009a5753",
+            "#00945f74",
+            "#00976ea9",
+            "#00916bd6",
+            "#008764d9",
+            "#007152df",
+            "#002217e2",
+        ],
+        dark: [
+            "#0d1512ff",
+            "#121c18ff",
+            "#0f2e22ff",
+            "#0b3b2cff",
+            "#114837ff",
+            "#1b5745ff",
+            "#246854ff",
+            "#2a7e68ff",
+            "#29a383ff",
+            "#27b08bff",
+            "#1fd8a4ff",
+            "#adf0d4ff",
+        ],
+        dark_alpha: [
+            "#00de4505",
+            "#27fba60c",
+            "#02f99920",
+            "#00ffaa2d",
+            "#11ffb63b",
+            "#34ffc24b",
+            "#45fdc75e",
+            "#48ffcf75",
+            "#38feca9d",
+            "#31fec7ab",
+            "#21fec0d6",
+            "#b8ffe1ef",
+        ],
+    }
+}
+
+fn green() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Green,
+        light: [
+            "#fbfefcff",
+            "#f4fbf6ff",
+            "#e6f6ebff",
+            "#d6f1dfff",
+            "#c4e8d1ff",
+            "#adddc0ff",
+            "#8eceaaff",
+            "#5bb98bff",
+            "#30a46cff",
+            "#2b9a66ff",
+            "#218358ff",
+            "#193b2dff",
+        ],
+        light_alpha: [
+            "#00c04004",
+            "#00a32f0b",
+            "#00a43319",
+            "#00a83829",
+            "#019c393b",
+            "#00963c52",
+            "#00914071",
+            "#00924ba4",
+            "#008f4acf",
+            "#008647d4",
+            "#00713fde",
+            "#002616e6",
+        ],
+        dark: [
+            "#0e1512ff",
+            "#121b17ff",
+            "#132d21ff",
+            "#113b29ff",
+            "#174933ff",
+            "#20573eff",
+            "#28684aff",
+            "#2f7c57ff",
+            "#30a46cff",
+            "#33b074ff",
+            "#3dd68cff",
+            "#b1f1cbff",
+        ],
+        dark_alpha: [
+            "#00de4505",
+            "#29f99d0b",
+            "#22ff991e",
+            "#11ff992d",
+            "#2bffa23c",
+            "#44ffaa4b",
+            "#50fdac5e",
+            "#54ffad73",
+            "#44ffa49e",
+            "#43fea4ab",
+            "#46fea5d4",
+            "#bbffd7f0",
+        ],
+    }
+}
+
+fn grass() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Grass,
+        light: [
+            "#fbfefbff",
+            "#f5fbf5ff",
+            "#e9f6e9ff",
+            "#daf1dbff",
+            "#c9e8caff",
+            "#b2ddb5ff",
+            "#94ce9aff",
+            "#65ba74ff",
+            "#46a758ff",
+            "#3e9b4fff",
+            "#2a7e3bff",
+            "#203c25ff",
+        ],
+        light_alpha: [
+            "#00c00004",
+            "#0099000a",
+            "#00970016",
+            "#009f0725",
+            "#00930536",
+            "#008f0a4d",
+            "#018b0f6b",
+            "#008d199a",
+            "#008619b9",
+            "#007b17c1",
+            "#006514d5",
+            "#002006df",
+        ],
+        dark: [
+            "#0e1511ff",
+            "#141a15ff",
+            "#1b2a1eff",
+            "#1d3a24ff",
+            "#25482dff",
+            "#2d5736ff",
+            "#366740ff",
+            "#3e7949ff",
+            "#46a758ff",
+            "#53b365ff",
+            "#71d083ff",
+            "#c2f0c2ff",
+        ],
+        dark_alpha: [
+            "#00de1205",
+            "#5ef7780a",
+            "#70fe8c1b",
+            "#57ff802c",
+            "#68ff8b3b",
+            "#71ff8f4b",
+            "#77fd925d",
+            "#77fd9070",
+            "#65ff82a1",
+            "#72ff8dae",
+            "#89ff9fcd",
+            "#ceffceef",
+        ],
+    }
+}
+
+fn lime() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Lime,
+        light: [
+            "#fcfdfaff",
+            "#f8faf3ff",
+            "#eef6d6ff",
+            "#e2f0bdff",
+            "#d3e7a6ff",
+            "#c2da91ff",
+            "#abc978ff",
+            "#8db654ff",
+            "#bdee63ff",
+            "#b0e64cff",
+            "#5c7c2fff",
+            "#37401cff",
+        ],
+        light_alpha: [
+            "#66990005",
+            "#6b95000c",
+            "#96c80029",
+            "#8fc60042",
+            "#81bb0059",
+            "#72aa006e",
+            "#61990087",
+            "#559200ab",
+            "#93e4009c",
+            "#8fdc00b3",
+            "#375f00d0",
+            "#1e2900e3",
+        ],
+        dark: [
+            "#11130cff",
+            "#151a10ff",
+            "#1f2917ff",
+            "#29371dff",
+            "#334423ff",
+            "#3d522aff",
+            "#496231ff",
+            "#577538ff",
+            "#bdee63ff",
+            "#d4ff70ff",
+            "#bde56cff",
+            "#e3f7baff",
+        ],
+        dark_alpha: [
+            "#11bb0003",
+            "#78f7000a",
+            "#9bfd4c1a",
+            "#a7fe5c29",
+            "#affe6537",
+            "#b2fe6d46",
+            "#b6ff6f57",
+            "#b6fd6d6c",
+            "#caff69ed",
+            "#d4ff70ff",
+            "#d1fe77e4",
+            "#e9febff7",
+        ],
+    }
+}
+
+fn mint() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Mint,
+        light: [
+            "#f9fefdff",
+            "#f2fbf9ff",
+            "#ddf9f2ff",
+            "#c8f4e9ff",
+            "#b3ecdeff",
+            "#9ce0d0ff",
+            "#7ecfbdff",
+            "#4cbba5ff",
+            "#86ead4ff",
+            "#7de0cbff",
+            "#027864ff",
+            "#16433cff",
+        ],
+        light_alpha: [
+            "#00d5aa06",
+            "#00b18a0d",
+            "#00d29e22",
+            "#00cc9937",
+            "#00c0914c",
+            "#00b08663",
+            "#00a17d81",
+            "#009e7fb3",
+            "#00d3a579",
+            "#00c39982",
+            "#007763fd",
+            "#00312ae9",
+        ],
+        dark: [
+            "#0e1515ff",
+            "#0f1b1bff",
+            "#092c2bff",
+            "#003a38ff",
+            "#004744ff",
+            "#105650ff",
+            "#1e685fff",
+            "#277f70ff",
+            "#86ead4ff",
+            "#a8f5e5ff",
+            "#58d5baff",
+            "#c4f5e1ff",
+        ],
+        dark_alpha: [
+            "#00dede05",
+            "#00f9f90b",
+            "#00fff61d",
+            "#00fff42c",
+            "#00fff23a",
+            "#0effeb4a",
+            "#34fde55e",
+            "#41ffdf76",
+            "#92ffe7e9",
+            "#aefeedf5",
+            "#67ffded2",
+            "#cbfee9f5",
+        ],
+    }
+}
+
+fn sky() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Sky,
+        light: [
+            "#f9feffff",
+            "#f1fafdff",
+            "#e1f6fdff",
+            "#d1f0faff",
+            "#bee7f5ff",
+            "#a9daedff",
+            "#8dcae3ff",
+            "#60b3d7ff",
+            "#7ce2feff",
+            "#74daf8ff",
+            "#00749eff",
+            "#1d3e56ff",
+        ],
+        light_alpha: [
+            "#00d5ff06",
+            "#00a4db0e",
+            "#00b3ee1e",
+            "#00ace42e",
+            "#00a1d841",
+            "#0092ca56",
+            "#0089c172",
+            "#0085bf9f",
+            "#00c7fe83",
+            "#00bcf38b",
+            "#00749eff",
+            "#002540e2",
+        ],
+        dark: [
+            "#0d141fff",
+            "#111a27ff",
+            "#112840ff",
+            "#113555ff",
+            "#154467ff",
+            "#1b537bff",
+            "#1f6692ff",
+            "#197caeff",
+            "#7ce2feff",
+            "#a8eeffff",
+            "#75c7f0ff",
+            "#c2f3ffff",
+        ],
+        dark_alpha: [
+            "#0044ff0f",
+            "#1171fb18",
+            "#1184fc33",
+            "#128fff49",
+            "#1c9dfd5d",
+            "#28a5ff72",
+            "#2badfe8b",
+            "#1db2fea9",
+            "#7ce3fffe",
+            "#a8eeffff",
+            "#7cd3ffef",
+            "#c2f3ffff",
+        ],
+    }
+}
+
+fn black() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::Black,
+        light: [
+            "#0000000d",
+            "#0000001a",
+            "#00000026",
+            "#00000033",
+            "#0000004d",
+            "#00000066",
+            "#00000080",
+            "#00000099",
+            "#000000b3",
+            "#000000cc",
+            "#000000e6",
+            "#000000f2",
+        ],
+        light_alpha: [
+            "#0000000d",
+            "#0000001a",
+            "#00000026",
+            "#00000033",
+            "#0000004d",
+            "#00000066",
+            "#00000080",
+            "#00000099",
+            "#000000b3",
+            "#000000cc",
+            "#000000e6",
+            "#000000f2",
+        ],
+        dark: [
+            "#0000000d",
+            "#0000001a",
+            "#00000026",
+            "#00000033",
+            "#0000004d",
+            "#00000066",
+            "#00000080",
+            "#00000099",
+            "#000000b3",
+            "#000000cc",
+            "#000000e6",
+            "#000000f2",
+        ],
+        dark_alpha: [
+            "#0000000d",
+            "#0000001a",
+            "#00000026",
+            "#00000033",
+            "#0000004d",
+            "#00000066",
+            "#00000080",
+            "#00000099",
+            "#000000b3",
+            "#000000cc",
+            "#000000e6",
+            "#000000f2",
+        ],
+    }
+}
+
+fn white() -> DefaultColorScaleSet {
+    DefaultColorScaleSet {
+        scale: ColorScaleName::White,
+        light: [
+            "#ffffff0d",
+            "#ffffff1a",
+            "#ffffff26",
+            "#ffffff33",
+            "#ffffff4d",
+            "#ffffff66",
+            "#ffffff80",
+            "#ffffff99",
+            "#ffffffb3",
+            "#ffffffcc",
+            "#ffffffe6",
+            "#fffffff2",
+        ],
+        light_alpha: [
+            "#ffffff0d",
+            "#ffffff1a",
+            "#ffffff26",
+            "#ffffff33",
+            "#ffffff4d",
+            "#ffffff66",
+            "#ffffff80",
+            "#ffffff99",
+            "#ffffffb3",
+            "#ffffffcc",
+            "#ffffffe6",
+            "#fffffff2",
+        ],
+        dark: [
+            "#ffffff0d",
+            "#ffffff1a",
+            "#ffffff26",
+            "#ffffff33",
+            "#ffffff4d",
+            "#ffffff66",
+            "#ffffff80",
+            "#ffffff99",
+            "#ffffffb3",
+            "#ffffffcc",
+            "#ffffffe6",
+            "#fffffff2",
+        ],
+        dark_alpha: [
+            "#ffffff0d",
+            "#ffffff1a",
+            "#ffffff26",
+            "#ffffff33",
+            "#ffffff4d",
+            "#ffffff66",
+            "#ffffff80",
+            "#ffffff99",
+            "#ffffffb3",
+            "#ffffffcc",
+            "#ffffffe6",
+            "#fffffff2",
+        ],
+    }
+}

crates/theme2/src/registry.rs 🔗

@@ -1,7 +1,4 @@
-use crate::{
-    themes::{one_dark, rose_pine, rose_pine_dawn, rose_pine_moon, sandcastle},
-    Theme, ThemeMetadata,
-};
+use crate::{themes, Theme, ThemeMetadata};
 use anyhow::{anyhow, Result};
 use gpui2::SharedString;
 use std::{collections::HashMap, sync::Arc};
@@ -41,11 +38,45 @@ impl Default for ThemeRegistry {
         };
 
         this.insert_themes([
-            one_dark(),
-            rose_pine(),
-            rose_pine_dawn(),
-            rose_pine_moon(),
-            sandcastle(),
+            themes::andromeda(),
+            themes::atelier_cave_dark(),
+            themes::atelier_cave_light(),
+            themes::atelier_dune_dark(),
+            themes::atelier_dune_light(),
+            themes::atelier_estuary_dark(),
+            themes::atelier_estuary_light(),
+            themes::atelier_forest_dark(),
+            themes::atelier_forest_light(),
+            themes::atelier_heath_dark(),
+            themes::atelier_heath_light(),
+            themes::atelier_lakeside_dark(),
+            themes::atelier_lakeside_light(),
+            themes::atelier_plateau_dark(),
+            themes::atelier_plateau_light(),
+            themes::atelier_savanna_dark(),
+            themes::atelier_savanna_light(),
+            themes::atelier_seaside_dark(),
+            themes::atelier_seaside_light(),
+            themes::atelier_sulphurpool_dark(),
+            themes::atelier_sulphurpool_light(),
+            themes::ayu_dark(),
+            themes::ayu_light(),
+            themes::ayu_mirage(),
+            themes::gruvbox_dark(),
+            themes::gruvbox_dark_hard(),
+            themes::gruvbox_dark_soft(),
+            themes::gruvbox_light(),
+            themes::gruvbox_light_hard(),
+            themes::gruvbox_light_soft(),
+            themes::one_dark(),
+            themes::one_light(),
+            themes::rose_pine(),
+            themes::rose_pine_dawn(),
+            themes::rose_pine_moon(),
+            themes::sandcastle(),
+            themes::solarized_dark(),
+            themes::solarized_light(),
+            themes::summercamp(),
         ]);
 
         this

crates/theme2/src/scale.rs 🔗

@@ -0,0 +1,164 @@
+use gpui2::{AppContext, Hsla};
+use indexmap::IndexMap;
+
+use crate::{theme, Appearance};
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum ColorScaleName {
+    Gray,
+    Mauve,
+    Slate,
+    Sage,
+    Olive,
+    Sand,
+    Gold,
+    Bronze,
+    Brown,
+    Yellow,
+    Amber,
+    Orange,
+    Tomato,
+    Red,
+    Ruby,
+    Crimson,
+    Pink,
+    Plum,
+    Purple,
+    Violet,
+    Iris,
+    Indigo,
+    Blue,
+    Cyan,
+    Teal,
+    Jade,
+    Green,
+    Grass,
+    Lime,
+    Mint,
+    Sky,
+    Black,
+    White,
+}
+
+impl std::fmt::Display for ColorScaleName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Self::Gray => "Gray",
+                Self::Mauve => "Mauve",
+                Self::Slate => "Slate",
+                Self::Sage => "Sage",
+                Self::Olive => "Olive",
+                Self::Sand => "Sand",
+                Self::Gold => "Gold",
+                Self::Bronze => "Bronze",
+                Self::Brown => "Brown",
+                Self::Yellow => "Yellow",
+                Self::Amber => "Amber",
+                Self::Orange => "Orange",
+                Self::Tomato => "Tomato",
+                Self::Red => "Red",
+                Self::Ruby => "Ruby",
+                Self::Crimson => "Crimson",
+                Self::Pink => "Pink",
+                Self::Plum => "Plum",
+                Self::Purple => "Purple",
+                Self::Violet => "Violet",
+                Self::Iris => "Iris",
+                Self::Indigo => "Indigo",
+                Self::Blue => "Blue",
+                Self::Cyan => "Cyan",
+                Self::Teal => "Teal",
+                Self::Jade => "Jade",
+                Self::Green => "Green",
+                Self::Grass => "Grass",
+                Self::Lime => "Lime",
+                Self::Mint => "Mint",
+                Self::Sky => "Sky",
+                Self::Black => "Black",
+                Self::White => "White",
+            }
+        )
+    }
+}
+
+pub type ColorScale = [Hsla; 12];
+
+pub type ColorScales = IndexMap<ColorScaleName, ColorScaleSet>;
+
+/// A one-based step in a [`ColorScale`].
+pub type ColorScaleStep = usize;
+
+pub struct ColorScaleSet {
+    name: ColorScaleName,
+    light: ColorScale,
+    dark: ColorScale,
+    light_alpha: ColorScale,
+    dark_alpha: ColorScale,
+}
+
+impl ColorScaleSet {
+    pub fn new(
+        name: ColorScaleName,
+        light: ColorScale,
+        light_alpha: ColorScale,
+        dark: ColorScale,
+        dark_alpha: ColorScale,
+    ) -> Self {
+        Self {
+            name,
+            light,
+            light_alpha,
+            dark,
+            dark_alpha,
+        }
+    }
+
+    pub fn name(&self) -> String {
+        self.name.to_string()
+    }
+
+    pub fn light(&self, step: ColorScaleStep) -> Hsla {
+        self.light[step - 1]
+    }
+
+    pub fn light_alpha(&self, step: ColorScaleStep) -> Hsla {
+        self.light_alpha[step - 1]
+    }
+
+    pub fn dark(&self, step: ColorScaleStep) -> Hsla {
+        self.dark[step - 1]
+    }
+
+    pub fn dark_alpha(&self, step: ColorScaleStep) -> Hsla {
+        self.dark_alpha[step - 1]
+    }
+
+    fn current_appearance(cx: &AppContext) -> Appearance {
+        let theme = theme(cx);
+        if theme.metadata.is_light {
+            Appearance::Light
+        } else {
+            Appearance::Dark
+        }
+    }
+
+    pub fn step(&self, cx: &AppContext, step: ColorScaleStep) -> Hsla {
+        let appearance = Self::current_appearance(cx);
+
+        match appearance {
+            Appearance::Light => self.light(step),
+            Appearance::Dark => self.dark(step),
+        }
+    }
+
+    pub fn step_alpha(&self, cx: &AppContext, step: ColorScaleStep) -> Hsla {
+        let appearance = Self::current_appearance(cx);
+        match appearance {
+            Appearance::Light => self.light_alpha(step),
+            Appearance::Dark => self.dark_alpha(step),
+        }
+    }
+}

crates/theme2/src/theme2.rs 🔗

@@ -1,14 +1,24 @@
+mod default;
 mod registry;
+mod scale;
 mod settings;
 mod themes;
 
+pub use default::*;
 pub use registry::*;
+pub use scale::*;
 pub use settings::*;
 
 use gpui2::{AppContext, HighlightStyle, Hsla, SharedString};
 use settings2::Settings;
 use std::sync::Arc;
 
+#[derive(Debug, Clone, PartialEq)]
+pub enum Appearance {
+    Light,
+    Dark,
+}
+
 pub fn init(cx: &mut AppContext) {
     cx.set_global(ThemeRegistry::default());
     ThemeSettings::register(cx);
@@ -18,6 +28,10 @@ pub fn active_theme<'a>(cx: &'a AppContext) -> &'a Arc<Theme> {
     &ThemeSettings::get_global(cx).active_theme
 }
 
+pub fn theme(cx: &AppContext) -> Arc<Theme> {
+    active_theme(cx).clone()
+}
+
 pub struct Theme {
     pub metadata: ThemeMetadata,
 
@@ -90,13 +104,40 @@ 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 {
+    // TOOD: Get this working with `#[cfg(test)]`. Why isn't it?
+    pub fn new_test(colors: impl IntoIterator<Item = (&'static str, Hsla)>) -> Self {
+        SyntaxTheme {
+            highlights: colors
+                .into_iter()
+                .map(|(key, color)| {
+                    (
+                        key.to_owned(),
+                        HighlightStyle {
+                            color: Some(color),
+                            ..Default::default()
+                        },
+                    )
+                })
+                .collect(),
+        }
+    }
+
+    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/andromeda.rs 🔗

@@ -0,0 +1,130 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn andromeda() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Andromeda".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x2b2f38ff).into(),
+        border_variant: rgba(0x2b2f38ff).into(),
+        border_focused: rgba(0x183934ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x262933ff).into(),
+        surface: rgba(0x21242bff).into(),
+        background: rgba(0x262933ff).into(),
+        filled_element: rgba(0x262933ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x12231fff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x12231fff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xf7f7f8ff).into(),
+        text_muted: rgba(0xaca8aeff).into(),
+        text_placeholder: rgba(0xf82871ff).into(),
+        text_disabled: rgba(0x6b6b73ff).into(),
+        text_accent: rgba(0x10a793ff).into(),
+        icon_muted: rgba(0xaca8aeff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("emphasis".into(), rgba(0x10a793ff).into()),
+                ("punctuation.bracket".into(), rgba(0xd8d5dbff).into()),
+                ("attribute".into(), rgba(0x10a793ff).into()),
+                ("variable".into(), rgba(0xf7f7f8ff).into()),
+                ("predictive".into(), rgba(0x315f70ff).into()),
+                ("property".into(), rgba(0x10a793ff).into()),
+                ("variant".into(), rgba(0x10a793ff).into()),
+                ("embedded".into(), rgba(0xf7f7f8ff).into()),
+                ("string.special".into(), rgba(0xf29c14ff).into()),
+                ("keyword".into(), rgba(0x10a793ff).into()),
+                ("tag".into(), rgba(0x10a793ff).into()),
+                ("enum".into(), rgba(0xf29c14ff).into()),
+                ("link_text".into(), rgba(0xf29c14ff).into()),
+                ("primary".into(), rgba(0xf7f7f8ff).into()),
+                ("punctuation".into(), rgba(0xd8d5dbff).into()),
+                ("punctuation.special".into(), rgba(0xd8d5dbff).into()),
+                ("function".into(), rgba(0xfee56cff).into()),
+                ("number".into(), rgba(0x96df71ff).into()),
+                ("preproc".into(), rgba(0xf7f7f8ff).into()),
+                ("operator".into(), rgba(0xf29c14ff).into()),
+                ("constructor".into(), rgba(0x10a793ff).into()),
+                ("string.escape".into(), rgba(0xafabb1ff).into()),
+                ("string.special.symbol".into(), rgba(0xf29c14ff).into()),
+                ("string".into(), rgba(0xf29c14ff).into()),
+                ("comment".into(), rgba(0xafabb1ff).into()),
+                ("hint".into(), rgba(0x618399ff).into()),
+                ("type".into(), rgba(0x08e7c5ff).into()),
+                ("label".into(), rgba(0x10a793ff).into()),
+                ("comment.doc".into(), rgba(0xafabb1ff).into()),
+                ("text.literal".into(), rgba(0xf29c14ff).into()),
+                ("constant".into(), rgba(0x96df71ff).into()),
+                ("string.regex".into(), rgba(0xf29c14ff).into()),
+                ("emphasis.strong".into(), rgba(0x10a793ff).into()),
+                ("title".into(), rgba(0xf7f7f8ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xd8d5dbff).into()),
+                ("link_uri".into(), rgba(0x96df71ff).into()),
+                ("boolean".into(), rgba(0x96df71ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xd8d5dbff).into()),
+            ],
+        },
+        status_bar: rgba(0x262933ff).into(),
+        title_bar: rgba(0x262933ff).into(),
+        toolbar: rgba(0x1e2025ff).into(),
+        tab_bar: rgba(0x21242bff).into(),
+        editor: rgba(0x1e2025ff).into(),
+        editor_subheader: rgba(0x21242bff).into(),
+        editor_active_line: rgba(0x21242bff).into(),
+        terminal: rgba(0x1e2025ff).into(),
+        image_fallback_background: rgba(0x262933ff).into(),
+        git_created: rgba(0x96df71ff).into(),
+        git_modified: rgba(0x10a793ff).into(),
+        git_deleted: rgba(0xf82871ff).into(),
+        git_conflict: rgba(0xfee56cff).into(),
+        git_ignored: rgba(0x6b6b73ff).into(),
+        git_renamed: rgba(0xfee56cff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x10a793ff).into(),
+                selection: rgba(0x10a7933d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x96df71ff).into(),
+                selection: rgba(0x96df713d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc74cecff).into(),
+                selection: rgba(0xc74cec3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf29c14ff).into(),
+                selection: rgba(0xf29c143d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x893ea6ff).into(),
+                selection: rgba(0x893ea63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x08e7c5ff).into(),
+                selection: rgba(0x08e7c53d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf82871ff).into(),
+                selection: rgba(0xf828713d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfee56cff).into(),
+                selection: rgba(0xfee56c3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_cave_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Cave Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x56505eff).into(),
+        border_variant: rgba(0x56505eff).into(),
+        border_focused: rgba(0x222953ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x3a353fff).into(),
+        surface: rgba(0x221f26ff).into(),
+        background: rgba(0x3a353fff).into(),
+        filled_element: rgba(0x3a353fff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x161a35ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x161a35ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xefecf4ff).into(),
+        text_muted: rgba(0x898591ff).into(),
+        text_placeholder: rgba(0xbe4677ff).into(),
+        text_disabled: rgba(0x756f7eff).into(),
+        text_accent: rgba(0x566ddaff).into(),
+        icon_muted: rgba(0x898591ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("comment.doc".into(), rgba(0x8b8792ff).into()),
+                ("tag".into(), rgba(0x566ddaff).into()),
+                ("link_text".into(), rgba(0xaa563bff).into()),
+                ("constructor".into(), rgba(0x566ddaff).into()),
+                ("punctuation".into(), rgba(0xe2dfe7ff).into()),
+                ("punctuation.special".into(), rgba(0xbf3fbfff).into()),
+                ("string.special.symbol".into(), rgba(0x299292ff).into()),
+                ("string.escape".into(), rgba(0x8b8792ff).into()),
+                ("emphasis".into(), rgba(0x566ddaff).into()),
+                ("type".into(), rgba(0xa06d3aff).into()),
+                ("punctuation.delimiter".into(), rgba(0x8b8792ff).into()),
+                ("variant".into(), rgba(0xa06d3aff).into()),
+                ("variable.special".into(), rgba(0x9559e7ff).into()),
+                ("text.literal".into(), rgba(0xaa563bff).into()),
+                ("punctuation.list_marker".into(), rgba(0xe2dfe7ff).into()),
+                ("comment".into(), rgba(0x655f6dff).into()),
+                ("function.method".into(), rgba(0x576cdbff).into()),
+                ("property".into(), rgba(0xbe4677ff).into()),
+                ("operator".into(), rgba(0x8b8792ff).into()),
+                ("emphasis.strong".into(), rgba(0x566ddaff).into()),
+                ("label".into(), rgba(0x566ddaff).into()),
+                ("enum".into(), rgba(0xaa563bff).into()),
+                ("number".into(), rgba(0xaa563bff).into()),
+                ("primary".into(), rgba(0xe2dfe7ff).into()),
+                ("keyword".into(), rgba(0x9559e7ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xa06d3aff).into(),
+                ),
+                ("punctuation.bracket".into(), rgba(0x8b8792ff).into()),
+                ("constant".into(), rgba(0x2b9292ff).into()),
+                ("string.special".into(), rgba(0xbf3fbfff).into()),
+                ("title".into(), rgba(0xefecf4ff).into()),
+                ("preproc".into(), rgba(0xefecf4ff).into()),
+                ("link_uri".into(), rgba(0x2b9292ff).into()),
+                ("string".into(), rgba(0x299292ff).into()),
+                ("embedded".into(), rgba(0xefecf4ff).into()),
+                ("hint".into(), rgba(0x706897ff).into()),
+                ("boolean".into(), rgba(0x2b9292ff).into()),
+                ("variable".into(), rgba(0xe2dfe7ff).into()),
+                ("predictive".into(), rgba(0x615787ff).into()),
+                ("string.regex".into(), rgba(0x388bc6ff).into()),
+                ("function".into(), rgba(0x576cdbff).into()),
+                ("attribute".into(), rgba(0x566ddaff).into()),
+            ],
+        },
+        status_bar: rgba(0x3a353fff).into(),
+        title_bar: rgba(0x3a353fff).into(),
+        toolbar: rgba(0x19171cff).into(),
+        tab_bar: rgba(0x221f26ff).into(),
+        editor: rgba(0x19171cff).into(),
+        editor_subheader: rgba(0x221f26ff).into(),
+        editor_active_line: rgba(0x221f26ff).into(),
+        terminal: rgba(0x19171cff).into(),
+        image_fallback_background: rgba(0x3a353fff).into(),
+        git_created: rgba(0x2b9292ff).into(),
+        git_modified: rgba(0x566ddaff).into(),
+        git_deleted: rgba(0xbe4677ff).into(),
+        git_conflict: rgba(0xa06d3aff).into(),
+        git_ignored: rgba(0x756f7eff).into(),
+        git_renamed: rgba(0xa06d3aff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x566ddaff).into(),
+                selection: rgba(0x566dda3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x2b9292ff).into(),
+                selection: rgba(0x2b92923d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbf41bfff).into(),
+                selection: rgba(0xbf41bf3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xaa563bff).into(),
+                selection: rgba(0xaa563b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x955ae6ff).into(),
+                selection: rgba(0x955ae63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x3a8bc6ff).into(),
+                selection: rgba(0x3a8bc63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbe4677ff).into(),
+                selection: rgba(0xbe46773d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa06d3aff).into(),
+                selection: rgba(0xa06d3a3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_cave_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Cave Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x8f8b96ff).into(),
+        border_variant: rgba(0x8f8b96ff).into(),
+        border_focused: rgba(0xc8c7f2ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xbfbcc5ff).into(),
+        surface: rgba(0xe6e3ebff).into(),
+        background: rgba(0xbfbcc5ff).into(),
+        filled_element: rgba(0xbfbcc5ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xe1e0f9ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xe1e0f9ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x19171cff).into(),
+        text_muted: rgba(0x5a5462ff).into(),
+        text_placeholder: rgba(0xbd4677ff).into(),
+        text_disabled: rgba(0x6e6876ff).into(),
+        text_accent: rgba(0x586cdaff).into(),
+        icon_muted: rgba(0x5a5462ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("link_text".into(), rgba(0xaa573cff).into()),
+                ("string".into(), rgba(0x299292ff).into()),
+                ("emphasis".into(), rgba(0x586cdaff).into()),
+                ("label".into(), rgba(0x586cdaff).into()),
+                ("property".into(), rgba(0xbe4677ff).into()),
+                ("emphasis.strong".into(), rgba(0x586cdaff).into()),
+                ("constant".into(), rgba(0x2b9292ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xa06d3aff).into(),
+                ),
+                ("embedded".into(), rgba(0x19171cff).into()),
+                ("punctuation.special".into(), rgba(0xbf3fbfff).into()),
+                ("function".into(), rgba(0x576cdbff).into()),
+                ("tag".into(), rgba(0x586cdaff).into()),
+                ("number".into(), rgba(0xaa563bff).into()),
+                ("primary".into(), rgba(0x26232aff).into()),
+                ("text.literal".into(), rgba(0xaa573cff).into()),
+                ("variant".into(), rgba(0xa06d3aff).into()),
+                ("type".into(), rgba(0xa06d3aff).into()),
+                ("punctuation".into(), rgba(0x26232aff).into()),
+                ("string.escape".into(), rgba(0x585260ff).into()),
+                ("keyword".into(), rgba(0x9559e7ff).into()),
+                ("title".into(), rgba(0x19171cff).into()),
+                ("constructor".into(), rgba(0x586cdaff).into()),
+                ("punctuation.list_marker".into(), rgba(0x26232aff).into()),
+                ("string.special".into(), rgba(0xbf3fbfff).into()),
+                ("operator".into(), rgba(0x585260ff).into()),
+                ("function.method".into(), rgba(0x576cdbff).into()),
+                ("link_uri".into(), rgba(0x2b9292ff).into()),
+                ("variable.special".into(), rgba(0x9559e7ff).into()),
+                ("hint".into(), rgba(0x776d9dff).into()),
+                ("punctuation.bracket".into(), rgba(0x585260ff).into()),
+                ("string.special.symbol".into(), rgba(0x299292ff).into()),
+                ("predictive".into(), rgba(0x887fafff).into()),
+                ("attribute".into(), rgba(0x586cdaff).into()),
+                ("enum".into(), rgba(0xaa573cff).into()),
+                ("preproc".into(), rgba(0x19171cff).into()),
+                ("boolean".into(), rgba(0x2b9292ff).into()),
+                ("variable".into(), rgba(0x26232aff).into()),
+                ("comment.doc".into(), rgba(0x585260ff).into()),
+                ("string.regex".into(), rgba(0x388bc6ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x585260ff).into()),
+                ("comment".into(), rgba(0x7d7787ff).into()),
+            ],
+        },
+        status_bar: rgba(0xbfbcc5ff).into(),
+        title_bar: rgba(0xbfbcc5ff).into(),
+        toolbar: rgba(0xefecf4ff).into(),
+        tab_bar: rgba(0xe6e3ebff).into(),
+        editor: rgba(0xefecf4ff).into(),
+        editor_subheader: rgba(0xe6e3ebff).into(),
+        editor_active_line: rgba(0xe6e3ebff).into(),
+        terminal: rgba(0xefecf4ff).into(),
+        image_fallback_background: rgba(0xbfbcc5ff).into(),
+        git_created: rgba(0x2b9292ff).into(),
+        git_modified: rgba(0x586cdaff).into(),
+        git_deleted: rgba(0xbd4677ff).into(),
+        git_conflict: rgba(0xa06e3bff).into(),
+        git_ignored: rgba(0x6e6876ff).into(),
+        git_renamed: rgba(0xa06e3bff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x586cdaff).into(),
+                selection: rgba(0x586cda3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x2b9292ff).into(),
+                selection: rgba(0x2b92923d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbf41bfff).into(),
+                selection: rgba(0xbf41bf3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xaa573cff).into(),
+                selection: rgba(0xaa573c3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x955ae6ff).into(),
+                selection: rgba(0x955ae63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x3a8bc6ff).into(),
+                selection: rgba(0x3a8bc63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbd4677ff).into(),
+                selection: rgba(0xbd46773d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa06e3bff).into(),
+                selection: rgba(0xa06e3b3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_dune_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Dune Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x6c695cff).into(),
+        border_variant: rgba(0x6c695cff).into(),
+        border_focused: rgba(0x262f56ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x45433bff).into(),
+        surface: rgba(0x262622ff).into(),
+        background: rgba(0x45433bff).into(),
+        filled_element: rgba(0x45433bff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x171e38ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x171e38ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xfefbecff).into(),
+        text_muted: rgba(0xa4a08bff).into(),
+        text_placeholder: rgba(0xd73837ff).into(),
+        text_disabled: rgba(0x8f8b77ff).into(),
+        text_accent: rgba(0x6684e0ff).into(),
+        icon_muted: rgba(0xa4a08bff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("constructor".into(), rgba(0x6684e0ff).into()),
+                ("punctuation".into(), rgba(0xe8e4cfff).into()),
+                ("punctuation.delimiter".into(), rgba(0xa6a28cff).into()),
+                ("string.special".into(), rgba(0xd43451ff).into()),
+                ("string.escape".into(), rgba(0xa6a28cff).into()),
+                ("comment".into(), rgba(0x7d7a68ff).into()),
+                ("enum".into(), rgba(0xb65611ff).into()),
+                ("variable.special".into(), rgba(0xb854d4ff).into()),
+                ("primary".into(), rgba(0xe8e4cfff).into()),
+                ("comment.doc".into(), rgba(0xa6a28cff).into()),
+                ("label".into(), rgba(0x6684e0ff).into()),
+                ("operator".into(), rgba(0xa6a28cff).into()),
+                ("string".into(), rgba(0x5fac38ff).into()),
+                ("variant".into(), rgba(0xae9512ff).into()),
+                ("variable".into(), rgba(0xe8e4cfff).into()),
+                ("function.method".into(), rgba(0x6583e1ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xae9512ff).into(),
+                ),
+                ("string.regex".into(), rgba(0x1ead82ff).into()),
+                ("emphasis.strong".into(), rgba(0x6684e0ff).into()),
+                ("punctuation.special".into(), rgba(0xd43451ff).into()),
+                ("punctuation.bracket".into(), rgba(0xa6a28cff).into()),
+                ("link_text".into(), rgba(0xb65611ff).into()),
+                ("link_uri".into(), rgba(0x5fac39ff).into()),
+                ("boolean".into(), rgba(0x5fac39ff).into()),
+                ("hint".into(), rgba(0xb17272ff).into()),
+                ("tag".into(), rgba(0x6684e0ff).into()),
+                ("function".into(), rgba(0x6583e1ff).into()),
+                ("title".into(), rgba(0xfefbecff).into()),
+                ("property".into(), rgba(0xd73737ff).into()),
+                ("type".into(), rgba(0xae9512ff).into()),
+                ("constant".into(), rgba(0x5fac39ff).into()),
+                ("attribute".into(), rgba(0x6684e0ff).into()),
+                ("predictive".into(), rgba(0x9c6262ff).into()),
+                ("string.special.symbol".into(), rgba(0x5fac38ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xe8e4cfff).into()),
+                ("emphasis".into(), rgba(0x6684e0ff).into()),
+                ("keyword".into(), rgba(0xb854d4ff).into()),
+                ("text.literal".into(), rgba(0xb65611ff).into()),
+                ("number".into(), rgba(0xb65610ff).into()),
+                ("preproc".into(), rgba(0xfefbecff).into()),
+                ("embedded".into(), rgba(0xfefbecff).into()),
+            ],
+        },
+        status_bar: rgba(0x45433bff).into(),
+        title_bar: rgba(0x45433bff).into(),
+        toolbar: rgba(0x20201dff).into(),
+        tab_bar: rgba(0x262622ff).into(),
+        editor: rgba(0x20201dff).into(),
+        editor_subheader: rgba(0x262622ff).into(),
+        editor_active_line: rgba(0x262622ff).into(),
+        terminal: rgba(0x20201dff).into(),
+        image_fallback_background: rgba(0x45433bff).into(),
+        git_created: rgba(0x5fac39ff).into(),
+        git_modified: rgba(0x6684e0ff).into(),
+        git_deleted: rgba(0xd73837ff).into(),
+        git_conflict: rgba(0xae9414ff).into(),
+        git_ignored: rgba(0x8f8b77ff).into(),
+        git_renamed: rgba(0xae9414ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x6684e0ff).into(),
+                selection: rgba(0x6684e03d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5fac39ff).into(),
+                selection: rgba(0x5fac393d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd43651ff).into(),
+                selection: rgba(0xd436513d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb65611ff).into(),
+                selection: rgba(0xb656113d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb854d3ff).into(),
+                selection: rgba(0xb854d33d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x20ad83ff).into(),
+                selection: rgba(0x20ad833d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd73837ff).into(),
+                selection: rgba(0xd738373d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xae9414ff).into(),
+                selection: rgba(0xae94143d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_dune_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Dune Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0xa8a48eff).into(),
+        border_variant: rgba(0xa8a48eff).into(),
+        border_focused: rgba(0xcdd1f5ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xcecab4ff).into(),
+        surface: rgba(0xeeebd7ff).into(),
+        background: rgba(0xcecab4ff).into(),
+        filled_element: rgba(0xcecab4ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xe3e5faff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xe3e5faff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x20201dff).into(),
+        text_muted: rgba(0x706d5fff).into(),
+        text_placeholder: rgba(0xd73737ff).into(),
+        text_disabled: rgba(0x878471ff).into(),
+        text_accent: rgba(0x6684dfff).into(),
+        icon_muted: rgba(0x706d5fff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("primary".into(), rgba(0x292824ff).into()),
+                ("comment".into(), rgba(0x999580ff).into()),
+                ("type".into(), rgba(0xae9512ff).into()),
+                ("variant".into(), rgba(0xae9512ff).into()),
+                ("label".into(), rgba(0x6684dfff).into()),
+                ("function.method".into(), rgba(0x6583e1ff).into()),
+                ("variable.special".into(), rgba(0xb854d4ff).into()),
+                ("string.regex".into(), rgba(0x1ead82ff).into()),
+                ("property".into(), rgba(0xd73737ff).into()),
+                ("keyword".into(), rgba(0xb854d4ff).into()),
+                ("number".into(), rgba(0xb65610ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x292824ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xae9512ff).into(),
+                ),
+                ("punctuation.special".into(), rgba(0xd43451ff).into()),
+                ("punctuation".into(), rgba(0x292824ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x6e6b5eff).into()),
+                ("tag".into(), rgba(0x6684dfff).into()),
+                ("link_text".into(), rgba(0xb65712ff).into()),
+                ("boolean".into(), rgba(0x61ac39ff).into()),
+                ("hint".into(), rgba(0xb37979ff).into()),
+                ("operator".into(), rgba(0x6e6b5eff).into()),
+                ("constant".into(), rgba(0x61ac39ff).into()),
+                ("function".into(), rgba(0x6583e1ff).into()),
+                ("text.literal".into(), rgba(0xb65712ff).into()),
+                ("string.special.symbol".into(), rgba(0x5fac38ff).into()),
+                ("attribute".into(), rgba(0x6684dfff).into()),
+                ("emphasis".into(), rgba(0x6684dfff).into()),
+                ("preproc".into(), rgba(0x20201dff).into()),
+                ("comment.doc".into(), rgba(0x6e6b5eff).into()),
+                ("punctuation.bracket".into(), rgba(0x6e6b5eff).into()),
+                ("string".into(), rgba(0x5fac38ff).into()),
+                ("enum".into(), rgba(0xb65712ff).into()),
+                ("variable".into(), rgba(0x292824ff).into()),
+                ("string.special".into(), rgba(0xd43451ff).into()),
+                ("embedded".into(), rgba(0x20201dff).into()),
+                ("emphasis.strong".into(), rgba(0x6684dfff).into()),
+                ("predictive".into(), rgba(0xc88a8aff).into()),
+                ("title".into(), rgba(0x20201dff).into()),
+                ("constructor".into(), rgba(0x6684dfff).into()),
+                ("link_uri".into(), rgba(0x61ac39ff).into()),
+                ("string.escape".into(), rgba(0x6e6b5eff).into()),
+            ],
+        },
+        status_bar: rgba(0xcecab4ff).into(),
+        title_bar: rgba(0xcecab4ff).into(),
+        toolbar: rgba(0xfefbecff).into(),
+        tab_bar: rgba(0xeeebd7ff).into(),
+        editor: rgba(0xfefbecff).into(),
+        editor_subheader: rgba(0xeeebd7ff).into(),
+        editor_active_line: rgba(0xeeebd7ff).into(),
+        terminal: rgba(0xfefbecff).into(),
+        image_fallback_background: rgba(0xcecab4ff).into(),
+        git_created: rgba(0x61ac39ff).into(),
+        git_modified: rgba(0x6684dfff).into(),
+        git_deleted: rgba(0xd73737ff).into(),
+        git_conflict: rgba(0xae9414ff).into(),
+        git_ignored: rgba(0x878471ff).into(),
+        git_renamed: rgba(0xae9414ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x6684dfff).into(),
+                selection: rgba(0x6684df3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x61ac39ff).into(),
+                selection: rgba(0x61ac393d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd43652ff).into(),
+                selection: rgba(0xd436523d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb65712ff).into(),
+                selection: rgba(0xb657123d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb755d3ff).into(),
+                selection: rgba(0xb755d33d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x21ad82ff).into(),
+                selection: rgba(0x21ad823d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd73737ff).into(),
+                selection: rgba(0xd737373d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xae9414ff).into(),
+                selection: rgba(0xae94143d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_estuary_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Estuary Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x5d5c4cff).into(),
+        border_variant: rgba(0x5d5c4cff).into(),
+        border_focused: rgba(0x1c3927ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x424136ff).into(),
+        surface: rgba(0x2c2b23ff).into(),
+        background: rgba(0x424136ff).into(),
+        filled_element: rgba(0x424136ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x142319ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x142319ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xf4f3ecff).into(),
+        text_muted: rgba(0x91907fff).into(),
+        text_placeholder: rgba(0xba6136ff).into(),
+        text_disabled: rgba(0x7d7c6aff).into(),
+        text_accent: rgba(0x36a165ff).into(),
+        icon_muted: rgba(0x91907fff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("string.special.symbol".into(), rgba(0x7c9725ff).into()),
+                ("comment".into(), rgba(0x6c6b5aff).into()),
+                ("operator".into(), rgba(0x929181ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x929181ff).into()),
+                ("keyword".into(), rgba(0x5f9182ff).into()),
+                ("punctuation.special".into(), rgba(0x9d6b7bff).into()),
+                ("preproc".into(), rgba(0xf4f3ecff).into()),
+                ("title".into(), rgba(0xf4f3ecff).into()),
+                ("string.escape".into(), rgba(0x929181ff).into()),
+                ("boolean".into(), rgba(0x7d9726ff).into()),
+                ("punctuation.bracket".into(), rgba(0x929181ff).into()),
+                ("emphasis.strong".into(), rgba(0x36a165ff).into()),
+                ("string".into(), rgba(0x7c9725ff).into()),
+                ("constant".into(), rgba(0x7d9726ff).into()),
+                ("link_text".into(), rgba(0xae7214ff).into()),
+                ("tag".into(), rgba(0x36a165ff).into()),
+                ("hint".into(), rgba(0x6f815aff).into()),
+                ("punctuation".into(), rgba(0xe7e6dfff).into()),
+                ("string.regex".into(), rgba(0x5a9d47ff).into()),
+                ("variant".into(), rgba(0xa5980cff).into()),
+                ("type".into(), rgba(0xa5980cff).into()),
+                ("attribute".into(), rgba(0x36a165ff).into()),
+                ("emphasis".into(), rgba(0x36a165ff).into()),
+                ("enum".into(), rgba(0xae7214ff).into()),
+                ("number".into(), rgba(0xae7312ff).into()),
+                ("property".into(), rgba(0xba6135ff).into()),
+                ("predictive".into(), rgba(0x5f724cff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xa5980cff).into(),
+                ),
+                ("link_uri".into(), rgba(0x7d9726ff).into()),
+                ("variable.special".into(), rgba(0x5f9182ff).into()),
+                ("text.literal".into(), rgba(0xae7214ff).into()),
+                ("label".into(), rgba(0x36a165ff).into()),
+                ("primary".into(), rgba(0xe7e6dfff).into()),
+                ("variable".into(), rgba(0xe7e6dfff).into()),
+                ("embedded".into(), rgba(0xf4f3ecff).into()),
+                ("function.method".into(), rgba(0x35a166ff).into()),
+                ("comment.doc".into(), rgba(0x929181ff).into()),
+                ("string.special".into(), rgba(0x9d6b7bff).into()),
+                ("constructor".into(), rgba(0x36a165ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xe7e6dfff).into()),
+                ("function".into(), rgba(0x35a166ff).into()),
+            ],
+        },
+        status_bar: rgba(0x424136ff).into(),
+        title_bar: rgba(0x424136ff).into(),
+        toolbar: rgba(0x22221bff).into(),
+        tab_bar: rgba(0x2c2b23ff).into(),
+        editor: rgba(0x22221bff).into(),
+        editor_subheader: rgba(0x2c2b23ff).into(),
+        editor_active_line: rgba(0x2c2b23ff).into(),
+        terminal: rgba(0x22221bff).into(),
+        image_fallback_background: rgba(0x424136ff).into(),
+        git_created: rgba(0x7d9726ff).into(),
+        git_modified: rgba(0x36a165ff).into(),
+        git_deleted: rgba(0xba6136ff).into(),
+        git_conflict: rgba(0xa5980fff).into(),
+        git_ignored: rgba(0x7d7c6aff).into(),
+        git_renamed: rgba(0xa5980fff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x36a165ff).into(),
+                selection: rgba(0x36a1653d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7d9726ff).into(),
+                selection: rgba(0x7d97263d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9d6b7bff).into(),
+                selection: rgba(0x9d6b7b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xae7214ff).into(),
+                selection: rgba(0xae72143d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5f9182ff).into(),
+                selection: rgba(0x5f91823d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5a9d47ff).into(),
+                selection: rgba(0x5a9d473d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xba6136ff).into(),
+                selection: rgba(0xba61363d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa5980fff).into(),
+                selection: rgba(0xa5980f3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_estuary_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Estuary Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x969585ff).into(),
+        border_variant: rgba(0x969585ff).into(),
+        border_focused: rgba(0xbbddc6ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xc5c4b9ff).into(),
+        surface: rgba(0xebeae3ff).into(),
+        background: rgba(0xc5c4b9ff).into(),
+        filled_element: rgba(0xc5c4b9ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xd9ecdfff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xd9ecdfff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x22221bff).into(),
+        text_muted: rgba(0x61604fff).into(),
+        text_placeholder: rgba(0xba6336ff).into(),
+        text_disabled: rgba(0x767463ff).into(),
+        text_accent: rgba(0x37a165ff).into(),
+        icon_muted: rgba(0x61604fff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("string.special".into(), rgba(0x9d6b7bff).into()),
+                ("link_text".into(), rgba(0xae7214ff).into()),
+                ("emphasis.strong".into(), rgba(0x37a165ff).into()),
+                ("tag".into(), rgba(0x37a165ff).into()),
+                ("primary".into(), rgba(0x302f27ff).into()),
+                ("emphasis".into(), rgba(0x37a165ff).into()),
+                ("hint".into(), rgba(0x758961ff).into()),
+                ("title".into(), rgba(0x22221bff).into()),
+                ("string.regex".into(), rgba(0x5a9d47ff).into()),
+                ("attribute".into(), rgba(0x37a165ff).into()),
+                ("string.escape".into(), rgba(0x5f5e4eff).into()),
+                ("embedded".into(), rgba(0x22221bff).into()),
+                ("punctuation.bracket".into(), rgba(0x5f5e4eff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xa5980cff).into(),
+                ),
+                ("operator".into(), rgba(0x5f5e4eff).into()),
+                ("constant".into(), rgba(0x7c9728ff).into()),
+                ("comment.doc".into(), rgba(0x5f5e4eff).into()),
+                ("label".into(), rgba(0x37a165ff).into()),
+                ("variable".into(), rgba(0x302f27ff).into()),
+                ("punctuation".into(), rgba(0x302f27ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x5f5e4eff).into()),
+                ("comment".into(), rgba(0x878573ff).into()),
+                ("punctuation.special".into(), rgba(0x9d6b7bff).into()),
+                ("string.special.symbol".into(), rgba(0x7c9725ff).into()),
+                ("enum".into(), rgba(0xae7214ff).into()),
+                ("variable.special".into(), rgba(0x5f9182ff).into()),
+                ("link_uri".into(), rgba(0x7c9728ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x302f27ff).into()),
+                ("number".into(), rgba(0xae7312ff).into()),
+                ("function".into(), rgba(0x35a166ff).into()),
+                ("text.literal".into(), rgba(0xae7214ff).into()),
+                ("boolean".into(), rgba(0x7c9728ff).into()),
+                ("predictive".into(), rgba(0x879a72ff).into()),
+                ("type".into(), rgba(0xa5980cff).into()),
+                ("constructor".into(), rgba(0x37a165ff).into()),
+                ("property".into(), rgba(0xba6135ff).into()),
+                ("keyword".into(), rgba(0x5f9182ff).into()),
+                ("function.method".into(), rgba(0x35a166ff).into()),
+                ("variant".into(), rgba(0xa5980cff).into()),
+                ("string".into(), rgba(0x7c9725ff).into()),
+                ("preproc".into(), rgba(0x22221bff).into()),
+            ],
+        },
+        status_bar: rgba(0xc5c4b9ff).into(),
+        title_bar: rgba(0xc5c4b9ff).into(),
+        toolbar: rgba(0xf4f3ecff).into(),
+        tab_bar: rgba(0xebeae3ff).into(),
+        editor: rgba(0xf4f3ecff).into(),
+        editor_subheader: rgba(0xebeae3ff).into(),
+        editor_active_line: rgba(0xebeae3ff).into(),
+        terminal: rgba(0xf4f3ecff).into(),
+        image_fallback_background: rgba(0xc5c4b9ff).into(),
+        git_created: rgba(0x7c9728ff).into(),
+        git_modified: rgba(0x37a165ff).into(),
+        git_deleted: rgba(0xba6336ff).into(),
+        git_conflict: rgba(0xa5980fff).into(),
+        git_ignored: rgba(0x767463ff).into(),
+        git_renamed: rgba(0xa5980fff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x37a165ff).into(),
+                selection: rgba(0x37a1653d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7c9728ff).into(),
+                selection: rgba(0x7c97283d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9d6b7bff).into(),
+                selection: rgba(0x9d6b7b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xae7214ff).into(),
+                selection: rgba(0xae72143d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5f9182ff).into(),
+                selection: rgba(0x5f91823d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5c9d49ff).into(),
+                selection: rgba(0x5c9d493d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xba6336ff).into(),
+                selection: rgba(0xba63363d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa5980fff).into(),
+                selection: rgba(0xa5980f3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_forest_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Forest Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x665f5cff).into(),
+        border_variant: rgba(0x665f5cff).into(),
+        border_focused: rgba(0x182d5bff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x443c39ff).into(),
+        surface: rgba(0x27211eff).into(),
+        background: rgba(0x443c39ff).into(),
+        filled_element: rgba(0x443c39ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x0f1c3dff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x0f1c3dff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xf0eeedff).into(),
+        text_muted: rgba(0xa79f9dff).into(),
+        text_placeholder: rgba(0xf22c3fff).into(),
+        text_disabled: rgba(0x8e8683ff).into(),
+        text_accent: rgba(0x407ee6ff).into(),
+        icon_muted: rgba(0xa79f9dff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("link_uri".into(), rgba(0x7a9726ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xe6e2e0ff).into()),
+                ("type".into(), rgba(0xc38417ff).into()),
+                ("punctuation.bracket".into(), rgba(0xa8a19fff).into()),
+                ("punctuation".into(), rgba(0xe6e2e0ff).into()),
+                ("preproc".into(), rgba(0xf0eeedff).into()),
+                ("punctuation.special".into(), rgba(0xc33ff3ff).into()),
+                ("variable.special".into(), rgba(0x6666eaff).into()),
+                ("tag".into(), rgba(0x407ee6ff).into()),
+                ("constructor".into(), rgba(0x407ee6ff).into()),
+                ("title".into(), rgba(0xf0eeedff).into()),
+                ("hint".into(), rgba(0xa77087ff).into()),
+                ("constant".into(), rgba(0x7a9726ff).into()),
+                ("number".into(), rgba(0xdf521fff).into()),
+                ("emphasis.strong".into(), rgba(0x407ee6ff).into()),
+                ("boolean".into(), rgba(0x7a9726ff).into()),
+                ("comment".into(), rgba(0x766e6bff).into()),
+                ("string.special".into(), rgba(0xc33ff3ff).into()),
+                ("text.literal".into(), rgba(0xdf5321ff).into()),
+                ("string.regex".into(), rgba(0x3c96b8ff).into()),
+                ("enum".into(), rgba(0xdf5321ff).into()),
+                ("operator".into(), rgba(0xa8a19fff).into()),
+                ("embedded".into(), rgba(0xf0eeedff).into()),
+                ("string.special.symbol".into(), rgba(0x7a9725ff).into()),
+                ("predictive".into(), rgba(0x8f5b70ff).into()),
+                ("comment.doc".into(), rgba(0xa8a19fff).into()),
+                ("variant".into(), rgba(0xc38417ff).into()),
+                ("label".into(), rgba(0x407ee6ff).into()),
+                ("property".into(), rgba(0xf22c40ff).into()),
+                ("keyword".into(), rgba(0x6666eaff).into()),
+                ("function".into(), rgba(0x3f7ee7ff).into()),
+                ("string.escape".into(), rgba(0xa8a19fff).into()),
+                ("string".into(), rgba(0x7a9725ff).into()),
+                ("primary".into(), rgba(0xe6e2e0ff).into()),
+                ("function.method".into(), rgba(0x3f7ee7ff).into()),
+                ("link_text".into(), rgba(0xdf5321ff).into()),
+                ("attribute".into(), rgba(0x407ee6ff).into()),
+                ("emphasis".into(), rgba(0x407ee6ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xc38417ff).into(),
+                ),
+                ("variable".into(), rgba(0xe6e2e0ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xa8a19fff).into()),
+            ],
+        },
+        status_bar: rgba(0x443c39ff).into(),
+        title_bar: rgba(0x443c39ff).into(),
+        toolbar: rgba(0x1b1918ff).into(),
+        tab_bar: rgba(0x27211eff).into(),
+        editor: rgba(0x1b1918ff).into(),
+        editor_subheader: rgba(0x27211eff).into(),
+        editor_active_line: rgba(0x27211eff).into(),
+        terminal: rgba(0x1b1918ff).into(),
+        image_fallback_background: rgba(0x443c39ff).into(),
+        git_created: rgba(0x7a9726ff).into(),
+        git_modified: rgba(0x407ee6ff).into(),
+        git_deleted: rgba(0xf22c3fff).into(),
+        git_conflict: rgba(0xc38418ff).into(),
+        git_ignored: rgba(0x8e8683ff).into(),
+        git_renamed: rgba(0xc38418ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x407ee6ff).into(),
+                selection: rgba(0x407ee63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7a9726ff).into(),
+                selection: rgba(0x7a97263d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc340f2ff).into(),
+                selection: rgba(0xc340f23d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xdf5321ff).into(),
+                selection: rgba(0xdf53213d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x6565e9ff).into(),
+                selection: rgba(0x6565e93d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x3d97b8ff).into(),
+                selection: rgba(0x3d97b83d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf22c3fff).into(),
+                selection: rgba(0xf22c3f3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc38418ff).into(),
+                selection: rgba(0xc384183d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_forest_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Forest Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0xaaa3a1ff).into(),
+        border_variant: rgba(0xaaa3a1ff).into(),
+        border_focused: rgba(0xc6cef7ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xccc7c5ff).into(),
+        surface: rgba(0xe9e6e4ff).into(),
+        background: rgba(0xccc7c5ff).into(),
+        filled_element: rgba(0xccc7c5ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xdfe3fbff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xdfe3fbff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x1b1918ff).into(),
+        text_muted: rgba(0x6a6360ff).into(),
+        text_placeholder: rgba(0xf22e40ff).into(),
+        text_disabled: rgba(0x837b78ff).into(),
+        text_accent: rgba(0x407ee6ff).into(),
+        icon_muted: rgba(0x6a6360ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("punctuation.special".into(), rgba(0xc33ff3ff).into()),
+                ("text.literal".into(), rgba(0xdf5421ff).into()),
+                ("string.escape".into(), rgba(0x68615eff).into()),
+                ("string.regex".into(), rgba(0x3c96b8ff).into()),
+                ("number".into(), rgba(0xdf521fff).into()),
+                ("preproc".into(), rgba(0x1b1918ff).into()),
+                ("keyword".into(), rgba(0x6666eaff).into()),
+                ("variable.special".into(), rgba(0x6666eaff).into()),
+                ("punctuation.delimiter".into(), rgba(0x68615eff).into()),
+                ("emphasis.strong".into(), rgba(0x407ee6ff).into()),
+                ("boolean".into(), rgba(0x7a9728ff).into()),
+                ("variant".into(), rgba(0xc38417ff).into()),
+                ("predictive".into(), rgba(0xbe899eff).into()),
+                ("tag".into(), rgba(0x407ee6ff).into()),
+                ("property".into(), rgba(0xf22c40ff).into()),
+                ("enum".into(), rgba(0xdf5421ff).into()),
+                ("attribute".into(), rgba(0x407ee6ff).into()),
+                ("function.method".into(), rgba(0x3f7ee7ff).into()),
+                ("function".into(), rgba(0x3f7ee7ff).into()),
+                ("emphasis".into(), rgba(0x407ee6ff).into()),
+                ("primary".into(), rgba(0x2c2421ff).into()),
+                ("variable".into(), rgba(0x2c2421ff).into()),
+                ("constant".into(), rgba(0x7a9728ff).into()),
+                ("title".into(), rgba(0x1b1918ff).into()),
+                ("comment.doc".into(), rgba(0x68615eff).into()),
+                ("constructor".into(), rgba(0x407ee6ff).into()),
+                ("type".into(), rgba(0xc38417ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x2c2421ff).into()),
+                ("punctuation".into(), rgba(0x2c2421ff).into()),
+                ("string".into(), rgba(0x7a9725ff).into()),
+                ("label".into(), rgba(0x407ee6ff).into()),
+                ("string.special".into(), rgba(0xc33ff3ff).into()),
+                ("embedded".into(), rgba(0x1b1918ff).into()),
+                ("link_text".into(), rgba(0xdf5421ff).into()),
+                ("punctuation.bracket".into(), rgba(0x68615eff).into()),
+                ("comment".into(), rgba(0x9c9491ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xc38417ff).into(),
+                ),
+                ("link_uri".into(), rgba(0x7a9728ff).into()),
+                ("operator".into(), rgba(0x68615eff).into()),
+                ("hint".into(), rgba(0xa67287ff).into()),
+                ("string.special.symbol".into(), rgba(0x7a9725ff).into()),
+            ],
+        },
+        status_bar: rgba(0xccc7c5ff).into(),
+        title_bar: rgba(0xccc7c5ff).into(),
+        toolbar: rgba(0xf0eeedff).into(),
+        tab_bar: rgba(0xe9e6e4ff).into(),
+        editor: rgba(0xf0eeedff).into(),
+        editor_subheader: rgba(0xe9e6e4ff).into(),
+        editor_active_line: rgba(0xe9e6e4ff).into(),
+        terminal: rgba(0xf0eeedff).into(),
+        image_fallback_background: rgba(0xccc7c5ff).into(),
+        git_created: rgba(0x7a9728ff).into(),
+        git_modified: rgba(0x407ee6ff).into(),
+        git_deleted: rgba(0xf22e40ff).into(),
+        git_conflict: rgba(0xc38419ff).into(),
+        git_ignored: rgba(0x837b78ff).into(),
+        git_renamed: rgba(0xc38419ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x407ee6ff).into(),
+                selection: rgba(0x407ee63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7a9728ff).into(),
+                selection: rgba(0x7a97283d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc340f2ff).into(),
+                selection: rgba(0xc340f23d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xdf5421ff).into(),
+                selection: rgba(0xdf54213d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x6765e9ff).into(),
+                selection: rgba(0x6765e93d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x3e96b8ff).into(),
+                selection: rgba(0x3e96b83d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf22e40ff).into(),
+                selection: rgba(0xf22e403d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc38419ff).into(),
+                selection: rgba(0xc384193d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_heath_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Heath Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x675b67ff).into(),
+        border_variant: rgba(0x675b67ff).into(),
+        border_focused: rgba(0x192961ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x433a43ff).into(),
+        surface: rgba(0x252025ff).into(),
+        background: rgba(0x433a43ff).into(),
+        filled_element: rgba(0x433a43ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x0d1a43ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x0d1a43ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xf7f3f7ff).into(),
+        text_muted: rgba(0xa899a8ff).into(),
+        text_placeholder: rgba(0xca3f2bff).into(),
+        text_disabled: rgba(0x908190ff).into(),
+        text_accent: rgba(0x5169ebff).into(),
+        icon_muted: rgba(0xa899a8ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("preproc".into(), rgba(0xf7f3f7ff).into()),
+                ("number".into(), rgba(0xa65825ff).into()),
+                ("boolean".into(), rgba(0x918b3aff).into()),
+                ("embedded".into(), rgba(0xf7f3f7ff).into()),
+                ("variable.special".into(), rgba(0x7b58bfff).into()),
+                ("operator".into(), rgba(0xab9babff).into()),
+                ("punctuation.delimiter".into(), rgba(0xab9babff).into()),
+                ("primary".into(), rgba(0xd8cad8ff).into()),
+                ("punctuation.bracket".into(), rgba(0xab9babff).into()),
+                ("comment.doc".into(), rgba(0xab9babff).into()),
+                ("variant".into(), rgba(0xbb8a34ff).into()),
+                ("attribute".into(), rgba(0x5169ebff).into()),
+                ("property".into(), rgba(0xca3f2aff).into()),
+                ("keyword".into(), rgba(0x7b58bfff).into()),
+                ("hint".into(), rgba(0x8d70a8ff).into()),
+                ("string.special.symbol".into(), rgba(0x918b3aff).into()),
+                ("punctuation.special".into(), rgba(0xcc32ccff).into()),
+                ("link_uri".into(), rgba(0x918b3aff).into()),
+                ("link_text".into(), rgba(0xa65827ff).into()),
+                ("enum".into(), rgba(0xa65827ff).into()),
+                ("function".into(), rgba(0x506aecff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xbb8a34ff).into(),
+                ),
+                ("constant".into(), rgba(0x918b3aff).into()),
+                ("title".into(), rgba(0xf7f3f7ff).into()),
+                ("string.regex".into(), rgba(0x149393ff).into()),
+                ("variable".into(), rgba(0xd8cad8ff).into()),
+                ("comment".into(), rgba(0x776977ff).into()),
+                ("predictive".into(), rgba(0x75588fff).into()),
+                ("function.method".into(), rgba(0x506aecff).into()),
+                ("type".into(), rgba(0xbb8a34ff).into()),
+                ("punctuation".into(), rgba(0xd8cad8ff).into()),
+                ("emphasis".into(), rgba(0x5169ebff).into()),
+                ("emphasis.strong".into(), rgba(0x5169ebff).into()),
+                ("tag".into(), rgba(0x5169ebff).into()),
+                ("text.literal".into(), rgba(0xa65827ff).into()),
+                ("string".into(), rgba(0x918b3aff).into()),
+                ("string.escape".into(), rgba(0xab9babff).into()),
+                ("constructor".into(), rgba(0x5169ebff).into()),
+                ("label".into(), rgba(0x5169ebff).into()),
+                ("punctuation.list_marker".into(), rgba(0xd8cad8ff).into()),
+                ("string.special".into(), rgba(0xcc32ccff).into()),
+            ],
+        },
+        status_bar: rgba(0x433a43ff).into(),
+        title_bar: rgba(0x433a43ff).into(),
+        toolbar: rgba(0x1b181bff).into(),
+        tab_bar: rgba(0x252025ff).into(),
+        editor: rgba(0x1b181bff).into(),
+        editor_subheader: rgba(0x252025ff).into(),
+        editor_active_line: rgba(0x252025ff).into(),
+        terminal: rgba(0x1b181bff).into(),
+        image_fallback_background: rgba(0x433a43ff).into(),
+        git_created: rgba(0x918b3aff).into(),
+        git_modified: rgba(0x5169ebff).into(),
+        git_deleted: rgba(0xca3f2bff).into(),
+        git_conflict: rgba(0xbb8a35ff).into(),
+        git_ignored: rgba(0x908190ff).into(),
+        git_renamed: rgba(0xbb8a35ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x5169ebff).into(),
+                selection: rgba(0x5169eb3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x918b3aff).into(),
+                selection: rgba(0x918b3a3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xcc34ccff).into(),
+                selection: rgba(0xcc34cc3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa65827ff).into(),
+                selection: rgba(0xa658273d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7b58bfff).into(),
+                selection: rgba(0x7b58bf3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x189393ff).into(),
+                selection: rgba(0x1893933d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xca3f2bff).into(),
+                selection: rgba(0xca3f2b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbb8a35ff).into(),
+                selection: rgba(0xbb8a353d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_heath_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Heath Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0xad9dadff).into(),
+        border_variant: rgba(0xad9dadff).into(),
+        border_focused: rgba(0xcac7faff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xc6b8c6ff).into(),
+        surface: rgba(0xe0d5e0ff).into(),
+        background: rgba(0xc6b8c6ff).into(),
+        filled_element: rgba(0xc6b8c6ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xe2dffcff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xe2dffcff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x1b181bff).into(),
+        text_muted: rgba(0x6b5e6bff).into(),
+        text_placeholder: rgba(0xca402bff).into(),
+        text_disabled: rgba(0x857785ff).into(),
+        text_accent: rgba(0x5169ebff).into(),
+        icon_muted: rgba(0x6b5e6bff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("enum".into(), rgba(0xa65927ff).into()),
+                ("string.escape".into(), rgba(0x695d69ff).into()),
+                ("link_uri".into(), rgba(0x918b3bff).into()),
+                ("function.method".into(), rgba(0x506aecff).into()),
+                ("comment.doc".into(), rgba(0x695d69ff).into()),
+                ("property".into(), rgba(0xca3f2aff).into()),
+                ("string.special".into(), rgba(0xcc32ccff).into()),
+                ("tag".into(), rgba(0x5169ebff).into()),
+                ("embedded".into(), rgba(0x1b181bff).into()),
+                ("primary".into(), rgba(0x292329ff).into()),
+                ("punctuation".into(), rgba(0x292329ff).into()),
+                ("punctuation.special".into(), rgba(0xcc32ccff).into()),
+                ("type".into(), rgba(0xbb8a34ff).into()),
+                ("number".into(), rgba(0xa65825ff).into()),
+                ("function".into(), rgba(0x506aecff).into()),
+                ("preproc".into(), rgba(0x1b181bff).into()),
+                ("punctuation.bracket".into(), rgba(0x695d69ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x695d69ff).into()),
+                ("variable".into(), rgba(0x292329ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xbb8a34ff).into(),
+                ),
+                ("label".into(), rgba(0x5169ebff).into()),
+                ("constructor".into(), rgba(0x5169ebff).into()),
+                ("emphasis.strong".into(), rgba(0x5169ebff).into()),
+                ("constant".into(), rgba(0x918b3bff).into()),
+                ("keyword".into(), rgba(0x7b58bfff).into()),
+                ("variable.special".into(), rgba(0x7b58bfff).into()),
+                ("variant".into(), rgba(0xbb8a34ff).into()),
+                ("title".into(), rgba(0x1b181bff).into()),
+                ("attribute".into(), rgba(0x5169ebff).into()),
+                ("comment".into(), rgba(0x9e8f9eff).into()),
+                ("string.special.symbol".into(), rgba(0x918b3aff).into()),
+                ("predictive".into(), rgba(0xa487bfff).into()),
+                ("link_text".into(), rgba(0xa65927ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x292329ff).into()),
+                ("boolean".into(), rgba(0x918b3bff).into()),
+                ("text.literal".into(), rgba(0xa65927ff).into()),
+                ("emphasis".into(), rgba(0x5169ebff).into()),
+                ("string.regex".into(), rgba(0x149393ff).into()),
+                ("hint".into(), rgba(0x8c70a6ff).into()),
+                ("string".into(), rgba(0x918b3aff).into()),
+                ("operator".into(), rgba(0x695d69ff).into()),
+            ],
+        },
+        status_bar: rgba(0xc6b8c6ff).into(),
+        title_bar: rgba(0xc6b8c6ff).into(),
+        toolbar: rgba(0xf7f3f7ff).into(),
+        tab_bar: rgba(0xe0d5e0ff).into(),
+        editor: rgba(0xf7f3f7ff).into(),
+        editor_subheader: rgba(0xe0d5e0ff).into(),
+        editor_active_line: rgba(0xe0d5e0ff).into(),
+        terminal: rgba(0xf7f3f7ff).into(),
+        image_fallback_background: rgba(0xc6b8c6ff).into(),
+        git_created: rgba(0x918b3bff).into(),
+        git_modified: rgba(0x5169ebff).into(),
+        git_deleted: rgba(0xca402bff).into(),
+        git_conflict: rgba(0xbb8a35ff).into(),
+        git_ignored: rgba(0x857785ff).into(),
+        git_renamed: rgba(0xbb8a35ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x5169ebff).into(),
+                selection: rgba(0x5169eb3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x918b3bff).into(),
+                selection: rgba(0x918b3b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xcc34ccff).into(),
+                selection: rgba(0xcc34cc3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa65927ff).into(),
+                selection: rgba(0xa659273d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7a5ac0ff).into(),
+                selection: rgba(0x7a5ac03d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x189393ff).into(),
+                selection: rgba(0x1893933d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xca402bff).into(),
+                selection: rgba(0xca402b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbb8a35ff).into(),
+                selection: rgba(0xbb8a353d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_lakeside_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Lakeside Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x4f6a78ff).into(),
+        border_variant: rgba(0x4f6a78ff).into(),
+        border_focused: rgba(0x1a2f3cff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x33444dff).into(),
+        surface: rgba(0x1c2529ff).into(),
+        background: rgba(0x33444dff).into(),
+        filled_element: rgba(0x33444dff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x121c24ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x121c24ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xebf8ffff).into(),
+        text_muted: rgba(0x7c9fb3ff).into(),
+        text_placeholder: rgba(0xd22e72ff).into(),
+        text_disabled: rgba(0x688c9dff).into(),
+        text_accent: rgba(0x267eadff).into(),
+        icon_muted: rgba(0x7c9fb3ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("punctuation.bracket".into(), rgba(0x7ea2b4ff).into()),
+                ("punctuation.special".into(), rgba(0xb72cd2ff).into()),
+                ("property".into(), rgba(0xd22c72ff).into()),
+                ("function.method".into(), rgba(0x247eadff).into()),
+                ("comment".into(), rgba(0x5a7b8cff).into()),
+                ("constructor".into(), rgba(0x267eadff).into()),
+                ("boolean".into(), rgba(0x558c3aff).into()),
+                ("hint".into(), rgba(0x52809aff).into()),
+                ("label".into(), rgba(0x267eadff).into()),
+                ("string.special".into(), rgba(0xb72cd2ff).into()),
+                ("title".into(), rgba(0xebf8ffff).into()),
+                ("punctuation.list_marker".into(), rgba(0xc1e4f6ff).into()),
+                ("emphasis.strong".into(), rgba(0x267eadff).into()),
+                ("enum".into(), rgba(0x935b25ff).into()),
+                ("type".into(), rgba(0x8a8a0eff).into()),
+                ("tag".into(), rgba(0x267eadff).into()),
+                ("punctuation.delimiter".into(), rgba(0x7ea2b4ff).into()),
+                ("primary".into(), rgba(0xc1e4f6ff).into()),
+                ("link_text".into(), rgba(0x935b25ff).into()),
+                ("variable".into(), rgba(0xc1e4f6ff).into()),
+                ("variable.special".into(), rgba(0x6a6ab7ff).into()),
+                ("string.special.symbol".into(), rgba(0x558c3aff).into()),
+                ("link_uri".into(), rgba(0x558c3aff).into()),
+                ("function".into(), rgba(0x247eadff).into()),
+                ("predictive".into(), rgba(0x426f88ff).into()),
+                ("punctuation".into(), rgba(0xc1e4f6ff).into()),
+                ("string.escape".into(), rgba(0x7ea2b4ff).into()),
+                ("keyword".into(), rgba(0x6a6ab7ff).into()),
+                ("attribute".into(), rgba(0x267eadff).into()),
+                ("string.regex".into(), rgba(0x2c8f6eff).into()),
+                ("embedded".into(), rgba(0xebf8ffff).into()),
+                ("emphasis".into(), rgba(0x267eadff).into()),
+                ("string".into(), rgba(0x558c3aff).into()),
+                ("operator".into(), rgba(0x7ea2b4ff).into()),
+                ("text.literal".into(), rgba(0x935b25ff).into()),
+                ("constant".into(), rgba(0x558c3aff).into()),
+                ("comment.doc".into(), rgba(0x7ea2b4ff).into()),
+                ("number".into(), rgba(0x935c24ff).into()),
+                ("preproc".into(), rgba(0xebf8ffff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0x8a8a0eff).into(),
+                ),
+                ("variant".into(), rgba(0x8a8a0eff).into()),
+            ],
+        },
+        status_bar: rgba(0x33444dff).into(),
+        title_bar: rgba(0x33444dff).into(),
+        toolbar: rgba(0x161b1dff).into(),
+        tab_bar: rgba(0x1c2529ff).into(),
+        editor: rgba(0x161b1dff).into(),
+        editor_subheader: rgba(0x1c2529ff).into(),
+        editor_active_line: rgba(0x1c2529ff).into(),
+        terminal: rgba(0x161b1dff).into(),
+        image_fallback_background: rgba(0x33444dff).into(),
+        git_created: rgba(0x558c3aff).into(),
+        git_modified: rgba(0x267eadff).into(),
+        git_deleted: rgba(0xd22e72ff).into(),
+        git_conflict: rgba(0x8a8a10ff).into(),
+        git_ignored: rgba(0x688c9dff).into(),
+        git_renamed: rgba(0x8a8a10ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x267eadff).into(),
+                selection: rgba(0x267ead3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x558c3aff).into(),
+                selection: rgba(0x558c3a3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb72ed2ff).into(),
+                selection: rgba(0xb72ed23d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x935b25ff).into(),
+                selection: rgba(0x935b253d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x6a6ab7ff).into(),
+                selection: rgba(0x6a6ab73d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x2d8f6fff).into(),
+                selection: rgba(0x2d8f6f3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd22e72ff).into(),
+                selection: rgba(0xd22e723d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8a8a10ff).into(),
+                selection: rgba(0x8a8a103d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_lakeside_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Lakeside Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x80a4b6ff).into(),
+        border_variant: rgba(0x80a4b6ff).into(),
+        border_focused: rgba(0xb9cee0ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xa6cadcff).into(),
+        surface: rgba(0xcdeaf9ff).into(),
+        background: rgba(0xa6cadcff).into(),
+        filled_element: rgba(0xa6cadcff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xd8e4eeff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xd8e4eeff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x161b1dff).into(),
+        text_muted: rgba(0x526f7dff).into(),
+        text_placeholder: rgba(0xd22e71ff).into(),
+        text_disabled: rgba(0x628496ff).into(),
+        text_accent: rgba(0x267eadff).into(),
+        icon_muted: rgba(0x526f7dff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("emphasis".into(), rgba(0x267eadff).into()),
+                ("number".into(), rgba(0x935c24ff).into()),
+                ("embedded".into(), rgba(0x161b1dff).into()),
+                ("link_text".into(), rgba(0x935c25ff).into()),
+                ("string".into(), rgba(0x558c3aff).into()),
+                ("constructor".into(), rgba(0x267eadff).into()),
+                ("punctuation.list_marker".into(), rgba(0x1f292eff).into()),
+                ("string.special".into(), rgba(0xb72cd2ff).into()),
+                ("title".into(), rgba(0x161b1dff).into()),
+                ("variant".into(), rgba(0x8a8a0eff).into()),
+                ("tag".into(), rgba(0x267eadff).into()),
+                ("attribute".into(), rgba(0x267eadff).into()),
+                ("keyword".into(), rgba(0x6a6ab7ff).into()),
+                ("enum".into(), rgba(0x935c25ff).into()),
+                ("function".into(), rgba(0x247eadff).into()),
+                ("string.escape".into(), rgba(0x516d7bff).into()),
+                ("operator".into(), rgba(0x516d7bff).into()),
+                ("function.method".into(), rgba(0x247eadff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0x8a8a0eff).into(),
+                ),
+                ("punctuation.delimiter".into(), rgba(0x516d7bff).into()),
+                ("comment".into(), rgba(0x7094a7ff).into()),
+                ("primary".into(), rgba(0x1f292eff).into()),
+                ("punctuation.bracket".into(), rgba(0x516d7bff).into()),
+                ("variable".into(), rgba(0x1f292eff).into()),
+                ("emphasis.strong".into(), rgba(0x267eadff).into()),
+                ("predictive".into(), rgba(0x6a97b2ff).into()),
+                ("punctuation.special".into(), rgba(0xb72cd2ff).into()),
+                ("hint".into(), rgba(0x5a87a0ff).into()),
+                ("text.literal".into(), rgba(0x935c25ff).into()),
+                ("string.special.symbol".into(), rgba(0x558c3aff).into()),
+                ("comment.doc".into(), rgba(0x516d7bff).into()),
+                ("constant".into(), rgba(0x568c3bff).into()),
+                ("boolean".into(), rgba(0x568c3bff).into()),
+                ("preproc".into(), rgba(0x161b1dff).into()),
+                ("variable.special".into(), rgba(0x6a6ab7ff).into()),
+                ("link_uri".into(), rgba(0x568c3bff).into()),
+                ("string.regex".into(), rgba(0x2c8f6eff).into()),
+                ("punctuation".into(), rgba(0x1f292eff).into()),
+                ("property".into(), rgba(0xd22c72ff).into()),
+                ("label".into(), rgba(0x267eadff).into()),
+                ("type".into(), rgba(0x8a8a0eff).into()),
+            ],
+        },
+        status_bar: rgba(0xa6cadcff).into(),
+        title_bar: rgba(0xa6cadcff).into(),
+        toolbar: rgba(0xebf8ffff).into(),
+        tab_bar: rgba(0xcdeaf9ff).into(),
+        editor: rgba(0xebf8ffff).into(),
+        editor_subheader: rgba(0xcdeaf9ff).into(),
+        editor_active_line: rgba(0xcdeaf9ff).into(),
+        terminal: rgba(0xebf8ffff).into(),
+        image_fallback_background: rgba(0xa6cadcff).into(),
+        git_created: rgba(0x568c3bff).into(),
+        git_modified: rgba(0x267eadff).into(),
+        git_deleted: rgba(0xd22e71ff).into(),
+        git_conflict: rgba(0x8a8a10ff).into(),
+        git_ignored: rgba(0x628496ff).into(),
+        git_renamed: rgba(0x8a8a10ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x267eadff).into(),
+                selection: rgba(0x267ead3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x568c3bff).into(),
+                selection: rgba(0x568c3b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb72ed2ff).into(),
+                selection: rgba(0xb72ed23d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x935c25ff).into(),
+                selection: rgba(0x935c253d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x6c6ab7ff).into(),
+                selection: rgba(0x6c6ab73d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x2e8f6eff).into(),
+                selection: rgba(0x2e8f6e3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd22e71ff).into(),
+                selection: rgba(0xd22e713d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8a8a10ff).into(),
+                selection: rgba(0x8a8a103d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_plateau_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Plateau Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x564e4eff).into(),
+        border_variant: rgba(0x564e4eff).into(),
+        border_focused: rgba(0x2c2b45ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x3b3535ff).into(),
+        surface: rgba(0x252020ff).into(),
+        background: rgba(0x3b3535ff).into(),
+        filled_element: rgba(0x3b3535ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x1c1b29ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x1c1b29ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xf4ececff).into(),
+        text_muted: rgba(0x898383ff).into(),
+        text_placeholder: rgba(0xca4848ff).into(),
+        text_disabled: rgba(0x756e6eff).into(),
+        text_accent: rgba(0x7272caff).into(),
+        icon_muted: rgba(0x898383ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("variant".into(), rgba(0xa06d3aff).into()),
+                ("label".into(), rgba(0x7272caff).into()),
+                ("punctuation.delimiter".into(), rgba(0x8a8585ff).into()),
+                ("string.regex".into(), rgba(0x5485b6ff).into()),
+                ("variable.special".into(), rgba(0x8464c4ff).into()),
+                ("string".into(), rgba(0x4b8b8bff).into()),
+                ("property".into(), rgba(0xca4848ff).into()),
+                ("hint".into(), rgba(0x8a647aff).into()),
+                ("comment.doc".into(), rgba(0x8a8585ff).into()),
+                ("attribute".into(), rgba(0x7272caff).into()),
+                ("tag".into(), rgba(0x7272caff).into()),
+                ("constructor".into(), rgba(0x7272caff).into()),
+                ("boolean".into(), rgba(0x4b8b8bff).into()),
+                ("preproc".into(), rgba(0xf4ececff).into()),
+                ("constant".into(), rgba(0x4b8b8bff).into()),
+                ("punctuation.special".into(), rgba(0xbd5187ff).into()),
+                ("function.method".into(), rgba(0x7272caff).into()),
+                ("comment".into(), rgba(0x655d5dff).into()),
+                ("variable".into(), rgba(0xe7dfdfff).into()),
+                ("primary".into(), rgba(0xe7dfdfff).into()),
+                ("title".into(), rgba(0xf4ececff).into()),
+                ("emphasis".into(), rgba(0x7272caff).into()),
+                ("emphasis.strong".into(), rgba(0x7272caff).into()),
+                ("function".into(), rgba(0x7272caff).into()),
+                ("type".into(), rgba(0xa06d3aff).into()),
+                ("operator".into(), rgba(0x8a8585ff).into()),
+                ("embedded".into(), rgba(0xf4ececff).into()),
+                ("predictive".into(), rgba(0x795369ff).into()),
+                ("punctuation".into(), rgba(0xe7dfdfff).into()),
+                ("link_text".into(), rgba(0xb4593bff).into()),
+                ("enum".into(), rgba(0xb4593bff).into()),
+                ("string.special".into(), rgba(0xbd5187ff).into()),
+                ("text.literal".into(), rgba(0xb4593bff).into()),
+                ("string.escape".into(), rgba(0x8a8585ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xa06d3aff).into(),
+                ),
+                ("keyword".into(), rgba(0x8464c4ff).into()),
+                ("link_uri".into(), rgba(0x4b8b8bff).into()),
+                ("number".into(), rgba(0xb4593bff).into()),
+                ("punctuation.bracket".into(), rgba(0x8a8585ff).into()),
+                ("string.special.symbol".into(), rgba(0x4b8b8bff).into()),
+                ("punctuation.list_marker".into(), rgba(0xe7dfdfff).into()),
+            ],
+        },
+        status_bar: rgba(0x3b3535ff).into(),
+        title_bar: rgba(0x3b3535ff).into(),
+        toolbar: rgba(0x1b1818ff).into(),
+        tab_bar: rgba(0x252020ff).into(),
+        editor: rgba(0x1b1818ff).into(),
+        editor_subheader: rgba(0x252020ff).into(),
+        editor_active_line: rgba(0x252020ff).into(),
+        terminal: rgba(0x1b1818ff).into(),
+        image_fallback_background: rgba(0x3b3535ff).into(),
+        git_created: rgba(0x4b8b8bff).into(),
+        git_modified: rgba(0x7272caff).into(),
+        git_deleted: rgba(0xca4848ff).into(),
+        git_conflict: rgba(0xa06d3aff).into(),
+        git_ignored: rgba(0x756e6eff).into(),
+        git_renamed: rgba(0xa06d3aff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x7272caff).into(),
+                selection: rgba(0x7272ca3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x4b8b8bff).into(),
+                selection: rgba(0x4b8b8b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbd5187ff).into(),
+                selection: rgba(0xbd51873d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb4593bff).into(),
+                selection: rgba(0xb4593b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8464c4ff).into(),
+                selection: rgba(0x8464c43d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5485b6ff).into(),
+                selection: rgba(0x5485b63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xca4848ff).into(),
+                selection: rgba(0xca48483d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa06d3aff).into(),
+                selection: rgba(0xa06d3a3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_plateau_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Plateau Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x8e8989ff).into(),
+        border_variant: rgba(0x8e8989ff).into(),
+        border_focused: rgba(0xcecaecff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xc1bbbbff).into(),
+        surface: rgba(0xebe3e3ff).into(),
+        background: rgba(0xc1bbbbff).into(),
+        filled_element: rgba(0xc1bbbbff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xe4e1f5ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xe4e1f5ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x1b1818ff).into(),
+        text_muted: rgba(0x5a5252ff).into(),
+        text_placeholder: rgba(0xca4a4aff).into(),
+        text_disabled: rgba(0x6e6666ff).into(),
+        text_accent: rgba(0x7272caff).into(),
+        icon_muted: rgba(0x5a5252ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("text.literal".into(), rgba(0xb45a3cff).into()),
+                ("punctuation.special".into(), rgba(0xbd5187ff).into()),
+                ("variant".into(), rgba(0xa06d3aff).into()),
+                ("punctuation".into(), rgba(0x292424ff).into()),
+                ("string.escape".into(), rgba(0x585050ff).into()),
+                ("emphasis".into(), rgba(0x7272caff).into()),
+                ("title".into(), rgba(0x1b1818ff).into()),
+                ("constructor".into(), rgba(0x7272caff).into()),
+                ("variable".into(), rgba(0x292424ff).into()),
+                ("predictive".into(), rgba(0xa27a91ff).into()),
+                ("label".into(), rgba(0x7272caff).into()),
+                ("function.method".into(), rgba(0x7272caff).into()),
+                ("link_uri".into(), rgba(0x4c8b8bff).into()),
+                ("punctuation.delimiter".into(), rgba(0x585050ff).into()),
+                ("link_text".into(), rgba(0xb45a3cff).into()),
+                ("hint".into(), rgba(0x91697fff).into()),
+                ("emphasis.strong".into(), rgba(0x7272caff).into()),
+                ("attribute".into(), rgba(0x7272caff).into()),
+                ("boolean".into(), rgba(0x4c8b8bff).into()),
+                ("string.special.symbol".into(), rgba(0x4b8b8bff).into()),
+                ("string".into(), rgba(0x4b8b8bff).into()),
+                ("type".into(), rgba(0xa06d3aff).into()),
+                ("string.regex".into(), rgba(0x5485b6ff).into()),
+                ("comment.doc".into(), rgba(0x585050ff).into()),
+                ("string.special".into(), rgba(0xbd5187ff).into()),
+                ("property".into(), rgba(0xca4848ff).into()),
+                ("preproc".into(), rgba(0x1b1818ff).into()),
+                ("embedded".into(), rgba(0x1b1818ff).into()),
+                ("comment".into(), rgba(0x7e7777ff).into()),
+                ("primary".into(), rgba(0x292424ff).into()),
+                ("number".into(), rgba(0xb4593bff).into()),
+                ("function".into(), rgba(0x7272caff).into()),
+                ("punctuation.bracket".into(), rgba(0x585050ff).into()),
+                ("tag".into(), rgba(0x7272caff).into()),
+                ("punctuation.list_marker".into(), rgba(0x292424ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xa06d3aff).into(),
+                ),
+                ("enum".into(), rgba(0xb45a3cff).into()),
+                ("keyword".into(), rgba(0x8464c4ff).into()),
+                ("operator".into(), rgba(0x585050ff).into()),
+                ("variable.special".into(), rgba(0x8464c4ff).into()),
+                ("constant".into(), rgba(0x4c8b8bff).into()),
+            ],
+        },
+        status_bar: rgba(0xc1bbbbff).into(),
+        title_bar: rgba(0xc1bbbbff).into(),
+        toolbar: rgba(0xf4ececff).into(),
+        tab_bar: rgba(0xebe3e3ff).into(),
+        editor: rgba(0xf4ececff).into(),
+        editor_subheader: rgba(0xebe3e3ff).into(),
+        editor_active_line: rgba(0xebe3e3ff).into(),
+        terminal: rgba(0xf4ececff).into(),
+        image_fallback_background: rgba(0xc1bbbbff).into(),
+        git_created: rgba(0x4c8b8bff).into(),
+        git_modified: rgba(0x7272caff).into(),
+        git_deleted: rgba(0xca4a4aff).into(),
+        git_conflict: rgba(0xa06e3bff).into(),
+        git_ignored: rgba(0x6e6666ff).into(),
+        git_renamed: rgba(0xa06e3bff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x7272caff).into(),
+                selection: rgba(0x7272ca3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x4c8b8bff).into(),
+                selection: rgba(0x4c8b8b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbd5186ff).into(),
+                selection: rgba(0xbd51863d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb45a3cff).into(),
+                selection: rgba(0xb45a3c3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8464c4ff).into(),
+                selection: rgba(0x8464c43d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5485b5ff).into(),
+                selection: rgba(0x5485b53d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xca4a4aff).into(),
+                selection: rgba(0xca4a4a3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa06e3bff).into(),
+                selection: rgba(0xa06e3b3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_savanna_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Savanna Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x505e55ff).into(),
+        border_variant: rgba(0x505e55ff).into(),
+        border_focused: rgba(0x1f3233ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x353f39ff).into(),
+        surface: rgba(0x1f2621ff).into(),
+        background: rgba(0x353f39ff).into(),
+        filled_element: rgba(0x353f39ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x151e20ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x151e20ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xecf4eeff).into(),
+        text_muted: rgba(0x859188ff).into(),
+        text_placeholder: rgba(0xb16038ff).into(),
+        text_disabled: rgba(0x6f7e74ff).into(),
+        text_accent: rgba(0x468b8fff).into(),
+        icon_muted: rgba(0x859188ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("function.method".into(), rgba(0x468b8fff).into()),
+                ("title".into(), rgba(0xecf4eeff).into()),
+                ("label".into(), rgba(0x468b8fff).into()),
+                ("text.literal".into(), rgba(0x9f703bff).into()),
+                ("boolean".into(), rgba(0x479962ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xdfe7e2ff).into()),
+                ("string.escape".into(), rgba(0x87928aff).into()),
+                ("string.special".into(), rgba(0x857368ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x87928aff).into()),
+                ("tag".into(), rgba(0x468b8fff).into()),
+                ("property".into(), rgba(0xb16038ff).into()),
+                ("preproc".into(), rgba(0xecf4eeff).into()),
+                ("primary".into(), rgba(0xdfe7e2ff).into()),
+                ("link_uri".into(), rgba(0x479962ff).into()),
+                ("comment".into(), rgba(0x5f6d64ff).into()),
+                ("type".into(), rgba(0xa07d3aff).into()),
+                ("hint".into(), rgba(0x607e76ff).into()),
+                ("punctuation".into(), rgba(0xdfe7e2ff).into()),
+                ("string.special.symbol".into(), rgba(0x479962ff).into()),
+                ("emphasis.strong".into(), rgba(0x468b8fff).into()),
+                ("keyword".into(), rgba(0x55859bff).into()),
+                ("comment.doc".into(), rgba(0x87928aff).into()),
+                ("punctuation.bracket".into(), rgba(0x87928aff).into()),
+                ("constant".into(), rgba(0x479962ff).into()),
+                ("link_text".into(), rgba(0x9f703bff).into()),
+                ("number".into(), rgba(0x9f703bff).into()),
+                ("function".into(), rgba(0x468b8fff).into()),
+                ("variable".into(), rgba(0xdfe7e2ff).into()),
+                ("emphasis".into(), rgba(0x468b8fff).into()),
+                ("punctuation.special".into(), rgba(0x857368ff).into()),
+                ("constructor".into(), rgba(0x468b8fff).into()),
+                ("variable.special".into(), rgba(0x55859bff).into()),
+                ("operator".into(), rgba(0x87928aff).into()),
+                ("enum".into(), rgba(0x9f703bff).into()),
+                ("string.regex".into(), rgba(0x1b9aa0ff).into()),
+                ("attribute".into(), rgba(0x468b8fff).into()),
+                ("predictive".into(), rgba(0x506d66ff).into()),
+                ("string".into(), rgba(0x479962ff).into()),
+                ("embedded".into(), rgba(0xecf4eeff).into()),
+                ("variant".into(), rgba(0xa07d3aff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xa07d3aff).into(),
+                ),
+            ],
+        },
+        status_bar: rgba(0x353f39ff).into(),
+        title_bar: rgba(0x353f39ff).into(),
+        toolbar: rgba(0x171c19ff).into(),
+        tab_bar: rgba(0x1f2621ff).into(),
+        editor: rgba(0x171c19ff).into(),
+        editor_subheader: rgba(0x1f2621ff).into(),
+        editor_active_line: rgba(0x1f2621ff).into(),
+        terminal: rgba(0x171c19ff).into(),
+        image_fallback_background: rgba(0x353f39ff).into(),
+        git_created: rgba(0x479962ff).into(),
+        git_modified: rgba(0x468b8fff).into(),
+        git_deleted: rgba(0xb16038ff).into(),
+        git_conflict: rgba(0xa07d3aff).into(),
+        git_ignored: rgba(0x6f7e74ff).into(),
+        git_renamed: rgba(0xa07d3aff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x468b8fff).into(),
+                selection: rgba(0x468b8f3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x479962ff).into(),
+                selection: rgba(0x4799623d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x857368ff).into(),
+                selection: rgba(0x8573683d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9f703bff).into(),
+                selection: rgba(0x9f703b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x55859bff).into(),
+                selection: rgba(0x55859b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x1d9aa0ff).into(),
+                selection: rgba(0x1d9aa03d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb16038ff).into(),
+                selection: rgba(0xb160383d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa07d3aff).into(),
+                selection: rgba(0xa07d3a3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_savanna_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Savanna Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x8b968eff).into(),
+        border_variant: rgba(0x8b968eff).into(),
+        border_focused: rgba(0xbed4d6ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xbcc5bfff).into(),
+        surface: rgba(0xe3ebe6ff).into(),
+        background: rgba(0xbcc5bfff).into(),
+        filled_element: rgba(0xbcc5bfff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xdae7e8ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xdae7e8ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x171c19ff).into(),
+        text_muted: rgba(0x546259ff).into(),
+        text_placeholder: rgba(0xb16139ff).into(),
+        text_disabled: rgba(0x68766dff).into(),
+        text_accent: rgba(0x488b90ff).into(),
+        icon_muted: rgba(0x546259ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("text.literal".into(), rgba(0x9f713cff).into()),
+                ("string".into(), rgba(0x479962ff).into()),
+                ("punctuation.special".into(), rgba(0x857368ff).into()),
+                ("type".into(), rgba(0xa07d3aff).into()),
+                ("enum".into(), rgba(0x9f713cff).into()),
+                ("title".into(), rgba(0x171c19ff).into()),
+                ("comment".into(), rgba(0x77877cff).into()),
+                ("predictive".into(), rgba(0x75958bff).into()),
+                ("punctuation.list_marker".into(), rgba(0x232a25ff).into()),
+                ("string.special.symbol".into(), rgba(0x479962ff).into()),
+                ("constructor".into(), rgba(0x488b90ff).into()),
+                ("variable".into(), rgba(0x232a25ff).into()),
+                ("label".into(), rgba(0x488b90ff).into()),
+                ("attribute".into(), rgba(0x488b90ff).into()),
+                ("constant".into(), rgba(0x499963ff).into()),
+                ("function".into(), rgba(0x468b8fff).into()),
+                ("variable.special".into(), rgba(0x55859bff).into()),
+                ("keyword".into(), rgba(0x55859bff).into()),
+                ("number".into(), rgba(0x9f703bff).into()),
+                ("boolean".into(), rgba(0x499963ff).into()),
+                ("embedded".into(), rgba(0x171c19ff).into()),
+                ("string.special".into(), rgba(0x857368ff).into()),
+                ("emphasis.strong".into(), rgba(0x488b90ff).into()),
+                ("string.regex".into(), rgba(0x1b9aa0ff).into()),
+                ("hint".into(), rgba(0x66847cff).into()),
+                ("preproc".into(), rgba(0x171c19ff).into()),
+                ("link_uri".into(), rgba(0x499963ff).into()),
+                ("variant".into(), rgba(0xa07d3aff).into()),
+                ("function.method".into(), rgba(0x468b8fff).into()),
+                ("punctuation.bracket".into(), rgba(0x526057ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x526057ff).into()),
+                ("punctuation".into(), rgba(0x232a25ff).into()),
+                ("primary".into(), rgba(0x232a25ff).into()),
+                ("string.escape".into(), rgba(0x526057ff).into()),
+                ("property".into(), rgba(0xb16038ff).into()),
+                ("operator".into(), rgba(0x526057ff).into()),
+                ("comment.doc".into(), rgba(0x526057ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xa07d3aff).into(),
+                ),
+                ("link_text".into(), rgba(0x9f713cff).into()),
+                ("tag".into(), rgba(0x488b90ff).into()),
+                ("emphasis".into(), rgba(0x488b90ff).into()),
+            ],
+        },
+        status_bar: rgba(0xbcc5bfff).into(),
+        title_bar: rgba(0xbcc5bfff).into(),
+        toolbar: rgba(0xecf4eeff).into(),
+        tab_bar: rgba(0xe3ebe6ff).into(),
+        editor: rgba(0xecf4eeff).into(),
+        editor_subheader: rgba(0xe3ebe6ff).into(),
+        editor_active_line: rgba(0xe3ebe6ff).into(),
+        terminal: rgba(0xecf4eeff).into(),
+        image_fallback_background: rgba(0xbcc5bfff).into(),
+        git_created: rgba(0x499963ff).into(),
+        git_modified: rgba(0x488b90ff).into(),
+        git_deleted: rgba(0xb16139ff).into(),
+        git_conflict: rgba(0xa07d3bff).into(),
+        git_ignored: rgba(0x68766dff).into(),
+        git_renamed: rgba(0xa07d3bff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x488b90ff).into(),
+                selection: rgba(0x488b903d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x499963ff).into(),
+                selection: rgba(0x4999633d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x857368ff).into(),
+                selection: rgba(0x8573683d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9f713cff).into(),
+                selection: rgba(0x9f713c3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x55859bff).into(),
+                selection: rgba(0x55859b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x1e9aa0ff).into(),
+                selection: rgba(0x1e9aa03d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb16139ff).into(),
+                selection: rgba(0xb161393d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa07d3bff).into(),
+                selection: rgba(0xa07d3b3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_seaside_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Seaside Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x5c6c5cff).into(),
+        border_variant: rgba(0x5c6c5cff).into(),
+        border_focused: rgba(0x102667ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x3b453bff).into(),
+        surface: rgba(0x1f231fff).into(),
+        background: rgba(0x3b453bff).into(),
+        filled_element: rgba(0x3b453bff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x051949ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x051949ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xf3faf3ff).into(),
+        text_muted: rgba(0x8ba48bff).into(),
+        text_placeholder: rgba(0xe61c3bff).into(),
+        text_disabled: rgba(0x778f77ff).into(),
+        text_accent: rgba(0x3e62f4ff).into(),
+        icon_muted: rgba(0x8ba48bff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("comment".into(), rgba(0x687d68ff).into()),
+                ("predictive".into(), rgba(0x00788bff).into()),
+                ("string.special".into(), rgba(0xe618c3ff).into()),
+                ("string.regex".into(), rgba(0x1899b3ff).into()),
+                ("boolean".into(), rgba(0x2aa329ff).into()),
+                ("string".into(), rgba(0x28a328ff).into()),
+                ("operator".into(), rgba(0x8ca68cff).into()),
+                ("primary".into(), rgba(0xcfe8cfff).into()),
+                ("number".into(), rgba(0x87711cff).into()),
+                ("punctuation.special".into(), rgba(0xe618c3ff).into()),
+                ("link_text".into(), rgba(0x87711dff).into()),
+                ("title".into(), rgba(0xf3faf3ff).into()),
+                ("comment.doc".into(), rgba(0x8ca68cff).into()),
+                ("label".into(), rgba(0x3e62f4ff).into()),
+                ("preproc".into(), rgba(0xf3faf3ff).into()),
+                ("punctuation.bracket".into(), rgba(0x8ca68cff).into()),
+                ("punctuation.delimiter".into(), rgba(0x8ca68cff).into()),
+                ("function.method".into(), rgba(0x3d62f5ff).into()),
+                ("tag".into(), rgba(0x3e62f4ff).into()),
+                ("embedded".into(), rgba(0xf3faf3ff).into()),
+                ("text.literal".into(), rgba(0x87711dff).into()),
+                ("punctuation".into(), rgba(0xcfe8cfff).into()),
+                ("string.special.symbol".into(), rgba(0x28a328ff).into()),
+                ("link_uri".into(), rgba(0x2aa329ff).into()),
+                ("keyword".into(), rgba(0xac2aeeff).into()),
+                ("function".into(), rgba(0x3d62f5ff).into()),
+                ("string.escape".into(), rgba(0x8ca68cff).into()),
+                ("variant".into(), rgba(0x98981bff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0x98981bff).into(),
+                ),
+                ("constructor".into(), rgba(0x3e62f4ff).into()),
+                ("constant".into(), rgba(0x2aa329ff).into()),
+                ("hint".into(), rgba(0x008b9fff).into()),
+                ("type".into(), rgba(0x98981bff).into()),
+                ("emphasis".into(), rgba(0x3e62f4ff).into()),
+                ("variable".into(), rgba(0xcfe8cfff).into()),
+                ("emphasis.strong".into(), rgba(0x3e62f4ff).into()),
+                ("attribute".into(), rgba(0x3e62f4ff).into()),
+                ("enum".into(), rgba(0x87711dff).into()),
+                ("property".into(), rgba(0xe6183bff).into()),
+                ("punctuation.list_marker".into(), rgba(0xcfe8cfff).into()),
+                ("variable.special".into(), rgba(0xac2aeeff).into()),
+            ],
+        },
+        status_bar: rgba(0x3b453bff).into(),
+        title_bar: rgba(0x3b453bff).into(),
+        toolbar: rgba(0x131513ff).into(),
+        tab_bar: rgba(0x1f231fff).into(),
+        editor: rgba(0x131513ff).into(),
+        editor_subheader: rgba(0x1f231fff).into(),
+        editor_active_line: rgba(0x1f231fff).into(),
+        terminal: rgba(0x131513ff).into(),
+        image_fallback_background: rgba(0x3b453bff).into(),
+        git_created: rgba(0x2aa329ff).into(),
+        git_modified: rgba(0x3e62f4ff).into(),
+        git_deleted: rgba(0xe61c3bff).into(),
+        git_conflict: rgba(0x98981bff).into(),
+        git_ignored: rgba(0x778f77ff).into(),
+        git_renamed: rgba(0x98981bff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x3e62f4ff).into(),
+                selection: rgba(0x3e62f43d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x2aa329ff).into(),
+                selection: rgba(0x2aa3293d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xe61cc3ff).into(),
+                selection: rgba(0xe61cc33d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x87711dff).into(),
+                selection: rgba(0x87711d3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xac2dedff).into(),
+                selection: rgba(0xac2ded3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x1b99b3ff).into(),
+                selection: rgba(0x1b99b33d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xe61c3bff).into(),
+                selection: rgba(0xe61c3b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x98981bff).into(),
+                selection: rgba(0x98981b3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_seaside_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Seaside Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x8ea88eff).into(),
+        border_variant: rgba(0x8ea88eff).into(),
+        border_focused: rgba(0xc9c4fdff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xb4ceb4ff).into(),
+        surface: rgba(0xdaeedaff).into(),
+        background: rgba(0xb4ceb4ff).into(),
+        filled_element: rgba(0xb4ceb4ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xe1ddfeff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xe1ddfeff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x131513ff).into(),
+        text_muted: rgba(0x5f705fff).into(),
+        text_placeholder: rgba(0xe61c3dff).into(),
+        text_disabled: rgba(0x718771ff).into(),
+        text_accent: rgba(0x3e61f4ff).into(),
+        icon_muted: rgba(0x5f705fff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("string.escape".into(), rgba(0x5e6e5eff).into()),
+                ("boolean".into(), rgba(0x2aa32aff).into()),
+                ("string.special".into(), rgba(0xe618c3ff).into()),
+                ("comment".into(), rgba(0x809980ff).into()),
+                ("number".into(), rgba(0x87711cff).into()),
+                ("comment.doc".into(), rgba(0x5e6e5eff).into()),
+                ("tag".into(), rgba(0x3e61f4ff).into()),
+                ("string.special.symbol".into(), rgba(0x28a328ff).into()),
+                ("primary".into(), rgba(0x242924ff).into()),
+                ("string".into(), rgba(0x28a328ff).into()),
+                ("enum".into(), rgba(0x87711fff).into()),
+                ("operator".into(), rgba(0x5e6e5eff).into()),
+                ("string.regex".into(), rgba(0x1899b3ff).into()),
+                ("keyword".into(), rgba(0xac2aeeff).into()),
+                ("emphasis".into(), rgba(0x3e61f4ff).into()),
+                ("link_uri".into(), rgba(0x2aa32aff).into()),
+                ("constant".into(), rgba(0x2aa32aff).into()),
+                ("constructor".into(), rgba(0x3e61f4ff).into()),
+                ("link_text".into(), rgba(0x87711fff).into()),
+                ("emphasis.strong".into(), rgba(0x3e61f4ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x242924ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x5e6e5eff).into()),
+                ("punctuation.special".into(), rgba(0xe618c3ff).into()),
+                ("variant".into(), rgba(0x98981bff).into()),
+                ("predictive".into(), rgba(0x00a2b5ff).into()),
+                ("attribute".into(), rgba(0x3e61f4ff).into()),
+                ("preproc".into(), rgba(0x131513ff).into()),
+                ("embedded".into(), rgba(0x131513ff).into()),
+                ("punctuation".into(), rgba(0x242924ff).into()),
+                ("label".into(), rgba(0x3e61f4ff).into()),
+                ("function.method".into(), rgba(0x3d62f5ff).into()),
+                ("property".into(), rgba(0xe6183bff).into()),
+                ("title".into(), rgba(0x131513ff).into()),
+                ("variable".into(), rgba(0x242924ff).into()),
+                ("function".into(), rgba(0x3d62f5ff).into()),
+                ("variable.special".into(), rgba(0xac2aeeff).into()),
+                ("type".into(), rgba(0x98981bff).into()),
+                ("text.literal".into(), rgba(0x87711fff).into()),
+                ("hint".into(), rgba(0x008fa1ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0x98981bff).into(),
+                ),
+                ("punctuation.bracket".into(), rgba(0x5e6e5eff).into()),
+            ],
+        },
+        status_bar: rgba(0xb4ceb4ff).into(),
+        title_bar: rgba(0xb4ceb4ff).into(),
+        toolbar: rgba(0xf3faf3ff).into(),
+        tab_bar: rgba(0xdaeedaff).into(),
+        editor: rgba(0xf3faf3ff).into(),
+        editor_subheader: rgba(0xdaeedaff).into(),
+        editor_active_line: rgba(0xdaeedaff).into(),
+        terminal: rgba(0xf3faf3ff).into(),
+        image_fallback_background: rgba(0xb4ceb4ff).into(),
+        git_created: rgba(0x2aa32aff).into(),
+        git_modified: rgba(0x3e61f4ff).into(),
+        git_deleted: rgba(0xe61c3dff).into(),
+        git_conflict: rgba(0x98981cff).into(),
+        git_ignored: rgba(0x718771ff).into(),
+        git_renamed: rgba(0x98981cff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x3e61f4ff).into(),
+                selection: rgba(0x3e61f43d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x2aa32aff).into(),
+                selection: rgba(0x2aa32a3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xe61cc2ff).into(),
+                selection: rgba(0xe61cc23d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x87711fff).into(),
+                selection: rgba(0x87711f3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xac2dedff).into(),
+                selection: rgba(0xac2ded3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x1c99b3ff).into(),
+                selection: rgba(0x1c99b33d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xe61c3dff).into(),
+                selection: rgba(0xe61c3d3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x98981cff).into(),
+                selection: rgba(0x98981c3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_sulphurpool_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Sulphurpool Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x5b6385ff).into(),
+        border_variant: rgba(0x5b6385ff).into(),
+        border_focused: rgba(0x203348ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x3e4769ff).into(),
+        surface: rgba(0x262f51ff).into(),
+        background: rgba(0x3e4769ff).into(),
+        filled_element: rgba(0x3e4769ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x161f2bff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x161f2bff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xf5f7ffff).into(),
+        text_muted: rgba(0x959bb2ff).into(),
+        text_placeholder: rgba(0xc94922ff).into(),
+        text_disabled: rgba(0x7e849eff).into(),
+        text_accent: rgba(0x3e8ed0ff).into(),
+        icon_muted: rgba(0x959bb2ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("title".into(), rgba(0xf5f7ffff).into()),
+                ("constructor".into(), rgba(0x3e8ed0ff).into()),
+                ("type".into(), rgba(0xc08b2fff).into()),
+                ("punctuation.list_marker".into(), rgba(0xdfe2f1ff).into()),
+                ("property".into(), rgba(0xc94821ff).into()),
+                ("link_uri".into(), rgba(0xac9739ff).into()),
+                ("string.escape".into(), rgba(0x979db4ff).into()),
+                ("constant".into(), rgba(0xac9739ff).into()),
+                ("embedded".into(), rgba(0xf5f7ffff).into()),
+                ("punctuation.special".into(), rgba(0x9b6279ff).into()),
+                ("punctuation.bracket".into(), rgba(0x979db4ff).into()),
+                ("preproc".into(), rgba(0xf5f7ffff).into()),
+                ("emphasis.strong".into(), rgba(0x3e8ed0ff).into()),
+                ("emphasis".into(), rgba(0x3e8ed0ff).into()),
+                ("enum".into(), rgba(0xc76a29ff).into()),
+                ("boolean".into(), rgba(0xac9739ff).into()),
+                ("primary".into(), rgba(0xdfe2f1ff).into()),
+                ("function.method".into(), rgba(0x3d8fd1ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xc08b2fff).into(),
+                ),
+                ("comment.doc".into(), rgba(0x979db4ff).into()),
+                ("string".into(), rgba(0xac9738ff).into()),
+                ("text.literal".into(), rgba(0xc76a29ff).into()),
+                ("operator".into(), rgba(0x979db4ff).into()),
+                ("number".into(), rgba(0xc76a28ff).into()),
+                ("string.special".into(), rgba(0x9b6279ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x979db4ff).into()),
+                ("tag".into(), rgba(0x3e8ed0ff).into()),
+                ("string.special.symbol".into(), rgba(0xac9738ff).into()),
+                ("variable".into(), rgba(0xdfe2f1ff).into()),
+                ("attribute".into(), rgba(0x3e8ed0ff).into()),
+                ("punctuation".into(), rgba(0xdfe2f1ff).into()),
+                ("string.regex".into(), rgba(0x21a2c9ff).into()),
+                ("keyword".into(), rgba(0x6679ccff).into()),
+                ("label".into(), rgba(0x3e8ed0ff).into()),
+                ("hint".into(), rgba(0x6c81a5ff).into()),
+                ("function".into(), rgba(0x3d8fd1ff).into()),
+                ("link_text".into(), rgba(0xc76a29ff).into()),
+                ("variant".into(), rgba(0xc08b2fff).into()),
+                ("variable.special".into(), rgba(0x6679ccff).into()),
+                ("predictive".into(), rgba(0x58709aff).into()),
+                ("comment".into(), rgba(0x6a7293ff).into()),
+            ],
+        },
+        status_bar: rgba(0x3e4769ff).into(),
+        title_bar: rgba(0x3e4769ff).into(),
+        toolbar: rgba(0x202646ff).into(),
+        tab_bar: rgba(0x262f51ff).into(),
+        editor: rgba(0x202646ff).into(),
+        editor_subheader: rgba(0x262f51ff).into(),
+        editor_active_line: rgba(0x262f51ff).into(),
+        terminal: rgba(0x202646ff).into(),
+        image_fallback_background: rgba(0x3e4769ff).into(),
+        git_created: rgba(0xac9739ff).into(),
+        git_modified: rgba(0x3e8ed0ff).into(),
+        git_deleted: rgba(0xc94922ff).into(),
+        git_conflict: rgba(0xc08b30ff).into(),
+        git_ignored: rgba(0x7e849eff).into(),
+        git_renamed: rgba(0xc08b30ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x3e8ed0ff).into(),
+                selection: rgba(0x3e8ed03d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xac9739ff).into(),
+                selection: rgba(0xac97393d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9b6279ff).into(),
+                selection: rgba(0x9b62793d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc76a29ff).into(),
+                selection: rgba(0xc76a293d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x6679ccff).into(),
+                selection: rgba(0x6679cc3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x24a1c9ff).into(),
+                selection: rgba(0x24a1c93d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc94922ff).into(),
+                selection: rgba(0xc949223d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc08b30ff).into(),
+                selection: rgba(0xc08b303d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,136 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn atelier_sulphurpool_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Atelier Sulphurpool Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x9a9fb6ff).into(),
+        border_variant: rgba(0x9a9fb6ff).into(),
+        border_focused: rgba(0xc2d5efff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xc1c5d8ff).into(),
+        surface: rgba(0xe5e8f5ff).into(),
+        background: rgba(0xc1c5d8ff).into(),
+        filled_element: rgba(0xc1c5d8ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xdde7f6ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xdde7f6ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x202646ff).into(),
+        text_muted: rgba(0x5f6789ff).into(),
+        text_placeholder: rgba(0xc94922ff).into(),
+        text_disabled: rgba(0x767d9aff).into(),
+        text_accent: rgba(0x3e8fd0ff).into(),
+        icon_muted: rgba(0x5f6789ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("string.special".into(), rgba(0x9b6279ff).into()),
+                ("string.regex".into(), rgba(0x21a2c9ff).into()),
+                ("embedded".into(), rgba(0x202646ff).into()),
+                ("string".into(), rgba(0xac9738ff).into()),
+                (
+                    "function.special.definition".into(),
+                    rgba(0xc08b2fff).into(),
+                ),
+                ("hint".into(), rgba(0x7087b2ff).into()),
+                ("function.method".into(), rgba(0x3d8fd1ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x293256ff).into()),
+                ("punctuation".into(), rgba(0x293256ff).into()),
+                ("constant".into(), rgba(0xac9739ff).into()),
+                ("label".into(), rgba(0x3e8fd0ff).into()),
+                ("comment.doc".into(), rgba(0x5d6587ff).into()),
+                ("property".into(), rgba(0xc94821ff).into()),
+                ("punctuation.bracket".into(), rgba(0x5d6587ff).into()),
+                ("constructor".into(), rgba(0x3e8fd0ff).into()),
+                ("variable.special".into(), rgba(0x6679ccff).into()),
+                ("emphasis".into(), rgba(0x3e8fd0ff).into()),
+                ("link_text".into(), rgba(0xc76a29ff).into()),
+                ("keyword".into(), rgba(0x6679ccff).into()),
+                ("primary".into(), rgba(0x293256ff).into()),
+                ("comment".into(), rgba(0x898ea4ff).into()),
+                ("title".into(), rgba(0x202646ff).into()),
+                ("link_uri".into(), rgba(0xac9739ff).into()),
+                ("text.literal".into(), rgba(0xc76a29ff).into()),
+                ("operator".into(), rgba(0x5d6587ff).into()),
+                ("number".into(), rgba(0xc76a28ff).into()),
+                ("preproc".into(), rgba(0x202646ff).into()),
+                ("attribute".into(), rgba(0x3e8fd0ff).into()),
+                ("emphasis.strong".into(), rgba(0x3e8fd0ff).into()),
+                ("string.escape".into(), rgba(0x5d6587ff).into()),
+                ("tag".into(), rgba(0x3e8fd0ff).into()),
+                ("variable".into(), rgba(0x293256ff).into()),
+                ("predictive".into(), rgba(0x8599beff).into()),
+                ("enum".into(), rgba(0xc76a29ff).into()),
+                ("string.special.symbol".into(), rgba(0xac9738ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x5d6587ff).into()),
+                ("function".into(), rgba(0x3d8fd1ff).into()),
+                ("type".into(), rgba(0xc08b2fff).into()),
+                ("punctuation.special".into(), rgba(0x9b6279ff).into()),
+                ("variant".into(), rgba(0xc08b2fff).into()),
+                ("boolean".into(), rgba(0xac9739ff).into()),
+            ],
+        },
+        status_bar: rgba(0xc1c5d8ff).into(),
+        title_bar: rgba(0xc1c5d8ff).into(),
+        toolbar: rgba(0xf5f7ffff).into(),
+        tab_bar: rgba(0xe5e8f5ff).into(),
+        editor: rgba(0xf5f7ffff).into(),
+        editor_subheader: rgba(0xe5e8f5ff).into(),
+        editor_active_line: rgba(0xe5e8f5ff).into(),
+        terminal: rgba(0xf5f7ffff).into(),
+        image_fallback_background: rgba(0xc1c5d8ff).into(),
+        git_created: rgba(0xac9739ff).into(),
+        git_modified: rgba(0x3e8fd0ff).into(),
+        git_deleted: rgba(0xc94922ff).into(),
+        git_conflict: rgba(0xc08b30ff).into(),
+        git_ignored: rgba(0x767d9aff).into(),
+        git_renamed: rgba(0xc08b30ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x3e8fd0ff).into(),
+                selection: rgba(0x3e8fd03d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xac9739ff).into(),
+                selection: rgba(0xac97393d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9b6279ff).into(),
+                selection: rgba(0x9b62793d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc76a29ff).into(),
+                selection: rgba(0xc76a293d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x6679cbff).into(),
+                selection: rgba(0x6679cb3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x24a1c9ff).into(),
+                selection: rgba(0x24a1c93d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc94922ff).into(),
+                selection: rgba(0xc949223d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc08b30ff).into(),
+                selection: rgba(0xc08b303d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,130 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn ayu_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Ayu Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x3f4043ff).into(),
+        border_variant: rgba(0x3f4043ff).into(),
+        border_focused: rgba(0x1b4a6eff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x313337ff).into(),
+        surface: rgba(0x1f2127ff).into(),
+        background: rgba(0x313337ff).into(),
+        filled_element: rgba(0x313337ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x0d2f4eff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x0d2f4eff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xbfbdb6ff).into(),
+        text_muted: rgba(0x8a8986ff).into(),
+        text_placeholder: rgba(0xef7177ff).into(),
+        text_disabled: rgba(0x696a6aff).into(),
+        text_accent: rgba(0x5ac1feff).into(),
+        icon_muted: rgba(0x8a8986ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("emphasis".into(), rgba(0x5ac1feff).into()),
+                ("punctuation.bracket".into(), rgba(0xa6a5a0ff).into()),
+                ("constructor".into(), rgba(0x5ac1feff).into()),
+                ("predictive".into(), rgba(0x5a728bff).into()),
+                ("emphasis.strong".into(), rgba(0x5ac1feff).into()),
+                ("string.regex".into(), rgba(0x95e6cbff).into()),
+                ("tag".into(), rgba(0x5ac1feff).into()),
+                ("punctuation".into(), rgba(0xa6a5a0ff).into()),
+                ("number".into(), rgba(0xd2a6ffff).into()),
+                ("punctuation.special".into(), rgba(0xd2a6ffff).into()),
+                ("primary".into(), rgba(0xbfbdb6ff).into()),
+                ("boolean".into(), rgba(0xd2a6ffff).into()),
+                ("variant".into(), rgba(0x5ac1feff).into()),
+                ("link_uri".into(), rgba(0xaad84cff).into()),
+                ("comment.doc".into(), rgba(0x8c8b88ff).into()),
+                ("title".into(), rgba(0xbfbdb6ff).into()),
+                ("text.literal".into(), rgba(0xfe8f40ff).into()),
+                ("link_text".into(), rgba(0xfe8f40ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xa6a5a0ff).into()),
+                ("string.escape".into(), rgba(0x8c8b88ff).into()),
+                ("hint".into(), rgba(0x628b80ff).into()),
+                ("type".into(), rgba(0x59c2ffff).into()),
+                ("variable".into(), rgba(0xbfbdb6ff).into()),
+                ("label".into(), rgba(0x5ac1feff).into()),
+                ("enum".into(), rgba(0xfe8f40ff).into()),
+                ("operator".into(), rgba(0xf29668ff).into()),
+                ("function".into(), rgba(0xffb353ff).into()),
+                ("preproc".into(), rgba(0xbfbdb6ff).into()),
+                ("embedded".into(), rgba(0xbfbdb6ff).into()),
+                ("string".into(), rgba(0xa9d94bff).into()),
+                ("attribute".into(), rgba(0x5ac1feff).into()),
+                ("keyword".into(), rgba(0xff8f3fff).into()),
+                ("string.special.symbol".into(), rgba(0xfe8f40ff).into()),
+                ("comment".into(), rgba(0xabb5be8c).into()),
+                ("property".into(), rgba(0x5ac1feff).into()),
+                ("punctuation.list_marker".into(), rgba(0xa6a5a0ff).into()),
+                ("constant".into(), rgba(0xd2a6ffff).into()),
+                ("string.special".into(), rgba(0xe5b572ff).into()),
+            ],
+        },
+        status_bar: rgba(0x313337ff).into(),
+        title_bar: rgba(0x313337ff).into(),
+        toolbar: rgba(0x0d1016ff).into(),
+        tab_bar: rgba(0x1f2127ff).into(),
+        editor: rgba(0x0d1016ff).into(),
+        editor_subheader: rgba(0x1f2127ff).into(),
+        editor_active_line: rgba(0x1f2127ff).into(),
+        terminal: rgba(0x0d1016ff).into(),
+        image_fallback_background: rgba(0x313337ff).into(),
+        git_created: rgba(0xaad84cff).into(),
+        git_modified: rgba(0x5ac1feff).into(),
+        git_deleted: rgba(0xef7177ff).into(),
+        git_conflict: rgba(0xfeb454ff).into(),
+        git_ignored: rgba(0x696a6aff).into(),
+        git_renamed: rgba(0xfeb454ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x5ac1feff).into(),
+                selection: rgba(0x5ac1fe3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xaad84cff).into(),
+                selection: rgba(0xaad84c3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x39bae5ff).into(),
+                selection: rgba(0x39bae53d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfe8f40ff).into(),
+                selection: rgba(0xfe8f403d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd2a6feff).into(),
+                selection: rgba(0xd2a6fe3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x95e5cbff).into(),
+                selection: rgba(0x95e5cb3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xef7177ff).into(),
+                selection: rgba(0xef71773d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfeb454ff).into(),
+                selection: rgba(0xfeb4543d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,130 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn ayu_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Ayu Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0xcfd1d2ff).into(),
+        border_variant: rgba(0xcfd1d2ff).into(),
+        border_focused: rgba(0xc4daf6ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xdcdddeff).into(),
+        surface: rgba(0xececedff).into(),
+        background: rgba(0xdcdddeff).into(),
+        filled_element: rgba(0xdcdddeff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xdeebfaff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xdeebfaff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x5c6166ff).into(),
+        text_muted: rgba(0x8b8e92ff).into(),
+        text_placeholder: rgba(0xef7271ff).into(),
+        text_disabled: rgba(0xa9acaeff).into(),
+        text_accent: rgba(0x3b9ee5ff).into(),
+        icon_muted: rgba(0x8b8e92ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("string".into(), rgba(0x86b300ff).into()),
+                ("enum".into(), rgba(0xf98d3fff).into()),
+                ("comment".into(), rgba(0x787b8099).into()),
+                ("comment.doc".into(), rgba(0x898d90ff).into()),
+                ("emphasis".into(), rgba(0x3b9ee5ff).into()),
+                ("keyword".into(), rgba(0xfa8d3eff).into()),
+                ("string.regex".into(), rgba(0x4bbf98ff).into()),
+                ("text.literal".into(), rgba(0xf98d3fff).into()),
+                ("string.escape".into(), rgba(0x898d90ff).into()),
+                ("link_text".into(), rgba(0xf98d3fff).into()),
+                ("punctuation".into(), rgba(0x73777bff).into()),
+                ("constructor".into(), rgba(0x3b9ee5ff).into()),
+                ("constant".into(), rgba(0xa37accff).into()),
+                ("variable".into(), rgba(0x5c6166ff).into()),
+                ("primary".into(), rgba(0x5c6166ff).into()),
+                ("emphasis.strong".into(), rgba(0x3b9ee5ff).into()),
+                ("string.special".into(), rgba(0xe6ba7eff).into()),
+                ("number".into(), rgba(0xa37accff).into()),
+                ("preproc".into(), rgba(0x5c6166ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x73777bff).into()),
+                ("string.special.symbol".into(), rgba(0xf98d3fff).into()),
+                ("boolean".into(), rgba(0xa37accff).into()),
+                ("property".into(), rgba(0x3b9ee5ff).into()),
+                ("title".into(), rgba(0x5c6166ff).into()),
+                ("hint".into(), rgba(0x8ca7c2ff).into()),
+                ("predictive".into(), rgba(0x9eb9d3ff).into()),
+                ("operator".into(), rgba(0xed9365ff).into()),
+                ("type".into(), rgba(0x389ee6ff).into()),
+                ("function".into(), rgba(0xf2ad48ff).into()),
+                ("variant".into(), rgba(0x3b9ee5ff).into()),
+                ("label".into(), rgba(0x3b9ee5ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x73777bff).into()),
+                ("punctuation.bracket".into(), rgba(0x73777bff).into()),
+                ("embedded".into(), rgba(0x5c6166ff).into()),
+                ("punctuation.special".into(), rgba(0xa37accff).into()),
+                ("attribute".into(), rgba(0x3b9ee5ff).into()),
+                ("tag".into(), rgba(0x3b9ee5ff).into()),
+                ("link_uri".into(), rgba(0x85b304ff).into()),
+            ],
+        },
+        status_bar: rgba(0xdcdddeff).into(),
+        title_bar: rgba(0xdcdddeff).into(),
+        toolbar: rgba(0xfcfcfcff).into(),
+        tab_bar: rgba(0xececedff).into(),
+        editor: rgba(0xfcfcfcff).into(),
+        editor_subheader: rgba(0xececedff).into(),
+        editor_active_line: rgba(0xececedff).into(),
+        terminal: rgba(0xfcfcfcff).into(),
+        image_fallback_background: rgba(0xdcdddeff).into(),
+        git_created: rgba(0x85b304ff).into(),
+        git_modified: rgba(0x3b9ee5ff).into(),
+        git_deleted: rgba(0xef7271ff).into(),
+        git_conflict: rgba(0xf1ad49ff).into(),
+        git_ignored: rgba(0xa9acaeff).into(),
+        git_renamed: rgba(0xf1ad49ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x3b9ee5ff).into(),
+                selection: rgba(0x3b9ee53d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x85b304ff).into(),
+                selection: rgba(0x85b3043d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x55b4d3ff).into(),
+                selection: rgba(0x55b4d33d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf98d3fff).into(),
+                selection: rgba(0xf98d3f3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa37accff).into(),
+                selection: rgba(0xa37acc3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x4dbf99ff).into(),
+                selection: rgba(0x4dbf993d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xef7271ff).into(),
+                selection: rgba(0xef72713d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf1ad49ff).into(),
+                selection: rgba(0xf1ad493d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,130 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn ayu_mirage() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Ayu Mirage".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x53565dff).into(),
+        border_variant: rgba(0x53565dff).into(),
+        border_focused: rgba(0x24556fff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x464a52ff).into(),
+        surface: rgba(0x353944ff).into(),
+        background: rgba(0x464a52ff).into(),
+        filled_element: rgba(0x464a52ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x123950ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x123950ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xcccac2ff).into(),
+        text_muted: rgba(0x9a9a98ff).into(),
+        text_placeholder: rgba(0xf18779ff).into(),
+        text_disabled: rgba(0x7b7d7fff).into(),
+        text_accent: rgba(0x72cffeff).into(),
+        icon_muted: rgba(0x9a9a98ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("text.literal".into(), rgba(0xfead66ff).into()),
+                ("link_text".into(), rgba(0xfead66ff).into()),
+                ("function".into(), rgba(0xffd173ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xb4b3aeff).into()),
+                ("property".into(), rgba(0x72cffeff).into()),
+                ("title".into(), rgba(0xcccac2ff).into()),
+                ("boolean".into(), rgba(0xdfbfffff).into()),
+                ("link_uri".into(), rgba(0xd5fe80ff).into()),
+                ("label".into(), rgba(0x72cffeff).into()),
+                ("primary".into(), rgba(0xcccac2ff).into()),
+                ("number".into(), rgba(0xdfbfffff).into()),
+                ("variant".into(), rgba(0x72cffeff).into()),
+                ("enum".into(), rgba(0xfead66ff).into()),
+                ("string.special.symbol".into(), rgba(0xfead66ff).into()),
+                ("operator".into(), rgba(0xf29e74ff).into()),
+                ("punctuation.special".into(), rgba(0xdfbfffff).into()),
+                ("constructor".into(), rgba(0x72cffeff).into()),
+                ("type".into(), rgba(0x73cfffff).into()),
+                ("emphasis.strong".into(), rgba(0x72cffeff).into()),
+                ("embedded".into(), rgba(0xcccac2ff).into()),
+                ("comment".into(), rgba(0xb8cfe680).into()),
+                ("tag".into(), rgba(0x72cffeff).into()),
+                ("keyword".into(), rgba(0xffad65ff).into()),
+                ("punctuation".into(), rgba(0xb4b3aeff).into()),
+                ("preproc".into(), rgba(0xcccac2ff).into()),
+                ("hint".into(), rgba(0x7399a3ff).into()),
+                ("string.special".into(), rgba(0xffdfb3ff).into()),
+                ("attribute".into(), rgba(0x72cffeff).into()),
+                ("string.regex".into(), rgba(0x95e6cbff).into()),
+                ("predictive".into(), rgba(0x6d839bff).into()),
+                ("comment.doc".into(), rgba(0x9b9b99ff).into()),
+                ("emphasis".into(), rgba(0x72cffeff).into()),
+                ("string".into(), rgba(0xd4fe7fff).into()),
+                ("constant".into(), rgba(0xdfbfffff).into()),
+                ("string.escape".into(), rgba(0x9b9b99ff).into()),
+                ("variable".into(), rgba(0xcccac2ff).into()),
+                ("punctuation.bracket".into(), rgba(0xb4b3aeff).into()),
+                ("punctuation.list_marker".into(), rgba(0xb4b3aeff).into()),
+            ],
+        },
+        status_bar: rgba(0x464a52ff).into(),
+        title_bar: rgba(0x464a52ff).into(),
+        toolbar: rgba(0x242835ff).into(),
+        tab_bar: rgba(0x353944ff).into(),
+        editor: rgba(0x242835ff).into(),
+        editor_subheader: rgba(0x353944ff).into(),
+        editor_active_line: rgba(0x353944ff).into(),
+        terminal: rgba(0x242835ff).into(),
+        image_fallback_background: rgba(0x464a52ff).into(),
+        git_created: rgba(0xd5fe80ff).into(),
+        git_modified: rgba(0x72cffeff).into(),
+        git_deleted: rgba(0xf18779ff).into(),
+        git_conflict: rgba(0xfecf72ff).into(),
+        git_ignored: rgba(0x7b7d7fff).into(),
+        git_renamed: rgba(0xfecf72ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x72cffeff).into(),
+                selection: rgba(0x72cffe3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd5fe80ff).into(),
+                selection: rgba(0xd5fe803d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5bcde5ff).into(),
+                selection: rgba(0x5bcde53d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfead66ff).into(),
+                selection: rgba(0xfead663d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xdebffeff).into(),
+                selection: rgba(0xdebffe3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x95e5cbff).into(),
+                selection: rgba(0x95e5cb3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf18779ff).into(),
+                selection: rgba(0xf187793d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfecf72ff).into(),
+                selection: rgba(0xfecf723d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,131 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn gruvbox_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Gruvbox Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x5b534dff).into(),
+        border_variant: rgba(0x5b534dff).into(),
+        border_focused: rgba(0x303a36ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x4c4642ff).into(),
+        surface: rgba(0x3a3735ff).into(),
+        background: rgba(0x4c4642ff).into(),
+        filled_element: rgba(0x4c4642ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x1e2321ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x1e2321ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xfbf1c7ff).into(),
+        text_muted: rgba(0xc5b597ff).into(),
+        text_placeholder: rgba(0xfb4a35ff).into(),
+        text_disabled: rgba(0x998b78ff).into(),
+        text_accent: rgba(0x83a598ff).into(),
+        icon_muted: rgba(0xc5b597ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("operator".into(), rgba(0x8ec07cff).into()),
+                ("string.special.symbol".into(), rgba(0x8ec07cff).into()),
+                ("emphasis.strong".into(), rgba(0x83a598ff).into()),
+                ("attribute".into(), rgba(0x83a598ff).into()),
+                ("property".into(), rgba(0xebdbb2ff).into()),
+                ("comment.doc".into(), rgba(0xc6b697ff).into()),
+                ("emphasis".into(), rgba(0x83a598ff).into()),
+                ("variant".into(), rgba(0x83a598ff).into()),
+                ("text.literal".into(), rgba(0x83a598ff).into()),
+                ("keyword".into(), rgba(0xfb4833ff).into()),
+                ("primary".into(), rgba(0xebdbb2ff).into()),
+                ("variable".into(), rgba(0x83a598ff).into()),
+                ("enum".into(), rgba(0xfe7f18ff).into()),
+                ("constructor".into(), rgba(0x83a598ff).into()),
+                ("punctuation".into(), rgba(0xd5c4a1ff).into()),
+                ("link_uri".into(), rgba(0xd3869bff).into()),
+                ("hint".into(), rgba(0x8c957dff).into()),
+                ("string.regex".into(), rgba(0xfe7f18ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xe5d5adff).into()),
+                ("string".into(), rgba(0xb8bb25ff).into()),
+                ("punctuation.special".into(), rgba(0xe5d5adff).into()),
+                ("link_text".into(), rgba(0x8ec07cff).into()),
+                ("tag".into(), rgba(0x8ec07cff).into()),
+                ("string.escape".into(), rgba(0xc6b697ff).into()),
+                ("label".into(), rgba(0x83a598ff).into()),
+                ("constant".into(), rgba(0xfabd2eff).into()),
+                ("type".into(), rgba(0xfabd2eff).into()),
+                ("number".into(), rgba(0xd3869bff).into()),
+                ("string.special".into(), rgba(0xd3869bff).into()),
+                ("function.builtin".into(), rgba(0xfb4833ff).into()),
+                ("boolean".into(), rgba(0xd3869bff).into()),
+                ("embedded".into(), rgba(0x8ec07cff).into()),
+                ("title".into(), rgba(0xb8bb25ff).into()),
+                ("function".into(), rgba(0xb8bb25ff).into()),
+                ("punctuation.bracket".into(), rgba(0xa89984ff).into()),
+                ("comment".into(), rgba(0xa89984ff).into()),
+                ("preproc".into(), rgba(0xfbf1c7ff).into()),
+                ("predictive".into(), rgba(0x717363ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xebdbb2ff).into()),
+            ],
+        },
+        status_bar: rgba(0x4c4642ff).into(),
+        title_bar: rgba(0x4c4642ff).into(),
+        toolbar: rgba(0x282828ff).into(),
+        tab_bar: rgba(0x3a3735ff).into(),
+        editor: rgba(0x282828ff).into(),
+        editor_subheader: rgba(0x3a3735ff).into(),
+        editor_active_line: rgba(0x3a3735ff).into(),
+        terminal: rgba(0x282828ff).into(),
+        image_fallback_background: rgba(0x4c4642ff).into(),
+        git_created: rgba(0xb7bb26ff).into(),
+        git_modified: rgba(0x83a598ff).into(),
+        git_deleted: rgba(0xfb4a35ff).into(),
+        git_conflict: rgba(0xf9bd2fff).into(),
+        git_ignored: rgba(0x998b78ff).into(),
+        git_renamed: rgba(0xf9bd2fff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x83a598ff).into(),
+                selection: rgba(0x83a5983d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb7bb26ff).into(),
+                selection: rgba(0xb7bb263d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa89984ff).into(),
+                selection: rgba(0xa899843d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfd801bff).into(),
+                selection: rgba(0xfd801b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd3869bff).into(),
+                selection: rgba(0xd3869b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8ec07cff).into(),
+                selection: rgba(0x8ec07c3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfb4a35ff).into(),
+                selection: rgba(0xfb4a353d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf9bd2fff).into(),
+                selection: rgba(0xf9bd2f3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,131 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn gruvbox_dark_hard() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Gruvbox Dark Hard".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x5b534dff).into(),
+        border_variant: rgba(0x5b534dff).into(),
+        border_focused: rgba(0x303a36ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x4c4642ff).into(),
+        surface: rgba(0x393634ff).into(),
+        background: rgba(0x4c4642ff).into(),
+        filled_element: rgba(0x4c4642ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x1e2321ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x1e2321ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xfbf1c7ff).into(),
+        text_muted: rgba(0xc5b597ff).into(),
+        text_placeholder: rgba(0xfb4a35ff).into(),
+        text_disabled: rgba(0x998b78ff).into(),
+        text_accent: rgba(0x83a598ff).into(),
+        icon_muted: rgba(0xc5b597ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("primary".into(), rgba(0xebdbb2ff).into()),
+                ("label".into(), rgba(0x83a598ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xe5d5adff).into()),
+                ("variant".into(), rgba(0x83a598ff).into()),
+                ("type".into(), rgba(0xfabd2eff).into()),
+                ("string.regex".into(), rgba(0xfe7f18ff).into()),
+                ("function.builtin".into(), rgba(0xfb4833ff).into()),
+                ("title".into(), rgba(0xb8bb25ff).into()),
+                ("string".into(), rgba(0xb8bb25ff).into()),
+                ("operator".into(), rgba(0x8ec07cff).into()),
+                ("embedded".into(), rgba(0x8ec07cff).into()),
+                ("punctuation.bracket".into(), rgba(0xa89984ff).into()),
+                ("string.special".into(), rgba(0xd3869bff).into()),
+                ("attribute".into(), rgba(0x83a598ff).into()),
+                ("comment".into(), rgba(0xa89984ff).into()),
+                ("link_text".into(), rgba(0x8ec07cff).into()),
+                ("punctuation.special".into(), rgba(0xe5d5adff).into()),
+                ("punctuation.list_marker".into(), rgba(0xebdbb2ff).into()),
+                ("comment.doc".into(), rgba(0xc6b697ff).into()),
+                ("preproc".into(), rgba(0xfbf1c7ff).into()),
+                ("text.literal".into(), rgba(0x83a598ff).into()),
+                ("function".into(), rgba(0xb8bb25ff).into()),
+                ("predictive".into(), rgba(0x717363ff).into()),
+                ("emphasis.strong".into(), rgba(0x83a598ff).into()),
+                ("punctuation".into(), rgba(0xd5c4a1ff).into()),
+                ("string.special.symbol".into(), rgba(0x8ec07cff).into()),
+                ("property".into(), rgba(0xebdbb2ff).into()),
+                ("keyword".into(), rgba(0xfb4833ff).into()),
+                ("constructor".into(), rgba(0x83a598ff).into()),
+                ("tag".into(), rgba(0x8ec07cff).into()),
+                ("variable".into(), rgba(0x83a598ff).into()),
+                ("enum".into(), rgba(0xfe7f18ff).into()),
+                ("hint".into(), rgba(0x8c957dff).into()),
+                ("number".into(), rgba(0xd3869bff).into()),
+                ("constant".into(), rgba(0xfabd2eff).into()),
+                ("boolean".into(), rgba(0xd3869bff).into()),
+                ("link_uri".into(), rgba(0xd3869bff).into()),
+                ("string.escape".into(), rgba(0xc6b697ff).into()),
+                ("emphasis".into(), rgba(0x83a598ff).into()),
+            ],
+        },
+        status_bar: rgba(0x4c4642ff).into(),
+        title_bar: rgba(0x4c4642ff).into(),
+        toolbar: rgba(0x1d2021ff).into(),
+        tab_bar: rgba(0x393634ff).into(),
+        editor: rgba(0x1d2021ff).into(),
+        editor_subheader: rgba(0x393634ff).into(),
+        editor_active_line: rgba(0x393634ff).into(),
+        terminal: rgba(0x1d2021ff).into(),
+        image_fallback_background: rgba(0x4c4642ff).into(),
+        git_created: rgba(0xb7bb26ff).into(),
+        git_modified: rgba(0x83a598ff).into(),
+        git_deleted: rgba(0xfb4a35ff).into(),
+        git_conflict: rgba(0xf9bd2fff).into(),
+        git_ignored: rgba(0x998b78ff).into(),
+        git_renamed: rgba(0xf9bd2fff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x83a598ff).into(),
+                selection: rgba(0x83a5983d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb7bb26ff).into(),
+                selection: rgba(0xb7bb263d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa89984ff).into(),
+                selection: rgba(0xa899843d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfd801bff).into(),
+                selection: rgba(0xfd801b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd3869bff).into(),
+                selection: rgba(0xd3869b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8ec07cff).into(),
+                selection: rgba(0x8ec07c3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfb4a35ff).into(),
+                selection: rgba(0xfb4a353d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf9bd2fff).into(),
+                selection: rgba(0xf9bd2f3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,131 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn gruvbox_dark_soft() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Gruvbox Dark Soft".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x5b534dff).into(),
+        border_variant: rgba(0x5b534dff).into(),
+        border_focused: rgba(0x303a36ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x4c4642ff).into(),
+        surface: rgba(0x3b3735ff).into(),
+        background: rgba(0x4c4642ff).into(),
+        filled_element: rgba(0x4c4642ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x1e2321ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x1e2321ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xfbf1c7ff).into(),
+        text_muted: rgba(0xc5b597ff).into(),
+        text_placeholder: rgba(0xfb4a35ff).into(),
+        text_disabled: rgba(0x998b78ff).into(),
+        text_accent: rgba(0x83a598ff).into(),
+        icon_muted: rgba(0xc5b597ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("punctuation.special".into(), rgba(0xe5d5adff).into()),
+                ("attribute".into(), rgba(0x83a598ff).into()),
+                ("preproc".into(), rgba(0xfbf1c7ff).into()),
+                ("keyword".into(), rgba(0xfb4833ff).into()),
+                ("emphasis".into(), rgba(0x83a598ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xe5d5adff).into()),
+                ("punctuation.bracket".into(), rgba(0xa89984ff).into()),
+                ("comment".into(), rgba(0xa89984ff).into()),
+                ("text.literal".into(), rgba(0x83a598ff).into()),
+                ("predictive".into(), rgba(0x717363ff).into()),
+                ("link_text".into(), rgba(0x8ec07cff).into()),
+                ("variant".into(), rgba(0x83a598ff).into()),
+                ("label".into(), rgba(0x83a598ff).into()),
+                ("function".into(), rgba(0xb8bb25ff).into()),
+                ("string.regex".into(), rgba(0xfe7f18ff).into()),
+                ("boolean".into(), rgba(0xd3869bff).into()),
+                ("number".into(), rgba(0xd3869bff).into()),
+                ("string.escape".into(), rgba(0xc6b697ff).into()),
+                ("constructor".into(), rgba(0x83a598ff).into()),
+                ("link_uri".into(), rgba(0xd3869bff).into()),
+                ("string.special.symbol".into(), rgba(0x8ec07cff).into()),
+                ("type".into(), rgba(0xfabd2eff).into()),
+                ("function.builtin".into(), rgba(0xfb4833ff).into()),
+                ("title".into(), rgba(0xb8bb25ff).into()),
+                ("primary".into(), rgba(0xebdbb2ff).into()),
+                ("tag".into(), rgba(0x8ec07cff).into()),
+                ("constant".into(), rgba(0xfabd2eff).into()),
+                ("emphasis.strong".into(), rgba(0x83a598ff).into()),
+                ("string.special".into(), rgba(0xd3869bff).into()),
+                ("hint".into(), rgba(0x8c957dff).into()),
+                ("comment.doc".into(), rgba(0xc6b697ff).into()),
+                ("property".into(), rgba(0xebdbb2ff).into()),
+                ("embedded".into(), rgba(0x8ec07cff).into()),
+                ("operator".into(), rgba(0x8ec07cff).into()),
+                ("punctuation".into(), rgba(0xd5c4a1ff).into()),
+                ("variable".into(), rgba(0x83a598ff).into()),
+                ("enum".into(), rgba(0xfe7f18ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xebdbb2ff).into()),
+                ("string".into(), rgba(0xb8bb25ff).into()),
+            ],
+        },
+        status_bar: rgba(0x4c4642ff).into(),
+        title_bar: rgba(0x4c4642ff).into(),
+        toolbar: rgba(0x32302fff).into(),
+        tab_bar: rgba(0x3b3735ff).into(),
+        editor: rgba(0x32302fff).into(),
+        editor_subheader: rgba(0x3b3735ff).into(),
+        editor_active_line: rgba(0x3b3735ff).into(),
+        terminal: rgba(0x32302fff).into(),
+        image_fallback_background: rgba(0x4c4642ff).into(),
+        git_created: rgba(0xb7bb26ff).into(),
+        git_modified: rgba(0x83a598ff).into(),
+        git_deleted: rgba(0xfb4a35ff).into(),
+        git_conflict: rgba(0xf9bd2fff).into(),
+        git_ignored: rgba(0x998b78ff).into(),
+        git_renamed: rgba(0xf9bd2fff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x83a598ff).into(),
+                selection: rgba(0x83a5983d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb7bb26ff).into(),
+                selection: rgba(0xb7bb263d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa89984ff).into(),
+                selection: rgba(0xa899843d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfd801bff).into(),
+                selection: rgba(0xfd801b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd3869bff).into(),
+                selection: rgba(0xd3869b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8ec07cff).into(),
+                selection: rgba(0x8ec07c3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfb4a35ff).into(),
+                selection: rgba(0xfb4a353d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf9bd2fff).into(),
+                selection: rgba(0xf9bd2f3d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,131 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn gruvbox_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Gruvbox Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0xc8b899ff).into(),
+        border_variant: rgba(0xc8b899ff).into(),
+        border_focused: rgba(0xadc5ccff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xd9c8a4ff).into(),
+        surface: rgba(0xecddb4ff).into(),
+        background: rgba(0xd9c8a4ff).into(),
+        filled_element: rgba(0xd9c8a4ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xd2dee2ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xd2dee2ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x282828ff).into(),
+        text_muted: rgba(0x5f5650ff).into(),
+        text_placeholder: rgba(0x9d0308ff).into(),
+        text_disabled: rgba(0x897b6eff).into(),
+        text_accent: rgba(0x0b6678ff).into(),
+        icon_muted: rgba(0x5f5650ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("number".into(), rgba(0x8f3e71ff).into()),
+                ("link_text".into(), rgba(0x427b58ff).into()),
+                ("string.special".into(), rgba(0x8f3e71ff).into()),
+                ("string.special.symbol".into(), rgba(0x427b58ff).into()),
+                ("function".into(), rgba(0x79740eff).into()),
+                ("title".into(), rgba(0x79740eff).into()),
+                ("emphasis".into(), rgba(0x0b6678ff).into()),
+                ("punctuation".into(), rgba(0x3c3836ff).into()),
+                ("string.escape".into(), rgba(0x5d544eff).into()),
+                ("type".into(), rgba(0xb57613ff).into()),
+                ("string".into(), rgba(0x79740eff).into()),
+                ("keyword".into(), rgba(0x9d0006ff).into()),
+                ("tag".into(), rgba(0x427b58ff).into()),
+                ("primary".into(), rgba(0x282828ff).into()),
+                ("link_uri".into(), rgba(0x8f3e71ff).into()),
+                ("comment.doc".into(), rgba(0x5d544eff).into()),
+                ("boolean".into(), rgba(0x8f3e71ff).into()),
+                ("embedded".into(), rgba(0x427b58ff).into()),
+                ("hint".into(), rgba(0x677562ff).into()),
+                ("emphasis.strong".into(), rgba(0x0b6678ff).into()),
+                ("operator".into(), rgba(0x427b58ff).into()),
+                ("label".into(), rgba(0x0b6678ff).into()),
+                ("comment".into(), rgba(0x7c6f64ff).into()),
+                ("function.builtin".into(), rgba(0x9d0006ff).into()),
+                ("punctuation.bracket".into(), rgba(0x665c54ff).into()),
+                ("text.literal".into(), rgba(0x066578ff).into()),
+                ("string.regex".into(), rgba(0xaf3a02ff).into()),
+                ("property".into(), rgba(0x282828ff).into()),
+                ("attribute".into(), rgba(0x0b6678ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x413d3aff).into()),
+                ("constructor".into(), rgba(0x0b6678ff).into()),
+                ("variable".into(), rgba(0x066578ff).into()),
+                ("constant".into(), rgba(0xb57613ff).into()),
+                ("preproc".into(), rgba(0x282828ff).into()),
+                ("punctuation.special".into(), rgba(0x413d3aff).into()),
+                ("punctuation.list_marker".into(), rgba(0x282828ff).into()),
+                ("variant".into(), rgba(0x0b6678ff).into()),
+                ("predictive".into(), rgba(0x7c9780ff).into()),
+                ("enum".into(), rgba(0xaf3a02ff).into()),
+            ],
+        },
+        status_bar: rgba(0xd9c8a4ff).into(),
+        title_bar: rgba(0xd9c8a4ff).into(),
+        toolbar: rgba(0xfbf1c7ff).into(),
+        tab_bar: rgba(0xecddb4ff).into(),
+        editor: rgba(0xfbf1c7ff).into(),
+        editor_subheader: rgba(0xecddb4ff).into(),
+        editor_active_line: rgba(0xecddb4ff).into(),
+        terminal: rgba(0xfbf1c7ff).into(),
+        image_fallback_background: rgba(0xd9c8a4ff).into(),
+        git_created: rgba(0x797410ff).into(),
+        git_modified: rgba(0x0b6678ff).into(),
+        git_deleted: rgba(0x9d0308ff).into(),
+        git_conflict: rgba(0xb57615ff).into(),
+        git_ignored: rgba(0x897b6eff).into(),
+        git_renamed: rgba(0xb57615ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x0b6678ff).into(),
+                selection: rgba(0x0b66783d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x797410ff).into(),
+                selection: rgba(0x7974103d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7c6f64ff).into(),
+                selection: rgba(0x7c6f643d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xaf3a04ff).into(),
+                selection: rgba(0xaf3a043d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8f3f70ff).into(),
+                selection: rgba(0x8f3f703d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x437b59ff).into(),
+                selection: rgba(0x437b593d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9d0308ff).into(),
+                selection: rgba(0x9d03083d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb57615ff).into(),
+                selection: rgba(0xb576153d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,131 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn gruvbox_light_hard() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Gruvbox Light Hard".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0xc8b899ff).into(),
+        border_variant: rgba(0xc8b899ff).into(),
+        border_focused: rgba(0xadc5ccff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xd9c8a4ff).into(),
+        surface: rgba(0xecddb5ff).into(),
+        background: rgba(0xd9c8a4ff).into(),
+        filled_element: rgba(0xd9c8a4ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xd2dee2ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xd2dee2ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x282828ff).into(),
+        text_muted: rgba(0x5f5650ff).into(),
+        text_placeholder: rgba(0x9d0308ff).into(),
+        text_disabled: rgba(0x897b6eff).into(),
+        text_accent: rgba(0x0b6678ff).into(),
+        icon_muted: rgba(0x5f5650ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("label".into(), rgba(0x0b6678ff).into()),
+                ("hint".into(), rgba(0x677562ff).into()),
+                ("boolean".into(), rgba(0x8f3e71ff).into()),
+                ("function.builtin".into(), rgba(0x9d0006ff).into()),
+                ("constant".into(), rgba(0xb57613ff).into()),
+                ("preproc".into(), rgba(0x282828ff).into()),
+                ("predictive".into(), rgba(0x7c9780ff).into()),
+                ("string".into(), rgba(0x79740eff).into()),
+                ("comment.doc".into(), rgba(0x5d544eff).into()),
+                ("function".into(), rgba(0x79740eff).into()),
+                ("title".into(), rgba(0x79740eff).into()),
+                ("text.literal".into(), rgba(0x066578ff).into()),
+                ("punctuation.bracket".into(), rgba(0x665c54ff).into()),
+                ("string.escape".into(), rgba(0x5d544eff).into()),
+                ("punctuation.delimiter".into(), rgba(0x413d3aff).into()),
+                ("string.special.symbol".into(), rgba(0x427b58ff).into()),
+                ("type".into(), rgba(0xb57613ff).into()),
+                ("constructor".into(), rgba(0x0b6678ff).into()),
+                ("property".into(), rgba(0x282828ff).into()),
+                ("comment".into(), rgba(0x7c6f64ff).into()),
+                ("enum".into(), rgba(0xaf3a02ff).into()),
+                ("emphasis".into(), rgba(0x0b6678ff).into()),
+                ("embedded".into(), rgba(0x427b58ff).into()),
+                ("operator".into(), rgba(0x427b58ff).into()),
+                ("attribute".into(), rgba(0x0b6678ff).into()),
+                ("emphasis.strong".into(), rgba(0x0b6678ff).into()),
+                ("link_text".into(), rgba(0x427b58ff).into()),
+                ("punctuation.special".into(), rgba(0x413d3aff).into()),
+                ("punctuation.list_marker".into(), rgba(0x282828ff).into()),
+                ("variant".into(), rgba(0x0b6678ff).into()),
+                ("primary".into(), rgba(0x282828ff).into()),
+                ("number".into(), rgba(0x8f3e71ff).into()),
+                ("tag".into(), rgba(0x427b58ff).into()),
+                ("keyword".into(), rgba(0x9d0006ff).into()),
+                ("link_uri".into(), rgba(0x8f3e71ff).into()),
+                ("string.regex".into(), rgba(0xaf3a02ff).into()),
+                ("variable".into(), rgba(0x066578ff).into()),
+                ("string.special".into(), rgba(0x8f3e71ff).into()),
+                ("punctuation".into(), rgba(0x3c3836ff).into()),
+            ],
+        },
+        status_bar: rgba(0xd9c8a4ff).into(),
+        title_bar: rgba(0xd9c8a4ff).into(),
+        toolbar: rgba(0xf9f5d7ff).into(),
+        tab_bar: rgba(0xecddb5ff).into(),
+        editor: rgba(0xf9f5d7ff).into(),
+        editor_subheader: rgba(0xecddb5ff).into(),
+        editor_active_line: rgba(0xecddb5ff).into(),
+        terminal: rgba(0xf9f5d7ff).into(),
+        image_fallback_background: rgba(0xd9c8a4ff).into(),
+        git_created: rgba(0x797410ff).into(),
+        git_modified: rgba(0x0b6678ff).into(),
+        git_deleted: rgba(0x9d0308ff).into(),
+        git_conflict: rgba(0xb57615ff).into(),
+        git_ignored: rgba(0x897b6eff).into(),
+        git_renamed: rgba(0xb57615ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x0b6678ff).into(),
+                selection: rgba(0x0b66783d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x797410ff).into(),
+                selection: rgba(0x7974103d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7c6f64ff).into(),
+                selection: rgba(0x7c6f643d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xaf3a04ff).into(),
+                selection: rgba(0xaf3a043d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8f3f70ff).into(),
+                selection: rgba(0x8f3f703d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x437b59ff).into(),
+                selection: rgba(0x437b593d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9d0308ff).into(),
+                selection: rgba(0x9d03083d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb57615ff).into(),
+                selection: rgba(0xb576153d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,131 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn gruvbox_light_soft() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Gruvbox Light Soft".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0xc8b899ff).into(),
+        border_variant: rgba(0xc8b899ff).into(),
+        border_focused: rgba(0xadc5ccff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xd9c8a4ff).into(),
+        surface: rgba(0xecdcb3ff).into(),
+        background: rgba(0xd9c8a4ff).into(),
+        filled_element: rgba(0xd9c8a4ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xd2dee2ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xd2dee2ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x282828ff).into(),
+        text_muted: rgba(0x5f5650ff).into(),
+        text_placeholder: rgba(0x9d0308ff).into(),
+        text_disabled: rgba(0x897b6eff).into(),
+        text_accent: rgba(0x0b6678ff).into(),
+        icon_muted: rgba(0x5f5650ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("preproc".into(), rgba(0x282828ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x282828ff).into()),
+                ("string".into(), rgba(0x79740eff).into()),
+                ("constant".into(), rgba(0xb57613ff).into()),
+                ("keyword".into(), rgba(0x9d0006ff).into()),
+                ("string.special.symbol".into(), rgba(0x427b58ff).into()),
+                ("comment.doc".into(), rgba(0x5d544eff).into()),
+                ("hint".into(), rgba(0x677562ff).into()),
+                ("number".into(), rgba(0x8f3e71ff).into()),
+                ("enum".into(), rgba(0xaf3a02ff).into()),
+                ("emphasis".into(), rgba(0x0b6678ff).into()),
+                ("operator".into(), rgba(0x427b58ff).into()),
+                ("comment".into(), rgba(0x7c6f64ff).into()),
+                ("embedded".into(), rgba(0x427b58ff).into()),
+                ("type".into(), rgba(0xb57613ff).into()),
+                ("title".into(), rgba(0x79740eff).into()),
+                ("constructor".into(), rgba(0x0b6678ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x413d3aff).into()),
+                ("function".into(), rgba(0x79740eff).into()),
+                ("link_uri".into(), rgba(0x8f3e71ff).into()),
+                ("emphasis.strong".into(), rgba(0x0b6678ff).into()),
+                ("boolean".into(), rgba(0x8f3e71ff).into()),
+                ("function.builtin".into(), rgba(0x9d0006ff).into()),
+                ("predictive".into(), rgba(0x7c9780ff).into()),
+                ("string.regex".into(), rgba(0xaf3a02ff).into()),
+                ("tag".into(), rgba(0x427b58ff).into()),
+                ("text.literal".into(), rgba(0x066578ff).into()),
+                ("punctuation".into(), rgba(0x3c3836ff).into()),
+                ("punctuation.bracket".into(), rgba(0x665c54ff).into()),
+                ("variable".into(), rgba(0x066578ff).into()),
+                ("attribute".into(), rgba(0x0b6678ff).into()),
+                ("string.special".into(), rgba(0x8f3e71ff).into()),
+                ("label".into(), rgba(0x0b6678ff).into()),
+                ("string.escape".into(), rgba(0x5d544eff).into()),
+                ("link_text".into(), rgba(0x427b58ff).into()),
+                ("punctuation.special".into(), rgba(0x413d3aff).into()),
+                ("property".into(), rgba(0x282828ff).into()),
+                ("variant".into(), rgba(0x0b6678ff).into()),
+                ("primary".into(), rgba(0x282828ff).into()),
+            ],
+        },
+        status_bar: rgba(0xd9c8a4ff).into(),
+        title_bar: rgba(0xd9c8a4ff).into(),
+        toolbar: rgba(0xf2e5bcff).into(),
+        tab_bar: rgba(0xecdcb3ff).into(),
+        editor: rgba(0xf2e5bcff).into(),
+        editor_subheader: rgba(0xecdcb3ff).into(),
+        editor_active_line: rgba(0xecdcb3ff).into(),
+        terminal: rgba(0xf2e5bcff).into(),
+        image_fallback_background: rgba(0xd9c8a4ff).into(),
+        git_created: rgba(0x797410ff).into(),
+        git_modified: rgba(0x0b6678ff).into(),
+        git_deleted: rgba(0x9d0308ff).into(),
+        git_conflict: rgba(0xb57615ff).into(),
+        git_ignored: rgba(0x897b6eff).into(),
+        git_renamed: rgba(0xb57615ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x0b6678ff).into(),
+                selection: rgba(0x0b66783d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x797410ff).into(),
+                selection: rgba(0x7974103d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7c6f64ff).into(),
+                selection: rgba(0x7c6f643d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xaf3a04ff).into(),
+                selection: rgba(0xaf3a043d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x8f3f70ff).into(),
+                selection: rgba(0x8f3f703d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x437b59ff).into(),
+                selection: rgba(0x437b593d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9d0308ff).into(),
+                selection: rgba(0x9d03083d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb57615ff).into(),
+                selection: rgba(0xb576153d).into(),
+            },
+        ],
+    }
+}

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

@@ -1,7 +1,79 @@
+mod andromeda;
+mod atelier_cave_dark;
+mod atelier_cave_light;
+mod atelier_dune_dark;
+mod atelier_dune_light;
+mod atelier_estuary_dark;
+mod atelier_estuary_light;
+mod atelier_forest_dark;
+mod atelier_forest_light;
+mod atelier_heath_dark;
+mod atelier_heath_light;
+mod atelier_lakeside_dark;
+mod atelier_lakeside_light;
+mod atelier_plateau_dark;
+mod atelier_plateau_light;
+mod atelier_savanna_dark;
+mod atelier_savanna_light;
+mod atelier_seaside_dark;
+mod atelier_seaside_light;
+mod atelier_sulphurpool_dark;
+mod atelier_sulphurpool_light;
+mod ayu_dark;
+mod ayu_light;
+mod ayu_mirage;
+mod gruvbox_dark;
+mod gruvbox_dark_hard;
+mod gruvbox_dark_soft;
+mod gruvbox_light;
+mod gruvbox_light_hard;
+mod gruvbox_light_soft;
 mod one_dark;
+mod one_light;
 mod rose_pine;
+mod rose_pine_dawn;
+mod rose_pine_moon;
 mod sandcastle;
+mod solarized_dark;
+mod solarized_light;
+mod summercamp;
 
+pub use andromeda::*;
+pub use atelier_cave_dark::*;
+pub use atelier_cave_light::*;
+pub use atelier_dune_dark::*;
+pub use atelier_dune_light::*;
+pub use atelier_estuary_dark::*;
+pub use atelier_estuary_light::*;
+pub use atelier_forest_dark::*;
+pub use atelier_forest_light::*;
+pub use atelier_heath_dark::*;
+pub use atelier_heath_light::*;
+pub use atelier_lakeside_dark::*;
+pub use atelier_lakeside_light::*;
+pub use atelier_plateau_dark::*;
+pub use atelier_plateau_light::*;
+pub use atelier_savanna_dark::*;
+pub use atelier_savanna_light::*;
+pub use atelier_seaside_dark::*;
+pub use atelier_seaside_light::*;
+pub use atelier_sulphurpool_dark::*;
+pub use atelier_sulphurpool_light::*;
+pub use ayu_dark::*;
+pub use ayu_light::*;
+pub use ayu_mirage::*;
+pub use gruvbox_dark::*;
+pub use gruvbox_dark_hard::*;
+pub use gruvbox_dark_soft::*;
+pub use gruvbox_light::*;
+pub use gruvbox_light_hard::*;
+pub use gruvbox_light_soft::*;
 pub use one_dark::*;
+pub use one_light::*;
 pub use rose_pine::*;
+pub use rose_pine_dawn::*;
+pub use rose_pine_moon::*;
 pub use sandcastle::*;
+pub use solarized_dark::*;
+pub use solarized_light::*;
+pub use summercamp::*;

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![
+                ("keyword".into(), rgba(0xb477cfff).into()),
+                ("comment.doc".into(), rgba(0x878e98ff).into()),
+                ("variant".into(), rgba(0x73ade9ff).into()),
+                ("property".into(), rgba(0xd07277ff).into()),
+                ("function".into(), rgba(0x73ade9ff).into()),
+                ("type".into(), rgba(0x6eb4bfff).into()),
+                ("tag".into(), rgba(0x74ade8ff).into()),
+                ("string.escape".into(), rgba(0x878e98ff).into()),
+                ("punctuation.bracket".into(), rgba(0xb2b9c6ff).into()),
+                ("hint".into(), rgba(0x5a6f89ff).into()),
+                ("punctuation".into(), rgba(0xacb2beff).into()),
+                ("comment".into(), rgba(0x5d636fff).into()),
+                ("emphasis".into(), rgba(0x74ade8ff).into()),
+                ("punctuation.special".into(), rgba(0xb1574bff).into()),
+                ("link_uri".into(), rgba(0x6eb4bfff).into()),
+                ("string.regex".into(), rgba(0xbf956aff).into()),
+                ("constructor".into(), rgba(0x73ade9ff).into()),
+                ("operator".into(), rgba(0x6eb4bfff).into()),
+                ("constant".into(), rgba(0xdfc184ff).into()),
+                ("string.special".into(), rgba(0xbf956aff).into()),
+                ("emphasis.strong".into(), rgba(0xbf956aff).into()),
+                ("string.special.symbol".into(), rgba(0xbf956aff).into()),
+                ("primary".into(), rgba(0xacb2beff).into()),
+                ("preproc".into(), rgba(0xc8ccd4ff).into()),
+                ("string".into(), rgba(0xa1c181ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xb2b9c6ff).into()),
+                ("embedded".into(), rgba(0xc8ccd4ff).into()),
+                ("enum".into(), rgba(0xd07277ff).into()),
+                ("variable.special".into(), rgba(0xbf956aff).into()),
+                ("text.literal".into(), rgba(0xa1c181ff).into()),
+                ("attribute".into(), rgba(0x74ade8ff).into()),
+                ("link_text".into(), rgba(0x73ade9ff).into()),
+                ("title".into(), rgba(0xd07277ff).into()),
+                ("predictive".into(), rgba(0x5a6a87ff).into()),
+                ("number".into(), rgba(0xbf956aff).into()),
+                ("label".into(), rgba(0x74ade8ff).into()),
+                ("variable".into(), rgba(0xc8ccd4ff).into()),
+                ("boolean".into(), rgba(0xbf956aff).into()),
+                ("punctuation.list_marker".into(), rgba(0xd07277ff).into()),
+            ],
         },
         status_bar: rgba(0x3b414dff).into(),
         title_bar: rgba(0x3b414dff).into(),

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

@@ -0,0 +1,131 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn one_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "One Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0xc9c9caff).into(),
+        border_variant: rgba(0xc9c9caff).into(),
+        border_focused: rgba(0xcbcdf6ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xdcdcddff).into(),
+        surface: rgba(0xebebecff).into(),
+        background: rgba(0xdcdcddff).into(),
+        filled_element: rgba(0xdcdcddff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xe2e2faff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xe2e2faff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x383a41ff).into(),
+        text_muted: rgba(0x7e8087ff).into(),
+        text_placeholder: rgba(0xd36151ff).into(),
+        text_disabled: rgba(0xa1a1a3ff).into(),
+        text_accent: rgba(0x5c78e2ff).into(),
+        icon_muted: rgba(0x7e8087ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("string.special.symbol".into(), rgba(0xad6e26ff).into()),
+                ("hint".into(), rgba(0x9294beff).into()),
+                ("link_uri".into(), rgba(0x3882b7ff).into()),
+                ("type".into(), rgba(0x3882b7ff).into()),
+                ("string.regex".into(), rgba(0xad6e26ff).into()),
+                ("constant".into(), rgba(0x669f59ff).into()),
+                ("function".into(), rgba(0x5b79e3ff).into()),
+                ("string.special".into(), rgba(0xad6e26ff).into()),
+                ("punctuation.bracket".into(), rgba(0x4d4f52ff).into()),
+                ("variable".into(), rgba(0x383a41ff).into()),
+                ("punctuation".into(), rgba(0x383a41ff).into()),
+                ("property".into(), rgba(0xd3604fff).into()),
+                ("string".into(), rgba(0x649f57ff).into()),
+                ("predictive".into(), rgba(0x9b9ec6ff).into()),
+                ("attribute".into(), rgba(0x5c78e2ff).into()),
+                ("number".into(), rgba(0xad6e25ff).into()),
+                ("constructor".into(), rgba(0x5c78e2ff).into()),
+                ("embedded".into(), rgba(0x383a41ff).into()),
+                ("title".into(), rgba(0xd3604fff).into()),
+                ("tag".into(), rgba(0x5c78e2ff).into()),
+                ("boolean".into(), rgba(0xad6e25ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xd3604fff).into()),
+                ("variant".into(), rgba(0x5b79e3ff).into()),
+                ("emphasis".into(), rgba(0x5c78e2ff).into()),
+                ("link_text".into(), rgba(0x5b79e3ff).into()),
+                ("comment".into(), rgba(0xa2a3a7ff).into()),
+                ("punctuation.special".into(), rgba(0xb92b46ff).into()),
+                ("emphasis.strong".into(), rgba(0xad6e25ff).into()),
+                ("primary".into(), rgba(0x383a41ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x4d4f52ff).into()),
+                ("label".into(), rgba(0x5c78e2ff).into()),
+                ("keyword".into(), rgba(0xa449abff).into()),
+                ("string.escape".into(), rgba(0x7c7e86ff).into()),
+                ("text.literal".into(), rgba(0x649f57ff).into()),
+                ("variable.special".into(), rgba(0xad6e25ff).into()),
+                ("comment.doc".into(), rgba(0x7c7e86ff).into()),
+                ("enum".into(), rgba(0xd3604fff).into()),
+                ("operator".into(), rgba(0x3882b7ff).into()),
+                ("preproc".into(), rgba(0x383a41ff).into()),
+            ],
+        },
+        status_bar: rgba(0xdcdcddff).into(),
+        title_bar: rgba(0xdcdcddff).into(),
+        toolbar: rgba(0xfafafaff).into(),
+        tab_bar: rgba(0xebebecff).into(),
+        editor: rgba(0xfafafaff).into(),
+        editor_subheader: rgba(0xebebecff).into(),
+        editor_active_line: rgba(0xebebecff).into(),
+        terminal: rgba(0xfafafaff).into(),
+        image_fallback_background: rgba(0xdcdcddff).into(),
+        git_created: rgba(0x669f59ff).into(),
+        git_modified: rgba(0x5c78e2ff).into(),
+        git_deleted: rgba(0xd36151ff).into(),
+        git_conflict: rgba(0xdec184ff).into(),
+        git_ignored: rgba(0xa1a1a3ff).into(),
+        git_renamed: rgba(0xdec184ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x5c78e2ff).into(),
+                selection: rgba(0x5c78e23d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x669f59ff).into(),
+                selection: rgba(0x669f593d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x984ea5ff).into(),
+                selection: rgba(0x984ea53d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xad6e26ff).into(),
+                selection: rgba(0xad6e263d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa349abff).into(),
+                selection: rgba(0xa349ab3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x3a82b7ff).into(),
+                selection: rgba(0x3a82b73d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd36151ff).into(),
+                selection: rgba(0xd361513d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xdec184ff).into(),
+                selection: rgba(0xdec1843d).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![
+                ("punctuation.delimiter".into(), rgba(0x9d99b6ff).into()),
+                ("number".into(), rgba(0x5cc1a3ff).into()),
+                ("punctuation.special".into(), rgba(0x9d99b6ff).into()),
+                ("string.escape".into(), rgba(0x76728fff).into()),
+                ("title".into(), rgba(0xf5c177ff).into()),
+                ("constant".into(), rgba(0x5cc1a3ff).into()),
+                ("string.regex".into(), rgba(0xc4a7e6ff).into()),
+                ("type.builtin".into(), rgba(0x9ccfd8ff).into()),
+                ("comment.doc".into(), rgba(0x76728fff).into()),
+                ("primary".into(), rgba(0xe0def4ff).into()),
+                ("string.special".into(), rgba(0xc4a7e6ff).into()),
+                ("punctuation".into(), rgba(0x908caaff).into()),
+                ("string.special.symbol".into(), rgba(0xc4a7e6ff).into()),
+                ("variant".into(), rgba(0x9bced6ff).into()),
+                ("function.method".into(), rgba(0xebbcbaff).into()),
+                ("comment".into(), rgba(0x6e6a86ff).into()),
+                ("boolean".into(), rgba(0xebbcbaff).into()),
+                ("preproc".into(), rgba(0xe0def4ff).into()),
+                ("link_uri".into(), rgba(0xebbcbaff).into()),
+                ("hint".into(), rgba(0x5e768cff).into()),
+                ("attribute".into(), rgba(0x9bced6ff).into()),
+                ("text.literal".into(), rgba(0xc4a7e6ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x9d99b6ff).into()),
+                ("operator".into(), rgba(0x30738fff).into()),
+                ("emphasis.strong".into(), rgba(0x9bced6ff).into()),
+                ("keyword".into(), rgba(0x30738fff).into()),
+                ("enum".into(), rgba(0xc4a7e6ff).into()),
+                ("tag".into(), rgba(0x9ccfd8ff).into()),
+                ("constructor".into(), rgba(0x9bced6ff).into()),
+                ("function".into(), rgba(0xebbcbaff).into()),
+                ("string".into(), rgba(0xf5c177ff).into()),
+                ("type".into(), rgba(0x9ccfd8ff).into()),
+                ("emphasis".into(), rgba(0x9bced6ff).into()),
+                ("link_text".into(), rgba(0x9ccfd8ff).into()),
+                ("property".into(), rgba(0x9bced6ff).into()),
+                ("predictive".into(), rgba(0x556b81ff).into()),
+                ("punctuation.bracket".into(), rgba(0x9d99b6ff).into()),
+                ("embedded".into(), rgba(0xe0def4ff).into()),
+                ("variable".into(), rgba(0xe0def4ff).into()),
+                ("label".into(), rgba(0x9bced6ff).into()),
+            ],
         },
         status_bar: rgba(0x292738ff).into(),
         title_bar: rgba(0x292738ff).into(),
@@ -93,187 +130,3 @@ pub fn rose_pine() -> Theme {
         ],
     }
 }
-
-pub fn rose_pine_dawn() -> Theme {
-    Theme {
-        metadata: ThemeMetadata {
-            name: "Rosé Pine Dawn".into(),
-            is_light: true,
-        },
-        transparent: rgba(0x00000000).into(),
-        mac_os_traffic_light_red: rgba(0xec695eff).into(),
-        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
-        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
-        border: rgba(0xdcd6d5ff).into(),
-        border_variant: rgba(0xdcd6d5ff).into(),
-        border_focused: rgba(0xc3d7dbff).into(),
-        border_transparent: rgba(0x00000000).into(),
-        elevated_surface: rgba(0xdcd8d8ff).into(),
-        surface: rgba(0xfef9f2ff).into(),
-        background: rgba(0xdcd8d8ff).into(),
-        filled_element: rgba(0xdcd8d8ff).into(),
-        filled_element_hover: rgba(0xffffff1e).into(),
-        filled_element_active: rgba(0xffffff28).into(),
-        filled_element_selected: rgba(0xdde9ebff).into(),
-        filled_element_disabled: rgba(0x00000000).into(),
-        ghost_element: rgba(0x00000000).into(),
-        ghost_element_hover: rgba(0xffffff14).into(),
-        ghost_element_active: rgba(0xffffff1e).into(),
-        ghost_element_selected: rgba(0xdde9ebff).into(),
-        ghost_element_disabled: rgba(0x00000000).into(),
-        text: rgba(0x575279ff).into(),
-        text_muted: rgba(0x706c8cff).into(),
-        text_placeholder: rgba(0xb4647aff).into(),
-        text_disabled: rgba(0x938fa3ff).into(),
-        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(),
-        },
-        status_bar: rgba(0xdcd8d8ff).into(),
-        title_bar: rgba(0xdcd8d8ff).into(),
-        toolbar: rgba(0xfaf4edff).into(),
-        tab_bar: rgba(0xfef9f2ff).into(),
-        editor: rgba(0xfaf4edff).into(),
-        editor_subheader: rgba(0xfef9f2ff).into(),
-        editor_active_line: rgba(0xfef9f2ff).into(),
-        terminal: rgba(0xfaf4edff).into(),
-        image_fallback_background: rgba(0xdcd8d8ff).into(),
-        git_created: rgba(0x3daa8eff).into(),
-        git_modified: rgba(0x57949fff).into(),
-        git_deleted: rgba(0xb4647aff).into(),
-        git_conflict: rgba(0xe99d35ff).into(),
-        git_ignored: rgba(0x938fa3ff).into(),
-        git_renamed: rgba(0xe99d35ff).into(),
-        players: [
-            PlayerTheme {
-                cursor: rgba(0x57949fff).into(),
-                selection: rgba(0x57949f3d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0x3daa8eff).into(),
-                selection: rgba(0x3daa8e3d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0x7c697fff).into(),
-                selection: rgba(0x7c697f3d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0x9079a9ff).into(),
-                selection: rgba(0x9079a93d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0x9079a9ff).into(),
-                selection: rgba(0x9079a93d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0x296983ff).into(),
-                selection: rgba(0x2969833d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0xb4647aff).into(),
-                selection: rgba(0xb4647a3d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0xe99d35ff).into(),
-                selection: rgba(0xe99d353d).into(),
-            },
-        ],
-    }
-}
-
-pub fn rose_pine_moon() -> Theme {
-    Theme {
-        metadata: ThemeMetadata {
-            name: "Rosé Pine Moon".into(),
-            is_light: false,
-        },
-        transparent: rgba(0x00000000).into(),
-        mac_os_traffic_light_red: rgba(0xec695eff).into(),
-        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
-        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
-        border: rgba(0x504c68ff).into(),
-        border_variant: rgba(0x504c68ff).into(),
-        border_focused: rgba(0x435255ff).into(),
-        border_transparent: rgba(0x00000000).into(),
-        elevated_surface: rgba(0x38354eff).into(),
-        surface: rgba(0x28253cff).into(),
-        background: rgba(0x38354eff).into(),
-        filled_element: rgba(0x38354eff).into(),
-        filled_element_hover: rgba(0xffffff1e).into(),
-        filled_element_active: rgba(0xffffff28).into(),
-        filled_element_selected: rgba(0x2f3639ff).into(),
-        filled_element_disabled: rgba(0x00000000).into(),
-        ghost_element: rgba(0x00000000).into(),
-        ghost_element_hover: rgba(0xffffff14).into(),
-        ghost_element_active: rgba(0xffffff1e).into(),
-        ghost_element_selected: rgba(0x2f3639ff).into(),
-        ghost_element_disabled: rgba(0x00000000).into(),
-        text: rgba(0xe0def4ff).into(),
-        text_muted: rgba(0x85819eff).into(),
-        text_placeholder: rgba(0xea6e92ff).into(),
-        text_disabled: rgba(0x605d7aff).into(),
-        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(),
-        },
-        status_bar: rgba(0x38354eff).into(),
-        title_bar: rgba(0x38354eff).into(),
-        toolbar: rgba(0x232136ff).into(),
-        tab_bar: rgba(0x28253cff).into(),
-        editor: rgba(0x232136ff).into(),
-        editor_subheader: rgba(0x28253cff).into(),
-        editor_active_line: rgba(0x28253cff).into(),
-        terminal: rgba(0x232136ff).into(),
-        image_fallback_background: rgba(0x38354eff).into(),
-        git_created: rgba(0x5cc1a3ff).into(),
-        git_modified: rgba(0x9bced6ff).into(),
-        git_deleted: rgba(0xea6e92ff).into(),
-        git_conflict: rgba(0xf5c177ff).into(),
-        git_ignored: rgba(0x605d7aff).into(),
-        git_renamed: rgba(0xf5c177ff).into(),
-        players: [
-            PlayerTheme {
-                cursor: rgba(0x9bced6ff).into(),
-                selection: rgba(0x9bced63d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0x5cc1a3ff).into(),
-                selection: rgba(0x5cc1a33d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0xa683a0ff).into(),
-                selection: rgba(0xa683a03d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0xc4a7e6ff).into(),
-                selection: rgba(0xc4a7e63d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0xc4a7e6ff).into(),
-                selection: rgba(0xc4a7e63d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0x3e8fb0ff).into(),
-                selection: rgba(0x3e8fb03d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0xea6e92ff).into(),
-                selection: rgba(0xea6e923d).into(),
-            },
-            PlayerTheme {
-                cursor: rgba(0xf5c177ff).into(),
-                selection: rgba(0xf5c1773d).into(),
-            },
-        ],
-    }
-}

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

@@ -0,0 +1,132 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn rose_pine_dawn() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Rosé Pine Dawn".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0xdcd6d5ff).into(),
+        border_variant: rgba(0xdcd6d5ff).into(),
+        border_focused: rgba(0xc3d7dbff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xdcd8d8ff).into(),
+        surface: rgba(0xfef9f2ff).into(),
+        background: rgba(0xdcd8d8ff).into(),
+        filled_element: rgba(0xdcd8d8ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xdde9ebff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xdde9ebff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x575279ff).into(),
+        text_muted: rgba(0x706c8cff).into(),
+        text_placeholder: rgba(0xb4647aff).into(),
+        text_disabled: rgba(0x938fa3ff).into(),
+        text_accent: rgba(0x57949fff).into(),
+        icon_muted: rgba(0x706c8cff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("primary".into(), rgba(0x575279ff).into()),
+                ("attribute".into(), rgba(0x57949fff).into()),
+                ("operator".into(), rgba(0x276983ff).into()),
+                ("boolean".into(), rgba(0xd7827dff).into()),
+                ("tag".into(), rgba(0x55949fff).into()),
+                ("enum".into(), rgba(0x9079a9ff).into()),
+                ("embedded".into(), rgba(0x575279ff).into()),
+                ("label".into(), rgba(0x57949fff).into()),
+                ("function.method".into(), rgba(0xd7827dff).into()),
+                ("punctuation.list_marker".into(), rgba(0x635e82ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x635e82ff).into()),
+                ("string".into(), rgba(0xea9d34ff).into()),
+                ("type".into(), rgba(0x55949fff).into()),
+                ("string.regex".into(), rgba(0x9079a9ff).into()),
+                ("variable".into(), rgba(0x575279ff).into()),
+                ("constructor".into(), rgba(0x57949fff).into()),
+                ("punctuation.bracket".into(), rgba(0x635e82ff).into()),
+                ("emphasis".into(), rgba(0x57949fff).into()),
+                ("comment.doc".into(), rgba(0x6e6a8bff).into()),
+                ("comment".into(), rgba(0x9893a5ff).into()),
+                ("keyword".into(), rgba(0x276983ff).into()),
+                ("preproc".into(), rgba(0x575279ff).into()),
+                ("string.special".into(), rgba(0x9079a9ff).into()),
+                ("string.escape".into(), rgba(0x6e6a8bff).into()),
+                ("constant".into(), rgba(0x3daa8eff).into()),
+                ("property".into(), rgba(0x57949fff).into()),
+                ("punctuation.special".into(), rgba(0x635e82ff).into()),
+                ("text.literal".into(), rgba(0x9079a9ff).into()),
+                ("type.builtin".into(), rgba(0x55949fff).into()),
+                ("string.special.symbol".into(), rgba(0x9079a9ff).into()),
+                ("link_uri".into(), rgba(0xd7827dff).into()),
+                ("number".into(), rgba(0x3daa8eff).into()),
+                ("emphasis.strong".into(), rgba(0x57949fff).into()),
+                ("function".into(), rgba(0xd7827dff).into()),
+                ("title".into(), rgba(0xea9d34ff).into()),
+                ("punctuation".into(), rgba(0x797593ff).into()),
+                ("link_text".into(), rgba(0x55949fff).into()),
+                ("variant".into(), rgba(0x57949fff).into()),
+                ("predictive".into(), rgba(0xa2acbeff).into()),
+                ("hint".into(), rgba(0x7a92aaff).into()),
+            ],
+        },
+        status_bar: rgba(0xdcd8d8ff).into(),
+        title_bar: rgba(0xdcd8d8ff).into(),
+        toolbar: rgba(0xfaf4edff).into(),
+        tab_bar: rgba(0xfef9f2ff).into(),
+        editor: rgba(0xfaf4edff).into(),
+        editor_subheader: rgba(0xfef9f2ff).into(),
+        editor_active_line: rgba(0xfef9f2ff).into(),
+        terminal: rgba(0xfaf4edff).into(),
+        image_fallback_background: rgba(0xdcd8d8ff).into(),
+        git_created: rgba(0x3daa8eff).into(),
+        git_modified: rgba(0x57949fff).into(),
+        git_deleted: rgba(0xb4647aff).into(),
+        git_conflict: rgba(0xe99d35ff).into(),
+        git_ignored: rgba(0x938fa3ff).into(),
+        git_renamed: rgba(0xe99d35ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x57949fff).into(),
+                selection: rgba(0x57949f3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x3daa8eff).into(),
+                selection: rgba(0x3daa8e3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x7c697fff).into(),
+                selection: rgba(0x7c697f3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9079a9ff).into(),
+                selection: rgba(0x9079a93d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x9079a9ff).into(),
+                selection: rgba(0x9079a93d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x296983ff).into(),
+                selection: rgba(0x2969833d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb4647aff).into(),
+                selection: rgba(0xb4647a3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xe99d35ff).into(),
+                selection: rgba(0xe99d353d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,132 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn rose_pine_moon() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Rosé Pine Moon".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x504c68ff).into(),
+        border_variant: rgba(0x504c68ff).into(),
+        border_focused: rgba(0x435255ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x38354eff).into(),
+        surface: rgba(0x28253cff).into(),
+        background: rgba(0x38354eff).into(),
+        filled_element: rgba(0x38354eff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x2f3639ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x2f3639ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xe0def4ff).into(),
+        text_muted: rgba(0x85819eff).into(),
+        text_placeholder: rgba(0xea6e92ff).into(),
+        text_disabled: rgba(0x605d7aff).into(),
+        text_accent: rgba(0x9bced6ff).into(),
+        icon_muted: rgba(0x85819eff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("type.builtin".into(), rgba(0x9ccfd8ff).into()),
+                ("variable".into(), rgba(0xe0def4ff).into()),
+                ("punctuation".into(), rgba(0x908caaff).into()),
+                ("number".into(), rgba(0x5cc1a3ff).into()),
+                ("comment".into(), rgba(0x6e6a86ff).into()),
+                ("string.special".into(), rgba(0xc4a7e6ff).into()),
+                ("string.escape".into(), rgba(0x8682a0ff).into()),
+                ("function.method".into(), rgba(0xea9a97ff).into()),
+                ("predictive".into(), rgba(0x516b83ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xaeabc6ff).into()),
+                ("primary".into(), rgba(0xe0def4ff).into()),
+                ("link_text".into(), rgba(0x9ccfd8ff).into()),
+                ("string.regex".into(), rgba(0xc4a7e6ff).into()),
+                ("constructor".into(), rgba(0x9bced6ff).into()),
+                ("constant".into(), rgba(0x5cc1a3ff).into()),
+                ("emphasis.strong".into(), rgba(0x9bced6ff).into()),
+                ("function".into(), rgba(0xea9a97ff).into()),
+                ("hint".into(), rgba(0x728aa2ff).into()),
+                ("preproc".into(), rgba(0xe0def4ff).into()),
+                ("property".into(), rgba(0x9bced6ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xaeabc6ff).into()),
+                ("emphasis".into(), rgba(0x9bced6ff).into()),
+                ("attribute".into(), rgba(0x9bced6ff).into()),
+                ("title".into(), rgba(0xf5c177ff).into()),
+                ("keyword".into(), rgba(0x3d8fb0ff).into()),
+                ("string".into(), rgba(0xf5c177ff).into()),
+                ("text.literal".into(), rgba(0xc4a7e6ff).into()),
+                ("embedded".into(), rgba(0xe0def4ff).into()),
+                ("comment.doc".into(), rgba(0x8682a0ff).into()),
+                ("variant".into(), rgba(0x9bced6ff).into()),
+                ("label".into(), rgba(0x9bced6ff).into()),
+                ("punctuation.special".into(), rgba(0xaeabc6ff).into()),
+                ("string.special.symbol".into(), rgba(0xc4a7e6ff).into()),
+                ("tag".into(), rgba(0x9ccfd8ff).into()),
+                ("enum".into(), rgba(0xc4a7e6ff).into()),
+                ("boolean".into(), rgba(0xea9a97ff).into()),
+                ("punctuation.bracket".into(), rgba(0xaeabc6ff).into()),
+                ("operator".into(), rgba(0x3d8fb0ff).into()),
+                ("type".into(), rgba(0x9ccfd8ff).into()),
+                ("link_uri".into(), rgba(0xea9a97ff).into()),
+            ],
+        },
+        status_bar: rgba(0x38354eff).into(),
+        title_bar: rgba(0x38354eff).into(),
+        toolbar: rgba(0x232136ff).into(),
+        tab_bar: rgba(0x28253cff).into(),
+        editor: rgba(0x232136ff).into(),
+        editor_subheader: rgba(0x28253cff).into(),
+        editor_active_line: rgba(0x28253cff).into(),
+        terminal: rgba(0x232136ff).into(),
+        image_fallback_background: rgba(0x38354eff).into(),
+        git_created: rgba(0x5cc1a3ff).into(),
+        git_modified: rgba(0x9bced6ff).into(),
+        git_deleted: rgba(0xea6e92ff).into(),
+        git_conflict: rgba(0xf5c177ff).into(),
+        git_ignored: rgba(0x605d7aff).into(),
+        git_renamed: rgba(0xf5c177ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x9bced6ff).into(),
+                selection: rgba(0x9bced63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5cc1a3ff).into(),
+                selection: rgba(0x5cc1a33d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa683a0ff).into(),
+                selection: rgba(0xa683a03d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc4a7e6ff).into(),
+                selection: rgba(0xc4a7e63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc4a7e6ff).into(),
+                selection: rgba(0xc4a7e63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x3e8fb0ff).into(),
+                selection: rgba(0x3e8fb03d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xea6e92ff).into(),
+                selection: rgba(0xea6e923d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf5c177ff).into(),
+                selection: rgba(0xf5c1773d).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![
+                ("comment".into(), rgba(0xa89984ff).into()),
+                ("type".into(), rgba(0x83a598ff).into()),
+                ("preproc".into(), rgba(0xfdf4c1ff).into()),
+                ("punctuation.bracket".into(), rgba(0xd5c5a1ff).into()),
+                ("hint".into(), rgba(0x727d68ff).into()),
+                ("link_uri".into(), rgba(0x83a598ff).into()),
+                ("text.literal".into(), rgba(0xa07d3aff).into()),
+                ("enum".into(), rgba(0xa07d3aff).into()),
+                ("string.special".into(), rgba(0xa07d3aff).into()),
+                ("string".into(), rgba(0xa07d3aff).into()),
+                ("punctuation.special".into(), rgba(0xd5c5a1ff).into()),
+                ("keyword".into(), rgba(0x518b8bff).into()),
+                ("constructor".into(), rgba(0x518b8bff).into()),
+                ("predictive".into(), rgba(0x5c6152ff).into()),
+                ("title".into(), rgba(0xfdf4c1ff).into()),
+                ("variable".into(), rgba(0xfdf4c1ff).into()),
+                ("emphasis.strong".into(), rgba(0x518b8bff).into()),
+                ("primary".into(), rgba(0xfdf4c1ff).into()),
+                ("emphasis".into(), rgba(0x518b8bff).into()),
+                ("punctuation".into(), rgba(0xd5c5a1ff).into()),
+                ("constant".into(), rgba(0x83a598ff).into()),
+                ("link_text".into(), rgba(0xa07d3aff).into()),
+                ("punctuation.delimiter".into(), rgba(0xd5c5a1ff).into()),
+                ("embedded".into(), rgba(0xfdf4c1ff).into()),
+                ("string.special.symbol".into(), rgba(0xa07d3aff).into()),
+                ("tag".into(), rgba(0x518b8bff).into()),
+                ("punctuation.list_marker".into(), rgba(0xd5c5a1ff).into()),
+                ("operator".into(), rgba(0xa07d3aff).into()),
+                ("boolean".into(), rgba(0x83a598ff).into()),
+                ("function".into(), rgba(0xa07d3aff).into()),
+                ("attribute".into(), rgba(0x518b8bff).into()),
+                ("number".into(), rgba(0x83a598ff).into()),
+                ("string.escape".into(), rgba(0xa89984ff).into()),
+                ("comment.doc".into(), rgba(0xa89984ff).into()),
+                ("label".into(), rgba(0x518b8bff).into()),
+                ("string.regex".into(), rgba(0xa07d3aff).into()),
+                ("property".into(), rgba(0x518b8bff).into()),
+                ("variant".into(), rgba(0x518b8bff).into()),
+            ],
         },
         status_bar: rgba(0x333944ff).into(),
         title_bar: rgba(0x333944ff).into(),

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

@@ -0,0 +1,130 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn solarized_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Solarized Dark".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x2b4e58ff).into(),
+        border_variant: rgba(0x2b4e58ff).into(),
+        border_focused: rgba(0x1b3149ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x073743ff).into(),
+        surface: rgba(0x04313bff).into(),
+        background: rgba(0x073743ff).into(),
+        filled_element: rgba(0x073743ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x141f2cff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x141f2cff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xfdf6e3ff).into(),
+        text_muted: rgba(0x93a1a1ff).into(),
+        text_placeholder: rgba(0xdc3330ff).into(),
+        text_disabled: rgba(0x6f8389ff).into(),
+        text_accent: rgba(0x278ad1ff).into(),
+        icon_muted: rgba(0x93a1a1ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("punctuation.special".into(), rgba(0xefe9d6ff).into()),
+                ("string".into(), rgba(0xcb4b16ff).into()),
+                ("variant".into(), rgba(0x278ad1ff).into()),
+                ("variable".into(), rgba(0xfdf6e3ff).into()),
+                ("string.special.symbol".into(), rgba(0xcb4b16ff).into()),
+                ("primary".into(), rgba(0xfdf6e3ff).into()),
+                ("type".into(), rgba(0x2ba198ff).into()),
+                ("boolean".into(), rgba(0x849903ff).into()),
+                ("string.special".into(), rgba(0xcb4b16ff).into()),
+                ("label".into(), rgba(0x278ad1ff).into()),
+                ("link_uri".into(), rgba(0x849903ff).into()),
+                ("constructor".into(), rgba(0x278ad1ff).into()),
+                ("hint".into(), rgba(0x4f8297ff).into()),
+                ("preproc".into(), rgba(0xfdf6e3ff).into()),
+                ("text.literal".into(), rgba(0xcb4b16ff).into()),
+                ("string.escape".into(), rgba(0x99a5a4ff).into()),
+                ("link_text".into(), rgba(0xcb4b16ff).into()),
+                ("comment".into(), rgba(0x99a5a4ff).into()),
+                ("enum".into(), rgba(0xcb4b16ff).into()),
+                ("constant".into(), rgba(0x849903ff).into()),
+                ("comment.doc".into(), rgba(0x99a5a4ff).into()),
+                ("emphasis".into(), rgba(0x278ad1ff).into()),
+                ("predictive".into(), rgba(0x3f718bff).into()),
+                ("attribute".into(), rgba(0x278ad1ff).into()),
+                ("punctuation.delimiter".into(), rgba(0xefe9d6ff).into()),
+                ("function".into(), rgba(0xb58902ff).into()),
+                ("emphasis.strong".into(), rgba(0x278ad1ff).into()),
+                ("tag".into(), rgba(0x278ad1ff).into()),
+                ("string.regex".into(), rgba(0xcb4b16ff).into()),
+                ("property".into(), rgba(0x278ad1ff).into()),
+                ("keyword".into(), rgba(0x278ad1ff).into()),
+                ("number".into(), rgba(0x849903ff).into()),
+                ("embedded".into(), rgba(0xfdf6e3ff).into()),
+                ("operator".into(), rgba(0xcb4b16ff).into()),
+                ("punctuation".into(), rgba(0xefe9d6ff).into()),
+                ("punctuation.bracket".into(), rgba(0xefe9d6ff).into()),
+                ("title".into(), rgba(0xfdf6e3ff).into()),
+                ("punctuation.list_marker".into(), rgba(0xefe9d6ff).into()),
+            ],
+        },
+        status_bar: rgba(0x073743ff).into(),
+        title_bar: rgba(0x073743ff).into(),
+        toolbar: rgba(0x002a35ff).into(),
+        tab_bar: rgba(0x04313bff).into(),
+        editor: rgba(0x002a35ff).into(),
+        editor_subheader: rgba(0x04313bff).into(),
+        editor_active_line: rgba(0x04313bff).into(),
+        terminal: rgba(0x002a35ff).into(),
+        image_fallback_background: rgba(0x073743ff).into(),
+        git_created: rgba(0x849903ff).into(),
+        git_modified: rgba(0x278ad1ff).into(),
+        git_deleted: rgba(0xdc3330ff).into(),
+        git_conflict: rgba(0xb58902ff).into(),
+        git_ignored: rgba(0x6f8389ff).into(),
+        git_renamed: rgba(0xb58902ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x278ad1ff).into(),
+                selection: rgba(0x278ad13d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x849903ff).into(),
+                selection: rgba(0x8499033d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd33781ff).into(),
+                selection: rgba(0xd337813d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xcb4b16ff).into(),
+                selection: rgba(0xcb4b163d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x6c71c4ff).into(),
+                selection: rgba(0x6c71c43d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x2ba198ff).into(),
+                selection: rgba(0x2ba1983d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xdc3330ff).into(),
+                selection: rgba(0xdc33303d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb58902ff).into(),
+                selection: rgba(0xb589023d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,130 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn solarized_light() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Solarized Light".into(),
+            is_light: true,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x9faaa8ff).into(),
+        border_variant: rgba(0x9faaa8ff).into(),
+        border_focused: rgba(0xbfd3efff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0xcfd0c4ff).into(),
+        surface: rgba(0xf3eddaff).into(),
+        background: rgba(0xcfd0c4ff).into(),
+        filled_element: rgba(0xcfd0c4ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0xdbe6f6ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0xdbe6f6ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0x002a35ff).into(),
+        text_muted: rgba(0x34555eff).into(),
+        text_placeholder: rgba(0xdc3330ff).into(),
+        text_disabled: rgba(0x6a7f86ff).into(),
+        text_accent: rgba(0x288bd1ff).into(),
+        icon_muted: rgba(0x34555eff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("string.escape".into(), rgba(0x30525bff).into()),
+                ("boolean".into(), rgba(0x849903ff).into()),
+                ("comment.doc".into(), rgba(0x30525bff).into()),
+                ("string.special".into(), rgba(0xcb4b17ff).into()),
+                ("punctuation".into(), rgba(0x04333eff).into()),
+                ("emphasis".into(), rgba(0x288bd1ff).into()),
+                ("type".into(), rgba(0x2ba198ff).into()),
+                ("preproc".into(), rgba(0x002a35ff).into()),
+                ("emphasis.strong".into(), rgba(0x288bd1ff).into()),
+                ("constant".into(), rgba(0x849903ff).into()),
+                ("title".into(), rgba(0x002a35ff).into()),
+                ("operator".into(), rgba(0xcb4b17ff).into()),
+                ("punctuation.bracket".into(), rgba(0x04333eff).into()),
+                ("link_uri".into(), rgba(0x849903ff).into()),
+                ("label".into(), rgba(0x288bd1ff).into()),
+                ("enum".into(), rgba(0xcb4b17ff).into()),
+                ("property".into(), rgba(0x288bd1ff).into()),
+                ("predictive".into(), rgba(0x679aafff).into()),
+                ("punctuation.special".into(), rgba(0x04333eff).into()),
+                ("text.literal".into(), rgba(0xcb4b17ff).into()),
+                ("string".into(), rgba(0xcb4b17ff).into()),
+                ("string.regex".into(), rgba(0xcb4b17ff).into()),
+                ("variable".into(), rgba(0x002a35ff).into()),
+                ("tag".into(), rgba(0x288bd1ff).into()),
+                ("string.special.symbol".into(), rgba(0xcb4b17ff).into()),
+                ("link_text".into(), rgba(0xcb4b17ff).into()),
+                ("punctuation.list_marker".into(), rgba(0x04333eff).into()),
+                ("keyword".into(), rgba(0x288bd1ff).into()),
+                ("constructor".into(), rgba(0x288bd1ff).into()),
+                ("attribute".into(), rgba(0x288bd1ff).into()),
+                ("variant".into(), rgba(0x288bd1ff).into()),
+                ("function".into(), rgba(0xb58903ff).into()),
+                ("primary".into(), rgba(0x002a35ff).into()),
+                ("hint".into(), rgba(0x5789a3ff).into()),
+                ("comment".into(), rgba(0x30525bff).into()),
+                ("number".into(), rgba(0x849903ff).into()),
+                ("punctuation.delimiter".into(), rgba(0x04333eff).into()),
+                ("embedded".into(), rgba(0x002a35ff).into()),
+            ],
+        },
+        status_bar: rgba(0xcfd0c4ff).into(),
+        title_bar: rgba(0xcfd0c4ff).into(),
+        toolbar: rgba(0xfdf6e3ff).into(),
+        tab_bar: rgba(0xf3eddaff).into(),
+        editor: rgba(0xfdf6e3ff).into(),
+        editor_subheader: rgba(0xf3eddaff).into(),
+        editor_active_line: rgba(0xf3eddaff).into(),
+        terminal: rgba(0xfdf6e3ff).into(),
+        image_fallback_background: rgba(0xcfd0c4ff).into(),
+        git_created: rgba(0x849903ff).into(),
+        git_modified: rgba(0x288bd1ff).into(),
+        git_deleted: rgba(0xdc3330ff).into(),
+        git_conflict: rgba(0xb58903ff).into(),
+        git_ignored: rgba(0x6a7f86ff).into(),
+        git_renamed: rgba(0xb58903ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x288bd1ff).into(),
+                selection: rgba(0x288bd13d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x849903ff).into(),
+                selection: rgba(0x8499033d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd33781ff).into(),
+                selection: rgba(0xd337813d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xcb4b17ff).into(),
+                selection: rgba(0xcb4b173d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x6c71c3ff).into(),
+                selection: rgba(0x6c71c33d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x2ba198ff).into(),
+                selection: rgba(0x2ba1983d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xdc3330ff).into(),
+                selection: rgba(0xdc33303d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb58903ff).into(),
+                selection: rgba(0xb589033d).into(),
+            },
+        ],
+    }
+}

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

@@ -0,0 +1,130 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn summercamp() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Summercamp".into(),
+            is_light: false,
+        },
+        transparent: rgba(0x00000000).into(),
+        mac_os_traffic_light_red: rgba(0xec695eff).into(),
+        mac_os_traffic_light_yellow: rgba(0xf4bf4eff).into(),
+        mac_os_traffic_light_green: rgba(0x61c553ff).into(),
+        border: rgba(0x302c21ff).into(),
+        border_variant: rgba(0x302c21ff).into(),
+        border_focused: rgba(0x193760ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x2a261cff).into(),
+        surface: rgba(0x231f16ff).into(),
+        background: rgba(0x2a261cff).into(),
+        filled_element: rgba(0x2a261cff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x0e2242ff).into(),
+        filled_element_disabled: rgba(0x00000000).into(),
+        ghost_element: rgba(0x00000000).into(),
+        ghost_element_hover: rgba(0xffffff14).into(),
+        ghost_element_active: rgba(0xffffff1e).into(),
+        ghost_element_selected: rgba(0x0e2242ff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xf8f5deff).into(),
+        text_muted: rgba(0x736e55ff).into(),
+        text_placeholder: rgba(0xe35041ff).into(),
+        text_disabled: rgba(0x4c4735ff).into(),
+        text_accent: rgba(0x499befff).into(),
+        icon_muted: rgba(0x736e55ff).into(),
+        syntax: SyntaxTheme {
+            highlights: vec![
+                ("predictive".into(), rgba(0x78434aff).into()),
+                ("title".into(), rgba(0xf8f5deff).into()),
+                ("primary".into(), rgba(0xf8f5deff).into()),
+                ("punctuation.special".into(), rgba(0xbfbb9bff).into()),
+                ("constant".into(), rgba(0x5dea5aff).into()),
+                ("string.regex".into(), rgba(0xfaa11cff).into()),
+                ("tag".into(), rgba(0x499befff).into()),
+                ("preproc".into(), rgba(0xf8f5deff).into()),
+                ("comment".into(), rgba(0x777159ff).into()),
+                ("punctuation.bracket".into(), rgba(0xbfbb9bff).into()),
+                ("constructor".into(), rgba(0x499befff).into()),
+                ("type".into(), rgba(0x5aeabbff).into()),
+                ("variable".into(), rgba(0xf8f5deff).into()),
+                ("operator".into(), rgba(0xfaa11cff).into()),
+                ("boolean".into(), rgba(0x5dea5aff).into()),
+                ("attribute".into(), rgba(0x499befff).into()),
+                ("link_text".into(), rgba(0xfaa11cff).into()),
+                ("string.escape".into(), rgba(0x777159ff).into()),
+                ("string.special".into(), rgba(0xfaa11cff).into()),
+                ("string.special.symbol".into(), rgba(0xfaa11cff).into()),
+                ("hint".into(), rgba(0x246e61ff).into()),
+                ("link_uri".into(), rgba(0x5dea5aff).into()),
+                ("comment.doc".into(), rgba(0x777159ff).into()),
+                ("emphasis".into(), rgba(0x499befff).into()),
+                ("punctuation".into(), rgba(0xbfbb9bff).into()),
+                ("text.literal".into(), rgba(0xfaa11cff).into()),
+                ("number".into(), rgba(0x5dea5aff).into()),
+                ("punctuation.delimiter".into(), rgba(0xbfbb9bff).into()),
+                ("label".into(), rgba(0x499befff).into()),
+                ("function".into(), rgba(0xf1fe28ff).into()),
+                ("property".into(), rgba(0x499befff).into()),
+                ("keyword".into(), rgba(0x499befff).into()),
+                ("embedded".into(), rgba(0xf8f5deff).into()),
+                ("string".into(), rgba(0xfaa11cff).into()),
+                ("punctuation.list_marker".into(), rgba(0xbfbb9bff).into()),
+                ("enum".into(), rgba(0xfaa11cff).into()),
+                ("emphasis.strong".into(), rgba(0x499befff).into()),
+                ("variant".into(), rgba(0x499befff).into()),
+            ],
+        },
+        status_bar: rgba(0x2a261cff).into(),
+        title_bar: rgba(0x2a261cff).into(),
+        toolbar: rgba(0x1b1810ff).into(),
+        tab_bar: rgba(0x231f16ff).into(),
+        editor: rgba(0x1b1810ff).into(),
+        editor_subheader: rgba(0x231f16ff).into(),
+        editor_active_line: rgba(0x231f16ff).into(),
+        terminal: rgba(0x1b1810ff).into(),
+        image_fallback_background: rgba(0x2a261cff).into(),
+        git_created: rgba(0x5dea5aff).into(),
+        git_modified: rgba(0x499befff).into(),
+        git_deleted: rgba(0xe35041ff).into(),
+        git_conflict: rgba(0xf1fe28ff).into(),
+        git_ignored: rgba(0x4c4735ff).into(),
+        git_renamed: rgba(0xf1fe28ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x499befff).into(),
+                selection: rgba(0x499bef3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5dea5aff).into(),
+                selection: rgba(0x5dea5a3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf59be6ff).into(),
+                selection: rgba(0xf59be63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfaa11cff).into(),
+                selection: rgba(0xfaa11c3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xfe8080ff).into(),
+                selection: rgba(0xfe80803d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x5aeabbff).into(),
+                selection: rgba(0x5aeabb3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xe35041ff).into(),
+                selection: rgba(0xe350413d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xf1fe28ff).into(),
+                selection: rgba(0xf1fe283d).into(),
+            },
+        ],
+    }
+}

crates/theme_converter/Cargo.toml 🔗

@@ -9,6 +9,7 @@ publish = false
 [dependencies]
 anyhow.workspace = true
 clap = { version = "4.4", features = ["derive", "string"] }
+convert_case = "0.6.0"
 gpui2 = { path = "../gpui2" }
 log.workspace = true
 rust-embed.workspace = true

crates/theme_converter/src/main.rs 🔗

@@ -1,16 +1,25 @@
+mod theme_printer;
+
 use std::borrow::Cow;
 use std::collections::HashMap;
 use std::fmt::{self, Debug};
+use std::fs::File;
+use std::io::Write;
+use std::path::PathBuf;
+use std::str::FromStr;
 
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
-use gpui2::{hsla, rgb, serde_json, AssetSource, Hsla, Rgba, SharedString};
+use convert_case::{Case, Casing};
+use gpui2::{hsla, rgb, serde_json, AssetSource, Hsla, SharedString};
 use log::LevelFilter;
 use rust_embed::RustEmbed;
 use serde::de::Visitor;
 use serde::{Deserialize, Deserializer};
 use simplelog::SimpleLogger;
-use theme2::{PlayerTheme, SyntaxTheme, ThemeMetadata};
+use theme2::{PlayerTheme, SyntaxTheme};
+
+use crate::theme_printer::ThemePrinter;
 
 #[derive(Parser)]
 #[command(author, version, about, long_about = None)]
@@ -22,13 +31,71 @@ struct Args {
 fn main() -> Result<()> {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
-    let args = Args::parse();
+    // let args = Args::parse();
+
+    let themes_path = PathBuf::from_str("crates/theme2/src/themes")?;
+
+    let mut theme_modules = Vec::new();
+
+    for theme_path in Assets.list("themes/")? {
+        let (_, theme_name) = theme_path.split_once("themes/").unwrap();
+
+        if theme_name == ".gitkeep" {
+            continue;
+        }
+
+        let (json_theme, legacy_theme) = load_theme(&theme_path)?;
+
+        let theme = convert_theme(json_theme, legacy_theme)?;
+
+        let theme_slug = theme
+            .metadata
+            .name
+            .as_ref()
+            .replace("é", "e")
+            .to_case(Case::Snake);
 
-    let legacy_theme = load_theme(args.theme)?;
+        let mut output_file = File::create(themes_path.join(format!("{theme_slug}.rs")))?;
 
-    let theme = convert_theme(legacy_theme)?;
+        let theme_module = format!(
+            r#"
+                use gpui2::rgba;
 
-    println!("{:#?}", ThemePrinter(theme));
+                use crate::{{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}};
+
+                pub fn {theme_slug}() -> Theme {{
+                    {theme_definition}
+                }}
+            "#,
+            theme_definition = format!("{:#?}", ThemePrinter::new(theme))
+        );
+
+        output_file.write_all(theme_module.as_bytes())?;
+
+        theme_modules.push(theme_slug);
+    }
+
+    let mut mod_rs_file = File::create(themes_path.join(format!("mod.rs")))?;
+
+    let mod_rs_contents = format!(
+        r#"
+        {mod_statements}
+
+        {use_statements}
+        "#,
+        mod_statements = theme_modules
+            .iter()
+            .map(|module| format!("mod {module};"))
+            .collect::<Vec<_>>()
+            .join("\n"),
+        use_statements = theme_modules
+            .iter()
+            .map(|module| format!("pub use {module}::*;"))
+            .collect::<Vec<_>>()
+            .join("\n")
+    );
+
+    mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
 
     Ok(())
 }
@@ -89,129 +156,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,13 +236,24 @@ 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> {
-    let theme_contents = Assets::get(&format!("themes/{name}.json"))
-        .with_context(|| format!("theme file not found: '{name}'"))?;
+fn load_theme(theme_path: &str) -> Result<(JsonTheme, LegacyTheme)> {
+    let theme_contents =
+        Assets::get(theme_path).with_context(|| format!("theme file not found: '{theme_path}'"))?;
 
     let json_theme: JsonTheme = serde_json::from_str(std::str::from_utf8(&theme_contents.data)?)
         .context("failed to parse legacy theme")?;
@@ -235,7 +261,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)]
@@ -362,159 +388,3 @@ where
     }
     deserializer.deserialize_map(SyntaxVisitor)
 }
-
-pub struct ThemePrinter(theme2::Theme);
-
-struct HslaPrinter(Hsla);
-
-impl Debug for HslaPrinter {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{:?}", IntoPrinter(&Rgba::from(self.0)))
-    }
-}
-
-struct IntoPrinter<'a, D: Debug>(&'a D);
-
-impl<'a, D: Debug> Debug for IntoPrinter<'a, D> {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{:?}.into()", self.0)
-    }
-}
-
-impl Debug for ThemePrinter {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("Theme")
-            .field("metadata", &ThemeMetadataPrinter(self.0.metadata.clone()))
-            .field("transparent", &HslaPrinter(self.0.transparent))
-            .field(
-                "mac_os_traffic_light_red",
-                &HslaPrinter(self.0.mac_os_traffic_light_red),
-            )
-            .field(
-                "mac_os_traffic_light_yellow",
-                &HslaPrinter(self.0.mac_os_traffic_light_yellow),
-            )
-            .field(
-                "mac_os_traffic_light_green",
-                &HslaPrinter(self.0.mac_os_traffic_light_green),
-            )
-            .field("border", &HslaPrinter(self.0.border))
-            .field("border_variant", &HslaPrinter(self.0.border_variant))
-            .field("border_focused", &HslaPrinter(self.0.border_focused))
-            .field(
-                "border_transparent",
-                &HslaPrinter(self.0.border_transparent),
-            )
-            .field("elevated_surface", &HslaPrinter(self.0.elevated_surface))
-            .field("surface", &HslaPrinter(self.0.surface))
-            .field("background", &HslaPrinter(self.0.background))
-            .field("filled_element", &HslaPrinter(self.0.filled_element))
-            .field(
-                "filled_element_hover",
-                &HslaPrinter(self.0.filled_element_hover),
-            )
-            .field(
-                "filled_element_active",
-                &HslaPrinter(self.0.filled_element_active),
-            )
-            .field(
-                "filled_element_selected",
-                &HslaPrinter(self.0.filled_element_selected),
-            )
-            .field(
-                "filled_element_disabled",
-                &HslaPrinter(self.0.filled_element_disabled),
-            )
-            .field("ghost_element", &HslaPrinter(self.0.ghost_element))
-            .field(
-                "ghost_element_hover",
-                &HslaPrinter(self.0.ghost_element_hover),
-            )
-            .field(
-                "ghost_element_active",
-                &HslaPrinter(self.0.ghost_element_active),
-            )
-            .field(
-                "ghost_element_selected",
-                &HslaPrinter(self.0.ghost_element_selected),
-            )
-            .field(
-                "ghost_element_disabled",
-                &HslaPrinter(self.0.ghost_element_disabled),
-            )
-            .field("text", &HslaPrinter(self.0.text))
-            .field("text_muted", &HslaPrinter(self.0.text_muted))
-            .field("text_placeholder", &HslaPrinter(self.0.text_placeholder))
-            .field("text_disabled", &HslaPrinter(self.0.text_disabled))
-            .field("text_accent", &HslaPrinter(self.0.text_accent))
-            .field("icon_muted", &HslaPrinter(self.0.icon_muted))
-            .field("syntax", &SyntaxThemePrinter(self.0.syntax.clone()))
-            .field("status_bar", &HslaPrinter(self.0.status_bar))
-            .field("title_bar", &HslaPrinter(self.0.title_bar))
-            .field("toolbar", &HslaPrinter(self.0.toolbar))
-            .field("tab_bar", &HslaPrinter(self.0.tab_bar))
-            .field("editor", &HslaPrinter(self.0.editor))
-            .field("editor_subheader", &HslaPrinter(self.0.editor_subheader))
-            .field(
-                "editor_active_line",
-                &HslaPrinter(self.0.editor_active_line),
-            )
-            .field("terminal", &HslaPrinter(self.0.terminal))
-            .field(
-                "image_fallback_background",
-                &HslaPrinter(self.0.image_fallback_background),
-            )
-            .field("git_created", &HslaPrinter(self.0.git_created))
-            .field("git_modified", &HslaPrinter(self.0.git_modified))
-            .field("git_deleted", &HslaPrinter(self.0.git_deleted))
-            .field("git_conflict", &HslaPrinter(self.0.git_conflict))
-            .field("git_ignored", &HslaPrinter(self.0.git_ignored))
-            .field("git_renamed", &HslaPrinter(self.0.git_renamed))
-            .field("players", &self.0.players.map(PlayerThemePrinter))
-            .finish()
-    }
-}
-
-pub struct ThemeMetadataPrinter(ThemeMetadata);
-
-impl Debug for ThemeMetadataPrinter {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("ThemeMetadata")
-            .field("name", &IntoPrinter(&self.0.name))
-            .field("is_light", &self.0.is_light)
-            .finish()
-    }
-}
-
-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))
-            .finish()
-    }
-}
-
-pub struct VecPrinter<'a, T>(&'a Vec<T>);
-
-impl<'a, T: Debug> Debug for VecPrinter<'a, T> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "vec!{:?}", &self.0)
-    }
-}
-
-pub struct PlayerThemePrinter(PlayerTheme);
-
-impl Debug for PlayerThemePrinter {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("PlayerTheme")
-            .field("cursor", &HslaPrinter(self.0.cursor))
-            .field("selection", &HslaPrinter(self.0.selection))
-            .finish()
-    }
-}

crates/theme_converter/src/theme_printer.rs 🔗

@@ -0,0 +1,174 @@
+use std::fmt::{self, Debug};
+
+use gpui2::{Hsla, Rgba};
+use theme2::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub struct ThemePrinter(Theme);
+
+impl ThemePrinter {
+    pub fn new(theme: Theme) -> Self {
+        Self(theme)
+    }
+}
+
+struct HslaPrinter(Hsla);
+
+impl Debug for HslaPrinter {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{:?}", IntoPrinter(&Rgba::from(self.0)))
+    }
+}
+
+struct IntoPrinter<'a, D: Debug>(&'a D);
+
+impl<'a, D: Debug> Debug for IntoPrinter<'a, D> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{:?}.into()", self.0)
+    }
+}
+
+impl Debug for ThemePrinter {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("Theme")
+            .field("metadata", &ThemeMetadataPrinter(self.0.metadata.clone()))
+            .field("transparent", &HslaPrinter(self.0.transparent))
+            .field(
+                "mac_os_traffic_light_red",
+                &HslaPrinter(self.0.mac_os_traffic_light_red),
+            )
+            .field(
+                "mac_os_traffic_light_yellow",
+                &HslaPrinter(self.0.mac_os_traffic_light_yellow),
+            )
+            .field(
+                "mac_os_traffic_light_green",
+                &HslaPrinter(self.0.mac_os_traffic_light_green),
+            )
+            .field("border", &HslaPrinter(self.0.border))
+            .field("border_variant", &HslaPrinter(self.0.border_variant))
+            .field("border_focused", &HslaPrinter(self.0.border_focused))
+            .field(
+                "border_transparent",
+                &HslaPrinter(self.0.border_transparent),
+            )
+            .field("elevated_surface", &HslaPrinter(self.0.elevated_surface))
+            .field("surface", &HslaPrinter(self.0.surface))
+            .field("background", &HslaPrinter(self.0.background))
+            .field("filled_element", &HslaPrinter(self.0.filled_element))
+            .field(
+                "filled_element_hover",
+                &HslaPrinter(self.0.filled_element_hover),
+            )
+            .field(
+                "filled_element_active",
+                &HslaPrinter(self.0.filled_element_active),
+            )
+            .field(
+                "filled_element_selected",
+                &HslaPrinter(self.0.filled_element_selected),
+            )
+            .field(
+                "filled_element_disabled",
+                &HslaPrinter(self.0.filled_element_disabled),
+            )
+            .field("ghost_element", &HslaPrinter(self.0.ghost_element))
+            .field(
+                "ghost_element_hover",
+                &HslaPrinter(self.0.ghost_element_hover),
+            )
+            .field(
+                "ghost_element_active",
+                &HslaPrinter(self.0.ghost_element_active),
+            )
+            .field(
+                "ghost_element_selected",
+                &HslaPrinter(self.0.ghost_element_selected),
+            )
+            .field(
+                "ghost_element_disabled",
+                &HslaPrinter(self.0.ghost_element_disabled),
+            )
+            .field("text", &HslaPrinter(self.0.text))
+            .field("text_muted", &HslaPrinter(self.0.text_muted))
+            .field("text_placeholder", &HslaPrinter(self.0.text_placeholder))
+            .field("text_disabled", &HslaPrinter(self.0.text_disabled))
+            .field("text_accent", &HslaPrinter(self.0.text_accent))
+            .field("icon_muted", &HslaPrinter(self.0.icon_muted))
+            .field("syntax", &SyntaxThemePrinter(self.0.syntax.clone()))
+            .field("status_bar", &HslaPrinter(self.0.status_bar))
+            .field("title_bar", &HslaPrinter(self.0.title_bar))
+            .field("toolbar", &HslaPrinter(self.0.toolbar))
+            .field("tab_bar", &HslaPrinter(self.0.tab_bar))
+            .field("editor", &HslaPrinter(self.0.editor))
+            .field("editor_subheader", &HslaPrinter(self.0.editor_subheader))
+            .field(
+                "editor_active_line",
+                &HslaPrinter(self.0.editor_active_line),
+            )
+            .field("terminal", &HslaPrinter(self.0.terminal))
+            .field(
+                "image_fallback_background",
+                &HslaPrinter(self.0.image_fallback_background),
+            )
+            .field("git_created", &HslaPrinter(self.0.git_created))
+            .field("git_modified", &HslaPrinter(self.0.git_modified))
+            .field("git_deleted", &HslaPrinter(self.0.git_deleted))
+            .field("git_conflict", &HslaPrinter(self.0.git_conflict))
+            .field("git_ignored", &HslaPrinter(self.0.git_ignored))
+            .field("git_renamed", &HslaPrinter(self.0.git_renamed))
+            .field("players", &self.0.players.map(PlayerThemePrinter))
+            .finish()
+    }
+}
+
+pub struct ThemeMetadataPrinter(ThemeMetadata);
+
+impl Debug for ThemeMetadataPrinter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ThemeMetadata")
+            .field("name", &IntoPrinter(&self.0.name))
+            .field("is_light", &self.0.is_light)
+            .finish()
+    }
+}
+
+pub struct SyntaxThemePrinter(SyntaxTheme);
+
+impl Debug for SyntaxThemePrinter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("SyntaxTheme")
+            .field(
+                "highlights",
+                &VecPrinter(
+                    &self
+                        .0
+                        .highlights
+                        .iter()
+                        .map(|(token, highlight)| {
+                            (IntoPrinter(token), HslaPrinter(highlight.color.unwrap()))
+                        })
+                        .collect(),
+                ),
+            )
+            .finish()
+    }
+}
+
+pub struct VecPrinter<'a, T>(&'a Vec<T>);
+
+impl<'a, T: Debug> Debug for VecPrinter<'a, T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "vec!{:?}", &self.0)
+    }
+}
+
+pub struct PlayerThemePrinter(PlayerTheme);
+
+impl Debug for PlayerThemePrinter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("PlayerTheme")
+            .field("cursor", &HslaPrinter(self.0.cursor))
+            .field("selection", &HslaPrinter(self.0.selection))
+            .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 🔗

@@ -1,7 +1,6 @@
-use gpui2::{rems, AbsoluteLength};
-
 use crate::prelude::*;
 use crate::{Icon, IconButton, Label, Panel, PanelSide};
+use gpui2::{rems, AbsoluteLength};
 
 #[derive(Component)]
 pub struct AssistantPanel {
@@ -76,19 +75,15 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+    pub struct AssistantPanelStory;
 
-    #[derive(Component)]
-    pub struct AssistantPanelStory {}
-
-    impl AssistantPanelStory {
-        pub fn new() -> Self {
-            Self {}
-        }
+    impl Render for AssistantPanelStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, AssistantPanel>(cx))
                 .child(Story::label(cx, "Default"))

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> {
@@ -76,21 +73,17 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use std::str::FromStr;
-
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::Render;
+    use std::str::FromStr;
 
-    #[derive(Component)]
     pub struct BreadcrumbStory;
 
-    impl BreadcrumbStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for BreadcrumbStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, view_state: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             let theme = theme(cx);
 
             Story::container(cx)
@@ -102,21 +95,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 🔗

@@ -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()
@@ -233,24 +233,19 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use gpui2::rems;
-
+    use super::*;
     use crate::{
         empty_buffer_example, hello_world_rust_buffer_example,
         hello_world_rust_buffer_with_status_example, Story,
     };
+    use gpui2::{rems, Div, Render};
 
-    use super::*;
-
-    #[derive(Component)]
     pub struct BufferStory;
 
-    impl BufferStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for BufferStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             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::{Div, Render, View, VisualContext};
 
 use crate::prelude::*;
 use crate::{h_stack, Icon, IconButton, IconColor, Input};
@@ -22,10 +22,14 @@ impl BufferSearch {
     }
 
     pub fn view(cx: &mut WindowContext) -> View<Self> {
-        view(cx.entity(|cx| Self::new()), Self::render)
+        cx.build_view(|cx| Self::new())
     }
+}
+
+impl Render for BufferSearch {
+    type Element = Div<Self>;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
         let theme = theme(cx);
 
         h_stack().bg(theme.toolbar).p_2().child(

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

@@ -108,20 +108,18 @@ pub use stories::*;
 #[cfg(feature = "stories")]
 mod stories {
     use chrono::DateTime;
+    use gpui2::{Div, Render};
 
     use crate::{Panel, Story};
 
     use super::*;
 
-    #[derive(Component)]
     pub struct ChatPanelStory;
 
-    impl ChatPanelStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for ChatPanelStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, ChatPanel>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -89,19 +89,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct CollabPanelStory;
 
-    impl CollabPanelStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for CollabPanelStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, CollabPanel>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -27,19 +27,18 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
+    use gpui2::{Div, Render};
+
     use crate::Story;
 
     use super::*;
 
-    #[derive(Component)]
     pub struct CommandPaletteStory;
 
-    impl CommandPaletteStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for CommandPaletteStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, CommandPalette>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -68,19 +68,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::story::Story;
-
     use super::*;
+    use crate::story::Story;
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct ContextMenuStory;
 
-    impl ContextMenuStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for ContextMenuStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, ContextMenu>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -25,19 +25,18 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
+    use gpui2::{Div, Render};
+
     use crate::Story;
 
     use super::*;
 
-    #[derive(Component)]
     pub struct CopilotModalStory;
 
-    impl CopilotModalStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for CopilotModalStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             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::{Div, Render, View, VisualContext};
 
 use crate::prelude::*;
 use crate::{
@@ -20,7 +20,7 @@ pub struct EditorPane {
 
 impl EditorPane {
     pub fn new(
-        cx: &mut WindowContext,
+        cx: &mut ViewContext<Self>,
         tabs: Vec<Tab>,
         path: PathBuf,
         symbols: Vec<Symbol>,
@@ -43,13 +43,14 @@ impl EditorPane {
     }
 
     pub fn view(cx: &mut WindowContext) -> View<Self> {
-        view(
-            cx.entity(|cx| hello_world_rust_editor_with_status_example(cx)),
-            Self::render,
-        )
+        cx.build_view(|cx| hello_world_rust_editor_with_status_example(cx))
     }
+}
+
+impl Render for EditorPane {
+    type Element = Div<Self>;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
         v_stack()
             .w_full()
             .h_full()

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

@@ -31,19 +31,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::{static_players, Story};
-
     use super::*;
+    use crate::{static_players, Story};
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct FacepileStory;
 
-    impl FacepileStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for FacepileStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             let players = static_players();
 
             Story::container(cx)

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

@@ -57,7 +57,10 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
-    pub fn on_click(mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext<V>) + 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
     }

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

@@ -158,21 +158,17 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use itertools::Itertools;
-
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+    use itertools::Itertools;
 
-    #[derive(Component)]
     pub struct KeybindingStory;
 
-    impl KeybindingStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for KeybindingStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             let all_modifier_permutations = ModifierKey::iter().permutations(2);
 
             Story::container(cx)

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

@@ -38,19 +38,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct LanguageSelectorStory;
 
-    impl LanguageSelectorStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for LanguageSelectorStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, LanguageSelector>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -40,19 +40,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::{hello_world_rust_buffer_example, Story};
-
     use super::*;
+    use crate::{hello_world_rust_buffer_example, Story};
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct MultiBufferStory;
 
-    impl MultiBufferStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for MultiBufferStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             let theme = theme(cx);
 
             Story::container(cx)

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

@@ -48,19 +48,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::{Panel, Story};
-
     use super::*;
+    use crate::{Panel, Story};
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct NotificationsPanelStory;
 
-    impl NotificationsPanelStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for NotificationsPanelStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, NotificationsPanel>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -152,62 +152,71 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
+    use gpui2::{Div, Render};
+
     use crate::{ModifierKeys, Story};
 
     use super::*;
 
-    #[derive(Component)]
     pub struct PaletteStory;
 
-    impl PaletteStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for PaletteStory {
+        type Element = Div<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))
-                .child(Story::label(cx, "Default"))
-                .child(Palette::new("palette-1"))
-                .child(Story::label(cx, "With Items"))
-                .child(
-                    Palette::new("palette-2")
-                        .placeholder("Execute a command...")
-                        .items(vec![
-                            PaletteItem::new("theme selector: toggle").keybinding(
-                                Keybinding::new_chord(
-                                    ("k".to_string(), ModifierKeys::new().command(true)),
-                                    ("t".to_string(), ModifierKeys::new().command(true)),
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            {
+                Story::container(cx)
+                    .child(Story::title_for::<_, Palette>(cx))
+                    .child(Story::label(cx, "Default"))
+                    .child(Palette::new("palette-1"))
+                    .child(Story::label(cx, "With Items"))
+                    .child(
+                        Palette::new("palette-2")
+                            .placeholder("Execute a command...")
+                            .items(vec![
+                                PaletteItem::new("theme selector: toggle").keybinding(
+                                    Keybinding::new_chord(
+                                        ("k".to_string(), ModifierKeys::new().command(true)),
+                                        ("t".to_string(), ModifierKeys::new().command(true)),
+                                    ),
+                                ),
+                                PaletteItem::new("assistant: inline assist").keybinding(
+                                    Keybinding::new(
+                                        "enter".to_string(),
+                                        ModifierKeys::new().command(true),
+                                    ),
+                                ),
+                                PaletteItem::new("assistant: quote selection").keybinding(
+                                    Keybinding::new(
+                                        ">".to_string(),
+                                        ModifierKeys::new().command(true),
+                                    ),
+                                ),
+                                PaletteItem::new("assistant: toggle focus").keybinding(
+                                    Keybinding::new(
+                                        "?".to_string(),
+                                        ModifierKeys::new().command(true),
+                                    ),
                                 ),
-                            ),
-                            PaletteItem::new("assistant: inline assist").keybinding(
-                                Keybinding::new(
-                                    "enter".to_string(),
-                                    ModifierKeys::new().command(true),
+                                PaletteItem::new("auto update: check"),
+                                PaletteItem::new("auto update: view release notes"),
+                                PaletteItem::new("branches: open recent").keybinding(
+                                    Keybinding::new(
+                                        "b".to_string(),
+                                        ModifierKeys::new().command(true).alt(true),
+                                    ),
                                 ),
-                            ),
-                            PaletteItem::new("assistant: quote selection").keybinding(
-                                Keybinding::new(">".to_string(), ModifierKeys::new().command(true)),
-                            ),
-                            PaletteItem::new("assistant: toggle focus").keybinding(
-                                Keybinding::new("?".to_string(), ModifierKeys::new().command(true)),
-                            ),
-                            PaletteItem::new("auto update: check"),
-                            PaletteItem::new("auto update: view release notes"),
-                            PaletteItem::new("branches: open recent").keybinding(Keybinding::new(
-                                "b".to_string(),
-                                ModifierKeys::new().command(true).alt(true),
-                            )),
-                            PaletteItem::new("chat panel: toggle focus"),
-                            PaletteItem::new("cli: install"),
-                            PaletteItem::new("client: sign in"),
-                            PaletteItem::new("client: sign out"),
-                            PaletteItem::new("editor: cancel").keybinding(Keybinding::new(
-                                "escape".to_string(),
-                                ModifierKeys::new(),
-                            )),
-                        ]),
-                )
+                                PaletteItem::new("chat panel: toggle focus"),
+                                PaletteItem::new("cli: install"),
+                                PaletteItem::new("client: sign in"),
+                                PaletteItem::new("client: sign out"),
+                                PaletteItem::new("editor: cancel").keybinding(Keybinding::new(
+                                    "escape".to_string(),
+                                    ModifierKeys::new(),
+                                )),
+                            ]),
+                    )
+            }
         }
     }
 }

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

@@ -128,21 +128,18 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::{Label, Story};
-
     use super::*;
+    use crate::{Label, Story};
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct PanelStory;
 
-    impl PanelStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for PanelStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
-                .child(Story::title_for::<_, Panel<V>>(cx))
+                .child(Story::title_for::<_, Panel<Self>>(cx))
                 .child(Story::label(cx, "Default"))
                 .child(
                     Panel::new("panel", cx).child(

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

@@ -1,4 +1,4 @@
-use gpui2::{hsla, red, AnyElement, ElementId, ExternalPaths, Hsla, Length, Size};
+use gpui2::{hsla, red, AnyElement, ElementId, ExternalPaths, Hsla, Length, Size, View};
 use smallvec::SmallVec;
 
 use crate::prelude::*;
@@ -18,13 +18,6 @@ pub struct Pane<V: 'static> {
     children: SmallVec<[AnyElement<V>; 2]>,
 }
 
-// impl<V: 'static> IntoAnyElement<V> for Pane<V> {
-//     fn into_any(self) -> AnyElement<V> {
-//         (move |view_state: &mut V, cx: &mut ViewContext<'_, '_, V>| self.render(view_state, cx))
-//             .into_any()
-//     }
-// }
-
 impl<V: 'static> Pane<V> {
     pub fn new(id: impl Into<ElementId>, size: Size<Length>) -> Self {
         // Fill is only here for debugging purposes, remove before release
@@ -57,8 +50,8 @@ impl<V: 'static> Pane<V> {
                     .z_index(1)
                     .id("drag-target")
                     .drag_over::<ExternalPaths>(|d| d.bg(red()))
-                    .on_drop(|_, files: ExternalPaths, _| {
-                        dbg!("dropped files!", files);
+                    .on_drop(|_, files: View<ExternalPaths>, cx| {
+                        dbg!("dropped files!", files.read(cx));
                     })
                     .absolute()
                     .inset_0(),

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

@@ -57,19 +57,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::{Panel, Story};
-
     use super::*;
+    use crate::{Panel, Story};
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct ProjectPanelStory;
 
-    impl ProjectPanelStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for ProjectPanelStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, ProjectPanel>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -34,19 +34,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct RecentProjectsStory;
 
-    impl RecentProjectsStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for RecentProjectsStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, RecentProjects>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -1,5 +1,6 @@
 use crate::prelude::*;
 use crate::{Icon, IconColor, IconElement, Label, LabelColor};
+use gpui2::{black, red, Div, ElementId, Render, View, VisualContext};
 
 #[derive(Component, Clone)]
 pub struct Tab {
@@ -19,6 +20,14 @@ struct TabDragState {
     title: String,
 }
 
+impl Render for TabDragState {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div().w_8().h_4().bg(red())
+    }
+}
+
 impl Tab {
     pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
@@ -118,12 +127,10 @@ impl Tab {
 
         div()
             .id(self.id.clone())
-            .on_drag(move |_view, _cx| {
-                Drag::new(drag_state.clone(), |view, cx| div().w_8().h_4().bg(red()))
-            })
+            .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
             .drag_over::<TabDragState>(|d| d.bg(black()))
-            .on_drop(|_view, state: TabDragState, cx| {
-                dbg!(state);
+            .on_drop(|_view, state: View<TabDragState>, cx| {
+                dbg!(state.read(cx));
             })
             .px_2()
             .py_0p5()
@@ -160,27 +167,21 @@ impl Tab {
     }
 }
 
-use gpui2::{black, red, Drag, ElementId};
 #[cfg(feature = "stories")]
 pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use strum::IntoEnumIterator;
-
-    use crate::{h_stack, v_stack, Icon, Story};
-
     use super::*;
+    use crate::{h_stack, v_stack, Icon, Story};
+    use strum::IntoEnumIterator;
 
-    #[derive(Component)]
     pub struct TabStory;
 
-    impl TabStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for TabStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             let git_statuses = GitStatus::iter();
             let fs_statuses = FileSystemStatus::iter();
 

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

@@ -92,19 +92,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct TabBarStory;
 
-    impl TabBarStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for TabBarStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, TabBar>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -83,19 +83,15 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
-
-    #[derive(Component)]
+    use crate::Story;
+    use gpui2::{Div, Render};
     pub struct TerminalStory;
 
-    impl TerminalStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for TerminalStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, Terminal>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -39,19 +39,18 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
+    use gpui2::{Div, Render};
+
     use crate::Story;
 
     use super::*;
 
-    #[derive(Component)]
     pub struct ThemeSelectorStory;
 
-    impl ThemeSelectorStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for ThemeSelectorStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             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::{Div, Render, View, VisualContext};
 
 use crate::prelude::*;
 use crate::settings::user_settings;
@@ -81,13 +81,14 @@ impl TitleBar {
     }
 
     pub fn view(cx: &mut WindowContext, livestream: Option<Livestream>) -> View<Self> {
-        view(
-            cx.entity(|cx| Self::new(cx).set_livestream(livestream)),
-            Self::render,
-        )
+        cx.build_view(|cx| Self::new(cx).set_livestream(livestream))
     }
+}
+
+impl Render for TitleBar {
+    type Element = Div<Self>;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
         let theme = theme(cx);
         let settings = user_settings(cx);
 
@@ -186,9 +187,8 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
 
     pub struct TitleBarStory {
         title_bar: View<TitleBar>,
@@ -196,15 +196,16 @@ mod stories {
 
     impl TitleBarStory {
         pub fn view(cx: &mut WindowContext) -> View<Self> {
-            view(
-                cx.entity(|cx| Self {
-                    title_bar: TitleBar::view(cx, None),
-                }),
-                Self::render,
-            )
+            cx.build_view(|cx| Self {
+                title_bar: TitleBar::view(cx, None),
+            })
         }
+    }
+
+    impl Render for TitleBarStory {
+        type Element = Div<Self>;
 
-        fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
             Story::container(cx)
                 .child(Story::title_for::<_, TitleBar>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -72,21 +72,20 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
+    use gpui2::{Div, Render};
+
     use crate::{Label, Story};
 
     use super::*;
 
-    #[derive(Component)]
     pub struct ToastStory;
 
-    impl ToastStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for ToastStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
-                .child(Story::title_for::<_, Toast<V>>(cx))
+                .child(Story::title_for::<_, Toast<Self>>(cx))
                 .child(Story::label(cx, "Default"))
                 .child(Toast::new(ToastOrigin::Bottom).child(Label::new("label")))
         }

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

@@ -75,23 +75,22 @@ mod stories {
     use std::path::PathBuf;
     use std::str::FromStr;
 
+    use gpui2::{Div, Render};
+
     use crate::{Breadcrumb, HighlightedText, Icon, IconButton, Story, Symbol};
 
     use super::*;
 
-    #[derive(Component)]
     pub struct ToolbarStory;
 
-    impl ToolbarStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for ToolbarStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             let theme = theme(cx);
 
             Story::container(cx)
-                .child(Story::title_for::<_, Toolbar<V>>(cx))
+                .child(Story::title_for::<_, Toolbar<Self>>(cx))
                 .child(Story::label(cx, "Default"))
                 .child(
                     Toolbar::new()
@@ -101,21 +100,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 🔗

@@ -77,19 +77,18 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
+    use gpui2::{Div, Render};
+
     use crate::Story;
 
     use super::*;
 
-    #[derive(Component)]
     pub struct TrafficLightsStory;
 
-    impl TrafficLightsStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for TrafficLightsStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, TrafficLights>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -1,15 +1,15 @@
 use std::sync::Arc;
 
 use chrono::DateTime;
-use gpui2::{px, relative, rems, view, Context, Size, View};
+use gpui2::{px, relative, rems, Div, Render, Size, View, VisualContext};
 
+use crate::{prelude::*, NotificationsPanel};
 use crate::{
-    old_theme, static_livestream, user_settings_mut, v_stack, AssistantPanel, Button, ChatMessage,
-    ChatPanel, CollabPanel, EditorPane, FakeSettings, Label, LanguageSelector, Pane, PaneGroup,
-    Panel, PanelAllowedSides, PanelSide, ProjectPanel, SettingValue, SplitDirection, StatusBar,
-    Terminal, TitleBar, Toast, ToastOrigin,
+    static_livestream, user_settings_mut, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel,
+    CollabPanel, EditorPane, FakeSettings, Label, LanguageSelector, Pane, PaneGroup, Panel,
+    PanelAllowedSides, PanelSide, ProjectPanel, SettingValue, SplitDirection, StatusBar, Terminal,
+    TitleBar, Toast, ToastOrigin,
 };
-use crate::{prelude::*, NotificationsPanel};
 
 #[derive(Clone)]
 pub struct Gpui2UiDebug {
@@ -171,11 +171,15 @@ impl Workspace {
     }
 
     pub fn view(cx: &mut WindowContext) -> View<Self> {
-        view(cx.entity(|cx| Self::new(cx)), Self::render)
+        cx.build_view(|cx| Self::new(cx))
     }
+}
 
-    pub fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
-        let theme = old_theme(cx).clone();
+impl Render for Workspace {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
+        let theme = theme(cx);
 
         // HACK: This should happen inside of `debug_toggle_user_settings`, but
         // we don't have `cx.global::<FakeSettings>()` in event handlers at the moment.
@@ -212,8 +216,8 @@ impl Workspace {
             .gap_0()
             .justify_start()
             .items_start()
-            .text_color(theme.lowest.base.default.foreground)
-            .bg(theme.lowest.base.default.background)
+            .text_color(theme.text)
+            .bg(theme.background)
             .child(self.title_bar.clone())
             .child(
                 div()
@@ -224,7 +228,7 @@ impl Workspace {
                     .overflow_hidden()
                     .border_t()
                     .border_b()
-                    .border_color(theme.lowest.base.default.border)
+                    .border_color(theme.border)
                     .children(
                         Some(
                             Panel::new("project-panel-outer", cx)
@@ -352,6 +356,7 @@ pub use stories::*;
 #[cfg(feature = "stories")]
 mod stories {
     use super::*;
+    use gpui2::VisualContext;
 
     pub struct WorkspaceStory {
         workspace: View<Workspace>,
@@ -359,12 +364,17 @@ mod stories {
 
     impl WorkspaceStory {
         pub fn view(cx: &mut WindowContext) -> View<Self> {
-            view(
-                cx.entity(|cx| Self {
-                    workspace: Workspace::view(cx),
-                }),
-                |view, cx| view.workspace.clone(),
-            )
+            cx.build_view(|cx| Self {
+                workspace: Workspace::view(cx),
+            })
+        }
+    }
+
+    impl Render for WorkspaceStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            div().child(self.workspace.clone())
         }
     }
 }

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

@@ -43,19 +43,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct AvatarStory;
 
-    impl AvatarStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for AvatarStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, Avatar>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -219,26 +219,21 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use gpui2::rems;
-    use strum::IntoEnumIterator;
-
-    use crate::{h_stack, v_stack, LabelColor, Story};
-
     use super::*;
+    use crate::{h_stack, v_stack, LabelColor, Story};
+    use gpui2::{rems, Div, Render};
+    use strum::IntoEnumIterator;
 
-    #[derive(Component)]
     pub struct ButtonStory;
 
-    impl ButtonStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for ButtonStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             let states = InteractionState::iter();
 
             Story::container(cx)
-                .child(Story::title_for::<_, Button<V>>(cx))
+                .child(Story::title_for::<_, Button<Self>>(cx))
                 .child(
                     div()
                         .flex()

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

@@ -46,21 +46,18 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::{Button, Story};
-
     use super::*;
+    use crate::{Button, Story};
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct DetailsStory;
 
-    impl DetailsStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for DetailsStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
-                .child(Story::title_for::<_, Details<V>>(cx))
+                .child(Story::title_for::<_, Details<Self>>(cx))
                 .child(Story::label(cx, "Default"))
                 .child(Details::new("The quick brown fox jumps over the lazy dog"))
                 .child(Story::label(cx, "With meta"))

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

@@ -2,7 +2,6 @@ use gpui2::{svg, Hsla};
 use strum::EnumIter;
 
 use crate::prelude::*;
-use crate::theme::old_theme;
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconSize {
@@ -27,17 +26,17 @@ pub enum IconColor {
 
 impl IconColor {
     pub fn color(self, cx: &WindowContext) -> Hsla {
-        let theme = old_theme(cx);
+        let theme = theme(cx);
         match self {
-            IconColor::Default => theme.lowest.base.default.foreground,
-            IconColor::Muted => theme.lowest.variant.default.foreground,
-            IconColor::Disabled => theme.lowest.base.disabled.foreground,
-            IconColor::Placeholder => theme.lowest.base.disabled.foreground,
-            IconColor::Accent => theme.lowest.accent.default.foreground,
-            IconColor::Error => theme.lowest.negative.default.foreground,
-            IconColor::Warning => theme.lowest.warning.default.foreground,
-            IconColor::Success => theme.lowest.positive.default.foreground,
-            IconColor::Info => theme.lowest.accent.default.foreground,
+            IconColor::Default => gpui2::red(),
+            IconColor::Muted => gpui2::red(),
+            IconColor::Disabled => gpui2::red(),
+            IconColor::Placeholder => gpui2::red(),
+            IconColor::Accent => gpui2::red(),
+            IconColor::Error => gpui2::red(),
+            IconColor::Warning => gpui2::red(),
+            IconColor::Success => gpui2::red(),
+            IconColor::Info => gpui2::red(),
         }
     }
 }
@@ -192,21 +191,19 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
+    use gpui2::{Div, Render};
     use strum::IntoEnumIterator;
 
     use crate::Story;
 
     use super::*;
 
-    #[derive(Component)]
     pub struct IconStory;
 
-    impl IconStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for IconStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             let icons = Icon::iter();
 
             Story::container(cx)

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

@@ -112,19 +112,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct InputStory;
 
-    impl InputStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for InputStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, Input>(cx))
                 .child(Story::label(cx, "Default"))

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

@@ -2,8 +2,6 @@ use gpui2::{relative, Hsla, WindowContext};
 use smallvec::SmallVec;
 
 use crate::prelude::*;
-use crate::theme::old_theme;
-
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum LabelColor {
     #[default]
@@ -21,19 +19,17 @@ pub enum LabelColor {
 impl LabelColor {
     pub fn hsla(&self, cx: &WindowContext) -> Hsla {
         let theme = theme(cx);
-        // TODO: Remove
-        let old_theme = old_theme(cx);
 
         match self {
             Self::Default => theme.text,
             Self::Muted => theme.text_muted,
-            Self::Created => old_theme.middle.positive.default.foreground,
-            Self::Modified => old_theme.middle.warning.default.foreground,
-            Self::Deleted => old_theme.middle.negative.default.foreground,
+            Self::Created => gpui2::red(),
+            Self::Modified => gpui2::red(),
+            Self::Deleted => gpui2::red(),
             Self::Disabled => theme.text_disabled,
-            Self::Hidden => old_theme.middle.variant.default.foreground,
+            Self::Hidden => gpui2::red(),
             Self::Placeholder => theme.text_placeholder,
-            Self::Accent => old_theme.middle.accent.default.foreground,
+            Self::Accent => gpui2::red(),
         }
     }
 }
@@ -201,19 +197,16 @@ pub use stories::*;
 
 #[cfg(feature = "stories")]
 mod stories {
-    use crate::Story;
-
     use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
 
-    #[derive(Component)]
     pub struct LabelStory;
 
-    impl LabelStory {
-        pub fn new() -> Self {
-            Self
-        }
+    impl Render for LabelStory {
+        type Element = Div<Self>;
 
-        fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
                 .child(Story::title_for::<_, Label>(cx))
                 .child(Story::label(cx, "Default"))

crates/ui2/src/lib.rs 🔗

@@ -23,7 +23,6 @@ mod elevation;
 pub mod prelude;
 pub mod settings;
 mod static_data;
-mod theme;
 
 pub use components::*;
 pub use elements::*;
@@ -38,7 +37,6 @@ pub use static_data::*;
 // AFAICT this is something to do with conflicting names between crates and modules that
 // interfaces with declaring the `ClassDecl`.
 pub use crate::settings::*;
-pub use crate::theme::*;
 
 #[cfg(feature = "stories")]
 mod story;

crates/ui2/src/prelude.rs 🔗

@@ -5,7 +5,8 @@ pub use gpui2::{
 
 pub use crate::elevation::*;
 use crate::settings::user_settings;
-pub use crate::{old_theme, theme, ButtonVariant, Theme};
+pub use crate::ButtonVariant;
+pub use theme2::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::ViewContext;
 use rand::Rng;
 use theme2::Theme;
 
@@ -628,7 +628,7 @@ pub fn example_editor_actions() -> Vec<PaletteItem> {
     ]
 }
 
-pub fn empty_editor_example(cx: &mut WindowContext) -> EditorPane {
+pub fn empty_editor_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
     EditorPane::new(
         cx,
         static_tabs_example(),
@@ -642,7 +642,7 @@ pub fn empty_buffer_example() -> Buffer {
     Buffer::new("empty-buffer").set_rows(Some(BufferRows::default()))
 }
 
-pub fn hello_world_rust_editor_example(cx: &mut WindowContext) -> EditorPane {
+pub fn hello_world_rust_editor_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
     let theme = theme(cx);
 
     EditorPane::new(
@@ -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 ViewContext<EditorPane>) -> 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/theme.rs 🔗

@@ -1,225 +0,0 @@
-use gpui2::{
-    AnyElement, Bounds, Component, Element, Hsla, LayoutId, Pixels, Result, ViewContext,
-    WindowContext,
-};
-use serde::{de::Visitor, Deserialize, Deserializer};
-use std::collections::HashMap;
-use std::fmt;
-use std::sync::Arc;
-
-#[derive(Deserialize, Clone, Default, Debug)]
-pub struct Theme {
-    pub name: String,
-    pub is_light: bool,
-    pub lowest: Layer,
-    pub middle: Layer,
-    pub highest: Layer,
-    pub popover_shadow: Shadow,
-    pub modal_shadow: Shadow,
-    #[serde(deserialize_with = "deserialize_player_colors")]
-    pub players: Vec<PlayerColors>,
-    #[serde(deserialize_with = "deserialize_syntax_colors")]
-    pub syntax: HashMap<String, Hsla>,
-}
-
-#[derive(Deserialize, Clone, Default, Debug)]
-pub struct Layer {
-    pub base: StyleSet,
-    pub variant: StyleSet,
-    pub on: StyleSet,
-    pub accent: StyleSet,
-    pub positive: StyleSet,
-    pub warning: StyleSet,
-    pub negative: StyleSet,
-}
-
-#[derive(Deserialize, Clone, Default, Debug)]
-pub struct StyleSet {
-    #[serde(rename = "default")]
-    pub default: ContainerColors,
-    pub hovered: ContainerColors,
-    pub pressed: ContainerColors,
-    pub active: ContainerColors,
-    pub disabled: ContainerColors,
-    pub inverted: ContainerColors,
-}
-
-#[derive(Deserialize, Clone, Default, Debug)]
-pub struct ContainerColors {
-    pub background: Hsla,
-    pub foreground: Hsla,
-    pub border: Hsla,
-}
-
-#[derive(Deserialize, Clone, Default, Debug)]
-pub struct PlayerColors {
-    pub selection: Hsla,
-    pub cursor: Hsla,
-}
-
-#[derive(Deserialize, Clone, Default, Debug)]
-pub struct Shadow {
-    pub blur: u8,
-    pub color: Hsla,
-    pub offset: Vec<u8>,
-}
-
-fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    struct PlayerArrayVisitor;
-
-    impl<'de> Visitor<'de> for PlayerArrayVisitor {
-        type Value = Vec<PlayerColors>;
-
-        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-            formatter.write_str("an object with integer keys")
-        }
-
-        fn visit_map<A: serde::de::MapAccess<'de>>(
-            self,
-            mut map: A,
-        ) -> Result<Self::Value, A::Error> {
-            let mut players = Vec::with_capacity(8);
-            while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
-                if key < 8 {
-                    players.push(value);
-                } else {
-                    return Err(serde::de::Error::invalid_value(
-                        serde::de::Unexpected::Unsigned(key as u64),
-                        &"a key in range 0..7",
-                    ));
-                }
-            }
-            Ok(players)
-        }
-    }
-
-    deserializer.deserialize_map(PlayerArrayVisitor)
-}
-
-fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String, Hsla>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    #[derive(Deserialize)]
-    struct ColorWrapper {
-        color: Hsla,
-    }
-
-    struct SyntaxVisitor;
-
-    impl<'de> Visitor<'de> for SyntaxVisitor {
-        type Value = HashMap<String, Hsla>;
-
-        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-            formatter.write_str("a map with keys and objects with a single color field as values")
-        }
-
-        fn visit_map<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, M::Error>
-        where
-            M: serde::de::MapAccess<'de>,
-        {
-            let mut result = HashMap::new();
-            while let Some(key) = map.next_key()? {
-                let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla
-                result.insert(key, wrapper.color);
-            }
-            Ok(result)
-        }
-    }
-    deserializer.deserialize_map(SyntaxVisitor)
-}
-
-pub fn themed<V, E, F>(theme: Theme, cx: &mut ViewContext<V>, build_child: F) -> Themed<E>
-where
-    V: 'static,
-    E: Element<V>,
-    F: FnOnce(&mut ViewContext<V>) -> E,
-{
-    cx.default_global::<ThemeStack>().0.push(theme.clone());
-    let child = build_child(cx);
-    cx.default_global::<ThemeStack>().0.pop();
-    Themed { theme, child }
-}
-
-pub struct Themed<E> {
-    pub(crate) theme: Theme,
-    pub(crate) child: E,
-}
-
-impl<V, E> Component<V> for Themed<E>
-where
-    V: 'static,
-    E: 'static + Element<V> + Send + Sync,
-    E::ElementState: Send + Sync,
-{
-    fn render(self) -> AnyElement<V> {
-        AnyElement::new(self)
-    }
-}
-
-#[derive(Default)]
-struct ThemeStack(Vec<Theme>);
-
-impl<V, E: 'static + Element<V> + Send + Sync> Element<V> for Themed<E>
-where
-    V: 'static,
-    E::ElementState: Send + Sync,
-{
-    type ElementState = E::ElementState;
-
-    fn id(&self) -> Option<gpui2::ElementId> {
-        None
-    }
-
-    fn initialize(
-        &mut self,
-        view_state: &mut V,
-        element_state: Option<Self::ElementState>,
-        cx: &mut ViewContext<V>,
-    ) -> Self::ElementState {
-        cx.default_global::<ThemeStack>().0.push(self.theme.clone());
-        let element_state = self.child.initialize(view_state, element_state, cx);
-        cx.default_global::<ThemeStack>().0.pop();
-        element_state
-    }
-
-    fn layout(
-        &mut self,
-        view_state: &mut V,
-        element_state: &mut Self::ElementState,
-        cx: &mut ViewContext<V>,
-    ) -> LayoutId
-    where
-        Self: Sized,
-    {
-        cx.default_global::<ThemeStack>().0.push(self.theme.clone());
-        let layout_id = self.child.layout(view_state, element_state, cx);
-        cx.default_global::<ThemeStack>().0.pop();
-        layout_id
-    }
-
-    fn paint(
-        &mut self,
-        bounds: Bounds<Pixels>,
-        view_state: &mut V,
-        frame_state: &mut Self::ElementState,
-        cx: &mut ViewContext<V>,
-    ) where
-        Self: Sized,
-    {
-        cx.default_global::<ThemeStack>().0.push(self.theme.clone());
-        self.child.paint(bounds, view_state, frame_state, cx);
-        cx.default_global::<ThemeStack>().0.pop();
-    }
-}
-
-pub fn old_theme(cx: &WindowContext) -> Arc<Theme> {
-    Arc::new(cx.global::<Theme>().clone())
-}
-
-pub fn theme(cx: &WindowContext) -> 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/workspace2/src/item.rs 🔗

@@ -0,0 +1,1096 @@
+// use crate::{
+//     pane, persistence::model::ItemId, searchable::SearchableItemHandle, FollowableItemBuilders,
+//     ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
+// };
+// use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
+use anyhow::Result;
+use client2::{
+    proto::{self, PeerId, ViewId},
+    Client,
+};
+use settings2::Settings;
+use theme2::Theme;
+// use client2::{
+//     proto::{self, PeerId},
+//     Client,
+// };
+// use gpui2::geometry::vector::Vector2F;
+// use gpui2::AnyWindowHandle;
+// use gpui2::{
+//     fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, Handle, Task, View,
+//     ViewContext, View, WeakViewHandle, WindowContext,
+// };
+// use project2::{Project, ProjectEntryId, ProjectPath};
+// use schemars::JsonSchema;
+// use serde_derive::{Deserialize, Serialize};
+// use settings2::Setting;
+// use smallvec::SmallVec;
+// use std::{
+//     any::{Any, TypeId},
+//     borrow::Cow,
+//     cell::RefCell,
+//     fmt,
+//     ops::Range,
+//     path::PathBuf,
+//     rc::Rc,
+//     sync::{
+//         atomic::{AtomicBool, Ordering},
+//         Arc,
+//     },
+//     time::Duration,
+// };
+// use theme2::Theme;
+
+// #[derive(Deserialize)]
+// pub struct ItemSettings {
+//     pub git_status: bool,
+//     pub close_position: ClosePosition,
+// }
+
+// #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+// #[serde(rename_all = "lowercase")]
+// pub enum ClosePosition {
+//     Left,
+//     #[default]
+//     Right,
+// }
+
+// impl ClosePosition {
+//     pub fn right(&self) -> bool {
+//         match self {
+//             ClosePosition::Left => false,
+//             ClosePosition::Right => true,
+//         }
+//     }
+// }
+
+// #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+// pub struct ItemSettingsContent {
+//     git_status: Option<bool>,
+//     close_position: Option<ClosePosition>,
+// }
+
+// impl Setting for ItemSettings {
+//     const KEY: Option<&'static str> = Some("tabs");
+
+//     type FileContent = ItemSettingsContent;
+
+//     fn load(
+//         default_value: &Self::FileContent,
+//         user_values: &[&Self::FileContent],
+//         _: &gpui2::AppContext,
+//     ) -> anyhow::Result<Self> {
+//         Self::load_via_json_merge(default_value, user_values)
+//     }
+// }
+
+#[derive(Eq, PartialEq, Hash, Debug)]
+pub enum ItemEvent {
+    CloseItem,
+    UpdateTab,
+    UpdateBreadcrumbs,
+    Edit,
+}
+
+// TODO: Combine this with existing HighlightedText struct?
+pub struct BreadcrumbText {
+    pub text: String,
+    pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
+}
+
+pub trait Item: EventEmitter + Sized {
+    //     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
+    //     fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
+    //     fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+    //         false
+    //     }
+    fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
+        None
+    }
+    fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
+        None
+    }
+    fn tab_content<V: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<V>;
+
+    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project2::Item)) {
+    } // (model id, Item)
+    fn is_singleton(&self, _cx: &AppContext) -> bool {
+        false
+    }
+    //     fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
+    fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
+    where
+        Self: Sized,
+    {
+        None
+    }
+    //     fn is_dirty(&self, _: &AppContext) -> bool {
+    //         false
+    //     }
+    //     fn has_conflict(&self, _: &AppContext) -> bool {
+    //         false
+    //     }
+    //     fn can_save(&self, _cx: &AppContext) -> bool {
+    //         false
+    //     }
+    //     fn save(
+    //         &mut self,
+    //         _project: Handle<Project>,
+    //         _cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<()>> {
+    //         unimplemented!("save() must be implemented if can_save() returns true")
+    //     }
+    //     fn save_as(
+    //         &mut self,
+    //         _project: Handle<Project>,
+    //         _abs_path: PathBuf,
+    //         _cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<()>> {
+    //         unimplemented!("save_as() must be implemented if can_save() returns true")
+    //     }
+    //     fn reload(
+    //         &mut self,
+    //         _project: Handle<Project>,
+    //         _cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<()>> {
+    //         unimplemented!("reload() must be implemented if can_save() returns true")
+    //     }
+    fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+        SmallVec::new()
+    }
+    //     fn should_close_item_on_event(_: &Self::Event) -> bool {
+    //         false
+    //     }
+    //     fn should_update_tab_on_event(_: &Self::Event) -> bool {
+    //         false
+    //     }
+
+    //     fn act_as_type<'a>(
+    //         &'a self,
+    //         type_id: TypeId,
+    //         self_handle: &'a View<Self>,
+    //         _: &'a AppContext,
+    //     ) -> Option<&AnyViewHandle> {
+    //         if TypeId::of::<Self>() == type_id {
+    //             Some(self_handle)
+    //         } else {
+    //             None
+    //         }
+    //     }
+
+    //     fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+    //         None
+    //     }
+
+    //     fn breadcrumb_location(&self) -> ToolbarItemLocation {
+    //         ToolbarItemLocation::Hidden
+    //     }
+
+    //     fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+    //         None
+    //     }
+
+    //     fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
+
+    //     fn serialized_item_kind() -> Option<&'static str> {
+    //         None
+    //     }
+
+    //     fn deserialize(
+    //         _project: Handle<Project>,
+    //         _workspace: WeakViewHandle<Workspace>,
+    //         _workspace_id: WorkspaceId,
+    //         _item_id: ItemId,
+    //         _cx: &mut ViewContext<Pane>,
+    //     ) -> Task<Result<View<Self>>> {
+    //         unimplemented!(
+    //             "deserialize() must be implemented if serialized_item_kind() returns Some(_)"
+    //         )
+    //     }
+    //     fn show_toolbar(&self) -> bool {
+    //         true
+    //     }
+    //     fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
+    //         None
+    //     }
+}
+
+use std::{
+    any::Any,
+    cell::RefCell,
+    ops::Range,
+    path::PathBuf,
+    rc::Rc,
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc,
+    },
+    time::Duration,
+};
+
+use gpui2::{
+    AnyElement, AnyWindowHandle, AppContext, EventEmitter, Handle, HighlightStyle, Pixels, Point,
+    SharedString, Task, View, ViewContext, VisualContext, WindowContext,
+};
+use project2::{Project, ProjectEntryId, ProjectPath};
+use smallvec::SmallVec;
+
+use crate::{
+    pane::{self, Pane},
+    searchable::SearchableItemHandle,
+    workspace_settings::{AutosaveSetting, WorkspaceSettings},
+    DelayedDebouncedEditAction, FollowableItemBuilders, ToolbarItemLocation, Workspace,
+    WorkspaceId,
+};
+
+pub trait ItemHandle: 'static + Send {
+    fn subscribe_to_item_events(
+        &self,
+        cx: &mut WindowContext,
+        handler: Box<dyn Fn(ItemEvent, &mut WindowContext) + Send>,
+    ) -> gpui2::Subscription;
+    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
+    fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
+    fn tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Pane>;
+    fn dragged_tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Workspace>;
+    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
+    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
+    fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]>;
+    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project2::Item));
+    fn is_singleton(&self, cx: &AppContext) -> bool;
+    fn boxed_clone(&self) -> Box<dyn ItemHandle>;
+    fn clone_on_split(
+        &self,
+        workspace_id: WorkspaceId,
+        cx: &mut WindowContext,
+    ) -> Option<Box<dyn ItemHandle>>;
+    fn added_to_pane(
+        &self,
+        workspace: &mut Workspace,
+        pane: View<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    );
+    fn deactivated(&self, cx: &mut WindowContext);
+    fn workspace_deactivated(&self, cx: &mut WindowContext);
+    fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
+    fn id(&self) -> usize;
+    fn window(&self) -> AnyWindowHandle;
+    // fn as_any(&self) -> &AnyView; todo!()
+    fn is_dirty(&self, cx: &AppContext) -> bool;
+    fn has_conflict(&self, cx: &AppContext) -> bool;
+    fn can_save(&self, cx: &AppContext) -> bool;
+    fn save(&self, project: Handle<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
+    fn save_as(
+        &self,
+        project: Handle<Project>,
+        abs_path: PathBuf,
+        cx: &mut WindowContext,
+    ) -> Task<Result<()>>;
+    fn reload(&self, project: Handle<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
+    // fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>; todo!()
+    fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
+    fn on_release(
+        &self,
+        cx: &mut AppContext,
+        callback: Box<dyn FnOnce(&mut AppContext)>,
+    ) -> gpui2::Subscription;
+    fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
+    fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
+    fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
+    fn serialized_item_kind(&self) -> Option<&'static str>;
+    fn show_toolbar(&self, cx: &AppContext) -> bool;
+    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>>;
+}
+
+pub trait WeakItemHandle {
+    fn id(&self) -> usize;
+    fn window(&self) -> AnyWindowHandle;
+    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
+}
+
+// todo!()
+// impl dyn ItemHandle {
+//     pub fn downcast<T: View>(&self) -> Option<View<T>> {
+//         self.as_any().clone().downcast()
+//     }
+
+//     pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<View<T>> {
+//         self.act_as_type(TypeId::of::<T>(), cx)
+//             .and_then(|t| t.clone().downcast())
+//     }
+// }
+
+impl<T: Item> ItemHandle for View<T> {
+    fn subscribe_to_item_events(
+        &self,
+        cx: &mut WindowContext,
+        handler: Box<dyn Fn(ItemEvent, &mut WindowContext) + Send>,
+    ) -> gpui2::Subscription {
+        cx.subscribe(self, move |_, event, cx| {
+            for item_event in T::to_item_events(event) {
+                handler(item_event, cx)
+            }
+        })
+    }
+
+    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
+        self.read(cx).tab_tooltip_text(cx)
+    }
+
+    fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString> {
+        self.read(cx).tab_description(detail, cx)
+    }
+
+    fn tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Pane> {
+        self.read(cx).tab_content(detail, cx)
+    }
+
+    fn dragged_tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Workspace> {
+        self.read(cx).tab_content(detail, cx)
+    }
+
+    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
+        let this = self.read(cx);
+        let mut result = None;
+        if this.is_singleton(cx) {
+            this.for_each_project_item(cx, &mut |_, item| {
+                result = item.project_path(cx);
+            });
+        }
+        result
+    }
+
+    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
+        let mut result = SmallVec::new();
+        self.read(cx).for_each_project_item(cx, &mut |_, item| {
+            if let Some(id) = item.entry_id(cx) {
+                result.push(id);
+            }
+        });
+        result
+    }
+
+    fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[usize; 3]> {
+        let mut result = SmallVec::new();
+        self.read(cx).for_each_project_item(cx, &mut |id, _| {
+            result.push(id);
+        });
+        result
+    }
+
+    fn for_each_project_item(
+        &self,
+        cx: &AppContext,
+        f: &mut dyn FnMut(usize, &dyn project2::Item),
+    ) {
+        self.read(cx).for_each_project_item(cx, f)
+    }
+
+    fn is_singleton(&self, cx: &AppContext) -> bool {
+        self.read(cx).is_singleton(cx)
+    }
+
+    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
+        Box::new(self.clone())
+    }
+
+    fn clone_on_split(
+        &self,
+        workspace_id: WorkspaceId,
+        cx: &mut WindowContext,
+    ) -> Option<Box<dyn ItemHandle>> {
+        self.update(cx, |item, cx| {
+            cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx))
+        })
+        .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
+    }
+
+    fn added_to_pane(
+        &self,
+        workspace: &mut Workspace,
+        pane: View<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let history = pane.read(cx).nav_history_for_item(self);
+        self.update(cx, |this, cx| {
+            this.set_nav_history(history, cx);
+            this.added_to_workspace(workspace, cx);
+        });
+
+        if let Some(followed_item) = self.to_followable_item_handle(cx) {
+            if let Some(message) = followed_item.to_state_proto(cx) {
+                workspace.update_followers(
+                    followed_item.is_project_item(cx),
+                    proto::update_followers::Variant::CreateView(proto::View {
+                        id: followed_item
+                            .remote_id(&workspace.app_state.client, cx)
+                            .map(|id| id.to_proto()),
+                        variant: Some(message),
+                        leader_id: workspace.leader_for_pane(&pane),
+                    }),
+                    cx,
+                );
+            }
+        }
+
+        if workspace
+            .panes_by_item
+            .insert(self.id(), pane.downgrade())
+            .is_none()
+        {
+            let mut pending_autosave = DelayedDebouncedEditAction::new();
+            let pending_update = Rc::new(RefCell::new(None));
+            let pending_update_scheduled = Rc::new(AtomicBool::new(false));
+
+            let mut event_subscription =
+                Some(cx.subscribe(self, move |workspace, item, event, cx| {
+                    let pane = if let Some(pane) = workspace
+                        .panes_by_item
+                        .get(&item.id())
+                        .and_then(|pane| pane.upgrade(cx))
+                    {
+                        pane
+                    } else {
+                        log::error!("unexpected item event after pane was dropped");
+                        return;
+                    };
+
+                    if let Some(item) = item.to_followable_item_handle(cx) {
+                        let is_project_item = item.is_project_item(cx);
+                        let leader_id = workspace.leader_for_pane(&pane);
+
+                        if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
+                            workspace.unfollow(&pane, cx);
+                        }
+
+                        if item.add_event_to_update_proto(
+                            event,
+                            &mut *pending_update.borrow_mut(),
+                            cx,
+                        ) && !pending_update_scheduled.load(Ordering::SeqCst)
+                        {
+                            pending_update_scheduled.store(true, Ordering::SeqCst);
+                            cx.after_window_update({
+                                let pending_update = pending_update.clone();
+                                let pending_update_scheduled = pending_update_scheduled.clone();
+                                move |this, cx| {
+                                    pending_update_scheduled.store(false, Ordering::SeqCst);
+                                    this.update_followers(
+                                        is_project_item,
+                                        proto::update_followers::Variant::UpdateView(
+                                            proto::UpdateView {
+                                                id: item
+                                                    .remote_id(&this.app_state.client, cx)
+                                                    .map(|id| id.to_proto()),
+                                                variant: pending_update.borrow_mut().take(),
+                                                leader_id,
+                                            },
+                                        ),
+                                        cx,
+                                    );
+                                }
+                            });
+                        }
+                    }
+
+                    for item_event in T::to_item_events(event).into_iter() {
+                        match item_event {
+                            ItemEvent::CloseItem => {
+                                pane.update(cx, |pane, cx| {
+                                    pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx)
+                                })
+                                .detach_and_log_err(cx);
+                                return;
+                            }
+
+                            ItemEvent::UpdateTab => {
+                                pane.update(cx, |_, cx| {
+                                    cx.emit(pane::Event::ChangeItemTitle);
+                                    cx.notify();
+                                });
+                            }
+
+                            ItemEvent::Edit => {
+                                let autosave = WorkspaceSettings::get_global(cx).autosave;
+                                if let AutosaveSetting::AfterDelay { milliseconds } = autosave {
+                                    let delay = Duration::from_millis(milliseconds);
+                                    let item = item.clone();
+                                    pending_autosave.fire_new(delay, cx, move |workspace, cx| {
+                                        Pane::autosave_item(&item, workspace.project().clone(), cx)
+                                    });
+                                }
+                            }
+
+                            _ => {}
+                        }
+                    }
+                }));
+
+            cx.observe_focus(self, move |workspace, item, focused, cx| {
+                if !focused
+                    && WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange
+                {
+                    Pane::autosave_item(&item, workspace.project.clone(), cx)
+                        .detach_and_log_err(cx);
+                }
+            })
+            .detach();
+
+            let item_id = self.id();
+            cx.observe_release(self, move |workspace, _, _| {
+                workspace.panes_by_item.remove(&item_id);
+                event_subscription.take();
+            })
+            .detach();
+        }
+
+        cx.defer(|workspace, cx| {
+            workspace.serialize_workspace(cx);
+        });
+    }
+
+    fn deactivated(&self, cx: &mut WindowContext) {
+        self.update(cx, |this, cx| this.deactivated(cx));
+    }
+
+    fn workspace_deactivated(&self, cx: &mut WindowContext) {
+        self.update(cx, |this, cx| this.workspace_deactivated(cx));
+    }
+
+    fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool {
+        self.update(cx, |this, cx| this.navigate(data, cx))
+    }
+
+    fn id(&self) -> usize {
+        self.id()
+    }
+
+    fn window(&self) -> AnyWindowHandle {
+        todo!()
+        // AnyViewHandle::window(self)
+    }
+
+    // todo!()
+    // fn as_any(&self) -> &AnyViewHandle {
+    //     self
+    // }
+
+    fn is_dirty(&self, cx: &AppContext) -> bool {
+        self.read(cx).is_dirty(cx)
+    }
+
+    fn has_conflict(&self, cx: &AppContext) -> bool {
+        self.read(cx).has_conflict(cx)
+    }
+
+    fn can_save(&self, cx: &AppContext) -> bool {
+        self.read(cx).can_save(cx)
+    }
+
+    fn save(&self, project: Handle<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
+        self.update(cx, |item, cx| item.save(project, cx))
+    }
+
+    fn save_as(
+        &self,
+        project: Handle<Project>,
+        abs_path: PathBuf,
+        cx: &mut WindowContext,
+    ) -> Task<anyhow::Result<()>> {
+        self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
+    }
+
+    fn reload(&self, project: Handle<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
+        self.update(cx, |item, cx| item.reload(project, cx))
+    }
+
+    // todo!()
+    // fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle> {
+    //     self.read(cx).act_as_type(type_id, self, cx)
+    // }
+
+    fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
+        if cx.has_global::<FollowableItemBuilders>() {
+            let builders = cx.global::<FollowableItemBuilders>();
+            let item = self.as_any();
+            Some(builders.get(&item.view_type())?.1(item))
+        } else {
+            None
+        }
+    }
+
+    fn on_release(
+        &self,
+        cx: &mut AppContext,
+        callback: Box<dyn FnOnce(&mut AppContext)>,
+    ) -> gpui2::Subscription {
+        cx.observe_release(self, move |_, cx| callback(cx))
+    }
+
+    fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
+        self.read(cx).as_searchable(self)
+    }
+
+    fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
+        self.read(cx).breadcrumb_location()
+    }
+
+    fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+        self.read(cx).breadcrumbs(theme, cx)
+    }
+
+    fn serialized_item_kind(&self) -> Option<&'static str> {
+        T::serialized_item_kind()
+    }
+
+    fn show_toolbar(&self, cx: &AppContext) -> bool {
+        self.read(cx).show_toolbar()
+    }
+
+    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
+        self.read(cx).pixel_position_of_cursor(cx)
+    }
+}
+
+// impl From<Box<dyn ItemHandle>> for AnyViewHandle {
+//     fn from(val: Box<dyn ItemHandle>) -> Self {
+//         val.as_any().clone()
+//     }
+// }
+
+// impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
+//     fn from(val: &Box<dyn ItemHandle>) -> Self {
+//         val.as_any().clone()
+//     }
+// }
+
+impl Clone for Box<dyn ItemHandle> {
+    fn clone(&self) -> Box<dyn ItemHandle> {
+        self.boxed_clone()
+    }
+}
+
+// impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
+//     fn id(&self) -> usize {
+//         self.id()
+//     }
+
+//     fn window(&self) -> AnyWindowHandle {
+//         self.window()
+//     }
+
+//     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
+//         self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
+//     }
+// }
+
+pub trait ProjectItem: Item {
+    type Item: project2::Item;
+
+    fn for_project_item(
+        project: Handle<Project>,
+        item: Handle<Self::Item>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self
+    where
+        Self: Sized;
+}
+
+pub trait FollowableItem: Item {
+    fn remote_id(&self) -> Option<ViewId>;
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+    fn from_state_proto(
+        pane: View<Pane>,
+        project: View<Workspace>,
+        id: ViewId,
+        state: &mut Option<proto::view::Variant>,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<View<Self>>>>;
+    fn add_event_to_update_proto(
+        &self,
+        event: &Self::Event,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool;
+    fn apply_update_proto(
+        &mut self,
+        project: &Handle<Project>,
+        message: proto::update_view::Variant,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>>;
+    fn is_project_item(&self, cx: &AppContext) -> bool;
+
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
+    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
+}
+
+pub trait FollowableItemHandle: ItemHandle {
+    fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
+    fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+    fn add_event_to_update_proto(
+        &self,
+        event: &dyn Any,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool;
+    fn apply_update_proto(
+        &self,
+        project: &Handle<Project>,
+        message: proto::update_view::Variant,
+        cx: &mut WindowContext,
+    ) -> Task<Result<()>>;
+    fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
+    fn is_project_item(&self, cx: &AppContext) -> bool;
+}
+
+// impl<T: FollowableItem> FollowableItemHandle for View<T> {
+//     fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
+//         self.read(cx).remote_id().or_else(|| {
+//             client.peer_id().map(|creator| ViewId {
+//                 creator,
+//                 id: self.id() as u64,
+//             })
+//         })
+//     }
+
+//     fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
+//         self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
+//     }
+
+//     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+//         self.read(cx).to_state_proto(cx)
+//     }
+
+//     fn add_event_to_update_proto(
+//         &self,
+//         event: &dyn Any,
+//         update: &mut Option<proto::update_view::Variant>,
+//         cx: &AppContext,
+//     ) -> bool {
+//         if let Some(event) = event.downcast_ref() {
+//             self.read(cx).add_event_to_update_proto(event, update, cx)
+//         } else {
+//             false
+//         }
+//     }
+
+//     fn apply_update_proto(
+//         &self,
+//         project: &Handle<Project>,
+//         message: proto::update_view::Variant,
+//         cx: &mut WindowContext,
+//     ) -> Task<Result<()>> {
+//         self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
+//     }
+
+//     fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
+//         if let Some(event) = event.downcast_ref() {
+//             T::should_unfollow_on_event(event, cx)
+//         } else {
+//             false
+//         }
+//     }
+
+//     fn is_project_item(&self, cx: &AppContext) -> bool {
+//         self.read(cx).is_project_item(cx)
+//     }
+// }
+
+// #[cfg(any(test, feature = "test-support"))]
+// pub mod test {
+//     use super::{Item, ItemEvent};
+//     use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
+//     use gpui2::{
+//         elements::Empty, AnyElement, AppContext, Element, Entity, Handle, Task, View,
+//         ViewContext, View, WeakViewHandle,
+//     };
+//     use project2::{Project, ProjectEntryId, ProjectPath, WorktreeId};
+//     use smallvec::SmallVec;
+//     use std::{any::Any, borrow::Cow, cell::Cell, path::Path};
+
+//     pub struct TestProjectItem {
+//         pub entry_id: Option<ProjectEntryId>,
+//         pub project_path: Option<ProjectPath>,
+//     }
+
+//     pub struct TestItem {
+//         pub workspace_id: WorkspaceId,
+//         pub state: String,
+//         pub label: String,
+//         pub save_count: usize,
+//         pub save_as_count: usize,
+//         pub reload_count: usize,
+//         pub is_dirty: bool,
+//         pub is_singleton: bool,
+//         pub has_conflict: bool,
+//         pub project_items: Vec<Handle<TestProjectItem>>,
+//         pub nav_history: Option<ItemNavHistory>,
+//         pub tab_descriptions: Option<Vec<&'static str>>,
+//         pub tab_detail: Cell<Option<usize>>,
+//     }
+
+//     impl Entity for TestProjectItem {
+//         type Event = ();
+//     }
+
+//     impl project2::Item for TestProjectItem {
+//         fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
+//             self.entry_id
+//         }
+
+//         fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
+//             self.project_path.clone()
+//         }
+//     }
+
+//     pub enum TestItemEvent {
+//         Edit,
+//     }
+
+//     impl Clone for TestItem {
+//         fn clone(&self) -> Self {
+//             Self {
+//                 state: self.state.clone(),
+//                 label: self.label.clone(),
+//                 save_count: self.save_count,
+//                 save_as_count: self.save_as_count,
+//                 reload_count: self.reload_count,
+//                 is_dirty: self.is_dirty,
+//                 is_singleton: self.is_singleton,
+//                 has_conflict: self.has_conflict,
+//                 project_items: self.project_items.clone(),
+//                 nav_history: None,
+//                 tab_descriptions: None,
+//                 tab_detail: Default::default(),
+//                 workspace_id: self.workspace_id,
+//             }
+//         }
+//     }
+
+//     impl TestProjectItem {
+//         pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Handle<Self> {
+//             let entry_id = Some(ProjectEntryId::from_proto(id));
+//             let project_path = Some(ProjectPath {
+//                 worktree_id: WorktreeId::from_usize(0),
+//                 path: Path::new(path).into(),
+//             });
+//             cx.add_model(|_| Self {
+//                 entry_id,
+//                 project_path,
+//             })
+//         }
+
+//         pub fn new_untitled(cx: &mut AppContext) -> Handle<Self> {
+//             cx.add_model(|_| Self {
+//                 project_path: None,
+//                 entry_id: None,
+//             })
+//         }
+//     }
+
+//     impl TestItem {
+//         pub fn new() -> Self {
+//             Self {
+//                 state: String::new(),
+//                 label: String::new(),
+//                 save_count: 0,
+//                 save_as_count: 0,
+//                 reload_count: 0,
+//                 is_dirty: false,
+//                 has_conflict: false,
+//                 project_items: Vec::new(),
+//                 is_singleton: true,
+//                 nav_history: None,
+//                 tab_descriptions: None,
+//                 tab_detail: Default::default(),
+//                 workspace_id: 0,
+//             }
+//         }
+
+//         pub fn new_deserialized(id: WorkspaceId) -> Self {
+//             let mut this = Self::new();
+//             this.workspace_id = id;
+//             this
+//         }
+
+//         pub fn with_label(mut self, state: &str) -> Self {
+//             self.label = state.to_string();
+//             self
+//         }
+
+//         pub fn with_singleton(mut self, singleton: bool) -> Self {
+//             self.is_singleton = singleton;
+//             self
+//         }
+
+//         pub fn with_dirty(mut self, dirty: bool) -> Self {
+//             self.is_dirty = dirty;
+//             self
+//         }
+
+//         pub fn with_conflict(mut self, has_conflict: bool) -> Self {
+//             self.has_conflict = has_conflict;
+//             self
+//         }
+
+//         pub fn with_project_items(mut self, items: &[Handle<TestProjectItem>]) -> Self {
+//             self.project_items.clear();
+//             self.project_items.extend(items.iter().cloned());
+//             self
+//         }
+
+//         pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
+//             self.push_to_nav_history(cx);
+//             self.state = state;
+//         }
+
+//         fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
+//             if let Some(history) = &mut self.nav_history {
+//                 history.push(Some(Box::new(self.state.clone())), cx);
+//             }
+//         }
+//     }
+
+//     impl Entity for TestItem {
+//         type Event = TestItemEvent;
+//     }
+
+//     impl View for TestItem {
+//         fn ui_name() -> &'static str {
+//             "TestItem"
+//         }
+
+//         fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+//             Empty::new().into_any()
+//         }
+//     }
+
+//     impl Item for TestItem {
+//         fn tab_description(&self, detail: usize, _: &AppContext) -> Option<Cow<str>> {
+//             self.tab_descriptions.as_ref().and_then(|descriptions| {
+//                 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
+//                 Some(description.into())
+//             })
+//         }
+
+//         fn tab_content<V: 'static>(
+//             &self,
+//             detail: Option<usize>,
+//             _: &theme2::Tab,
+//             _: &AppContext,
+//         ) -> AnyElement<V> {
+//             self.tab_detail.set(detail);
+//             Empty::new().into_any()
+//         }
+
+//         fn for_each_project_item(
+//             &self,
+//             cx: &AppContext,
+//             f: &mut dyn FnMut(usize, &dyn project2::Item),
+//         ) {
+//             self.project_items
+//                 .iter()
+//                 .for_each(|item| f(item.id(), item.read(cx)))
+//         }
+
+// fn is_singleton(&self, _: &AppContext) -> bool {
+//     self.is_singleton
+// }
+
+//         fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+//             self.nav_history = Some(history);
+//         }
+
+//         fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+//             let state = *state.downcast::<String>().unwrap_or_default();
+//             if state != self.state {
+//                 self.state = state;
+//                 true
+//             } else {
+//                 false
+//             }
+//         }
+
+//         fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+//             self.push_to_nav_history(cx);
+//         }
+
+//         fn clone_on_split(
+//             &self,
+//             _workspace_id: WorkspaceId,
+//             _: &mut ViewContext<Self>,
+//         ) -> Option<Self>
+//         where
+//             Self: Sized,
+//         {
+//             Some(self.clone())
+//         }
+
+//         fn is_dirty(&self, _: &AppContext) -> bool {
+//             self.is_dirty
+//         }
+
+//         fn has_conflict(&self, _: &AppContext) -> bool {
+//             self.has_conflict
+//         }
+
+//         fn can_save(&self, cx: &AppContext) -> bool {
+//             !self.project_items.is_empty()
+//                 && self
+//                     .project_items
+//                     .iter()
+//                     .all(|item| item.read(cx).entry_id.is_some())
+//         }
+
+//         fn save(
+//             &mut self,
+//             _: Handle<Project>,
+//             _: &mut ViewContext<Self>,
+//         ) -> Task<anyhow::Result<()>> {
+//             self.save_count += 1;
+//             self.is_dirty = false;
+//             Task::ready(Ok(()))
+//         }
+
+//         fn save_as(
+//             &mut self,
+//             _: Handle<Project>,
+//             _: std::path::PathBuf,
+//             _: &mut ViewContext<Self>,
+//         ) -> Task<anyhow::Result<()>> {
+//             self.save_as_count += 1;
+//             self.is_dirty = false;
+//             Task::ready(Ok(()))
+//         }
+
+//         fn reload(
+//             &mut self,
+//             _: Handle<Project>,
+//             _: &mut ViewContext<Self>,
+//         ) -> Task<anyhow::Result<()>> {
+//             self.reload_count += 1;
+//             self.is_dirty = false;
+//             Task::ready(Ok(()))
+//         }
+
+//         fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+//             [ItemEvent::UpdateTab, ItemEvent::Edit].into()
+//         }
+
+//         fn serialized_item_kind() -> Option<&'static str> {
+//             Some("TestItem")
+//         }
+
+//         fn deserialize(
+//             _project: Handle<Project>,
+//             _workspace: WeakViewHandle<Workspace>,
+//             workspace_id: WorkspaceId,
+//             _item_id: ItemId,
+//             cx: &mut ViewContext<Pane>,
+//         ) -> Task<anyhow::Result<View<Self>>> {
+//             let view = cx.add_view(|_cx| Self::new_deserialized(workspace_id));
+//             Task::Ready(Some(anyhow::Ok(view)))
+//         }
+//     }
+// }

crates/workspace2/src/pane.rs 🔗

@@ -0,0 +1,2754 @@
+// mod dragged_item_receiver;
+
+// use super::{ItemHandle, SplitDirection};
+// pub use crate::toolbar::Toolbar;
+// use crate::{
+//     item::{ItemSettings, WeakItemHandle},
+//     notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile, NewSearch, ToggleZoom,
+//     Workspace, WorkspaceSettings,
+// };
+// use anyhow::Result;
+// use collections::{HashMap, HashSet, VecDeque};
+// // use context_menu::{ContextMenu, ContextMenuItem};
+
+// use dragged_item_receiver::dragged_item_receiver;
+// use fs2::repository::GitFileStatus;
+// use futures::StreamExt;
+// use gpui2::{
+//     actions,
+//     elements::*,
+//     geometry::{
+//         rect::RectF,
+//         vector::{vec2f, Vector2F},
+//     },
+//     impl_actions,
+//     keymap_matcher::KeymapContext,
+//     platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
+//     Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
+//     ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+//     WindowContext,
+// };
+// use project2::{Project, ProjectEntryId, ProjectPath};
+use serde::Deserialize;
+// use std::{
+//     any::Any,
+//     cell::RefCell,
+//     cmp, mem,
+//     path::{Path, PathBuf},
+//     rc::Rc,
+//     sync::{
+//         atomic::{AtomicUsize, Ordering},
+//         Arc,
+//     },
+// };
+// use theme2::{Theme, ThemeSettings};
+// use util::truncate_and_remove_front;
+
+#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub enum SaveIntent {
+    /// write all files (even if unchanged)
+    /// prompt before overwriting on-disk changes
+    Save,
+    /// write any files that have local changes
+    /// prompt before overwriting on-disk changes
+    SaveAll,
+    /// always prompt for a new path
+    SaveAs,
+    /// prompt "you have unsaved changes" before writing
+    Close,
+    /// write all dirty files, don't prompt on conflict
+    Overwrite,
+    /// skip all save-related behavior
+    Skip,
+}
+
+// #[derive(Clone, Deserialize, PartialEq)]
+// pub struct ActivateItem(pub usize);
+
+// #[derive(Clone, PartialEq)]
+// pub struct CloseItemById {
+//     pub item_id: usize,
+//     pub pane: WeakViewHandle<Pane>,
+// }
+
+// #[derive(Clone, PartialEq)]
+// pub struct CloseItemsToTheLeftById {
+//     pub item_id: usize,
+//     pub pane: WeakViewHandle<Pane>,
+// }
+
+// #[derive(Clone, PartialEq)]
+// pub struct CloseItemsToTheRightById {
+//     pub item_id: usize,
+//     pub pane: WeakViewHandle<Pane>,
+// }
+
+// #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+// #[serde(rename_all = "camelCase")]
+// pub struct CloseActiveItem {
+//     pub save_intent: Option<SaveIntent>,
+// }
+
+// #[derive(Clone, PartialEq, Debug, Deserialize)]
+// #[serde(rename_all = "camelCase")]
+// pub struct CloseAllItems {
+//     pub save_intent: Option<SaveIntent>,
+// }
+
+// actions!(
+//     pane,
+//     [
+//         ActivatePrevItem,
+//         ActivateNextItem,
+//         ActivateLastItem,
+//         CloseInactiveItems,
+//         CloseCleanItems,
+//         CloseItemsToTheLeft,
+//         CloseItemsToTheRight,
+//         GoBack,
+//         GoForward,
+//         ReopenClosedItem,
+//         SplitLeft,
+//         SplitUp,
+//         SplitRight,
+//         SplitDown,
+//     ]
+// );
+
+// impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]);
+
+// const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
+
+// pub fn init(cx: &mut AppContext) {
+//     cx.add_action(Pane::toggle_zoom);
+//     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
+//         pane.activate_item(action.0, true, true, cx);
+//     });
+//     cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
+//         pane.activate_item(pane.items.len() - 1, true, true, cx);
+//     });
+//     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
+//         pane.activate_prev_item(true, cx);
+//     });
+//     cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
+//         pane.activate_next_item(true, cx);
+//     });
+//     cx.add_async_action(Pane::close_active_item);
+//     cx.add_async_action(Pane::close_inactive_items);
+//     cx.add_async_action(Pane::close_clean_items);
+//     cx.add_async_action(Pane::close_items_to_the_left);
+//     cx.add_async_action(Pane::close_items_to_the_right);
+//     cx.add_async_action(Pane::close_all_items);
+//     cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
+//     cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
+//     cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
+//     cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
+// }
+
+#[derive(Debug)]
+pub enum Event {
+    AddItem { item: Box<dyn ItemHandle> },
+    ActivateItem { local: bool },
+    Remove,
+    RemoveItem { item_id: usize },
+    Split(SplitDirection),
+    ChangeItemTitle,
+    Focus,
+    ZoomIn,
+    ZoomOut,
+}
+
+use crate::{
+    item::{ItemHandle, WeakItemHandle},
+    SplitDirection,
+};
+use collections::{HashMap, VecDeque};
+use gpui2::{Handle, ViewContext, WeakView};
+use project2::{Project, ProjectEntryId, ProjectPath};
+use std::{
+    any::Any,
+    cell::RefCell,
+    cmp, mem,
+    path::PathBuf,
+    rc::Rc,
+    sync::{atomic::AtomicUsize, Arc},
+};
+
+pub struct Pane {
+    items: Vec<Box<dyn ItemHandle>>,
+    //     activation_history: Vec<usize>,
+    //     zoomed: bool,
+    //     active_item_index: usize,
+    //     last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
+    //     autoscroll: bool,
+    nav_history: NavHistory,
+    //     toolbar: ViewHandle<Toolbar>,
+    //     tab_bar_context_menu: TabBarContextMenu,
+    //     tab_context_menu: ViewHandle<ContextMenu>,
+    //     workspace: WeakViewHandle<Workspace>,
+    project: Handle<Project>,
+    //     has_focus: bool,
+    //     can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
+    //     can_split: bool,
+    //     render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
+}
+
+pub struct ItemNavHistory {
+    history: NavHistory,
+    item: Rc<dyn WeakItemHandle>,
+}
+
+#[derive(Clone)]
+pub struct NavHistory(Rc<RefCell<NavHistoryState>>);
+
+struct NavHistoryState {
+    mode: NavigationMode,
+    backward_stack: VecDeque<NavigationEntry>,
+    forward_stack: VecDeque<NavigationEntry>,
+    closed_stack: VecDeque<NavigationEntry>,
+    paths_by_item: HashMap<usize, (ProjectPath, Option<PathBuf>)>,
+    pane: WeakView<Pane>,
+    next_timestamp: Arc<AtomicUsize>,
+}
+
+#[derive(Copy, Clone)]
+pub enum NavigationMode {
+    Normal,
+    GoingBack,
+    GoingForward,
+    ClosingItem,
+    ReopeningClosedItem,
+    Disabled,
+}
+
+impl Default for NavigationMode {
+    fn default() -> Self {
+        Self::Normal
+    }
+}
+
+pub struct NavigationEntry {
+    pub item: Rc<dyn WeakItemHandle>,
+    pub data: Option<Box<dyn Any>>,
+    pub timestamp: usize,
+}
+
+// pub struct DraggedItem {
+//     pub handle: Box<dyn ItemHandle>,
+//     pub pane: WeakViewHandle<Pane>,
+// }
+
+// pub enum ReorderBehavior {
+//     None,
+//     MoveAfterActive,
+//     MoveToIndex(usize),
+// }
+
+// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+// enum TabBarContextMenuKind {
+//     New,
+//     Split,
+// }
+
+// struct TabBarContextMenu {
+//     kind: TabBarContextMenuKind,
+//     handle: ViewHandle<ContextMenu>,
+// }
+
+// impl TabBarContextMenu {
+//     fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option<ViewHandle<ContextMenu>> {
+//         if self.kind == kind {
+//             return Some(self.handle.clone());
+//         }
+//         None
+//     }
+// }
+
+// #[allow(clippy::too_many_arguments)]
+// fn nav_button<A: Action, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>)>(
+//     svg_path: &'static str,
+//     style: theme2::Interactive<theme2::IconButton>,
+//     nav_button_height: f32,
+//     tooltip_style: TooltipStyle,
+//     enabled: bool,
+//     on_click: F,
+//     tooltip_action: A,
+//     action_name: &str,
+//     cx: &mut ViewContext<Pane>,
+// ) -> AnyElement<Pane> {
+//     MouseEventHandler::new::<A, _>(0, cx, |state, _| {
+//         let style = if enabled {
+//             style.style_for(state)
+//         } else {
+//             style.disabled_style()
+//         };
+//         Svg::new(svg_path)
+//             .with_color(style.color)
+//             .constrained()
+//             .with_width(style.icon_width)
+//             .aligned()
+//             .contained()
+//             .with_style(style.container)
+//             .constrained()
+//             .with_width(style.button_width)
+//             .with_height(nav_button_height)
+//             .aligned()
+//             .top()
+//     })
+//     .with_cursor_style(if enabled {
+//         CursorStyle::PointingHand
+//     } else {
+//         CursorStyle::default()
+//     })
+//     .on_click(MouseButton::Left, move |_, toolbar, cx| {
+//         on_click(toolbar, cx)
+//     })
+//     .with_tooltip::<A>(
+//         0,
+//         action_name.to_string(),
+//         Some(Box::new(tooltip_action)),
+//         tooltip_style,
+//         cx,
+//     )
+//     .contained()
+//     .into_any_named("nav button")
+// }
+
+impl Pane {
+    //     pub fn new(
+    //         workspace: WeakViewHandle<Workspace>,
+    //         project: ModelHandle<Project>,
+    //         next_timestamp: Arc<AtomicUsize>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Self {
+    //         let pane_view_id = cx.view_id();
+    //         let handle = cx.weak_handle();
+    //         let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx));
+    //         context_menu.update(cx, |menu, _| {
+    //             menu.set_position_mode(OverlayPositionMode::Local)
+    //         });
+
+    //         Self {
+    //             items: Vec::new(),
+    //             activation_history: Vec::new(),
+    //             zoomed: false,
+    //             active_item_index: 0,
+    //             last_focused_view_by_item: Default::default(),
+    //             autoscroll: false,
+    //             nav_history: NavHistory(Rc::new(RefCell::new(NavHistoryState {
+    //                 mode: NavigationMode::Normal,
+    //                 backward_stack: Default::default(),
+    //                 forward_stack: Default::default(),
+    //                 closed_stack: Default::default(),
+    //                 paths_by_item: Default::default(),
+    //                 pane: handle.clone(),
+    //                 next_timestamp,
+    //             }))),
+    //             toolbar: cx.add_view(|_| Toolbar::new()),
+    //             tab_bar_context_menu: TabBarContextMenu {
+    //                 kind: TabBarContextMenuKind::New,
+    //                 handle: context_menu,
+    //             },
+    //             tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
+    //             workspace,
+    //             project,
+    //             has_focus: false,
+    //             can_drop: Rc::new(|_, _| true),
+    //             can_split: true,
+    //             render_tab_bar_buttons: Rc::new(move |pane, cx| {
+    //                 Flex::row()
+    //                     // New menu
+    //                     .with_child(Self::render_tab_bar_button(
+    //                         0,
+    //                         "icons/plus.svg",
+    //                         false,
+    //                         Some(("New...".into(), None)),
+    //                         cx,
+    //                         |pane, cx| pane.deploy_new_menu(cx),
+    //                         |pane, cx| {
+    //                             pane.tab_bar_context_menu
+    //                                 .handle
+    //                                 .update(cx, |menu, _| menu.delay_cancel())
+    //                         },
+    //                         pane.tab_bar_context_menu
+    //                             .handle_if_kind(TabBarContextMenuKind::New),
+    //                     ))
+    //                     .with_child(Self::render_tab_bar_button(
+    //                         1,
+    //                         "icons/split.svg",
+    //                         false,
+    //                         Some(("Split Pane".into(), None)),
+    //                         cx,
+    //                         |pane, cx| pane.deploy_split_menu(cx),
+    //                         |pane, cx| {
+    //                             pane.tab_bar_context_menu
+    //                                 .handle
+    //                                 .update(cx, |menu, _| menu.delay_cancel())
+    //                         },
+    //                         pane.tab_bar_context_menu
+    //                             .handle_if_kind(TabBarContextMenuKind::Split),
+    //                     ))
+    //                     .with_child({
+    //                         let icon_path;
+    //                         let tooltip_label;
+    //                         if pane.is_zoomed() {
+    //                             icon_path = "icons/minimize.svg";
+    //                             tooltip_label = "Zoom In";
+    //                         } else {
+    //                             icon_path = "icons/maximize.svg";
+    //                             tooltip_label = "Zoom In";
+    //                         }
+
+    //                         Pane::render_tab_bar_button(
+    //                             2,
+    //                             icon_path,
+    //                             pane.is_zoomed(),
+    //                             Some((tooltip_label, Some(Box::new(ToggleZoom)))),
+    //                             cx,
+    //                             move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+    //                             move |_, _| {},
+    //                             None,
+    //                         )
+    //                     })
+    //                     .into_any()
+    //             }),
+    //         }
+    //     }
+
+    //     pub(crate) fn workspace(&self) -> &WeakViewHandle<Workspace> {
+    //         &self.workspace
+    //     }
+
+    //     pub fn has_focus(&self) -> bool {
+    //         self.has_focus
+    //     }
+
+    //     pub fn active_item_index(&self) -> usize {
+    //         self.active_item_index
+    //     }
+
+    //     pub fn on_can_drop<F>(&mut self, can_drop: F)
+    //     where
+    //         F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
+    //     {
+    //         self.can_drop = Rc::new(can_drop);
+    //     }
+
+    //     pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
+    //         self.can_split = can_split;
+    //         cx.notify();
+    //     }
+
+    //     pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
+    //         self.toolbar.update(cx, |toolbar, cx| {
+    //             toolbar.set_can_navigate(can_navigate, cx);
+    //         });
+    //         cx.notify();
+    //     }
+
+    //     pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
+    //     where
+    //         F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
+    //     {
+    //         self.render_tab_bar_buttons = Rc::new(render);
+    //         cx.notify();
+    //     }
+
+    //     pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
+    //         ItemNavHistory {
+    //             history: self.nav_history.clone(),
+    //             item: Rc::new(item.downgrade()),
+    //         }
+    //     }
+
+    //     pub fn nav_history(&self) -> &NavHistory {
+    //         &self.nav_history
+    //     }
+
+    //     pub fn nav_history_mut(&mut self) -> &mut NavHistory {
+    //         &mut self.nav_history
+    //     }
+
+    //     pub fn disable_history(&mut self) {
+    //         self.nav_history.disable();
+    //     }
+
+    //     pub fn enable_history(&mut self) {
+    //         self.nav_history.enable();
+    //     }
+
+    //     pub fn can_navigate_backward(&self) -> bool {
+    //         !self.nav_history.0.borrow().backward_stack.is_empty()
+    //     }
+
+    //     pub fn can_navigate_forward(&self) -> bool {
+    //         !self.nav_history.0.borrow().forward_stack.is_empty()
+    //     }
+
+    //     fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
+    //         self.toolbar.update(cx, |_, cx| cx.notify());
+    //     }
+
+    pub(crate) fn open_item(
+        &mut self,
+        project_entry_id: ProjectEntryId,
+        focus_item: bool,
+        cx: &mut ViewContext<Self>,
+        build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
+    ) -> Box<dyn ItemHandle> {
+        let mut existing_item = None;
+        for (index, item) in self.items.iter().enumerate() {
+            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
+            {
+                let item = item.boxed_clone();
+                existing_item = Some((index, item));
+                break;
+            }
+        }
+
+        if let Some((index, existing_item)) = existing_item {
+            self.activate_item(index, focus_item, focus_item, cx);
+            existing_item
+        } else {
+            let new_item = build_item(cx);
+            self.add_item(new_item.clone(), true, focus_item, None, cx);
+            new_item
+        }
+    }
+
+    pub fn add_item(
+        &mut self,
+        item: Box<dyn ItemHandle>,
+        activate_pane: bool,
+        focus_item: bool,
+        destination_index: Option<usize>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if item.is_singleton(cx) {
+            if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
+                let project = self.project.read(cx);
+                if let Some(project_path) = project.path_for_entry(entry_id, cx) {
+                    let abs_path = project.absolute_path(&project_path, cx);
+                    self.nav_history
+                        .0
+                        .borrow_mut()
+                        .paths_by_item
+                        .insert(item.id(), (project_path, abs_path));
+                }
+            }
+        }
+        // If no destination index is specified, add or move the item after the active item.
+        let mut insertion_index = {
+            cmp::min(
+                if let Some(destination_index) = destination_index {
+                    destination_index
+                } else {
+                    self.active_item_index + 1
+                },
+                self.items.len(),
+            )
+        };
+
+        // Does the item already exist?
+        let project_entry_id = if item.is_singleton(cx) {
+            item.project_entry_ids(cx).get(0).copied()
+        } else {
+            None
+        };
+
+        let existing_item_index = self.items.iter().position(|existing_item| {
+            if existing_item.id() == item.id() {
+                true
+            } else if existing_item.is_singleton(cx) {
+                existing_item
+                    .project_entry_ids(cx)
+                    .get(0)
+                    .map_or(false, |existing_entry_id| {
+                        Some(existing_entry_id) == project_entry_id.as_ref()
+                    })
+            } else {
+                false
+            }
+        });
+
+        if let Some(existing_item_index) = existing_item_index {
+            // If the item already exists, move it to the desired destination and activate it
+
+            if existing_item_index != insertion_index {
+                let existing_item_is_active = existing_item_index == self.active_item_index;
+
+                // If the caller didn't specify a destination and the added item is already
+                // the active one, don't move it
+                if existing_item_is_active && destination_index.is_none() {
+                    insertion_index = existing_item_index;
+                } else {
+                    self.items.remove(existing_item_index);
+                    if existing_item_index < self.active_item_index {
+                        self.active_item_index -= 1;
+                    }
+                    insertion_index = insertion_index.min(self.items.len());
+
+                    self.items.insert(insertion_index, item.clone());
+
+                    if existing_item_is_active {
+                        self.active_item_index = insertion_index;
+                    } else if insertion_index <= self.active_item_index {
+                        self.active_item_index += 1;
+                    }
+                }
+
+                cx.notify();
+            }
+
+            self.activate_item(insertion_index, activate_pane, focus_item, cx);
+        } else {
+            self.items.insert(insertion_index, item.clone());
+            if insertion_index <= self.active_item_index {
+                self.active_item_index += 1;
+            }
+
+            self.activate_item(insertion_index, activate_pane, focus_item, cx);
+            cx.notify();
+        }
+
+        cx.emit(Event::AddItem { item });
+    }
+
+    //     pub fn items_len(&self) -> usize {
+    //         self.items.len()
+    //     }
+
+    //     pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> + DoubleEndedIterator {
+    //         self.items.iter()
+    //     }
+
+    //     pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
+    //         self.items
+    //             .iter()
+    //             .filter_map(|item| item.as_any().clone().downcast())
+    //     }
+
+    //     pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
+    //         self.items.get(self.active_item_index).cloned()
+    //     }
+
+    //     pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+    //         self.items
+    //             .get(self.active_item_index)?
+    //             .pixel_position_of_cursor(cx)
+    //     }
+
+    //     pub fn item_for_entry(
+    //         &self,
+    //         entry_id: ProjectEntryId,
+    //         cx: &AppContext,
+    //     ) -> Option<Box<dyn ItemHandle>> {
+    //         self.items.iter().find_map(|item| {
+    //             if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
+    //                 Some(item.boxed_clone())
+    //             } else {
+    //                 None
+    //             }
+    //         })
+    //     }
+
+    //     pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
+    //         self.items.iter().position(|i| i.id() == item.id())
+    //     }
+
+    //     pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+    //         // Potentially warn the user of the new keybinding
+    //         let workspace_handle = self.workspace().clone();
+    //         cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
+    //             .detach();
+
+    //         if self.zoomed {
+    //             cx.emit(Event::ZoomOut);
+    //         } else if !self.items.is_empty() {
+    //             if !self.has_focus {
+    //                 cx.focus_self();
+    //             }
+    //             cx.emit(Event::ZoomIn);
+    //         }
+    //     }
+
+    pub fn activate_item(
+        &mut self,
+        index: usize,
+        activate_pane: bool,
+        focus_item: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        use NavigationMode::{GoingBack, GoingForward};
+
+        if index < self.items.len() {
+            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
+            if prev_active_item_ix != self.active_item_index
+                || matches!(self.nav_history.mode(), GoingBack | GoingForward)
+            {
+                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
+                    prev_item.deactivated(cx);
+                }
+
+                cx.emit(Event::ActivateItem {
+                    local: activate_pane,
+                });
+            }
+
+            if let Some(newly_active_item) = self.items.get(index) {
+                self.activation_history
+                    .retain(|&previously_active_item_id| {
+                        previously_active_item_id != newly_active_item.id()
+                    });
+                self.activation_history.push(newly_active_item.id());
+            }
+
+            self.update_toolbar(cx);
+
+            if focus_item {
+                self.focus_active_item(cx);
+            }
+
+            self.autoscroll = true;
+            cx.notify();
+        }
+    }
+
+    //     pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
+    //         let mut index = self.active_item_index;
+    //         if index > 0 {
+    //             index -= 1;
+    //         } else if !self.items.is_empty() {
+    //             index = self.items.len() - 1;
+    //         }
+    //         self.activate_item(index, activate_pane, activate_pane, cx);
+    //     }
+
+    //     pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
+    //         let mut index = self.active_item_index;
+    //         if index + 1 < self.items.len() {
+    //             index += 1;
+    //         } else {
+    //             index = 0;
+    //         }
+    //         self.activate_item(index, activate_pane, activate_pane, cx);
+    //     }
+
+    //     pub fn close_active_item(
+    //         &mut self,
+    //         action: &CloseActiveItem,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         if self.items.is_empty() {
+    //             return None;
+    //         }
+    //         let active_item_id = self.items[self.active_item_index].id();
+    //         Some(self.close_item_by_id(
+    //             active_item_id,
+    //             action.save_intent.unwrap_or(SaveIntent::Close),
+    //             cx,
+    //         ))
+    //     }
+
+    //     pub fn close_item_by_id(
+    //         &mut self,
+    //         item_id_to_close: usize,
+    //         save_intent: SaveIntent,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<()>> {
+    //         self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
+    //     }
+
+    //     pub fn close_inactive_items(
+    //         &mut self,
+    //         _: &CloseInactiveItems,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         if self.items.is_empty() {
+    //             return None;
+    //         }
+
+    //         let active_item_id = self.items[self.active_item_index].id();
+    //         Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+    //             item_id != active_item_id
+    //         }))
+    //     }
+
+    //     pub fn close_clean_items(
+    //         &mut self,
+    //         _: &CloseCleanItems,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         let item_ids: Vec<_> = self
+    //             .items()
+    //             .filter(|item| !item.is_dirty(cx))
+    //             .map(|item| item.id())
+    //             .collect();
+    //         Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+    //             item_ids.contains(&item_id)
+    //         }))
+    //     }
+
+    //     pub fn close_items_to_the_left(
+    //         &mut self,
+    //         _: &CloseItemsToTheLeft,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         if self.items.is_empty() {
+    //             return None;
+    //         }
+    //         let active_item_id = self.items[self.active_item_index].id();
+    //         Some(self.close_items_to_the_left_by_id(active_item_id, cx))
+    //     }
+
+    //     pub fn close_items_to_the_left_by_id(
+    //         &mut self,
+    //         item_id: usize,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<()>> {
+    //         let item_ids: Vec<_> = self
+    //             .items()
+    //             .take_while(|item| item.id() != item_id)
+    //             .map(|item| item.id())
+    //             .collect();
+    //         self.close_items(cx, SaveIntent::Close, move |item_id| {
+    //             item_ids.contains(&item_id)
+    //         })
+    //     }
+
+    //     pub fn close_items_to_the_right(
+    //         &mut self,
+    //         _: &CloseItemsToTheRight,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         if self.items.is_empty() {
+    //             return None;
+    //         }
+    //         let active_item_id = self.items[self.active_item_index].id();
+    //         Some(self.close_items_to_the_right_by_id(active_item_id, cx))
+    //     }
+
+    //     pub fn close_items_to_the_right_by_id(
+    //         &mut self,
+    //         item_id: usize,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<()>> {
+    //         let item_ids: Vec<_> = self
+    //             .items()
+    //             .rev()
+    //             .take_while(|item| item.id() != item_id)
+    //             .map(|item| item.id())
+    //             .collect();
+    //         self.close_items(cx, SaveIntent::Close, move |item_id| {
+    //             item_ids.contains(&item_id)
+    //         })
+    //     }
+
+    //     pub fn close_all_items(
+    //         &mut self,
+    //         action: &CloseAllItems,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         if self.items.is_empty() {
+    //             return None;
+    //         }
+
+    //         Some(
+    //             self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
+    //                 true
+    //             }),
+    //         )
+    //     }
+
+    //     pub(super) fn file_names_for_prompt(
+    //         items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
+    //         all_dirty_items: usize,
+    //         cx: &AppContext,
+    //     ) -> String {
+    //         /// Quantity of item paths displayed in prompt prior to cutoff..
+    //         const FILE_NAMES_CUTOFF_POINT: usize = 10;
+    //         let mut file_names: Vec<_> = items
+    //             .filter_map(|item| {
+    //                 item.project_path(cx).and_then(|project_path| {
+    //                     project_path
+    //                         .path
+    //                         .file_name()
+    //                         .and_then(|name| name.to_str().map(ToOwned::to_owned))
+    //                 })
+    //             })
+    //             .take(FILE_NAMES_CUTOFF_POINT)
+    //             .collect();
+    //         let should_display_followup_text =
+    //             all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
+    //         if should_display_followup_text {
+    //             let not_shown_files = all_dirty_items - file_names.len();
+    //             if not_shown_files == 1 {
+    //                 file_names.push(".. 1 file not shown".into());
+    //             } else {
+    //                 file_names.push(format!(".. {} files not shown", not_shown_files).into());
+    //             }
+    //         }
+    //         let file_names = file_names.join("\n");
+    //         format!(
+    //             "Do you want to save changes to the following {} files?\n{file_names}",
+    //             all_dirty_items
+    //         )
+    //     }
+
+    //     pub fn close_items(
+    //         &mut self,
+    //         cx: &mut ViewContext<Pane>,
+    //         mut save_intent: SaveIntent,
+    //         should_close: impl 'static + Fn(usize) -> bool,
+    //     ) -> Task<Result<()>> {
+    //         // Find the items to close.
+    //         let mut items_to_close = Vec::new();
+    //         let mut dirty_items = Vec::new();
+    //         for item in &self.items {
+    //             if should_close(item.id()) {
+    //                 items_to_close.push(item.boxed_clone());
+    //                 if item.is_dirty(cx) {
+    //                     dirty_items.push(item.boxed_clone());
+    //                 }
+    //             }
+    //         }
+
+    //         // If a buffer is open both in a singleton editor and in a multibuffer, make sure
+    //         // to focus the singleton buffer when prompting to save that buffer, as opposed
+    //         // to focusing the multibuffer, because this gives the user a more clear idea
+    //         // of what content they would be saving.
+    //         items_to_close.sort_by_key(|item| !item.is_singleton(cx));
+
+    //         let workspace = self.workspace.clone();
+    //         cx.spawn(|pane, mut cx| async move {
+    //             if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
+    //                 let mut answer = pane.update(&mut cx, |_, cx| {
+    //                     let prompt =
+    //                         Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
+    //                     cx.prompt(
+    //                         PromptLevel::Warning,
+    //                         &prompt,
+    //                         &["Save all", "Discard all", "Cancel"],
+    //                     )
+    //                 })?;
+    //                 match answer.next().await {
+    //                     Some(0) => save_intent = SaveIntent::SaveAll,
+    //                     Some(1) => save_intent = SaveIntent::Skip,
+    //                     _ => {}
+    //                 }
+    //             }
+    //             let mut saved_project_items_ids = HashSet::default();
+    //             for item in items_to_close.clone() {
+    //                 // Find the item's current index and its set of project item models. Avoid
+    //                 // storing these in advance, in case they have changed since this task
+    //                 // was started.
+    //                 let (item_ix, mut project_item_ids) = pane.read_with(&cx, |pane, cx| {
+    //                     (pane.index_for_item(&*item), item.project_item_model_ids(cx))
+    //                 })?;
+    //                 let item_ix = if let Some(ix) = item_ix {
+    //                     ix
+    //                 } else {
+    //                     continue;
+    //                 };
+
+    //                 // Check if this view has any project items that are not open anywhere else
+    //                 // in the workspace, AND that the user has not already been prompted to save.
+    //                 // If there are any such project entries, prompt the user to save this item.
+    //                 let project = workspace.read_with(&cx, |workspace, cx| {
+    //                     for item in workspace.items(cx) {
+    //                         if !items_to_close
+    //                             .iter()
+    //                             .any(|item_to_close| item_to_close.id() == item.id())
+    //                         {
+    //                             let other_project_item_ids = item.project_item_model_ids(cx);
+    //                             project_item_ids.retain(|id| !other_project_item_ids.contains(id));
+    //                         }
+    //                     }
+    //                     workspace.project().clone()
+    //                 })?;
+    //                 let should_save = project_item_ids
+    //                     .iter()
+    //                     .any(|id| saved_project_items_ids.insert(*id));
+
+    //                 if should_save
+    //                     && !Self::save_item(
+    //                         project.clone(),
+    //                         &pane,
+    //                         item_ix,
+    //                         &*item,
+    //                         save_intent,
+    //                         &mut cx,
+    //                     )
+    //                     .await?
+    //                 {
+    //                     break;
+    //                 }
+
+    //                 // Remove the item from the pane.
+    //                 pane.update(&mut cx, |pane, cx| {
+    //                     if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
+    //                         pane.remove_item(item_ix, false, cx);
+    //                     }
+    //                 })?;
+    //             }
+
+    //             pane.update(&mut cx, |_, cx| cx.notify())?;
+    //             Ok(())
+    //         })
+    //     }
+
+    //     pub fn remove_item(
+    //         &mut self,
+    //         item_index: usize,
+    //         activate_pane: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         self.activation_history
+    //             .retain(|&history_entry| history_entry != self.items[item_index].id());
+
+    //         if item_index == self.active_item_index {
+    //             let index_to_activate = self
+    //                 .activation_history
+    //                 .pop()
+    //                 .and_then(|last_activated_item| {
+    //                     self.items.iter().enumerate().find_map(|(index, item)| {
+    //                         (item.id() == last_activated_item).then_some(index)
+    //                     })
+    //                 })
+    //                 // We didn't have a valid activation history entry, so fallback
+    //                 // to activating the item to the left
+    //                 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
+
+    //             let should_activate = activate_pane || self.has_focus;
+    //             self.activate_item(index_to_activate, should_activate, should_activate, cx);
+    //         }
+
+    //         let item = self.items.remove(item_index);
+
+    //         cx.emit(Event::RemoveItem { item_id: item.id() });
+    //         if self.items.is_empty() {
+    //             item.deactivated(cx);
+    //             self.update_toolbar(cx);
+    //             cx.emit(Event::Remove);
+    //         }
+
+    //         if item_index < self.active_item_index {
+    //             self.active_item_index -= 1;
+    //         }
+
+    //         self.nav_history.set_mode(NavigationMode::ClosingItem);
+    //         item.deactivated(cx);
+    //         self.nav_history.set_mode(NavigationMode::Normal);
+
+    //         if let Some(path) = item.project_path(cx) {
+    //             let abs_path = self
+    //                 .nav_history
+    //                 .0
+    //                 .borrow()
+    //                 .paths_by_item
+    //                 .get(&item.id())
+    //                 .and_then(|(_, abs_path)| abs_path.clone());
+
+    //             self.nav_history
+    //                 .0
+    //                 .borrow_mut()
+    //                 .paths_by_item
+    //                 .insert(item.id(), (path, abs_path));
+    //         } else {
+    //             self.nav_history
+    //                 .0
+    //                 .borrow_mut()
+    //                 .paths_by_item
+    //                 .remove(&item.id());
+    //         }
+
+    //         if self.items.is_empty() && self.zoomed {
+    //             cx.emit(Event::ZoomOut);
+    //         }
+
+    //         cx.notify();
+    //     }
+
+    //     pub async fn save_item(
+    //         project: ModelHandle<Project>,
+    //         pane: &WeakViewHandle<Pane>,
+    //         item_ix: usize,
+    //         item: &dyn ItemHandle,
+    //         save_intent: SaveIntent,
+    //         cx: &mut AsyncAppContext,
+    //     ) -> Result<bool> {
+    //         const CONFLICT_MESSAGE: &str =
+    //             "This file has changed on disk since you started editing it. Do you want to overwrite it?";
+
+    //         if save_intent == SaveIntent::Skip {
+    //             return Ok(true);
+    //         }
+
+    //         let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.read(|cx| {
+    //             (
+    //                 item.has_conflict(cx),
+    //                 item.is_dirty(cx),
+    //                 item.can_save(cx),
+    //                 item.is_singleton(cx),
+    //             )
+    //         });
+
+    //         // when saving a single buffer, we ignore whether or not it's dirty.
+    //         if save_intent == SaveIntent::Save {
+    //             is_dirty = true;
+    //         }
+
+    //         if save_intent == SaveIntent::SaveAs {
+    //             is_dirty = true;
+    //             has_conflict = false;
+    //             can_save = false;
+    //         }
+
+    //         if save_intent == SaveIntent::Overwrite {
+    //             has_conflict = false;
+    //         }
+
+    //         if has_conflict && can_save {
+    //             let mut answer = pane.update(cx, |pane, cx| {
+    //                 pane.activate_item(item_ix, true, true, cx);
+    //                 cx.prompt(
+    //                     PromptLevel::Warning,
+    //                     CONFLICT_MESSAGE,
+    //                     &["Overwrite", "Discard", "Cancel"],
+    //                 )
+    //             })?;
+    //             match answer.next().await {
+    //                 Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
+    //                 Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
+    //                 _ => return Ok(false),
+    //             }
+    //         } else if is_dirty && (can_save || can_save_as) {
+    //             if save_intent == SaveIntent::Close {
+    //                 let will_autosave = cx.read(|cx| {
+    //                     matches!(
+    //                         settings::get::<WorkspaceSettings>(cx).autosave,
+    //                         AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
+    //                     ) && Self::can_autosave_item(&*item, cx)
+    //                 });
+    //                 if !will_autosave {
+    //                     let mut answer = pane.update(cx, |pane, cx| {
+    //                         pane.activate_item(item_ix, true, true, cx);
+    //                         let prompt = dirty_message_for(item.project_path(cx));
+    //                         cx.prompt(
+    //                             PromptLevel::Warning,
+    //                             &prompt,
+    //                             &["Save", "Don't Save", "Cancel"],
+    //                         )
+    //                     })?;
+    //                     match answer.next().await {
+    //                         Some(0) => {}
+    //                         Some(1) => return Ok(true), // Don't save his file
+    //                         _ => return Ok(false),      // Cancel
+    //                     }
+    //                 }
+    //             }
+
+    //             if can_save {
+    //                 pane.update(cx, |_, cx| item.save(project, cx))?.await?;
+    //             } else if can_save_as {
+    //                 let start_abs_path = project
+    //                     .read_with(cx, |project, cx| {
+    //                         let worktree = project.visible_worktrees(cx).next()?;
+    //                         Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
+    //                     })
+    //                     .unwrap_or_else(|| Path::new("").into());
+
+    //                 let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
+    //                 if let Some(abs_path) = abs_path.next().await.flatten() {
+    //                     pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
+    //                         .await?;
+    //                 } else {
+    //                     return Ok(false);
+    //                 }
+    //             }
+    //         }
+    //         Ok(true)
+    //     }
+
+    //     fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
+    //         let is_deleted = item.project_entry_ids(cx).is_empty();
+    //         item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
+    //     }
+
+    //     pub fn autosave_item(
+    //         item: &dyn ItemHandle,
+    //         project: ModelHandle<Project>,
+    //         cx: &mut WindowContext,
+    //     ) -> Task<Result<()>> {
+    //         if Self::can_autosave_item(item, cx) {
+    //             item.save(project, cx)
+    //         } else {
+    //             Task::ready(Ok(()))
+    //         }
+    //     }
+
+    //     pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
+    //         if let Some(active_item) = self.active_item() {
+    //             cx.focus(active_item.as_any());
+    //         }
+    //     }
+
+    //     pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
+    //         cx.emit(Event::Split(direction));
+    //     }
+
+    //     fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
+    //         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
+    //             menu.toggle(
+    //                 Default::default(),
+    //                 AnchorCorner::TopRight,
+    //                 vec![
+    //                     ContextMenuItem::action("Split Right", SplitRight),
+    //                     ContextMenuItem::action("Split Left", SplitLeft),
+    //                     ContextMenuItem::action("Split Up", SplitUp),
+    //                     ContextMenuItem::action("Split Down", SplitDown),
+    //                 ],
+    //                 cx,
+    //             );
+    //         });
+
+    //         self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
+    //     }
+
+    //     fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
+    //         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
+    //             menu.toggle(
+    //                 Default::default(),
+    //                 AnchorCorner::TopRight,
+    //                 vec![
+    //                     ContextMenuItem::action("New File", NewFile),
+    //                     ContextMenuItem::action("New Terminal", NewCenterTerminal),
+    //                     ContextMenuItem::action("New Search", NewSearch),
+    //                 ],
+    //                 cx,
+    //             );
+    //         });
+
+    //         self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
+    //     }
+
+    //     fn deploy_tab_context_menu(
+    //         &mut self,
+    //         position: Vector2F,
+    //         target_item_id: usize,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         let active_item_id = self.items[self.active_item_index].id();
+    //         let is_active_item = target_item_id == active_item_id;
+    //         let target_pane = cx.weak_handle();
+
+    //         // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on.  Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
+
+    //         self.tab_context_menu.update(cx, |menu, cx| {
+    //             menu.show(
+    //                 position,
+    //                 AnchorCorner::TopLeft,
+    //                 if is_active_item {
+    //                     vec![
+    //                         ContextMenuItem::action(
+    //                             "Close Active Item",
+    //                             CloseActiveItem { save_intent: None },
+    //                         ),
+    //                         ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
+    //                         ContextMenuItem::action("Close Clean Items", CloseCleanItems),
+    //                         ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
+    //                         ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
+    //                         ContextMenuItem::action(
+    //                             "Close All Items",
+    //                             CloseAllItems { save_intent: None },
+    //                         ),
+    //                     ]
+    //                 } else {
+    //                     // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
+    //                     vec![
+    //                         ContextMenuItem::handler("Close Inactive Item", {
+    //                             let pane = target_pane.clone();
+    //                             move |cx| {
+    //                                 if let Some(pane) = pane.upgrade(cx) {
+    //                                     pane.update(cx, |pane, cx| {
+    //                                         pane.close_item_by_id(
+    //                                             target_item_id,
+    //                                             SaveIntent::Close,
+    //                                             cx,
+    //                                         )
+    //                                         .detach_and_log_err(cx);
+    //                                     })
+    //                                 }
+    //                             }
+    //                         }),
+    //                         ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
+    //                         ContextMenuItem::action("Close Clean Items", CloseCleanItems),
+    //                         ContextMenuItem::handler("Close Items To The Left", {
+    //                             let pane = target_pane.clone();
+    //                             move |cx| {
+    //                                 if let Some(pane) = pane.upgrade(cx) {
+    //                                     pane.update(cx, |pane, cx| {
+    //                                         pane.close_items_to_the_left_by_id(target_item_id, cx)
+    //                                             .detach_and_log_err(cx);
+    //                                     })
+    //                                 }
+    //                             }
+    //                         }),
+    //                         ContextMenuItem::handler("Close Items To The Right", {
+    //                             let pane = target_pane.clone();
+    //                             move |cx| {
+    //                                 if let Some(pane) = pane.upgrade(cx) {
+    //                                     pane.update(cx, |pane, cx| {
+    //                                         pane.close_items_to_the_right_by_id(target_item_id, cx)
+    //                                             .detach_and_log_err(cx);
+    //                                     })
+    //                                 }
+    //                             }
+    //                         }),
+    //                         ContextMenuItem::action(
+    //                             "Close All Items",
+    //                             CloseAllItems { save_intent: None },
+    //                         ),
+    //                     ]
+    //                 },
+    //                 cx,
+    //             );
+    //         });
+    //     }
+
+    //     pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
+    //         &self.toolbar
+    //     }
+
+    //     pub fn handle_deleted_project_item(
+    //         &mut self,
+    //         entry_id: ProjectEntryId,
+    //         cx: &mut ViewContext<Pane>,
+    //     ) -> Option<()> {
+    //         let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
+    //             if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
+    //                 Some((i, item.id()))
+    //             } else {
+    //                 None
+    //             }
+    //         })?;
+
+    //         self.remove_item(item_index_to_delete, false, cx);
+    //         self.nav_history.remove_item(item_id);
+
+    //         Some(())
+    //     }
+
+    //     fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
+    //         let active_item = self
+    //             .items
+    //             .get(self.active_item_index)
+    //             .map(|item| item.as_ref());
+    //         self.toolbar.update(cx, |toolbar, cx| {
+    //             toolbar.set_active_item(active_item, cx);
+    //         });
+    //     }
+
+    //     fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
+    //         let theme = theme::current(cx).clone();
+
+    //         let pane = cx.handle().downgrade();
+    //         let autoscroll = if mem::take(&mut self.autoscroll) {
+    //             Some(self.active_item_index)
+    //         } else {
+    //             None
+    //         };
+
+    //         let pane_active = self.has_focus;
+
+    //         enum Tabs {}
+    //         let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
+    //         for (ix, (item, detail)) in self
+    //             .items
+    //             .iter()
+    //             .cloned()
+    //             .zip(self.tab_details(cx))
+    //             .enumerate()
+    //         {
+    //             let git_status = item
+    //                 .project_path(cx)
+    //                 .and_then(|path| self.project.read(cx).entry_for_path(&path, cx))
+    //                 .and_then(|entry| entry.git_status());
+
+    //             let detail = if detail == 0 { None } else { Some(detail) };
+    //             let tab_active = ix == self.active_item_index;
+
+    //             row.add_child({
+    //                 enum TabDragReceiver {}
+    //                 let mut receiver =
+    //                     dragged_item_receiver::<TabDragReceiver, _, _>(self, ix, ix, true, None, cx, {
+    //                         let item = item.clone();
+    //                         let pane = pane.clone();
+    //                         let detail = detail.clone();
+
+    //                         let theme = theme::current(cx).clone();
+    //                         let mut tooltip_theme = theme.tooltip.clone();
+    //                         tooltip_theme.max_text_width = None;
+    //                         let tab_tooltip_text =
+    //                             item.tab_tooltip_text(cx).map(|text| text.into_owned());
+
+    //                         let mut tab_style = theme
+    //                             .workspace
+    //                             .tab_bar
+    //                             .tab_style(pane_active, tab_active)
+    //                             .clone();
+    //                         let should_show_status = settings::get::<ItemSettings>(cx).git_status;
+    //                         if should_show_status && git_status != None {
+    //                             tab_style.label.text.color = match git_status.unwrap() {
+    //                                 GitFileStatus::Added => tab_style.git.inserted,
+    //                                 GitFileStatus::Modified => tab_style.git.modified,
+    //                                 GitFileStatus::Conflict => tab_style.git.conflict,
+    //                             };
+    //                         }
+
+    //                         move |mouse_state, cx| {
+    //                             let hovered = mouse_state.hovered();
+
+    //                             enum Tab {}
+    //                             let mouse_event_handler =
+    //                                 MouseEventHandler::new::<Tab, _>(ix, cx, |_, cx| {
+    //                                     Self::render_tab(
+    //                                         &item,
+    //                                         pane.clone(),
+    //                                         ix == 0,
+    //                                         detail,
+    //                                         hovered,
+    //                                         &tab_style,
+    //                                         cx,
+    //                                     )
+    //                                 })
+    //                                 .on_down(MouseButton::Left, move |_, this, cx| {
+    //                                     this.activate_item(ix, true, true, cx);
+    //                                 })
+    //                                 .on_click(MouseButton::Middle, {
+    //                                     let item_id = item.id();
+    //                                     move |_, pane, cx| {
+    //                                         pane.close_item_by_id(item_id, SaveIntent::Close, cx)
+    //                                             .detach_and_log_err(cx);
+    //                                     }
+    //                                 })
+    //                                 .on_down(
+    //                                     MouseButton::Right,
+    //                                     move |event, pane, cx| {
+    //                                         pane.deploy_tab_context_menu(event.position, item.id(), cx);
+    //                                     },
+    //                                 );
+
+    //                             if let Some(tab_tooltip_text) = tab_tooltip_text {
+    //                                 mouse_event_handler
+    //                                     .with_tooltip::<Self>(
+    //                                         ix,
+    //                                         tab_tooltip_text,
+    //                                         None,
+    //                                         tooltip_theme,
+    //                                         cx,
+    //                                     )
+    //                                     .into_any()
+    //                             } else {
+    //                                 mouse_event_handler.into_any()
+    //                             }
+    //                         }
+    //                     });
+
+    //                 if !pane_active || !tab_active {
+    //                     receiver = receiver.with_cursor_style(CursorStyle::PointingHand);
+    //                 }
+
+    //                 receiver.as_draggable(
+    //                     DraggedItem {
+    //                         handle: item,
+    //                         pane: pane.clone(),
+    //                     },
+    //                     {
+    //                         let theme = theme::current(cx).clone();
+
+    //                         let detail = detail.clone();
+    //                         move |_, dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
+    //                             let tab_style = &theme.workspace.tab_bar.dragged_tab;
+    //                             Self::render_dragged_tab(
+    //                                 &dragged_item.handle,
+    //                                 dragged_item.pane.clone(),
+    //                                 false,
+    //                                 detail,
+    //                                 false,
+    //                                 &tab_style,
+    //                                 cx,
+    //                             )
+    //                         }
+    //                     },
+    //                 )
+    //             })
+    //         }
+
+    //         // Use the inactive tab style along with the current pane's active status to decide how to render
+    //         // the filler
+    //         let filler_index = self.items.len();
+    //         let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
+    //         enum Filler {}
+    //         row.add_child(
+    //             dragged_item_receiver::<Filler, _, _>(self, 0, filler_index, true, None, cx, |_, _| {
+    //                 Empty::new()
+    //                     .contained()
+    //                     .with_style(filler_style.container)
+    //                     .with_border(filler_style.container.border)
+    //             })
+    //             .flex(1., true)
+    //             .into_any_named("filler"),
+    //         );
+
+    //         row
+    //     }
+
+    //     fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
+    //         let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
+
+    //         let mut tab_descriptions = HashMap::default();
+    //         let mut done = false;
+    //         while !done {
+    //             done = true;
+
+    //             // Store item indices by their tab description.
+    //             for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
+    //                 if let Some(description) = item.tab_description(*detail, cx) {
+    //                     if *detail == 0
+    //                         || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
+    //                     {
+    //                         tab_descriptions
+    //                             .entry(description)
+    //                             .or_insert(Vec::new())
+    //                             .push(ix);
+    //                     }
+    //                 }
+    //             }
+
+    //             // If two or more items have the same tab description, increase their level
+    //             // of detail and try again.
+    //             for (_, item_ixs) in tab_descriptions.drain() {
+    //                 if item_ixs.len() > 1 {
+    //                     done = false;
+    //                     for ix in item_ixs {
+    //                         tab_details[ix] += 1;
+    //                     }
+    //                 }
+    //             }
+    //         }
+
+    //         tab_details
+    //     }
+
+    //     fn render_tab(
+    //         item: &Box<dyn ItemHandle>,
+    //         pane: WeakViewHandle<Pane>,
+    //         first: bool,
+    //         detail: Option<usize>,
+    //         hovered: bool,
+    //         tab_style: &theme::Tab,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> AnyElement<Self> {
+    //         let title = item.tab_content(detail, &tab_style, cx);
+    //         Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
+    //     }
+
+    //     fn render_dragged_tab(
+    //         item: &Box<dyn ItemHandle>,
+    //         pane: WeakViewHandle<Pane>,
+    //         first: bool,
+    //         detail: Option<usize>,
+    //         hovered: bool,
+    //         tab_style: &theme::Tab,
+    //         cx: &mut ViewContext<Workspace>,
+    //     ) -> AnyElement<Workspace> {
+    //         let title = item.dragged_tab_content(detail, &tab_style, cx);
+    //         Self::render_tab_with_title(title, item, pane, first, hovered, tab_style, cx)
+    //     }
+
+    //     fn render_tab_with_title<T: View>(
+    //         title: AnyElement<T>,
+    //         item: &Box<dyn ItemHandle>,
+    //         pane: WeakViewHandle<Pane>,
+    //         first: bool,
+    //         hovered: bool,
+    //         tab_style: &theme::Tab,
+    //         cx: &mut ViewContext<T>,
+    //     ) -> AnyElement<T> {
+    //         let mut container = tab_style.container.clone();
+    //         if first {
+    //             container.border.left = false;
+    //         }
+
+    //         let buffer_jewel_element = {
+    //             let diameter = 7.0;
+    //             let icon_color = if item.has_conflict(cx) {
+    //                 Some(tab_style.icon_conflict)
+    //             } else if item.is_dirty(cx) {
+    //                 Some(tab_style.icon_dirty)
+    //             } else {
+    //                 None
+    //             };
+
+    //             Canvas::new(move |bounds, _, _, cx| {
+    //                 if let Some(color) = icon_color {
+    //                     let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
+    //                     cx.scene().push_quad(Quad {
+    //                         bounds: square,
+    //                         background: Some(color),
+    //                         border: Default::default(),
+    //                         corner_radii: (diameter / 2.).into(),
+    //                     });
+    //                 }
+    //             })
+    //             .constrained()
+    //             .with_width(diameter)
+    //             .with_height(diameter)
+    //             .aligned()
+    //         };
+
+    //         let title_element = title.aligned().contained().with_style(ContainerStyle {
+    //             margin: Margin {
+    //                 left: tab_style.spacing,
+    //                 right: tab_style.spacing,
+    //                 ..Default::default()
+    //             },
+    //             ..Default::default()
+    //         });
+
+    //         let close_element = if hovered {
+    //             let item_id = item.id();
+    //             enum TabCloseButton {}
+    //             let icon = Svg::new("icons/x.svg");
+    //             MouseEventHandler::new::<TabCloseButton, _>(item_id, cx, |mouse_state, _| {
+    //                 if mouse_state.hovered() {
+    //                     icon.with_color(tab_style.icon_close_active)
+    //                 } else {
+    //                     icon.with_color(tab_style.icon_close)
+    //                 }
+    //             })
+    //             .with_padding(Padding::uniform(4.))
+    //             .with_cursor_style(CursorStyle::PointingHand)
+    //             .on_click(MouseButton::Left, {
+    //                 let pane = pane.clone();
+    //                 move |_, _, cx| {
+    //                     let pane = pane.clone();
+    //                     cx.window_context().defer(move |cx| {
+    //                         if let Some(pane) = pane.upgrade(cx) {
+    //                             pane.update(cx, |pane, cx| {
+    //                                 pane.close_item_by_id(item_id, SaveIntent::Close, cx)
+    //                                     .detach_and_log_err(cx);
+    //                             });
+    //                         }
+    //                     });
+    //                 }
+    //             })
+    //             .into_any_named("close-tab-icon")
+    //             .constrained()
+    //         } else {
+    //             Empty::new().constrained()
+    //         }
+    //         .with_width(tab_style.close_icon_width)
+    //         .aligned();
+
+    //         let close_right = settings::get::<ItemSettings>(cx).close_position.right();
+
+    //         if close_right {
+    //             Flex::row()
+    //                 .with_child(buffer_jewel_element)
+    //                 .with_child(title_element)
+    //                 .with_child(close_element)
+    //         } else {
+    //             Flex::row()
+    //                 .with_child(close_element)
+    //                 .with_child(title_element)
+    //                 .with_child(buffer_jewel_element)
+    //         }
+    //         .contained()
+    //         .with_style(container)
+    //         .constrained()
+    //         .with_height(tab_style.height)
+    //         .into_any()
+    //     }
+
+    //     pub fn render_tab_bar_button<
+    //         F1: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
+    //         F2: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
+    //     >(
+    //         index: usize,
+    //         icon: &'static str,
+    //         is_active: bool,
+    //         tooltip: Option<(&'static str, Option<Box<dyn Action>>)>,
+    //         cx: &mut ViewContext<Pane>,
+    //         on_click: F1,
+    //         on_down: F2,
+    //         context_menu: Option<ViewHandle<ContextMenu>>,
+    //     ) -> AnyElement<Pane> {
+    //         enum TabBarButton {}
+
+    //         let mut button = MouseEventHandler::new::<TabBarButton, _>(index, cx, |mouse_state, cx| {
+    //             let theme = &settings2::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
+    //             let style = theme.pane_button.in_state(is_active).style_for(mouse_state);
+    //             Svg::new(icon)
+    //                 .with_color(style.color)
+    //                 .constrained()
+    //                 .with_width(style.icon_width)
+    //                 .aligned()
+    //                 .constrained()
+    //                 .with_width(style.button_width)
+    //                 .with_height(style.button_width)
+    //         })
+    //         .with_cursor_style(CursorStyle::PointingHand)
+    //         .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx))
+    //         .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
+    //         .into_any();
+    //         if let Some((tooltip, action)) = tooltip {
+    //             let tooltip_style = settings::get::<ThemeSettings>(cx).theme.tooltip.clone();
+    //             button = button
+    //                 .with_tooltip::<TabBarButton>(index, tooltip, action, tooltip_style, cx)
+    //                 .into_any();
+    //         }
+
+    //         Stack::new()
+    //             .with_child(button)
+    //             .with_children(
+    //                 context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
+    //             )
+    //             .flex(1., false)
+    //             .into_any_named("tab bar button")
+    //     }
+
+    //     fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+    //         let background = theme.workspace.background;
+    //         Empty::new()
+    //             .contained()
+    //             .with_background_color(background)
+    //             .into_any()
+    //     }
+
+    //     pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
+    //         self.zoomed = zoomed;
+    //         cx.notify();
+    //     }
+
+    //     pub fn is_zoomed(&self) -> bool {
+    //         self.zoomed
+    //     }
+    // }
+
+    // impl Entity for Pane {
+    //     type Event = Event;
+    // }
+
+    // impl View for Pane {
+    //     fn ui_name() -> &'static str {
+    //         "Pane"
+    //     }
+
+    //     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+    //         enum MouseNavigationHandler {}
+
+    //         MouseEventHandler::new::<MouseNavigationHandler, _>(0, cx, |_, cx| {
+    //             let active_item_index = self.active_item_index;
+
+    //             if let Some(active_item) = self.active_item() {
+    //                 Flex::column()
+    //                     .with_child({
+    //                         let theme = theme::current(cx).clone();
+
+    //                         let mut stack = Stack::new();
+
+    //                         enum TabBarEventHandler {}
+    //                         stack.add_child(
+    //                             MouseEventHandler::new::<TabBarEventHandler, _>(0, cx, |_, _| {
+    //                                 Empty::new()
+    //                                     .contained()
+    //                                     .with_style(theme.workspace.tab_bar.container)
+    //                             })
+    //                             .on_down(
+    //                                 MouseButton::Left,
+    //                                 move |_, this, cx| {
+    //                                     this.activate_item(active_item_index, true, true, cx);
+    //                                 },
+    //                             ),
+    //                         );
+    //                         let tooltip_style = theme.tooltip.clone();
+    //                         let tab_bar_theme = theme.workspace.tab_bar.clone();
+
+    //                         let nav_button_height = tab_bar_theme.height;
+    //                         let button_style = tab_bar_theme.nav_button;
+    //                         let border_for_nav_buttons = tab_bar_theme
+    //                             .tab_style(false, false)
+    //                             .container
+    //                             .border
+    //                             .clone();
+
+    //                         let mut tab_row = Flex::row()
+    //                             .with_child(nav_button(
+    //                                 "icons/arrow_left.svg",
+    //                                 button_style.clone(),
+    //                                 nav_button_height,
+    //                                 tooltip_style.clone(),
+    //                                 self.can_navigate_backward(),
+    //                                 {
+    //                                     move |pane, cx| {
+    //                                         if let Some(workspace) = pane.workspace.upgrade(cx) {
+    //                                             let pane = cx.weak_handle();
+    //                                             cx.window_context().defer(move |cx| {
+    //                                                 workspace.update(cx, |workspace, cx| {
+    //                                                     workspace
+    //                                                         .go_back(pane, cx)
+    //                                                         .detach_and_log_err(cx)
+    //                                                 })
+    //                                             })
+    //                                         }
+    //                                     }
+    //                                 },
+    //                                 super::GoBack,
+    //                                 "Go Back",
+    //                                 cx,
+    //                             ))
+    //                             .with_child(
+    //                                 nav_button(
+    //                                     "icons/arrow_right.svg",
+    //                                     button_style.clone(),
+    //                                     nav_button_height,
+    //                                     tooltip_style,
+    //                                     self.can_navigate_forward(),
+    //                                     {
+    //                                         move |pane, cx| {
+    //                                             if let Some(workspace) = pane.workspace.upgrade(cx) {
+    //                                                 let pane = cx.weak_handle();
+    //                                                 cx.window_context().defer(move |cx| {
+    //                                                     workspace.update(cx, |workspace, cx| {
+    //                                                         workspace
+    //                                                             .go_forward(pane, cx)
+    //                                                             .detach_and_log_err(cx)
+    //                                                     })
+    //                                                 })
+    //                                             }
+    //                                         }
+    //                                     },
+    //                                     super::GoForward,
+    //                                     "Go Forward",
+    //                                     cx,
+    //                                 )
+    //                                 .contained()
+    //                                 .with_border(border_for_nav_buttons),
+    //                             )
+    //                             .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
+
+    //                         if self.has_focus {
+    //                             let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
+    //                             tab_row.add_child(
+    //                                 (render_tab_bar_buttons)(self, cx)
+    //                                     .contained()
+    //                                     .with_style(theme.workspace.tab_bar.pane_button_container)
+    //                                     .flex(1., false)
+    //                                     .into_any(),
+    //                             )
+    //                         }
+
+    //                         stack.add_child(tab_row);
+    //                         stack
+    //                             .constrained()
+    //                             .with_height(theme.workspace.tab_bar.height)
+    //                             .flex(1., false)
+    //                             .into_any_named("tab bar")
+    //                     })
+    //                     .with_child({
+    //                         enum PaneContentTabDropTarget {}
+    //                         dragged_item_receiver::<PaneContentTabDropTarget, _, _>(
+    //                             self,
+    //                             0,
+    //                             self.active_item_index + 1,
+    //                             !self.can_split,
+    //                             if self.can_split { Some(100.) } else { None },
+    //                             cx,
+    //                             {
+    //                                 let toolbar = self.toolbar.clone();
+    //                                 let toolbar_hidden = toolbar.read(cx).hidden();
+    //                                 move |_, cx| {
+    //                                     Flex::column()
+    //                                         .with_children(
+    //                                             (!toolbar_hidden)
+    //                                                 .then(|| ChildView::new(&toolbar, cx).expanded()),
+    //                                         )
+    //                                         .with_child(
+    //                                             ChildView::new(active_item.as_any(), cx).flex(1., true),
+    //                                         )
+    //                                 }
+    //                             },
+    //                         )
+    //                         .flex(1., true)
+    //                     })
+    //                     .with_child(ChildView::new(&self.tab_context_menu, cx))
+    //                     .into_any()
+    //             } else {
+    //                 enum EmptyPane {}
+    //                 let theme = theme::current(cx).clone();
+
+    //                 dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
+    //                     self.render_blank_pane(&theme, cx)
+    //                 })
+    //                 .on_down(MouseButton::Left, |_, _, cx| {
+    //                     cx.focus_parent();
+    //                 })
+    //                 .into_any()
+    //             }
+    //         })
+    //         .on_down(
+    //             MouseButton::Navigate(NavigationDirection::Back),
+    //             move |_, pane, cx| {
+    //                 if let Some(workspace) = pane.workspace.upgrade(cx) {
+    //                     let pane = cx.weak_handle();
+    //                     cx.window_context().defer(move |cx| {
+    //                         workspace.update(cx, |workspace, cx| {
+    //                             workspace.go_back(pane, cx).detach_and_log_err(cx)
+    //                         })
+    //                     })
+    //                 }
+    //             },
+    //         )
+    //         .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
+    //             move |_, pane, cx| {
+    //                 if let Some(workspace) = pane.workspace.upgrade(cx) {
+    //                     let pane = cx.weak_handle();
+    //                     cx.window_context().defer(move |cx| {
+    //                         workspace.update(cx, |workspace, cx| {
+    //                             workspace.go_forward(pane, cx).detach_and_log_err(cx)
+    //                         })
+    //                     })
+    //                 }
+    //             }
+    //         })
+    //         .into_any_named("pane")
+    //     }
+
+    //     fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
+    //         if !self.has_focus {
+    //             self.has_focus = true;
+    //             cx.emit(Event::Focus);
+    //             cx.notify();
+    //         }
+
+    //         self.toolbar.update(cx, |toolbar, cx| {
+    //             toolbar.focus_changed(true, cx);
+    //         });
+
+    //         if let Some(active_item) = self.active_item() {
+    //             if cx.is_self_focused() {
+    //                 // Pane was focused directly. We need to either focus a view inside the active item,
+    //                 // or focus the active item itself
+    //                 if let Some(weak_last_focused_view) =
+    //                     self.last_focused_view_by_item.get(&active_item.id())
+    //                 {
+    //                     if let Some(last_focused_view) = weak_last_focused_view.upgrade(cx) {
+    //                         cx.focus(&last_focused_view);
+    //                         return;
+    //                     } else {
+    //                         self.last_focused_view_by_item.remove(&active_item.id());
+    //                     }
+    //                 }
+
+    //                 cx.focus(active_item.as_any());
+    //             } else if focused != self.tab_bar_context_menu.handle {
+    //                 self.last_focused_view_by_item
+    //                     .insert(active_item.id(), focused.downgrade());
+    //             }
+    //         }
+    //     }
+
+    //     fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+    //         self.has_focus = false;
+    //         self.toolbar.update(cx, |toolbar, cx| {
+    //             toolbar.focus_changed(false, cx);
+    //         });
+    //         cx.notify();
+    //     }
+
+    //     fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+    //         Self::reset_to_default_keymap_context(keymap);
+    //     }
+    // }
+
+    // impl ItemNavHistory {
+    //     pub fn push<D: 'static + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
+    //         self.history.push(data, self.item.clone(), cx);
+    //     }
+
+    //     pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
+    //         self.history.pop(NavigationMode::GoingBack, cx)
+    //     }
+
+    //     pub fn pop_forward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
+    //         self.history.pop(NavigationMode::GoingForward, cx)
+    //     }
+    // }
+
+    // impl NavHistory {
+    //     pub fn for_each_entry(
+    //         &self,
+    //         cx: &AppContext,
+    //         mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
+    //     ) {
+    //         let borrowed_history = self.0.borrow();
+    //         borrowed_history
+    //             .forward_stack
+    //             .iter()
+    //             .chain(borrowed_history.backward_stack.iter())
+    //             .chain(borrowed_history.closed_stack.iter())
+    //             .for_each(|entry| {
+    //                 if let Some(project_and_abs_path) =
+    //                     borrowed_history.paths_by_item.get(&entry.item.id())
+    //                 {
+    //                     f(entry, project_and_abs_path.clone());
+    //                 } else if let Some(item) = entry.item.upgrade(cx) {
+    //                     if let Some(path) = item.project_path(cx) {
+    //                         f(entry, (path, None));
+    //                     }
+    //                 }
+    //             })
+    //     }
+
+    //     pub fn set_mode(&mut self, mode: NavigationMode) {
+    //         self.0.borrow_mut().mode = mode;
+    //     }
+
+    pub fn mode(&self) -> NavigationMode {
+        self.0.borrow().mode
+    }
+
+    //     pub fn disable(&mut self) {
+    //         self.0.borrow_mut().mode = NavigationMode::Disabled;
+    //     }
+
+    //     pub fn enable(&mut self) {
+    //         self.0.borrow_mut().mode = NavigationMode::Normal;
+    //     }
+
+    //     pub fn pop(&mut self, mode: NavigationMode, cx: &mut WindowContext) -> Option<NavigationEntry> {
+    //         let mut state = self.0.borrow_mut();
+    //         let entry = match mode {
+    //             NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
+    //                 return None
+    //             }
+    //             NavigationMode::GoingBack => &mut state.backward_stack,
+    //             NavigationMode::GoingForward => &mut state.forward_stack,
+    //             NavigationMode::ReopeningClosedItem => &mut state.closed_stack,
+    //         }
+    //         .pop_back();
+    //         if entry.is_some() {
+    //             state.did_update(cx);
+    //         }
+    //         entry
+    //     }
+
+    //     pub fn push<D: 'static + Any>(
+    //         &mut self,
+    //         data: Option<D>,
+    //         item: Rc<dyn WeakItemHandle>,
+    //         cx: &mut WindowContext,
+    //     ) {
+    //         let state = &mut *self.0.borrow_mut();
+    //         match state.mode {
+    //             NavigationMode::Disabled => {}
+    //             NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
+    //                 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+    //                     state.backward_stack.pop_front();
+    //                 }
+    //                 state.backward_stack.push_back(NavigationEntry {
+    //                     item,
+    //                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
+    //                     timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
+    //                 });
+    //                 state.forward_stack.clear();
+    //             }
+    //             NavigationMode::GoingBack => {
+    //                 if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+    //                     state.forward_stack.pop_front();
+    //                 }
+    //                 state.forward_stack.push_back(NavigationEntry {
+    //                     item,
+    //                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
+    //                     timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
+    //                 });
+    //             }
+    //             NavigationMode::GoingForward => {
+    //                 if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+    //                     state.backward_stack.pop_front();
+    //                 }
+    //                 state.backward_stack.push_back(NavigationEntry {
+    //                     item,
+    //                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
+    //                     timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
+    //                 });
+    //             }
+    //             NavigationMode::ClosingItem => {
+    //                 if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+    //                     state.closed_stack.pop_front();
+    //                 }
+    //                 state.closed_stack.push_back(NavigationEntry {
+    //                     item,
+    //                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
+    //                     timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
+    //                 });
+    //             }
+    //         }
+    //         state.did_update(cx);
+    //     }
+
+    //     pub fn remove_item(&mut self, item_id: usize) {
+    //         let mut state = self.0.borrow_mut();
+    //         state.paths_by_item.remove(&item_id);
+    //         state
+    //             .backward_stack
+    //             .retain(|entry| entry.item.id() != item_id);
+    //         state
+    //             .forward_stack
+    //             .retain(|entry| entry.item.id() != item_id);
+    //         state
+    //             .closed_stack
+    //             .retain(|entry| entry.item.id() != item_id);
+    //     }
+
+    //     pub fn path_for_item(&self, item_id: usize) -> Option<(ProjectPath, Option<PathBuf>)> {
+    //         self.0.borrow().paths_by_item.get(&item_id).cloned()
+    //     }
+}
+
+// impl NavHistoryState {
+//     pub fn did_update(&self, cx: &mut WindowContext) {
+//         if let Some(pane) = self.pane.upgrade(cx) {
+//             cx.defer(move |cx| {
+//                 pane.update(cx, |pane, cx| pane.history_updated(cx));
+//             });
+//         }
+//     }
+// }
+
+// pub struct PaneBackdrop<V> {
+//     child_view: usize,
+//     child: AnyElement<V>,
+// }
+
+// impl<V> PaneBackdrop<V> {
+//     pub fn new(pane_item_view: usize, child: AnyElement<V>) -> Self {
+//         PaneBackdrop {
+//             child,
+//             child_view: pane_item_view,
+//         }
+//     }
+// }
+
+// impl<V: 'static> Element<V> for PaneBackdrop<V> {
+//     type LayoutState = ();
+
+//     type PaintState = ();
+
+//     fn layout(
+//         &mut self,
+//         constraint: gpui::SizeConstraint,
+//         view: &mut V,
+//         cx: &mut ViewContext<V>,
+//     ) -> (Vector2F, Self::LayoutState) {
+//         let size = self.child.layout(constraint, view, cx);
+//         (size, ())
+//     }
+
+//     fn paint(
+//         &mut self,
+//         bounds: RectF,
+//         visible_bounds: RectF,
+//         _: &mut Self::LayoutState,
+//         view: &mut V,
+//         cx: &mut ViewContext<V>,
+//     ) -> Self::PaintState {
+//         let background = theme::current(cx).editor.background;
+
+//         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+
+//         cx.scene().push_quad(gpui::Quad {
+//             bounds: RectF::new(bounds.origin(), bounds.size()),
+//             background: Some(background),
+//             ..Default::default()
+//         });
+
+//         let child_view_id = self.child_view;
+//         cx.scene().push_mouse_region(
+//             MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
+//                 gpui::platform::MouseButton::Left,
+//                 move |_, _: &mut V, cx| {
+//                     let window = cx.window();
+//                     cx.app_context().focus(window, Some(child_view_id))
+//                 },
+//             ),
+//         );
+
+//         cx.scene().push_layer(Some(bounds));
+//         self.child.paint(bounds.origin(), visible_bounds, view, cx);
+//         cx.scene().pop_layer();
+//     }
+
+//     fn rect_for_text_range(
+//         &self,
+//         range_utf16: std::ops::Range<usize>,
+//         _bounds: RectF,
+//         _visible_bounds: RectF,
+//         _layout: &Self::LayoutState,
+//         _paint: &Self::PaintState,
+//         view: &V,
+//         cx: &gpui::ViewContext<V>,
+//     ) -> Option<RectF> {
+//         self.child.rect_for_text_range(range_utf16, view, cx)
+//     }
+
+//     fn debug(
+//         &self,
+//         _bounds: RectF,
+//         _layout: &Self::LayoutState,
+//         _paint: &Self::PaintState,
+//         view: &V,
+//         cx: &gpui::ViewContext<V>,
+//     ) -> serde_json::Value {
+//         gpui::json::json!({
+//             "type": "Pane Back Drop",
+//             "view": self.child_view,
+//             "child": self.child.debug(view, cx),
+//         })
+//     }
+// }
+
+// fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
+//     let path = buffer_path
+//         .as_ref()
+//         .and_then(|p| p.path.to_str())
+//         .unwrap_or(&"This buffer");
+//     let path = truncate_and_remove_front(path, 80);
+//     format!("{path} contains unsaved edits. Do you want to save it?")
+// }
+
+// todo!("uncomment tests")
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use crate::item::test::{TestItem, TestProjectItem};
+//     use gpui::TestAppContext;
+//     use project::FakeFs;
+//     use settings::SettingsStore;
+
+//     #[gpui::test]
+//     async fn test_remove_active_empty(cx: &mut TestAppContext) {
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         pane.update(cx, |pane, cx| {
+//             assert!(pane
+//                 .close_active_item(&CloseActiveItem { save_intent: None }, cx)
+//                 .is_none())
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
+//         cx.foreground().forbid_parking();
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         // 1. Add with a destination index
+//         //   a. Add before the active item
+//         set_labeled_items(&pane, ["A", "B*", "C"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(
+//                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+//                 false,
+//                 false,
+//                 Some(0),
+//                 cx,
+//             );
+//         });
+//         assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
+
+//         //   b. Add after the active item
+//         set_labeled_items(&pane, ["A", "B*", "C"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(
+//                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+//                 false,
+//                 false,
+//                 Some(2),
+//                 cx,
+//             );
+//         });
+//         assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
+
+//         //   c. Add at the end of the item list (including off the length)
+//         set_labeled_items(&pane, ["A", "B*", "C"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(
+//                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+//                 false,
+//                 false,
+//                 Some(5),
+//                 cx,
+//             );
+//         });
+//         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
+
+//         // 2. Add without a destination index
+//         //   a. Add with active item at the start of the item list
+//         set_labeled_items(&pane, ["A*", "B", "C"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(
+//                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+//                 false,
+//                 false,
+//                 None,
+//                 cx,
+//             );
+//         });
+//         set_labeled_items(&pane, ["A", "D*", "B", "C"], cx);
+
+//         //   b. Add with active item at the end of the item list
+//         set_labeled_items(&pane, ["A", "B", "C*"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(
+//                 Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+//                 false,
+//                 false,
+//                 None,
+//                 cx,
+//             );
+//         });
+//         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
+//     }
+
+//     #[gpui::test]
+//     async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
+//         cx.foreground().forbid_parking();
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         // 1. Add with a destination index
+//         //   1a. Add before the active item
+//         let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(d, false, false, Some(0), cx);
+//         });
+//         assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
+
+//         //   1b. Add after the active item
+//         let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(d, false, false, Some(2), cx);
+//         });
+//         assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
+
+//         //   1c. Add at the end of the item list (including off the length)
+//         let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(a, false, false, Some(5), cx);
+//         });
+//         assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
+
+//         //   1d. Add same item to active index
+//         let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(b, false, false, Some(1), cx);
+//         });
+//         assert_item_labels(&pane, ["A", "B*", "C"], cx);
+
+//         //   1e. Add item to index after same item in last position
+//         let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(c, false, false, Some(2), cx);
+//         });
+//         assert_item_labels(&pane, ["A", "B", "C*"], cx);
+
+//         // 2. Add without a destination index
+//         //   2a. Add with active item at the start of the item list
+//         let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(d, false, false, None, cx);
+//         });
+//         assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
+
+//         //   2b. Add with active item at the end of the item list
+//         let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(a, false, false, None, cx);
+//         });
+//         assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
+
+//         //   2c. Add active item to active item at end of list
+//         let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(c, false, false, None, cx);
+//         });
+//         assert_item_labels(&pane, ["A", "B", "C*"], cx);
+
+//         //   2d. Add active item to active item at start of list
+//         let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx);
+//         pane.update(cx, |pane, cx| {
+//             pane.add_item(a, false, false, None, cx);
+//         });
+//         assert_item_labels(&pane, ["A*", "B", "C"], cx);
+//     }
+
+//     #[gpui::test]
+//     async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
+//         cx.foreground().forbid_parking();
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         // singleton view
+//         pane.update(cx, |pane, cx| {
+//             let item = TestItem::new()
+//                 .with_singleton(true)
+//                 .with_label("buffer 1")
+//                 .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]);
+
+//             pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
+//         });
+//         assert_item_labels(&pane, ["buffer 1*"], cx);
+
+//         // new singleton view with the same project entry
+//         pane.update(cx, |pane, cx| {
+//             let item = TestItem::new()
+//                 .with_singleton(true)
+//                 .with_label("buffer 1")
+//                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
+
+//             pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
+//         });
+//         assert_item_labels(&pane, ["buffer 1*"], cx);
+
+//         // new singleton view with different project entry
+//         pane.update(cx, |pane, cx| {
+//             let item = TestItem::new()
+//                 .with_singleton(true)
+//                 .with_label("buffer 2")
+//                 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]);
+//             pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
+//         });
+//         assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
+
+//         // new multibuffer view with the same project entry
+//         pane.update(cx, |pane, cx| {
+//             let item = TestItem::new()
+//                 .with_singleton(false)
+//                 .with_label("multibuffer 1")
+//                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
+
+//             pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
+//         });
+//         assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
+
+//         // another multibuffer view with the same project entry
+//         pane.update(cx, |pane, cx| {
+//             let item = TestItem::new()
+//                 .with_singleton(false)
+//                 .with_label("multibuffer 1b")
+//                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]);
+
+//             pane.add_item(Box::new(cx.add_view(|_| item)), false, false, None, cx);
+//         });
+//         assert_item_labels(
+//             &pane,
+//             ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
+//             cx,
+//         );
+//     }
+
+//     #[gpui::test]
+//     async fn test_remove_item_ordering(cx: &mut TestAppContext) {
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         add_labeled_item(&pane, "A", false, cx);
+//         add_labeled_item(&pane, "B", false, cx);
+//         add_labeled_item(&pane, "C", false, cx);
+//         add_labeled_item(&pane, "D", false, cx);
+//         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
+
+//         pane.update(cx, |pane, cx| pane.activate_item(1, false, false, cx));
+//         add_labeled_item(&pane, "1", false, cx);
+//         assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
+//         })
+//         .unwrap()
+//         .await
+//         .unwrap();
+//         assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
+
+//         pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
+//         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
+//         })
+//         .unwrap()
+//         .await
+//         .unwrap();
+//         assert_item_labels(&pane, ["A", "B*", "C"], cx);
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
+//         })
+//         .unwrap()
+//         .await
+//         .unwrap();
+//         assert_item_labels(&pane, ["A", "C*"], cx);
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
+//         })
+//         .unwrap()
+//         .await
+//         .unwrap();
+//         assert_item_labels(&pane, ["A*"], cx);
+//     }
+
+//     #[gpui::test]
+//     async fn test_close_inactive_items(cx: &mut TestAppContext) {
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_inactive_items(&CloseInactiveItems, cx)
+//         })
+//         .unwrap()
+//         .await
+//         .unwrap();
+//         assert_item_labels(&pane, ["C*"], cx);
+//     }
+
+//     #[gpui::test]
+//     async fn test_close_clean_items(cx: &mut TestAppContext) {
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         add_labeled_item(&pane, "A", true, cx);
+//         add_labeled_item(&pane, "B", false, cx);
+//         add_labeled_item(&pane, "C", true, cx);
+//         add_labeled_item(&pane, "D", false, cx);
+//         add_labeled_item(&pane, "E", false, cx);
+//         assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
+
+//         pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
+//             .unwrap()
+//             .await
+//             .unwrap();
+//         assert_item_labels(&pane, ["A^", "C*^"], cx);
+//     }
+
+//     #[gpui::test]
+//     async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
+//         })
+//         .unwrap()
+//         .await
+//         .unwrap();
+//         assert_item_labels(&pane, ["C*", "D", "E"], cx);
+//     }
+
+//     #[gpui::test]
+//     async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_items_to_the_right(&CloseItemsToTheRight, cx)
+//         })
+//         .unwrap()
+//         .await
+//         .unwrap();
+//         assert_item_labels(&pane, ["A", "B", "C*"], cx);
+//     }
+
+//     #[gpui::test]
+//     async fn test_close_all_items(cx: &mut TestAppContext) {
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         add_labeled_item(&pane, "A", false, cx);
+//         add_labeled_item(&pane, "B", false, cx);
+//         add_labeled_item(&pane, "C", false, cx);
+//         assert_item_labels(&pane, ["A", "B", "C*"], cx);
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
+//         })
+//         .unwrap()
+//         .await
+//         .unwrap();
+//         assert_item_labels(&pane, [], cx);
+
+//         add_labeled_item(&pane, "A", true, cx);
+//         add_labeled_item(&pane, "B", true, cx);
+//         add_labeled_item(&pane, "C", true, cx);
+//         assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
+
+//         let save = pane
+//             .update(cx, |pane, cx| {
+//                 pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
+//             })
+//             .unwrap();
+
+//         cx.foreground().run_until_parked();
+//         window.simulate_prompt_answer(2, cx);
+//         save.await.unwrap();
+//         assert_item_labels(&pane, [], cx);
+//     }
+
+//     fn init_test(cx: &mut TestAppContext) {
+//         cx.update(|cx| {
+//             cx.set_global(SettingsStore::test(cx));
+//             theme::init((), cx);
+//             crate::init_settings(cx);
+//             Project::init_settings(cx);
+//         });
+//     }
+
+//     fn add_labeled_item(
+//         pane: &ViewHandle<Pane>,
+//         label: &str,
+//         is_dirty: bool,
+//         cx: &mut TestAppContext,
+//     ) -> Box<ViewHandle<TestItem>> {
+//         pane.update(cx, |pane, cx| {
+//             let labeled_item =
+//                 Box::new(cx.add_view(|_| TestItem::new().with_label(label).with_dirty(is_dirty)));
+//             pane.add_item(labeled_item.clone(), false, false, None, cx);
+//             labeled_item
+//         })
+//     }
+
+//     fn set_labeled_items<const COUNT: usize>(
+//         pane: &ViewHandle<Pane>,
+//         labels: [&str; COUNT],
+//         cx: &mut TestAppContext,
+//     ) -> [Box<ViewHandle<TestItem>>; COUNT] {
+//         pane.update(cx, |pane, cx| {
+//             pane.items.clear();
+//             let mut active_item_index = 0;
+
+//             let mut index = 0;
+//             let items = labels.map(|mut label| {
+//                 if label.ends_with("*") {
+//                     label = label.trim_end_matches("*");
+//                     active_item_index = index;
+//                 }
+
+//                 let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
+//                 pane.add_item(labeled_item.clone(), false, false, None, cx);
+//                 index += 1;
+//                 labeled_item
+//             });
+
+//             pane.activate_item(active_item_index, false, false, cx);
+
+//             items
+//         })
+//     }
+
+//     // Assert the item label, with the active item label suffixed with a '*'
+//     fn assert_item_labels<const COUNT: usize>(
+//         pane: &ViewHandle<Pane>,
+//         expected_states: [&str; COUNT],
+//         cx: &mut TestAppContext,
+//     ) {
+//         pane.read_with(cx, |pane, cx| {
+//             let actual_states = pane
+//                 .items
+//                 .iter()
+//                 .enumerate()
+//                 .map(|(ix, item)| {
+//                     let mut state = item
+//                         .as_any()
+//                         .downcast_ref::<TestItem>()
+//                         .unwrap()
+//                         .read(cx)
+//                         .label
+//                         .clone();
+//                     if ix == pane.active_item_index {
+//                         state.push('*');
+//                     }
+//                     if item.is_dirty(cx) {
+//                         state.push('^');
+//                     }
+//                     state
+//                 })
+//                 .collect::<Vec<_>>();
+
+//             assert_eq!(
+//                 actual_states, expected_states,
+//                 "pane items do not match expectation"
+//             );
+//         })
+//     }
+// }

crates/workspace2/src/pane_group.rs 🔗

@@ -0,0 +1,993 @@
+use crate::{AppState, FollowerState, Pane, Workspace};
+use anyhow::{anyhow, Result};
+use call2::ActiveCall;
+use collections::HashMap;
+use gpui2::{size, AnyElement, AnyView, Bounds, Handle, Pixels, Point, View, ViewContext};
+use project2::Project;
+use serde::Deserialize;
+use std::{cell::RefCell, rc::Rc, sync::Arc};
+use theme2::Theme;
+
+const HANDLE_HITBOX_SIZE: f32 = 4.0;
+const HORIZONTAL_MIN_SIZE: f32 = 80.;
+const VERTICAL_MIN_SIZE: f32 = 100.;
+
+pub enum Axis {
+    Vertical,
+    Horizontal,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct PaneGroup {
+    pub(crate) root: Member,
+}
+
+impl PaneGroup {
+    pub(crate) fn with_root(root: Member) -> Self {
+        Self { root }
+    }
+
+    pub fn new(pane: View<Pane>) -> Self {
+        Self {
+            root: Member::Pane(pane),
+        }
+    }
+
+    pub fn split(
+        &mut self,
+        old_pane: &View<Pane>,
+        new_pane: &View<Pane>,
+        direction: SplitDirection,
+    ) -> Result<()> {
+        match &mut self.root {
+            Member::Pane(pane) => {
+                if pane == old_pane {
+                    self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
+                    Ok(())
+                } else {
+                    Err(anyhow!("Pane not found"))
+                }
+            }
+            Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
+        }
+    }
+
+    pub fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
+        match &self.root {
+            Member::Pane(_) => None,
+            Member::Axis(axis) => axis.bounding_box_for_pane(pane),
+        }
+    }
+
+    pub fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&View<Pane>> {
+        match &self.root {
+            Member::Pane(pane) => Some(pane),
+            Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
+        }
+    }
+
+    /// Returns:
+    /// - Ok(true) if it found and removed a pane
+    /// - Ok(false) if it found but did not remove the pane
+    /// - Err(_) if it did not find the pane
+    pub fn remove(&mut self, pane: &View<Pane>) -> Result<bool> {
+        match &mut self.root {
+            Member::Pane(_) => Ok(false),
+            Member::Axis(axis) => {
+                if let Some(last_pane) = axis.remove(pane)? {
+                    self.root = last_pane;
+                }
+                Ok(true)
+            }
+        }
+    }
+
+    pub fn swap(&mut self, from: &View<Pane>, to: &View<Pane>) {
+        match &mut self.root {
+            Member::Pane(_) => {}
+            Member::Axis(axis) => axis.swap(from, to),
+        };
+    }
+
+    pub(crate) fn render(
+        &self,
+        project: &Handle<Project>,
+        theme: &Theme,
+        follower_states: &HashMap<View<Pane>, FollowerState>,
+        active_call: Option<&Handle<ActiveCall>>,
+        active_pane: &View<Pane>,
+        zoomed: Option<&AnyView>,
+        app_state: &Arc<AppState>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> AnyElement<Workspace> {
+        self.root.render(
+            project,
+            0,
+            theme,
+            follower_states,
+            active_call,
+            active_pane,
+            zoomed,
+            app_state,
+            cx,
+        )
+    }
+
+    pub(crate) fn panes(&self) -> Vec<&View<Pane>> {
+        let mut panes = Vec::new();
+        self.root.collect_panes(&mut panes);
+        panes
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) enum Member {
+    Axis(PaneAxis),
+    Pane(View<Pane>),
+}
+
+impl Member {
+    fn new_axis(old_pane: View<Pane>, new_pane: View<Pane>, direction: SplitDirection) -> Self {
+        use Axis::*;
+        use SplitDirection::*;
+
+        let axis = match direction {
+            Up | Down => Vertical,
+            Left | Right => Horizontal,
+        };
+
+        let members = match direction {
+            Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)],
+            Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
+        };
+
+        Member::Axis(PaneAxis::new(axis, members))
+    }
+
+    fn contains(&self, needle: &View<Pane>) -> bool {
+        match self {
+            Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)),
+            Member::Pane(pane) => pane == needle,
+        }
+    }
+
+    pub fn render(
+        &self,
+        project: &Handle<Project>,
+        basis: usize,
+        theme: &Theme,
+        follower_states: &HashMap<View<Pane>, FollowerState>,
+        active_call: Option<&Handle<ActiveCall>>,
+        active_pane: &View<Pane>,
+        zoomed: Option<&AnyView>,
+        app_state: &Arc<AppState>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> AnyElement<Workspace> {
+        todo!()
+
+        // enum FollowIntoExternalProject {}
+
+        // match self {
+        //     Member::Pane(pane) => {
+        //         let pane_element = if Some(&**pane) == zoomed {
+        //             Empty::new().into_any()
+        //         } else {
+        //             ChildView::new(pane, cx).into_any()
+        //         };
+
+        //         let leader = follower_states.get(pane).and_then(|state| {
+        //             let room = active_call?.read(cx).room()?.read(cx);
+        //             room.remote_participant_for_peer_id(state.leader_id)
+        //         });
+
+        //         let mut leader_border = Border::default();
+        //         let mut leader_status_box = None;
+        //         if let Some(leader) = &leader {
+        //             let leader_color = theme
+        //                 .editor
+        //                 .selection_style_for_room_participant(leader.participant_index.0)
+        //                 .cursor;
+        //             leader_border = Border::all(theme.workspace.leader_border_width, leader_color);
+        //             leader_border
+        //                 .color
+        //                 .fade_out(1. - theme.workspace.leader_border_opacity);
+        //             leader_border.overlay = true;
+
+        //             leader_status_box = match leader.location {
+        //                 ParticipantLocation::SharedProject {
+        //                     project_id: leader_project_id,
+        //                 } => {
+        //                     if Some(leader_project_id) == project.read(cx).remote_id() {
+        //                         None
+        //                     } else {
+        //                         let leader_user = leader.user.clone();
+        //                         let leader_user_id = leader.user.id;
+        //                         Some(
+        //                             MouseEventHandler::new::<FollowIntoExternalProject, _>(
+        //                                 pane.id(),
+        //                                 cx,
+        //                                 |_, _| {
+        //                                     Label::new(
+        //                                         format!(
+        //                                             "Follow {} to their active project",
+        //                                             leader_user.github_login,
+        //                                         ),
+        //                                         theme
+        //                                             .workspace
+        //                                             .external_location_message
+        //                                             .text
+        //                                             .clone(),
+        //                                     )
+        //                                     .contained()
+        //                                     .with_style(
+        //                                         theme.workspace.external_location_message.container,
+        //                                     )
+        //                                 },
+        //                             )
+        //                             .with_cursor_style(CursorStyle::PointingHand)
+        //                             .on_click(MouseButton::Left, move |_, this, cx| {
+        //                                 crate::join_remote_project(
+        //                                     leader_project_id,
+        //                                     leader_user_id,
+        //                                     this.app_state().clone(),
+        //                                     cx,
+        //                                 )
+        //                                 .detach_and_log_err(cx);
+        //                             })
+        //                             .aligned()
+        //                             .bottom()
+        //                             .right()
+        //                             .into_any(),
+        //                         )
+        //                     }
+        //                 }
+        //                 ParticipantLocation::UnsharedProject => Some(
+        //                     Label::new(
+        //                         format!(
+        //                             "{} is viewing an unshared Zed project",
+        //                             leader.user.github_login
+        //                         ),
+        //                         theme.workspace.external_location_message.text.clone(),
+        //                     )
+        //                     .contained()
+        //                     .with_style(theme.workspace.external_location_message.container)
+        //                     .aligned()
+        //                     .bottom()
+        //                     .right()
+        //                     .into_any(),
+        //                 ),
+        //                 ParticipantLocation::External => Some(
+        //                     Label::new(
+        //                         format!(
+        //                             "{} is viewing a window outside of Zed",
+        //                             leader.user.github_login
+        //                         ),
+        //                         theme.workspace.external_location_message.text.clone(),
+        //                     )
+        //                     .contained()
+        //                     .with_style(theme.workspace.external_location_message.container)
+        //                     .aligned()
+        //                     .bottom()
+        //                     .right()
+        //                     .into_any(),
+        //                 ),
+        //             };
+        //         }
+
+        //         Stack::new()
+        //             .with_child(pane_element.contained().with_border(leader_border))
+        //             .with_children(leader_status_box)
+        //             .into_any()
+        //     }
+        //     Member::Axis(axis) => axis.render(
+        //         project,
+        //         basis + 1,
+        //         theme,
+        //         follower_states,
+        //         active_call,
+        //         active_pane,
+        //         zoomed,
+        //         app_state,
+        //         cx,
+        //     ),
+        // }
+    }
+
+    fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a View<Pane>>) {
+        match self {
+            Member::Axis(axis) => {
+                for member in &axis.members {
+                    member.collect_panes(panes);
+                }
+            }
+            Member::Pane(pane) => panes.push(pane),
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) struct PaneAxis {
+    pub axis: Axis,
+    pub members: Vec<Member>,
+    pub flexes: Rc<RefCell<Vec<f32>>>,
+    pub bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
+}
+
+impl PaneAxis {
+    pub fn new(axis: Axis, members: Vec<Member>) -> Self {
+        let flexes = Rc::new(RefCell::new(vec![1.; members.len()]));
+        let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
+        Self {
+            axis,
+            members,
+            flexes,
+            bounding_boxes,
+        }
+    }
+
+    pub fn load(axis: Axis, members: Vec<Member>, flexes: Option<Vec<f32>>) -> Self {
+        let flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]);
+        debug_assert!(members.len() == flexes.len());
+
+        let flexes = Rc::new(RefCell::new(flexes));
+        let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
+        Self {
+            axis,
+            members,
+            flexes,
+            bounding_boxes,
+        }
+    }
+
+    fn split(
+        &mut self,
+        old_pane: &View<Pane>,
+        new_pane: &View<Pane>,
+        direction: SplitDirection,
+    ) -> Result<()> {
+        for (mut idx, member) in self.members.iter_mut().enumerate() {
+            match member {
+                Member::Axis(axis) => {
+                    if axis.split(old_pane, new_pane, direction).is_ok() {
+                        return Ok(());
+                    }
+                }
+                Member::Pane(pane) => {
+                    if pane == old_pane {
+                        if direction.axis() == self.axis {
+                            if direction.increasing() {
+                                idx += 1;
+                            }
+
+                            self.members.insert(idx, Member::Pane(new_pane.clone()));
+                            *self.flexes.borrow_mut() = vec![1.; self.members.len()];
+                        } else {
+                            *member =
+                                Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
+                        }
+                        return Ok(());
+                    }
+                }
+            }
+        }
+        Err(anyhow!("Pane not found"))
+    }
+
+    fn remove(&mut self, pane_to_remove: &View<Pane>) -> Result<Option<Member>> {
+        let mut found_pane = false;
+        let mut remove_member = None;
+        for (idx, member) in self.members.iter_mut().enumerate() {
+            match member {
+                Member::Axis(axis) => {
+                    if let Ok(last_pane) = axis.remove(pane_to_remove) {
+                        if let Some(last_pane) = last_pane {
+                            *member = last_pane;
+                        }
+                        found_pane = true;
+                        break;
+                    }
+                }
+                Member::Pane(pane) => {
+                    if pane == pane_to_remove {
+                        found_pane = true;
+                        remove_member = Some(idx);
+                        break;
+                    }
+                }
+            }
+        }
+
+        if found_pane {
+            if let Some(idx) = remove_member {
+                self.members.remove(idx);
+                *self.flexes.borrow_mut() = vec![1.; self.members.len()];
+            }
+
+            if self.members.len() == 1 {
+                let result = self.members.pop();
+                *self.flexes.borrow_mut() = vec![1.; self.members.len()];
+                Ok(result)
+            } else {
+                Ok(None)
+            }
+        } else {
+            Err(anyhow!("Pane not found"))
+        }
+    }
+
+    fn swap(&mut self, from: &View<Pane>, to: &View<Pane>) {
+        for member in self.members.iter_mut() {
+            match member {
+                Member::Axis(axis) => axis.swap(from, to),
+                Member::Pane(pane) => {
+                    if pane == from {
+                        *member = Member::Pane(to.clone());
+                    } else if pane == to {
+                        *member = Member::Pane(from.clone())
+                    }
+                }
+            }
+        }
+    }
+
+    fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
+        debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
+
+        for (idx, member) in self.members.iter().enumerate() {
+            match member {
+                Member::Pane(found) => {
+                    if pane == found {
+                        return self.bounding_boxes.borrow()[idx];
+                    }
+                }
+                Member::Axis(axis) => {
+                    if let Some(rect) = axis.bounding_box_for_pane(pane) {
+                        return Some(rect);
+                    }
+                }
+            }
+        }
+        None
+    }
+
+    fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&View<Pane>> {
+        debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
+
+        let bounding_boxes = self.bounding_boxes.borrow();
+
+        for (idx, member) in self.members.iter().enumerate() {
+            if let Some(coordinates) = bounding_boxes[idx] {
+                if coordinates.contains_point(&coordinate) {
+                    return match member {
+                        Member::Pane(found) => Some(found),
+                        Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
+                    };
+                }
+            }
+        }
+        None
+    }
+
+    fn render(
+        &self,
+        project: &Handle<Project>,
+        basis: usize,
+        theme: &Theme,
+        follower_states: &HashMap<View<Pane>, FollowerState>,
+        active_call: Option<&Handle<ActiveCall>>,
+        active_pane: &View<Pane>,
+        zoomed: Option<&AnyView>,
+        app_state: &Arc<AppState>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> AnyElement<Workspace> {
+        debug_assert!(self.members.len() == self.flexes.borrow().len());
+
+        todo!()
+        // let mut pane_axis = PaneAxisElement::new(
+        //     self.axis,
+        //     basis,
+        //     self.flexes.clone(),
+        //     self.bounding_boxes.clone(),
+        // );
+        // let mut active_pane_ix = None;
+
+        // let mut members = self.members.iter().enumerate().peekable();
+        // while let Some((ix, member)) = members.next() {
+        //     let last = members.peek().is_none();
+
+        //     if member.contains(active_pane) {
+        //         active_pane_ix = Some(ix);
+        //     }
+
+        //     let mut member = member.render(
+        //         project,
+        //         (basis + ix) * 10,
+        //         theme,
+        //         follower_states,
+        //         active_call,
+        //         active_pane,
+        //         zoomed,
+        //         app_state,
+        //         cx,
+        //     );
+
+        //     if !last {
+        //         let mut border = theme.workspace.pane_divider;
+        //         border.left = false;
+        //         border.right = false;
+        //         border.top = false;
+        //         border.bottom = false;
+
+        //         match self.axis {
+        //             Axis::Vertical => border.bottom = true,
+        //             Axis::Horizontal => border.right = true,
+        //         }
+
+        //         member = member.contained().with_border(border).into_any();
+        //     }
+
+        //     pane_axis = pane_axis.with_child(member.into_any());
+        // }
+        // pane_axis.set_active_pane(active_pane_ix);
+        // pane_axis.into_any()
+    }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
+pub enum SplitDirection {
+    Up,
+    Down,
+    Left,
+    Right,
+}
+
+impl SplitDirection {
+    pub fn all() -> [Self; 4] {
+        [Self::Up, Self::Down, Self::Left, Self::Right]
+    }
+
+    pub fn edge(&self, rect: Bounds<Pixels>) -> f32 {
+        match self {
+            Self::Up => rect.min_y(),
+            Self::Down => rect.max_y(),
+            Self::Left => rect.min_x(),
+            Self::Right => rect.max_x(),
+        }
+    }
+
+    pub fn along_edge(&self, bounds: Bounds<Pixels>, length: Pixels) -> Bounds<Pixels> {
+        match self {
+            Self::Up => Bounds {
+                origin: bounds.origin(),
+                size: size(bounds.width(), length),
+            },
+            Self::Down => Bounds {
+                origin: size(bounds.min_x(), bounds.max_y() - length),
+                size: size(bounds.width(), length),
+            },
+            Self::Left => Bounds {
+                origin: bounds.origin(),
+                size: size(length, bounds.height()),
+            },
+            Self::Right => Bounds {
+                origin: size(bounds.max_x() - length, bounds.min_y()),
+                size: size(length, bounds.height()),
+            },
+        }
+    }
+
+    pub fn axis(&self) -> Axis {
+        match self {
+            Self::Up | Self::Down => Axis::Vertical,
+            Self::Left | Self::Right => Axis::Horizontal,
+        }
+    }
+
+    pub fn increasing(&self) -> bool {
+        match self {
+            Self::Left | Self::Up => false,
+            Self::Down | Self::Right => true,
+        }
+    }
+}
+
+// mod element {
+//     // use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
+
+//     // use gpui::{
+//     //     geometry::{
+//     //         rect::Bounds<Pixels>,
+//     //         vector::{vec2f, Vector2F},
+//     //     },
+//     //     json::{self, ToJson},
+//     //     platform::{CursorStyle, MouseButton},
+//     //     scene::MouseDrag,
+//     //     AnyElement, Axis, CursorRegion, Element, EventContext, MouseRegion, Bounds<Pixels>Ext,
+//     //     SizeConstraint, Vector2FExt, ViewContext,
+//     // };
+
+//     use crate::{
+//         pane_group::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE},
+//         Workspace, WorkspaceSettings,
+//     };
+
+//     pub struct PaneAxisElement {
+//         axis: Axis,
+//         basis: usize,
+//         active_pane_ix: Option<usize>,
+//         flexes: Rc<RefCell<Vec<f32>>>,
+//         children: Vec<AnyElement<Workspace>>,
+//         bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
+//     }
+
+//     impl PaneAxisElement {
+//         pub fn new(
+//             axis: Axis,
+//             basis: usize,
+//             flexes: Rc<RefCell<Vec<f32>>>,
+//             bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
+//         ) -> Self {
+//             Self {
+//                 axis,
+//                 basis,
+//                 flexes,
+//                 bounding_boxes,
+//                 active_pane_ix: None,
+//                 children: Default::default(),
+//             }
+//         }
+
+//         pub fn set_active_pane(&mut self, active_pane_ix: Option<usize>) {
+//             self.active_pane_ix = active_pane_ix;
+//         }
+
+//         fn layout_children(
+//             &mut self,
+//             active_pane_magnification: f32,
+//             constraint: SizeConstraint,
+//             remaining_space: &mut f32,
+//             remaining_flex: &mut f32,
+//             cross_axis_max: &mut f32,
+//             view: &mut Workspace,
+//             cx: &mut ViewContext<Workspace>,
+//         ) {
+//             let flexes = self.flexes.borrow();
+//             let cross_axis = self.axis.invert();
+//             for (ix, child) in self.children.iter_mut().enumerate() {
+//                 let flex = if active_pane_magnification != 1. {
+//                     if let Some(active_pane_ix) = self.active_pane_ix {
+//                         if ix == active_pane_ix {
+//                             active_pane_magnification
+//                         } else {
+//                             1.
+//                         }
+//                     } else {
+//                         1.
+//                     }
+//                 } else {
+//                     flexes[ix]
+//                 };
+
+//                 let child_size = if *remaining_flex == 0.0 {
+//                     *remaining_space
+//                 } else {
+//                     let space_per_flex = *remaining_space / *remaining_flex;
+//                     space_per_flex * flex
+//                 };
+
+//                 let child_constraint = match self.axis {
+//                     Axis::Horizontal => SizeConstraint::new(
+//                         vec2f(child_size, constraint.min.y()),
+//                         vec2f(child_size, constraint.max.y()),
+//                     ),
+//                     Axis::Vertical => SizeConstraint::new(
+//                         vec2f(constraint.min.x(), child_size),
+//                         vec2f(constraint.max.x(), child_size),
+//                     ),
+//                 };
+//                 let child_size = child.layout(child_constraint, view, cx);
+//                 *remaining_space -= child_size.along(self.axis);
+//                 *remaining_flex -= flex;
+//                 *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
+//             }
+//         }
+
+//         fn handle_resize(
+//             flexes: Rc<RefCell<Vec<f32>>>,
+//             axis: Axis,
+//             preceding_ix: usize,
+//             child_start: Vector2F,
+//             drag_bounds: Bounds<Pixels>,
+//         ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
+//             let size = move |ix, flexes: &[f32]| {
+//                 drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32)
+//             };
+
+//             move |drag, workspace: &mut Workspace, cx| {
+//                 if drag.end {
+//                     // TODO: Clear cascading resize state
+//                     return;
+//                 }
+//                 let min_size = match axis {
+//                     Axis::Horizontal => HORIZONTAL_MIN_SIZE,
+//                     Axis::Vertical => VERTICAL_MIN_SIZE,
+//                 };
+//                 let mut flexes = flexes.borrow_mut();
+
+//                 // Don't allow resizing to less than the minimum size, if elements are already too small
+//                 if min_size - 1. > size(preceding_ix, flexes.as_slice()) {
+//                     return;
+//                 }
+
+//                 let mut proposed_current_pixel_change = (drag.position - child_start).along(axis)
+//                     - size(preceding_ix, flexes.as_slice());
+
+//                 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
+//                     let flex_change = pixel_dx / drag_bounds.length_along(axis);
+//                     let current_target_flex = flexes[target_ix] + flex_change;
+//                     let next_target_flex =
+//                         flexes[(target_ix as isize + next) as usize] - flex_change;
+//                     (current_target_flex, next_target_flex)
+//                 };
+
+//                 let mut successors = from_fn({
+//                     let forward = proposed_current_pixel_change > 0.;
+//                     let mut ix_offset = 0;
+//                     let len = flexes.len();
+//                     move || {
+//                         let result = if forward {
+//                             (preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset)
+//                         } else {
+//                             (preceding_ix as isize - ix_offset as isize >= 0)
+//                                 .then(|| preceding_ix - ix_offset)
+//                         };
+
+//                         ix_offset += 1;
+
+//                         result
+//                     }
+//                 });
+
+//                 while proposed_current_pixel_change.abs() > 0. {
+//                     let Some(current_ix) = successors.next() else {
+//                         break;
+//                     };
+
+//                     let next_target_size = f32::max(
+//                         size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
+//                         min_size,
+//                     );
+
+//                     let current_target_size = f32::max(
+//                         size(current_ix, flexes.as_slice())
+//                             + size(current_ix + 1, flexes.as_slice())
+//                             - next_target_size,
+//                         min_size,
+//                     );
+
+//                     let current_pixel_change =
+//                         current_target_size - size(current_ix, flexes.as_slice());
+
+//                     let (current_target_flex, next_target_flex) =
+//                         flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
+
+//                     flexes[current_ix] = current_target_flex;
+//                     flexes[current_ix + 1] = next_target_flex;
+
+//                     proposed_current_pixel_change -= current_pixel_change;
+//                 }
+
+//                 workspace.schedule_serialize(cx);
+//                 cx.notify();
+//             }
+//         }
+//     }
+
+//     impl Extend<AnyElement<Workspace>> for PaneAxisElement {
+//         fn extend<T: IntoIterator<Item = AnyElement<Workspace>>>(&mut self, children: T) {
+//             self.children.extend(children);
+//         }
+//     }
+
+//     impl Element<Workspace> for PaneAxisElement {
+//         type LayoutState = f32;
+//         type PaintState = ();
+
+//         fn layout(
+//             &mut self,
+//             constraint: SizeConstraint,
+//             view: &mut Workspace,
+//             cx: &mut ViewContext<Workspace>,
+//         ) -> (Vector2F, Self::LayoutState) {
+//             debug_assert!(self.children.len() == self.flexes.borrow().len());
+
+//             let active_pane_magnification =
+//                 settings::get::<WorkspaceSettings>(cx).active_pane_magnification;
+
+//             let mut remaining_flex = 0.;
+
+//             if active_pane_magnification != 1. {
+//                 let active_pane_flex = self
+//                     .active_pane_ix
+//                     .map(|_| active_pane_magnification)
+//                     .unwrap_or(1.);
+//                 remaining_flex += self.children.len() as f32 - 1. + active_pane_flex;
+//             } else {
+//                 for flex in self.flexes.borrow().iter() {
+//                     remaining_flex += flex;
+//                 }
+//             }
+
+//             let mut cross_axis_max: f32 = 0.0;
+//             let mut remaining_space = constraint.max_along(self.axis);
+
+//             if remaining_space.is_infinite() {
+//                 panic!("flex contains flexible children but has an infinite constraint along the flex axis");
+//             }
+
+//             self.layout_children(
+//                 active_pane_magnification,
+//                 constraint,
+//                 &mut remaining_space,
+//                 &mut remaining_flex,
+//                 &mut cross_axis_max,
+//                 view,
+//                 cx,
+//             );
+
+//             let mut size = match self.axis {
+//                 Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
+//                 Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
+//             };
+
+//             if constraint.min.x().is_finite() {
+//                 size.set_x(size.x().max(constraint.min.x()));
+//             }
+//             if constraint.min.y().is_finite() {
+//                 size.set_y(size.y().max(constraint.min.y()));
+//             }
+
+//             if size.x() > constraint.max.x() {
+//                 size.set_x(constraint.max.x());
+//             }
+//             if size.y() > constraint.max.y() {
+//                 size.set_y(constraint.max.y());
+//             }
+
+//             (size, remaining_space)
+//         }
+
+//         fn paint(
+//             &mut self,
+//             bounds: Bounds<Pixels>,
+//             visible_bounds: Bounds<Pixels>,
+//             remaining_space: &mut Self::LayoutState,
+//             view: &mut Workspace,
+//             cx: &mut ViewContext<Workspace>,
+//         ) -> Self::PaintState {
+//             let can_resize = settings::get::<WorkspaceSettings>(cx).active_pane_magnification == 1.;
+//             let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+
+//             let overflowing = *remaining_space < 0.;
+//             if overflowing {
+//                 cx.scene().push_layer(Some(visible_bounds));
+//             }
+
+//             let mut child_origin = bounds.origin();
+
+//             let mut bounding_boxes = self.bounding_boxes.borrow_mut();
+//             bounding_boxes.clear();
+
+//             let mut children_iter = self.children.iter_mut().enumerate().peekable();
+//             while let Some((ix, child)) = children_iter.next() {
+//                 let child_start = child_origin.clone();
+//                 child.paint(child_origin, visible_bounds, view, cx);
+
+//                 bounding_boxes.push(Some(Bounds<Pixels>::new(child_origin, child.size())));
+
+//                 match self.axis {
+//                     Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
+//                     Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
+//                 }
+
+//                 if can_resize && children_iter.peek().is_some() {
+//                     cx.scene().push_stacking_context(None, None);
+
+//                     let handle_origin = match self.axis {
+//                         Axis::Horizontal => child_origin - vec2f(HANDLE_HITBOX_SIZE / 2., 0.0),
+//                         Axis::Vertical => child_origin - vec2f(0.0, HANDLE_HITBOX_SIZE / 2.),
+//                     };
+
+//                     let handle_bounds = match self.axis {
+//                         Axis::Horizontal => Bounds<Pixels>::new(
+//                             handle_origin,
+//                             vec2f(HANDLE_HITBOX_SIZE, visible_bounds.height()),
+//                         ),
+//                         Axis::Vertical => Bounds<Pixels>::new(
+//                             handle_origin,
+//                             vec2f(visible_bounds.width(), HANDLE_HITBOX_SIZE),
+//                         ),
+//                     };
+
+//                     let style = match self.axis {
+//                         Axis::Horizontal => CursorStyle::ResizeLeftRight,
+//                         Axis::Vertical => CursorStyle::ResizeUpDown,
+//                     };
+
+//                     cx.scene().push_cursor_region(CursorRegion {
+//                         bounds: handle_bounds,
+//                         style,
+//                     });
+
+//                     enum ResizeHandle {}
+//                     let mut mouse_region = MouseRegion::new::<ResizeHandle>(
+//                         cx.view_id(),
+//                         self.basis + ix,
+//                         handle_bounds,
+//                     );
+//                     mouse_region = mouse_region
+//                         .on_drag(
+//                             MouseButton::Left,
+//                             Self::handle_resize(
+//                                 self.flexes.clone(),
+//                                 self.axis,
+//                                 ix,
+//                                 child_start,
+//                                 visible_bounds.clone(),
+//                             ),
+//                         )
+//                         .on_click(MouseButton::Left, {
+//                             let flexes = self.flexes.clone();
+//                             move |e, v: &mut Workspace, cx| {
+//                                 if e.click_count >= 2 {
+//                                     let mut borrow = flexes.borrow_mut();
+//                                     *borrow = vec![1.; borrow.len()];
+//                                     v.schedule_serialize(cx);
+//                                     cx.notify();
+//                                 }
+//                             }
+//                         });
+//                     cx.scene().push_mouse_region(mouse_region);
+
+//                     cx.scene().pop_stacking_context();
+//                 }
+//             }
+
+//             if overflowing {
+//                 cx.scene().pop_layer();
+//             }
+//         }
+
+//         fn rect_for_text_range(
+//             &self,
+//             range_utf16: Range<usize>,
+//             _: Bounds<Pixels>,
+//             _: Bounds<Pixels>,
+//             _: &Self::LayoutState,
+//             _: &Self::PaintState,
+//             view: &Workspace,
+//             cx: &ViewContext<Workspace>,
+//         ) -> Option<Bounds<Pixels>> {
+//             self.children
+//                 .iter()
+//                 .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
+//         }
+
+//         fn debug(
+//             &self,
+//             bounds: Bounds<Pixels>,
+//             _: &Self::LayoutState,
+//             _: &Self::PaintState,
+//             view: &Workspace,
+//             cx: &ViewContext<Workspace>,
+//         ) -> json::Value {
+//             serde_json::json!({
+//                 "type": "PaneAxis",
+//                 "bounds": bounds.to_json(),
+//                 "axis": self.axis.to_json(),
+//                 "flexes": *self.flexes.borrow(),
+//                 "children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
+//             })
+//         }
+//     }
+// }

crates/workspace2/src/persistence/model.rs 🔗

@@ -0,0 +1,340 @@
+use crate::{
+    item::ItemHandle, Axis, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId,
+};
+use anyhow::{Context, Result};
+use async_recursion::async_recursion;
+use db2::sqlez::{
+    bindable::{Bind, Column, StaticColumnCount},
+    statement::Statement,
+};
+use gpui2::{AsyncAppContext, Handle, Task, View, WeakView, WindowBounds};
+use project2::Project;
+use std::{
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+use uuid::Uuid;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
+
+impl WorkspaceLocation {
+    pub fn paths(&self) -> Arc<Vec<PathBuf>> {
+        self.0.clone()
+    }
+}
+
+impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
+    fn from(iterator: T) -> Self {
+        let mut roots = iterator
+            .into_iter()
+            .map(|p| p.as_ref().to_path_buf())
+            .collect::<Vec<_>>();
+        roots.sort();
+        Self(Arc::new(roots))
+    }
+}
+
+impl StaticColumnCount for WorkspaceLocation {}
+impl Bind for &WorkspaceLocation {
+    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+        bincode::serialize(&self.0)
+            .expect("Bincode serialization of paths should not fail")
+            .bind(statement, start_index)
+    }
+}
+
+impl Column for WorkspaceLocation {
+    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+        let blob = statement.column_blob(start_index)?;
+        Ok((
+            WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
+            start_index + 1,
+        ))
+    }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct SerializedWorkspace {
+    pub id: WorkspaceId,
+    pub location: WorkspaceLocation,
+    pub center_group: SerializedPaneGroup,
+    pub bounds: Option<WindowBounds>,
+    pub display: Option<Uuid>,
+    pub docks: DockStructure,
+}
+
+#[derive(Debug, PartialEq, Clone, Default)]
+pub struct DockStructure {
+    pub(crate) left: DockData,
+    pub(crate) right: DockData,
+    pub(crate) bottom: DockData,
+}
+
+impl Column for DockStructure {
+    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+        let (left, next_index) = DockData::column(statement, start_index)?;
+        let (right, next_index) = DockData::column(statement, next_index)?;
+        let (bottom, next_index) = DockData::column(statement, next_index)?;
+        Ok((
+            DockStructure {
+                left,
+                right,
+                bottom,
+            },
+            next_index,
+        ))
+    }
+}
+
+impl Bind for DockStructure {
+    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+        let next_index = statement.bind(&self.left, start_index)?;
+        let next_index = statement.bind(&self.right, next_index)?;
+        statement.bind(&self.bottom, next_index)
+    }
+}
+
+#[derive(Debug, PartialEq, Clone, Default)]
+pub struct DockData {
+    pub(crate) visible: bool,
+    pub(crate) active_panel: Option<String>,
+    pub(crate) zoom: bool,
+}
+
+impl Column for DockData {
+    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+        let (visible, next_index) = Option::<bool>::column(statement, start_index)?;
+        let (active_panel, next_index) = Option::<String>::column(statement, next_index)?;
+        let (zoom, next_index) = Option::<bool>::column(statement, next_index)?;
+        Ok((
+            DockData {
+                visible: visible.unwrap_or(false),
+                active_panel,
+                zoom: zoom.unwrap_or(false),
+            },
+            next_index,
+        ))
+    }
+}
+
+impl Bind for DockData {
+    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+        let next_index = statement.bind(&self.visible, start_index)?;
+        let next_index = statement.bind(&self.active_panel, next_index)?;
+        statement.bind(&self.zoom, next_index)
+    }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum SerializedPaneGroup {
+    Group {
+        axis: Axis,
+        flexes: Option<Vec<f32>>,
+        children: Vec<SerializedPaneGroup>,
+    },
+    Pane(SerializedPane),
+}
+
+#[cfg(test)]
+impl Default for SerializedPaneGroup {
+    fn default() -> Self {
+        Self::Pane(SerializedPane {
+            children: vec![SerializedItem::default()],
+            active: false,
+        })
+    }
+}
+
+impl SerializedPaneGroup {
+    #[async_recursion(?Send)]
+    pub(crate) async fn deserialize(
+        self,
+        project: &Handle<Project>,
+        workspace_id: WorkspaceId,
+        workspace: &WeakView<Workspace>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<(Member, Option<View<Pane>>, Vec<Option<Box<dyn ItemHandle>>>)> {
+        match self {
+            SerializedPaneGroup::Group {
+                axis,
+                children,
+                flexes,
+            } => {
+                let mut current_active_pane = None;
+                let mut members = Vec::new();
+                let mut items = Vec::new();
+                for child in children {
+                    if let Some((new_member, active_pane, new_items)) = child
+                        .deserialize(project, workspace_id, workspace, cx)
+                        .await
+                    {
+                        members.push(new_member);
+                        items.extend(new_items);
+                        current_active_pane = current_active_pane.or(active_pane);
+                    }
+                }
+
+                if members.is_empty() {
+                    return None;
+                }
+
+                if members.len() == 1 {
+                    return Some((members.remove(0), current_active_pane, items));
+                }
+
+                Some((
+                    Member::Axis(PaneAxis::load(axis, members, flexes)),
+                    current_active_pane,
+                    items,
+                ))
+            }
+            SerializedPaneGroup::Pane(serialized_pane) => {
+                let pane = workspace
+                    .update(cx, |workspace, cx| workspace.add_pane(cx).downgrade())
+                    .log_err()?;
+                let active = serialized_pane.active;
+                let new_items = serialized_pane
+                    .deserialize_to(project, &pane, workspace_id, workspace, cx)
+                    .await
+                    .log_err()?;
+
+                if pane
+                    .read_with(cx, |pane, _| pane.items_len() != 0)
+                    .log_err()?
+                {
+                    let pane = pane.upgrade()?;
+                    Some((Member::Pane(pane.clone()), active.then(|| pane), new_items))
+                } else {
+                    let pane = pane.upgrade()?;
+                    workspace
+                        .update(cx, |workspace, cx| workspace.force_remove_pane(&pane, cx))
+                        .log_err()?;
+                    None
+                }
+            }
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Default, Clone)]
+pub struct SerializedPane {
+    pub(crate) active: bool,
+    pub(crate) children: Vec<SerializedItem>,
+}
+
+impl SerializedPane {
+    pub fn new(children: Vec<SerializedItem>, active: bool) -> Self {
+        SerializedPane { children, active }
+    }
+
+    pub async fn deserialize_to(
+        &self,
+        project: &Handle<Project>,
+        pane: &WeakView<Pane>,
+        workspace_id: WorkspaceId,
+        workspace: &WeakView<Workspace>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
+        let mut items = Vec::new();
+        let mut active_item_index = None;
+        for (index, item) in self.children.iter().enumerate() {
+            let project = project.clone();
+            let item_handle = pane
+                .update(cx, |_, cx| {
+                    if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
+                        deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
+                    } else {
+                        Task::ready(Err(anyhow::anyhow!(
+                            "Deserializer does not exist for item kind: {}",
+                            item.kind
+                        )))
+                    }
+                })?
+                .await
+                .log_err();
+
+            items.push(item_handle.clone());
+
+            if let Some(item_handle) = item_handle {
+                pane.update(cx, |pane, cx| {
+                    pane.add_item(item_handle.clone(), true, true, None, cx);
+                })?;
+            }
+
+            if item.active {
+                active_item_index = Some(index);
+            }
+        }
+
+        if let Some(active_item_index) = active_item_index {
+            pane.update(cx, |pane, cx| {
+                pane.activate_item(active_item_index, false, false, cx);
+            })?;
+        }
+
+        anyhow::Ok(items)
+    }
+}
+
+pub type GroupId = i64;
+pub type PaneId = i64;
+pub type ItemId = usize;
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct SerializedItem {
+    pub kind: Arc<str>,
+    pub item_id: ItemId,
+    pub active: bool,
+}
+
+impl SerializedItem {
+    pub fn new(kind: impl AsRef<str>, item_id: ItemId, active: bool) -> Self {
+        Self {
+            kind: Arc::from(kind.as_ref()),
+            item_id,
+            active,
+        }
+    }
+}
+
+#[cfg(test)]
+impl Default for SerializedItem {
+    fn default() -> Self {
+        SerializedItem {
+            kind: Arc::from("Terminal"),
+            item_id: 100000,
+            active: false,
+        }
+    }
+}
+
+impl StaticColumnCount for SerializedItem {
+    fn column_count() -> usize {
+        3
+    }
+}
+impl Bind for &SerializedItem {
+    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+        let next_index = statement.bind(&self.kind, start_index)?;
+        let next_index = statement.bind(&self.item_id, next_index)?;
+        statement.bind(&self.active, next_index)
+    }
+}
+
+impl Column for SerializedItem {
+    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+        let (kind, next_index) = Arc::<str>::column(statement, start_index)?;
+        let (item_id, next_index) = ItemId::column(statement, next_index)?;
+        let (active, next_index) = bool::column(statement, next_index)?;
+        Ok((
+            SerializedItem {
+                kind,
+                item_id,
+                active,
+            },
+            next_index,
+        ))
+    }
+}

crates/workspace2/src/workspace2.rs 🔗

@@ -0,0 +1,5535 @@
+// pub mod dock;
+pub mod item;
+// pub mod notifications;
+pub mod pane;
+pub mod pane_group;
+mod persistence;
+pub mod searchable;
+// pub mod shared_screen;
+// mod status_bar;
+mod toolbar;
+mod workspace_settings;
+
+use anyhow::{anyhow, Result};
+// use call2::ActiveCall;
+// use client2::{
+//     proto::{self, PeerId},
+//     Client, Status, TypedEnvelope, UserStore,
+// };
+// use collections::{hash_map, HashMap, HashSet};
+// use futures::{
+//     channel::{mpsc, oneshot},
+//     future::try_join_all,
+//     FutureExt, StreamExt,
+// };
+// use gpui2::{
+//     actions,
+//     elements::*,
+//     geometry::{
+//         rect::RectF,
+//         vector::{vec2f, Vector2F},
+//     },
+//     impl_actions,
+//     platform::{
+//         CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
+//         WindowBounds, WindowOptions,
+//     },
+//     AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext,
+//     Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext,
+//     View, WeakViewHandle, WindowContext, WindowHandle,
+// };
+// use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
+// use itertools::Itertools;
+// use language2::{LanguageRegistry, Rope};
+// use node_runtime::NodeRuntime;// //
+
+use futures::channel::oneshot;
+// use crate::{
+//     notifications::{simple_message_notification::MessageNotification, NotificationTracker},
+//     persistence::model::{
+//         DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
+//     },
+// };
+// use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
+// use lazy_static::lazy_static;
+// use notifications::{NotificationHandle, NotifyResultExt};
+pub use pane::*;
+pub use pane_group::*;
+// use persistence::{model::SerializedItem, DB};
+// pub use persistence::{
+//     model::{ItemId, WorkspaceLocation},
+//     WorkspaceDb, DB as WORKSPACE_DB,
+// };
+// use postage::prelude::Stream;
+// use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+// use serde::Deserialize;
+// use shared_screen::SharedScreen;
+// use status_bar::StatusBar;
+// pub use status_bar::StatusItemView;
+// use theme::{Theme, ThemeSettings};
+pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
+// use util::ResultExt;
+// pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
+
+// lazy_static! {
+//     static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
+//         .ok()
+//         .as_deref()
+//         .and_then(parse_pixel_position_env_var);
+//     static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
+//         .ok()
+//         .as_deref()
+//         .and_then(parse_pixel_position_env_var);
+// }
+
+// pub trait Modal: View {
+//     fn has_focus(&self) -> bool;
+//     fn dismiss_on_event(event: &Self::Event) -> bool;
+// }
+
+// trait ModalHandle {
+//     fn as_any(&self) -> &AnyViewHandle;
+//     fn has_focus(&self, cx: &WindowContext) -> bool;
+// }
+
+// impl<T: Modal> ModalHandle for View<T> {
+//     fn as_any(&self) -> &AnyViewHandle {
+//         self
+//     }
+
+//     fn has_focus(&self, cx: &WindowContext) -> bool {
+//         self.read(cx).has_focus()
+//     }
+// }
+
+// #[derive(Clone, PartialEq)]
+// pub struct RemoveWorktreeFromProject(pub WorktreeId);
+
+// actions!(
+//     workspace,
+//     [
+//         Open,
+//         NewFile,
+//         NewWindow,
+//         CloseWindow,
+//         CloseInactiveTabsAndPanes,
+//         AddFolderToProject,
+//         Unfollow,
+//         SaveAs,
+//         ReloadActiveItem,
+//         ActivatePreviousPane,
+//         ActivateNextPane,
+//         FollowNextCollaborator,
+//         NewTerminal,
+//         NewCenterTerminal,
+//         ToggleTerminalFocus,
+//         NewSearch,
+//         Feedback,
+//         Restart,
+//         Welcome,
+//         ToggleZoom,
+//         ToggleLeftDock,
+//         ToggleRightDock,
+//         ToggleBottomDock,
+//         CloseAllDocks,
+//     ]
+// );
+
+// #[derive(Clone, PartialEq)]
+// pub struct OpenPaths {
+//     pub paths: Vec<PathBuf>,
+// }
+
+// #[derive(Clone, Deserialize, PartialEq)]
+// pub struct ActivatePane(pub usize);
+
+// #[derive(Clone, Deserialize, PartialEq)]
+// pub struct ActivatePaneInDirection(pub SplitDirection);
+
+// #[derive(Clone, Deserialize, PartialEq)]
+// pub struct SwapPaneInDirection(pub SplitDirection);
+
+// #[derive(Clone, Deserialize, PartialEq)]
+// pub struct NewFileInDirection(pub SplitDirection);
+
+// #[derive(Clone, PartialEq, Debug, Deserialize)]
+// #[serde(rename_all = "camelCase")]
+// pub struct SaveAll {
+//     pub save_intent: Option<SaveIntent>,
+// }
+
+// #[derive(Clone, PartialEq, Debug, Deserialize)]
+// #[serde(rename_all = "camelCase")]
+// pub struct Save {
+//     pub save_intent: Option<SaveIntent>,
+// }
+
+// #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+// #[serde(rename_all = "camelCase")]
+// pub struct CloseAllItemsAndPanes {
+//     pub save_intent: Option<SaveIntent>,
+// }
+
+// #[derive(Deserialize)]
+// pub struct Toast {
+//     id: usize,
+//     msg: Cow<'static, str>,
+//     #[serde(skip)]
+//     on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
+// }
+
+// impl Toast {
+//     pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
+//         Toast {
+//             id,
+//             msg: msg.into(),
+//             on_click: None,
+//         }
+//     }
+
+//     pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
+//     where
+//         M: Into<Cow<'static, str>>,
+//         F: Fn(&mut WindowContext) + 'static,
+//     {
+//         self.on_click = Some((message.into(), Arc::new(on_click)));
+//         self
+//     }
+// }
+
+// impl PartialEq for Toast {
+//     fn eq(&self, other: &Self) -> bool {
+//         self.id == other.id
+//             && self.msg == other.msg
+//             && self.on_click.is_some() == other.on_click.is_some()
+//     }
+// }
+
+// impl Clone for Toast {
+//     fn clone(&self) -> Self {
+//         Toast {
+//             id: self.id,
+//             msg: self.msg.to_owned(),
+//             on_click: self.on_click.clone(),
+//         }
+//     }
+// }
+
+// #[derive(Clone, Deserialize, PartialEq)]
+// pub struct OpenTerminal {
+//     pub working_directory: PathBuf,
+// }
+
+// impl_actions!(
+//     workspace,
+//     [
+//         ActivatePane,
+//         ActivatePaneInDirection,
+//         SwapPaneInDirection,
+//         NewFileInDirection,
+//         Toast,
+//         OpenTerminal,
+//         SaveAll,
+//         Save,
+//         CloseAllItemsAndPanes,
+//     ]
+// );
+
+pub type WorkspaceId = i64;
+
+// pub fn init_settings(cx: &mut AppContext) {
+//     settings::register::<WorkspaceSettings>(cx);
+//     settings::register::<item::ItemSettings>(cx);
+// }
+
+// pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+//     init_settings(cx);
+//     pane::init(cx);
+//     notifications::init(cx);
+
+//     cx.add_global_action({
+//         let app_state = Arc::downgrade(&app_state);
+//         move |_: &Open, cx: &mut AppContext| {
+//             let mut paths = cx.prompt_for_paths(PathPromptOptions {
+//                 files: true,
+//                 directories: true,
+//                 multiple: true,
+//             });
+
+//             if let Some(app_state) = app_state.upgrade() {
+//                 cx.spawn(move |mut cx| async move {
+//                     if let Some(paths) = paths.recv().await.flatten() {
+//                         cx.update(|cx| {
+//                             open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
+//                         });
+//                     }
+//                 })
+//                 .detach();
+//             }
+//         }
+//     });
+//     cx.add_async_action(Workspace::open);
+
+//     cx.add_async_action(Workspace::follow_next_collaborator);
+//     cx.add_async_action(Workspace::close);
+//     cx.add_async_action(Workspace::close_inactive_items_and_panes);
+//     cx.add_async_action(Workspace::close_all_items_and_panes);
+//     cx.add_global_action(Workspace::close_global);
+//     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();
+//             workspace.unfollow(&pane, cx);
+//         },
+//     );
+//     cx.add_action(
+//         |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
+//             workspace
+//                 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
+//                 .detach_and_log_err(cx);
+//         },
+//     );
+//     cx.add_action(
+//         |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
+//             workspace
+//                 .save_active_item(SaveIntent::SaveAs, cx)
+//                 .detach_and_log_err(cx);
+//         },
+//     );
+//     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
+//         workspace.activate_previous_pane(cx)
+//     });
+//     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
+//         workspace.activate_next_pane(cx)
+//     });
+
+//     cx.add_action(
+//         |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
+//             workspace.activate_pane_in_direction(action.0, cx)
+//         },
+//     );
+
+//     cx.add_action(
+//         |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
+//             workspace.swap_pane_in_direction(action.0, cx)
+//         },
+//     );
+
+//     cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
+//         workspace.toggle_dock(DockPosition::Left, cx);
+//     });
+//     cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
+//         workspace.toggle_dock(DockPosition::Right, cx);
+//     });
+//     cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
+//         workspace.toggle_dock(DockPosition::Bottom, cx);
+//     });
+//     cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
+//         workspace.close_all_docks(cx);
+//     });
+//     cx.add_action(Workspace::activate_pane_at_index);
+//     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
+//         workspace.reopen_closed_item(cx).detach();
+//     });
+//     cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
+//         workspace
+//             .go_back(workspace.active_pane().downgrade(), cx)
+//             .detach();
+//     });
+//     cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
+//         workspace
+//             .go_forward(workspace.active_pane().downgrade(), cx)
+//             .detach();
+//     });
+
+//     cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
+//         cx.spawn(|workspace, mut cx| async move {
+//             let err = install_cli::install_cli(&cx)
+//                 .await
+//                 .context("Failed to create CLI symlink");
+
+//             workspace.update(&mut cx, |workspace, cx| {
+//                 if matches!(err, Err(_)) {
+//                     err.notify_err(workspace, cx);
+//                 } else {
+//                     workspace.show_notification(1, cx, |cx| {
+//                         cx.add_view(|_| {
+//                             MessageNotification::new("Successfully installed the `zed` binary")
+//                         })
+//                     });
+//                 }
+//             })
+//         })
+//         .detach();
+//     });
+// }
+
+type ProjectItemBuilders =
+    HashMap<TypeId, fn(Handle<Project>, AnyHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
+pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
+    cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
+        builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
+            let item = model.downcast::<I::Item>().unwrap();
+            Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
+        });
+    });
+}
+
+type FollowableItemBuilder = fn(
+    View<Pane>,
+    View<Workspace>,
+    ViewId,
+    &mut Option<proto::view::Variant>,
+    &mut AppContext,
+) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
+type FollowableItemBuilders = HashMap<
+    TypeId,
+    (
+        FollowableItemBuilder,
+        fn(&AnyView) -> Box<dyn FollowableItemHandle>,
+    ),
+>;
+pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
+    cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
+        builders.insert(
+            TypeId::of::<I>(),
+            (
+                |pane, workspace, id, state, cx| {
+                    I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
+                        cx.foreground()
+                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
+                    })
+                },
+                |this| Box::new(this.clone().downcast::<I>().unwrap()),
+            ),
+        );
+    });
+}
+
+type ItemDeserializers = HashMap<
+    Arc<str>,
+    fn(
+        Handle<Project>,
+        WeakView<Workspace>,
+        WorkspaceId,
+        ItemId,
+        &mut ViewContext<Pane>,
+    ) -> Task<Result<Box<dyn ItemHandle>>>,
+>;
+pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
+    cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
+        if let Some(serialized_item_kind) = I::serialized_item_kind() {
+            deserializers.insert(
+                Arc::from(serialized_item_kind),
+                |project, workspace, workspace_id, item_id, cx| {
+                    let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
+                    cx.foreground()
+                        .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
+                },
+            );
+        }
+    });
+}
+
+pub struct AppState {
+    pub languages: Arc<LanguageRegistry>,
+    pub client: Arc<Client>,
+    pub user_store: Handle<UserStore>,
+    pub workspace_store: Handle<WorkspaceStore>,
+    pub fs: Arc<dyn fs2::Fs>,
+    pub build_window_options:
+        fn(Option<WindowBounds>, Option<DisplayId>, &MainThread<AppContext>) -> WindowOptions,
+    pub initialize_workspace:
+        fn(WeakHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<anyhow::Result<()>>,
+    pub node_runtime: Arc<dyn NodeRuntime>,
+}
+
+pub struct WorkspaceStore {
+    workspaces: HashSet<WeakHandle<Workspace>>,
+    followers: Vec<Follower>,
+    client: Arc<Client>,
+    _subscriptions: Vec<client2::Subscription>,
+}
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
+struct Follower {
+    project_id: Option<u64>,
+    peer_id: PeerId,
+}
+
+// impl AppState {
+//     #[cfg(any(test, feature = "test-support"))]
+//     pub fn test(cx: &mut AppContext) -> Arc<Self> {
+//         use node_runtime::FakeNodeRuntime;
+//         use settings::SettingsStore;
+
+//         if !cx.has_global::<SettingsStore>() {
+//             cx.set_global(SettingsStore::test(cx));
+//         }
+
+//         let fs = fs::FakeFs::new(cx.background().clone());
+//         let languages = Arc::new(LanguageRegistry::test());
+//         let http_client = util::http::FakeHttpClient::with_404_response();
+//         let client = Client::new(http_client.clone(), cx);
+//         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+//         let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
+
+//         theme::init((), cx);
+//         client::init(&client, cx);
+//         crate::init_settings(cx);
+
+//         Arc::new(Self {
+//             client,
+//             fs,
+//             languages,
+//             user_store,
+//             // channel_store,
+//             workspace_store,
+//             node_runtime: FakeNodeRuntime::new(),
+//             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
+//             build_window_options: |_, _, _| Default::default(),
+//         })
+//     }
+// }
+
+struct DelayedDebouncedEditAction {
+    task: Option<Task<()>>,
+    cancel_channel: Option<oneshot::Sender<()>>,
+}
+
+impl DelayedDebouncedEditAction {
+    fn new() -> DelayedDebouncedEditAction {
+        DelayedDebouncedEditAction {
+            task: None,
+            cancel_channel: None,
+        }
+    }
+
+    fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
+    where
+        F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
+    {
+        if let Some(channel) = self.cancel_channel.take() {
+            _ = channel.send(());
+        }
+
+        let (sender, mut receiver) = oneshot::channel::<()>();
+        self.cancel_channel = Some(sender);
+
+        let previous_task = self.task.take();
+        self.task = Some(cx.spawn(|workspace, mut cx| async move {
+            let mut timer = cx.background().timer(delay).fuse();
+            if let Some(previous_task) = previous_task {
+                previous_task.await;
+            }
+
+            futures::select_biased! {
+                _ = receiver => return,
+                    _ = timer => {}
+            }
+
+            if let Some(result) = workspace
+                .update(&mut cx, |workspace, cx| (func)(workspace, cx))
+                .log_err()
+            {
+                result.await.log_err();
+            }
+        }));
+    }
+}
+
+// pub enum Event {
+//     PaneAdded(View<Pane>),
+//     ContactRequestedJoin(u64),
+// }
+
+pub struct Workspace {
+    weak_self: WeakHandle<Self>,
+    //     modal: Option<ActiveModal>,
+    //     zoomed: Option<AnyWeakViewHandle>,
+    //     zoomed_position: Option<DockPosition>,
+    //     center: PaneGroup,
+    //     left_dock: View<Dock>,
+    //     bottom_dock: View<Dock>,
+    //     right_dock: View<Dock>,
+    panes: Vec<View<Pane>>,
+    //     panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
+    //     active_pane: View<Pane>,
+    last_active_center_pane: Option<WeakView<Pane>>,
+    //     last_active_view_id: Option<proto::ViewId>,
+    //     status_bar: View<StatusBar>,
+    //     titlebar_item: Option<AnyViewHandle>,
+    //     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
+    project: Handle<Project>,
+    //     follower_states: HashMap<View<Pane>, FollowerState>,
+    //     last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
+    //     window_edited: bool,
+    //     active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
+    //     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
+    //     database_id: WorkspaceId,
+    app_state: Arc<AppState>,
+    //     subscriptions: Vec<Subscription>,
+    //     _apply_leader_updates: Task<Result<()>>,
+    //     _observe_current_user: Task<Result<()>>,
+    //     _schedule_serialize: Option<Task<()>>,
+    //     pane_history_timestamp: Arc<AtomicUsize>,
+}
+
+// struct ActiveModal {
+//     view: Box<dyn ModalHandle>,
+//     previously_focused_view_id: Option<usize>,
+// }
+
+// #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+// pub struct ViewId {
+//     pub creator: PeerId,
+//     pub id: u64,
+// }
+
+#[derive(Default)]
+struct FollowerState {
+    leader_id: PeerId,
+    active_view_id: Option<ViewId>,
+    items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
+}
+
+// enum WorkspaceBounds {}
+
+impl Workspace {
+    //     pub fn new(
+    //         workspace_id: WorkspaceId,
+    //         project: ModelHandle<Project>,
+    //         app_state: Arc<AppState>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Self {
+    //         cx.observe(&project, |_, _, cx| cx.notify()).detach();
+    //         cx.subscribe(&project, move |this, _, event, cx| {
+    //             match event {
+    //                 project::Event::RemoteIdChanged(_) => {
+    //                     this.update_window_title(cx);
+    //                 }
+
+    //                 project::Event::CollaboratorLeft(peer_id) => {
+    //                     this.collaborator_left(*peer_id, cx);
+    //                 }
+
+    //                 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
+    //                     this.update_window_title(cx);
+    //                     this.serialize_workspace(cx);
+    //                 }
+
+    //                 project::Event::DisconnectedFromHost => {
+    //                     this.update_window_edited(cx);
+    //                     cx.blur();
+    //                 }
+
+    //                 project::Event::Closed => {
+    //                     cx.remove_window();
+    //                 }
+
+    //                 project::Event::DeletedEntry(entry_id) => {
+    //                     for pane in this.panes.iter() {
+    //                         pane.update(cx, |pane, cx| {
+    //                             pane.handle_deleted_project_item(*entry_id, cx)
+    //                         });
+    //                     }
+    //                 }
+
+    //                 project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
+    //                     cx.add_view(|_| MessageNotification::new(message.clone()))
+    //                 }),
+
+    //                 _ => {}
+    //             }
+    //             cx.notify()
+    //         })
+    //         .detach();
+
+    //         let weak_handle = cx.weak_handle();
+    //         let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
+
+    //         let center_pane = cx.add_view(|cx| {
+    //             Pane::new(
+    //                 weak_handle.clone(),
+    //                 project.clone(),
+    //                 pane_history_timestamp.clone(),
+    //                 cx,
+    //             )
+    //         });
+    //         cx.subscribe(&center_pane, Self::handle_pane_event).detach();
+    //         cx.focus(&center_pane);
+    //         cx.emit(Event::PaneAdded(center_pane.clone()));
+
+    //         app_state.workspace_store.update(cx, |store, _| {
+    //             store.workspaces.insert(weak_handle.clone());
+    //         });
+
+    //         let mut current_user = app_state.user_store.read(cx).watch_current_user();
+    //         let mut connection_status = app_state.client.status();
+    //         let _observe_current_user = cx.spawn(|this, mut cx| async move {
+    //             current_user.recv().await;
+    //             connection_status.recv().await;
+    //             let mut stream =
+    //                 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
+
+    //             while stream.recv().await.is_some() {
+    //                 this.update(&mut cx, |_, cx| cx.notify())?;
+    //             }
+    //             anyhow::Ok(())
+    //         });
+
+    //         // All leader updates are enqueued and then processed in a single task, so
+    //         // that each asynchronous operation can be run in order.
+    //         let (leader_updates_tx, mut leader_updates_rx) =
+    //             mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
+    //         let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
+    //             while let Some((leader_id, update)) = leader_updates_rx.next().await {
+    //                 Self::process_leader_update(&this, leader_id, update, &mut cx)
+    //                     .await
+    //                     .log_err();
+    //             }
+
+    //             Ok(())
+    //         });
+
+    //         cx.emit_global(WorkspaceCreated(weak_handle.clone()));
+
+    //         let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
+    //         let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
+    //         let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
+    //         let left_dock_buttons =
+    //             cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
+    //         let bottom_dock_buttons =
+    //             cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
+    //         let right_dock_buttons =
+    //             cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
+    //         let status_bar = cx.add_view(|cx| {
+    //             let mut status_bar = StatusBar::new(&center_pane.clone(), cx);
+    //             status_bar.add_left_item(left_dock_buttons, cx);
+    //             status_bar.add_right_item(right_dock_buttons, cx);
+    //             status_bar.add_right_item(bottom_dock_buttons, cx);
+    //             status_bar
+    //         });
+
+    //         cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
+    //             drag_and_drop.register_container(weak_handle.clone());
+    //         });
+
+    //         let mut active_call = None;
+    //         if cx.has_global::<ModelHandle<ActiveCall>>() {
+    //             let call = cx.global::<ModelHandle<ActiveCall>>().clone();
+    //             let mut subscriptions = Vec::new();
+    //             subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
+    //             active_call = Some((call, subscriptions));
+    //         }
+
+    //         let subscriptions = vec![
+    //             cx.observe_fullscreen(|_, _, cx| cx.notify()),
+    //             cx.observe_window_activation(Self::on_window_activation_changed),
+    //             cx.observe_window_bounds(move |_, mut bounds, display, cx| {
+    //                 // Transform fixed bounds to be stored in terms of the containing display
+    //                 if let WindowBounds::Fixed(mut window_bounds) = bounds {
+    //                     if let Some(screen) = cx.platform().screen_by_id(display) {
+    //                         let screen_bounds = screen.bounds();
+    //                         window_bounds
+    //                             .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
+    //                         window_bounds
+    //                             .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
+    //                         bounds = WindowBounds::Fixed(window_bounds);
+    //                     }
+    //                 }
+
+    //                 cx.background()
+    //                     .spawn(DB.set_window_bounds(workspace_id, bounds, display))
+    //                     .detach_and_log_err(cx);
+    //             }),
+    //             cx.observe(&left_dock, |this, _, cx| {
+    //                 this.serialize_workspace(cx);
+    //                 cx.notify();
+    //             }),
+    //             cx.observe(&bottom_dock, |this, _, cx| {
+    //                 this.serialize_workspace(cx);
+    //                 cx.notify();
+    //             }),
+    //             cx.observe(&right_dock, |this, _, cx| {
+    //                 this.serialize_workspace(cx);
+    //                 cx.notify();
+    //             }),
+    //         ];
+
+    //         cx.defer(|this, cx| this.update_window_title(cx));
+    //         Workspace {
+    //             weak_self: weak_handle.clone(),
+    //             modal: None,
+    //             zoomed: None,
+    //             zoomed_position: None,
+    //             center: PaneGroup::new(center_pane.clone()),
+    //             panes: vec![center_pane.clone()],
+    //             panes_by_item: Default::default(),
+    //             active_pane: center_pane.clone(),
+    //             last_active_center_pane: Some(center_pane.downgrade()),
+    //             last_active_view_id: None,
+    //             status_bar,
+    //             titlebar_item: None,
+    //             notifications: Default::default(),
+    //             left_dock,
+    //             bottom_dock,
+    //             right_dock,
+    //             project: project.clone(),
+    //             follower_states: Default::default(),
+    //             last_leaders_by_pane: Default::default(),
+    //             window_edited: false,
+    //             active_call,
+    //             database_id: workspace_id,
+    //             app_state,
+    //             _observe_current_user,
+    //             _apply_leader_updates,
+    //             _schedule_serialize: None,
+    //             leader_updates_tx,
+    //             subscriptions,
+    //             pane_history_timestamp,
+    //         }
+    //     }
+
+    //     fn new_local(
+    //         abs_paths: Vec<PathBuf>,
+    //         app_state: Arc<AppState>,
+    //         requesting_window: Option<WindowHandle<Workspace>>,
+    //         cx: &mut AppContext,
+    //     ) -> Task<(
+    //         WeakViewHandle<Workspace>,
+    //         Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
+    //     )> {
+    //         let project_handle = Project::local(
+    //             app_state.client.clone(),
+    //             app_state.node_runtime.clone(),
+    //             app_state.user_store.clone(),
+    //             app_state.languages.clone(),
+    //             app_state.fs.clone(),
+    //             cx,
+    //         );
+
+    //         cx.spawn(|mut cx| async move {
+    //             let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
+
+    //             let paths_to_open = Arc::new(abs_paths);
+
+    //             // Get project paths for all of the abs_paths
+    //             let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
+    //             let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
+    //                 Vec::with_capacity(paths_to_open.len());
+    //             for path in paths_to_open.iter().cloned() {
+    //                 if let Some((worktree, project_entry)) = cx
+    //                     .update(|cx| {
+    //                         Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
+    //                     })
+    //                     .await
+    //                     .log_err()
+    //                 {
+    //                     worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
+    //                     project_paths.push((path, Some(project_entry)));
+    //                 } else {
+    //                     project_paths.push((path, None));
+    //                 }
+    //             }
+
+    //             let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
+    //                 serialized_workspace.id
+    //             } else {
+    //                 DB.next_id().await.unwrap_or(0)
+    //             };
+
+    //             let window = if let Some(window) = requesting_window {
+    //                 window.replace_root(&mut cx, |cx| {
+    //                     Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
+    //                 });
+    //                 window
+    //             } else {
+    //                 {
+    //                     let window_bounds_override = window_bounds_env_override(&cx);
+    //                     let (bounds, display) = if let Some(bounds) = window_bounds_override {
+    //                         (Some(bounds), None)
+    //                     } else {
+    //                         serialized_workspace
+    //                             .as_ref()
+    //                             .and_then(|serialized_workspace| {
+    //                                 let display = serialized_workspace.display?;
+    //                                 let mut bounds = serialized_workspace.bounds?;
+
+    //                                 // Stored bounds are relative to the containing display.
+    //                                 // So convert back to global coordinates if that screen still exists
+    //                                 if let WindowBounds::Fixed(mut window_bounds) = bounds {
+    //                                     if let Some(screen) = cx.platform().screen_by_id(display) {
+    //                                         let screen_bounds = screen.bounds();
+    //                                         window_bounds.set_origin_x(
+    //                                             window_bounds.origin_x() + screen_bounds.origin_x(),
+    //                                         );
+    //                                         window_bounds.set_origin_y(
+    //                                             window_bounds.origin_y() + screen_bounds.origin_y(),
+    //                                         );
+    //                                         bounds = WindowBounds::Fixed(window_bounds);
+    //                                     } else {
+    //                                         // Screen no longer exists. Return none here.
+    //                                         return None;
+    //                                     }
+    //                                 }
+
+    //                                 Some((bounds, display))
+    //                             })
+    //                             .unzip()
+    //                     };
+
+    //                     // Use the serialized workspace to construct the new window
+    //                     cx.add_window(
+    //                         (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
+    //                         |cx| {
+    //                             Workspace::new(
+    //                                 workspace_id,
+    //                                 project_handle.clone(),
+    //                                 app_state.clone(),
+    //                                 cx,
+    //                             )
+    //                         },
+    //                     )
+    //                 }
+    //             };
+
+    //             // We haven't yielded the main thread since obtaining the window handle,
+    //             // so the window exists.
+    //             let workspace = window.root(&cx).unwrap();
+
+    //             (app_state.initialize_workspace)(
+    //                 workspace.downgrade(),
+    //                 serialized_workspace.is_some(),
+    //                 app_state.clone(),
+    //                 cx.clone(),
+    //             )
+    //             .await
+    //             .log_err();
+
+    //             window.update(&mut cx, |cx| cx.activate_window());
+
+    //             let workspace = workspace.downgrade();
+    //             notify_if_database_failed(&workspace, &mut cx);
+    //             let opened_items = open_items(
+    //                 serialized_workspace,
+    //                 &workspace,
+    //                 project_paths,
+    //                 app_state,
+    //                 cx,
+    //             )
+    //             .await
+    //             .unwrap_or_default();
+
+    //             (workspace, opened_items)
+    //         })
+    //     }
+
+    //     pub fn weak_handle(&self) -> WeakViewHandle<Self> {
+    //         self.weak_self.clone()
+    //     }
+
+    //     pub fn left_dock(&self) -> &View<Dock> {
+    //         &self.left_dock
+    //     }
+
+    //     pub fn bottom_dock(&self) -> &View<Dock> {
+    //         &self.bottom_dock
+    //     }
+
+    //     pub fn right_dock(&self) -> &View<Dock> {
+    //         &self.right_dock
+    //     }
+
+    //     pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>)
+    //     where
+    //         T::Event: std::fmt::Debug,
+    //     {
+    //         self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
+    //     }
+
+    //     pub fn add_panel_with_extra_event_handler<T: Panel, F>(
+    //         &mut self,
+    //         panel: View<T>,
+    //         cx: &mut ViewContext<Self>,
+    //         handler: F,
+    //     ) where
+    //         T::Event: std::fmt::Debug,
+    //         F: Fn(&mut Self, &View<T>, &T::Event, &mut ViewContext<Self>) + 'static,
+    //     {
+    //         let dock = match panel.position(cx) {
+    //             DockPosition::Left => &self.left_dock,
+    //             DockPosition::Bottom => &self.bottom_dock,
+    //             DockPosition::Right => &self.right_dock,
+    //         };
+
+    //         self.subscriptions.push(cx.subscribe(&panel, {
+    //             let mut dock = dock.clone();
+    //             let mut prev_position = panel.position(cx);
+    //             move |this, panel, event, cx| {
+    //                 if T::should_change_position_on_event(event) {
+    //                     let new_position = panel.read(cx).position(cx);
+    //                     let mut was_visible = false;
+    //                     dock.update(cx, |dock, cx| {
+    //                         prev_position = new_position;
+
+    //                         was_visible = dock.is_open()
+    //                             && dock
+    //                                 .visible_panel()
+    //                                 .map_or(false, |active_panel| active_panel.id() == panel.id());
+    //                         dock.remove_panel(&panel, cx);
+    //                     });
+
+    //                     if panel.is_zoomed(cx) {
+    //                         this.zoomed_position = Some(new_position);
+    //                     }
+
+    //                     dock = match panel.read(cx).position(cx) {
+    //                         DockPosition::Left => &this.left_dock,
+    //                         DockPosition::Bottom => &this.bottom_dock,
+    //                         DockPosition::Right => &this.right_dock,
+    //                     }
+    //                     .clone();
+    //                     dock.update(cx, |dock, cx| {
+    //                         dock.add_panel(panel.clone(), cx);
+    //                         if was_visible {
+    //                             dock.set_open(true, cx);
+    //                             dock.activate_panel(dock.panels_len() - 1, cx);
+    //                         }
+    //                     });
+    //                 } else if T::should_zoom_in_on_event(event) {
+    //                     dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
+    //                     if !panel.has_focus(cx) {
+    //                         cx.focus(&panel);
+    //                     }
+    //                     this.zoomed = Some(panel.downgrade().into_any());
+    //                     this.zoomed_position = Some(panel.read(cx).position(cx));
+    //                 } else if T::should_zoom_out_on_event(event) {
+    //                     dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
+    //                     if this.zoomed_position == Some(prev_position) {
+    //                         this.zoomed = None;
+    //                         this.zoomed_position = None;
+    //                     }
+    //                     cx.notify();
+    //                 } else if T::is_focus_event(event) {
+    //                     let position = panel.read(cx).position(cx);
+    //                     this.dismiss_zoomed_items_to_reveal(Some(position), cx);
+    //                     if panel.is_zoomed(cx) {
+    //                         this.zoomed = Some(panel.downgrade().into_any());
+    //                         this.zoomed_position = Some(position);
+    //                     } else {
+    //                         this.zoomed = None;
+    //                         this.zoomed_position = None;
+    //                     }
+    //                     this.update_active_view_for_followers(cx);
+    //                     cx.notify();
+    //                 } else {
+    //                     handler(this, &panel, event, cx)
+    //                 }
+    //             }
+    //         }));
+
+    //         dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
+    //     }
+
+    //     pub fn status_bar(&self) -> &View<StatusBar> {
+    //         &self.status_bar
+    //     }
+
+    //     pub fn app_state(&self) -> &Arc<AppState> {
+    //         &self.app_state
+    //     }
+
+    //     pub fn user_store(&self) -> &ModelHandle<UserStore> {
+    //         &self.app_state.user_store
+    //     }
+
+    pub fn project(&self) -> &Handle<Project> {
+        &self.project
+    }
+
+    //     pub fn recent_navigation_history(
+    //         &self,
+    //         limit: Option<usize>,
+    //         cx: &AppContext,
+    //     ) -> Vec<(ProjectPath, Option<PathBuf>)> {
+    //         let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
+    //         let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
+    //         for pane in &self.panes {
+    //             let pane = pane.read(cx);
+    //             pane.nav_history()
+    //                 .for_each_entry(cx, |entry, (project_path, fs_path)| {
+    //                     if let Some(fs_path) = &fs_path {
+    //                         abs_paths_opened
+    //                             .entry(fs_path.clone())
+    //                             .or_default()
+    //                             .insert(project_path.clone());
+    //                     }
+    //                     let timestamp = entry.timestamp;
+    //                     match history.entry(project_path) {
+    //                         hash_map::Entry::Occupied(mut entry) => {
+    //                             let (_, old_timestamp) = entry.get();
+    //                             if &timestamp > old_timestamp {
+    //                                 entry.insert((fs_path, timestamp));
+    //                             }
+    //                         }
+    //                         hash_map::Entry::Vacant(entry) => {
+    //                             entry.insert((fs_path, timestamp));
+    //                         }
+    //                     }
+    //                 });
+    //         }
+
+    //         history
+    //             .into_iter()
+    //             .sorted_by_key(|(_, (_, timestamp))| *timestamp)
+    //             .map(|(project_path, (fs_path, _))| (project_path, fs_path))
+    //             .rev()
+    //             .filter(|(history_path, abs_path)| {
+    //                 let latest_project_path_opened = abs_path
+    //                     .as_ref()
+    //                     .and_then(|abs_path| abs_paths_opened.get(abs_path))
+    //                     .and_then(|project_paths| {
+    //                         project_paths
+    //                             .iter()
+    //                             .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
+    //                     });
+
+    //                 match latest_project_path_opened {
+    //                     Some(latest_project_path_opened) => latest_project_path_opened == history_path,
+    //                     None => true,
+    //                 }
+    //             })
+    //             .take(limit.unwrap_or(usize::MAX))
+    //             .collect()
+    //     }
+
+    //     fn navigate_history(
+    //         &mut self,
+    //         pane: WeakViewHandle<Pane>,
+    //         mode: NavigationMode,
+    //         cx: &mut ViewContext<Workspace>,
+    //     ) -> Task<Result<()>> {
+    //         let to_load = if let Some(pane) = pane.upgrade(cx) {
+    //             cx.focus(&pane);
+
+    //             pane.update(cx, |pane, cx| {
+    //                 loop {
+    //                     // Retrieve the weak item handle from the history.
+    //                     let entry = pane.nav_history_mut().pop(mode, cx)?;
+
+    //                     // If the item is still present in this pane, then activate it.
+    //                     if let Some(index) = entry
+    //                         .item
+    //                         .upgrade(cx)
+    //                         .and_then(|v| pane.index_for_item(v.as_ref()))
+    //                     {
+    //                         let prev_active_item_index = pane.active_item_index();
+    //                         pane.nav_history_mut().set_mode(mode);
+    //                         pane.activate_item(index, true, true, cx);
+    //                         pane.nav_history_mut().set_mode(NavigationMode::Normal);
+
+    //                         let mut navigated = prev_active_item_index != pane.active_item_index();
+    //                         if let Some(data) = entry.data {
+    //                             navigated |= pane.active_item()?.navigate(data, cx);
+    //                         }
+
+    //                         if navigated {
+    //                             break None;
+    //                         }
+    //                     }
+    //                     // If the item is no longer present in this pane, then retrieve its
+    //                     // project path in order to reopen it.
+    //                     else {
+    //                         break pane
+    //                             .nav_history()
+    //                             .path_for_item(entry.item.id())
+    //                             .map(|(project_path, _)| (project_path, entry));
+    //                     }
+    //                 }
+    //             })
+    //         } else {
+    //             None
+    //         };
+
+    //         if let Some((project_path, entry)) = to_load {
+    //             // If the item was no longer present, then load it again from its previous path.
+    //             let task = self.load_path(project_path, cx);
+    //             cx.spawn(|workspace, mut cx| async move {
+    //                 let task = task.await;
+    //                 let mut navigated = false;
+    //                 if let Some((project_entry_id, build_item)) = task.log_err() {
+    //                     let prev_active_item_id = pane.update(&mut cx, |pane, _| {
+    //                         pane.nav_history_mut().set_mode(mode);
+    //                         pane.active_item().map(|p| p.id())
+    //                     })?;
+
+    //                     pane.update(&mut cx, |pane, cx| {
+    //                         let item = pane.open_item(project_entry_id, true, cx, build_item);
+    //                         navigated |= Some(item.id()) != prev_active_item_id;
+    //                         pane.nav_history_mut().set_mode(NavigationMode::Normal);
+    //                         if let Some(data) = entry.data {
+    //                             navigated |= item.navigate(data, cx);
+    //                         }
+    //                     })?;
+    //                 }
+
+    //                 if !navigated {
+    //                     workspace
+    //                         .update(&mut cx, |workspace, cx| {
+    //                             Self::navigate_history(workspace, pane, mode, cx)
+    //                         })?
+    //                         .await?;
+    //                 }
+
+    //                 Ok(())
+    //             })
+    //         } else {
+    //             Task::ready(Ok(()))
+    //         }
+    //     }
+
+    //     pub fn go_back(
+    //         &mut self,
+    //         pane: WeakViewHandle<Pane>,
+    //         cx: &mut ViewContext<Workspace>,
+    //     ) -> Task<Result<()>> {
+    //         self.navigate_history(pane, NavigationMode::GoingBack, cx)
+    //     }
+
+    //     pub fn go_forward(
+    //         &mut self,
+    //         pane: WeakViewHandle<Pane>,
+    //         cx: &mut ViewContext<Workspace>,
+    //     ) -> Task<Result<()>> {
+    //         self.navigate_history(pane, NavigationMode::GoingForward, cx)
+    //     }
+
+    //     pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
+    //         self.navigate_history(
+    //             self.active_pane().downgrade(),
+    //             NavigationMode::ReopeningClosedItem,
+    //             cx,
+    //         )
+    //     }
+
+    //     pub fn client(&self) -> &Client {
+    //         &self.app_state.client
+    //     }
+
+    //     pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
+    //         self.titlebar_item = Some(item);
+    //         cx.notify();
+    //     }
+
+    //     pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
+    //         self.titlebar_item.clone()
+    //     }
+
+    //     /// Call the given callback with a workspace whose project is local.
+    //     ///
+    //     /// If the given workspace has a local project, then it will be passed
+    //     /// to the callback. Otherwise, a new empty window will be created.
+    //     pub fn with_local_workspace<T, F>(
+    //         &mut self,
+    //         cx: &mut ViewContext<Self>,
+    //         callback: F,
+    //     ) -> Task<Result<T>>
+    //     where
+    //         T: 'static,
+    //         F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+    //     {
+    //         if self.project.read(cx).is_local() {
+    //             Task::Ready(Some(Ok(callback(self, cx))))
+    //         } else {
+    //             let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
+    //             cx.spawn(|_vh, mut cx| async move {
+    //                 let (workspace, _) = task.await;
+    //                 workspace.update(&mut cx, callback)
+    //             })
+    //         }
+    //     }
+
+    //     pub fn worktrees<'a>(
+    //         &self,
+    //         cx: &'a AppContext,
+    //     ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
+    //         self.project.read(cx).worktrees(cx)
+    //     }
+
+    //     pub fn visible_worktrees<'a>(
+    //         &self,
+    //         cx: &'a AppContext,
+    //     ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
+    //         self.project.read(cx).visible_worktrees(cx)
+    //     }
+
+    //     pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
+    //         let futures = self
+    //             .worktrees(cx)
+    //             .filter_map(|worktree| worktree.read(cx).as_local())
+    //             .map(|worktree| worktree.scan_complete())
+    //             .collect::<Vec<_>>();
+    //         async move {
+    //             for future in futures {
+    //                 future.await;
+    //             }
+    //         }
+    //     }
+
+    //     pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
+    //         cx.spawn(|mut cx| async move {
+    //             let window = cx
+    //                 .windows()
+    //                 .into_iter()
+    //                 .find(|window| window.is_active(&cx).unwrap_or(false));
+    //             if let Some(window) = window {
+    //                 //This can only get called when the window's project connection has been lost
+    //                 //so we don't need to prompt the user for anything and instead just close the window
+    //                 window.remove(&mut cx);
+    //             }
+    //         })
+    //         .detach();
+    //     }
+
+    //     pub fn close(
+    //         &mut self,
+    //         _: &CloseWindow,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         let window = cx.window();
+    //         let prepare = self.prepare_to_close(false, cx);
+    //         Some(cx.spawn(|_, mut cx| async move {
+    //             if prepare.await? {
+    //                 window.remove(&mut cx);
+    //             }
+    //             Ok(())
+    //         }))
+    //     }
+
+    //     pub fn prepare_to_close(
+    //         &mut self,
+    //         quitting: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<bool>> {
+    //         let active_call = self.active_call().cloned();
+    //         let window = cx.window();
+
+    //         cx.spawn(|this, mut cx| async move {
+    //             let workspace_count = cx
+    //                 .windows()
+    //                 .into_iter()
+    //                 .filter(|window| window.root_is::<Workspace>())
+    //                 .count();
+
+    //             if let Some(active_call) = active_call {
+    //                 if !quitting
+    //                     && workspace_count == 1
+    //                     && active_call.read_with(&cx, |call, _| call.room().is_some())
+    //                 {
+    //                     let answer = window.prompt(
+    //                         PromptLevel::Warning,
+    //                         "Do you want to leave the current call?",
+    //                         &["Close window and hang up", "Cancel"],
+    //                         &mut cx,
+    //                     );
+
+    //                     if let Some(mut answer) = answer {
+    //                         if answer.next().await == Some(1) {
+    //                             return anyhow::Ok(false);
+    //                         } else {
+    //                             active_call
+    //                                 .update(&mut cx, |call, cx| call.hang_up(cx))
+    //                                 .await
+    //                                 .log_err();
+    //                         }
+    //                     }
+    //                 }
+    //             }
+
+    //             Ok(this
+    //                 .update(&mut cx, |this, cx| {
+    //                     this.save_all_internal(SaveIntent::Close, cx)
+    //                 })?
+    //                 .await?)
+    //         })
+    //     }
+
+    //     fn save_all(
+    //         &mut self,
+    //         action: &SaveAll,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         let save_all =
+    //             self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx);
+    //         Some(cx.foreground().spawn(async move {
+    //             save_all.await?;
+    //             Ok(())
+    //         }))
+    //     }
+
+    //     fn save_all_internal(
+    //         &mut self,
+    //         mut save_intent: SaveIntent,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<bool>> {
+    //         if self.project.read(cx).is_read_only() {
+    //             return Task::ready(Ok(true));
+    //         }
+    //         let dirty_items = self
+    //             .panes
+    //             .iter()
+    //             .flat_map(|pane| {
+    //                 pane.read(cx).items().filter_map(|item| {
+    //                     if item.is_dirty(cx) {
+    //                         Some((pane.downgrade(), item.boxed_clone()))
+    //                     } else {
+    //                         None
+    //                     }
+    //                 })
+    //             })
+    //             .collect::<Vec<_>>();
+
+    //         let project = self.project.clone();
+    //         cx.spawn(|workspace, mut cx| async move {
+    //             // Override save mode and display "Save all files" prompt
+    //             if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
+    //                 let mut answer = workspace.update(&mut cx, |_, cx| {
+    //                     let prompt = Pane::file_names_for_prompt(
+    //                         &mut dirty_items.iter().map(|(_, handle)| handle),
+    //                         dirty_items.len(),
+    //                         cx,
+    //                     );
+    //                     cx.prompt(
+    //                         PromptLevel::Warning,
+    //                         &prompt,
+    //                         &["Save all", "Discard all", "Cancel"],
+    //                     )
+    //                 })?;
+    //                 match answer.next().await {
+    //                     Some(0) => save_intent = SaveIntent::SaveAll,
+    //                     Some(1) => save_intent = SaveIntent::Skip,
+    //                     _ => {}
+    //                 }
+    //             }
+    //             for (pane, item) in dirty_items {
+    //                 let (singleton, project_entry_ids) =
+    //                     cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
+    //                 if singleton || !project_entry_ids.is_empty() {
+    //                     if let Some(ix) =
+    //                         pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
+    //                     {
+    //                         if !Pane::save_item(
+    //                             project.clone(),
+    //                             &pane,
+    //                             ix,
+    //                             &*item,
+    //                             save_intent,
+    //                             &mut cx,
+    //                         )
+    //                         .await?
+    //                         {
+    //                             return Ok(false);
+    //                         }
+    //                     }
+    //                 }
+    //             }
+    //             Ok(true)
+    //         })
+    //     }
+
+    //     pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+    //         let mut paths = cx.prompt_for_paths(PathPromptOptions {
+    //             files: true,
+    //             directories: true,
+    //             multiple: true,
+    //         });
+
+    //         Some(cx.spawn(|this, mut cx| async move {
+    //             if let Some(paths) = paths.recv().await.flatten() {
+    //                 if let Some(task) = this
+    //                     .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
+    //                     .log_err()
+    //                 {
+    //                     task.await?
+    //                 }
+    //             }
+    //             Ok(())
+    //         }))
+    //     }
+
+    //     pub fn open_workspace_for_paths(
+    //         &mut self,
+    //         paths: Vec<PathBuf>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<()>> {
+    //         let window = cx.window().downcast::<Self>();
+    //         let is_remote = self.project.read(cx).is_remote();
+    //         let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
+    //         let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
+    //         let close_task = if is_remote || has_worktree || has_dirty_items {
+    //             None
+    //         } else {
+    //             Some(self.prepare_to_close(false, cx))
+    //         };
+    //         let app_state = self.app_state.clone();
+
+    //         cx.spawn(|_, mut cx| async move {
+    //             let window_to_replace = if let Some(close_task) = close_task {
+    //                 if !close_task.await? {
+    //                     return Ok(());
+    //                 }
+    //                 window
+    //             } else {
+    //                 None
+    //             };
+    //             cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))
+    //                 .await?;
+    //             Ok(())
+    //         })
+    //     }
+
+    #[allow(clippy::type_complexity)]
+    pub fn open_paths(
+        &mut self,
+        mut abs_paths: Vec<PathBuf>,
+        visible: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
+        log::info!("open paths {:?}", abs_paths);
+
+        let fs = self.app_state.fs.clone();
+
+        // Sort the paths to ensure we add worktrees for parents before their children.
+        abs_paths.sort_unstable();
+        cx.spawn(|this, mut cx| async move {
+            let mut tasks = Vec::with_capacity(abs_paths.len());
+            for abs_path in &abs_paths {
+                let project_path = match this
+                    .update(&mut cx, |this, cx| {
+                        Workspace::project_path_for_path(
+                            this.project.clone(),
+                            abs_path,
+                            visible,
+                            cx,
+                        )
+                    })
+                    .log_err()
+                {
+                    Some(project_path) => project_path.await.log_err(),
+                    None => None,
+                };
+
+                let this = this.clone();
+                let task = cx.spawn(|mut cx| {
+                    let fs = fs.clone();
+                    let abs_path = abs_path.clone();
+                    async move {
+                        let (worktree, project_path) = project_path?;
+                        if fs.is_file(&abs_path).await {
+                            Some(
+                                this.update(&mut cx, |this, cx| {
+                                    this.open_path(project_path, None, true, cx)
+                                })
+                                .log_err()?
+                                .await,
+                            )
+                        } else {
+                            this.update(&mut cx, |workspace, cx| {
+                                let worktree = worktree.read(cx);
+                                let worktree_abs_path = worktree.abs_path();
+                                let entry_id = if abs_path == worktree_abs_path.as_ref() {
+                                    worktree.root_entry()
+                                } else {
+                                    abs_path
+                                        .strip_prefix(worktree_abs_path.as_ref())
+                                        .ok()
+                                        .and_then(|relative_path| {
+                                            worktree.entry_for_path(relative_path)
+                                        })
+                                }
+                                .map(|entry| entry.id);
+                                if let Some(entry_id) = entry_id {
+                                    workspace.project.update(cx, |_, cx| {
+                                        cx.emit(project2::Event::ActiveEntryChanged(Some(
+                                            entry_id,
+                                        )));
+                                    })
+                                }
+                            })
+                            .log_err()?;
+                            None
+                        }
+                    }
+                });
+                tasks.push(task);
+            }
+
+            futures::future::join_all(tasks).await
+        })
+    }
+
+    //     fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
+    //         let mut paths = cx.prompt_for_paths(PathPromptOptions {
+    //             files: false,
+    //             directories: true,
+    //             multiple: true,
+    //         });
+    //         cx.spawn(|this, mut cx| async move {
+    //             if let Some(paths) = paths.recv().await.flatten() {
+    //                 let results = this
+    //                     .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
+    //                     .await;
+    //                 for result in results.into_iter().flatten() {
+    //                     result.log_err();
+    //                 }
+    //             }
+    //             anyhow::Ok(())
+    //         })
+    //         .detach_and_log_err(cx);
+    //     }
+
+    fn project_path_for_path(
+        project: Handle<Project>,
+        abs_path: &Path,
+        visible: bool,
+        cx: &mut AppContext,
+    ) -> Task<Result<(Handle<Worktree>, ProjectPath)>> {
+        let entry = project.update(cx, |project, cx| {
+            project.find_or_create_local_worktree(abs_path, visible, cx)
+        });
+        cx.spawn(|cx| async move {
+            let (worktree, path) = entry.await?;
+            let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
+            Ok((
+                worktree,
+                ProjectPath {
+                    worktree_id,
+                    path: path.into(),
+                },
+            ))
+        })
+    }
+
+    //     /// Returns the modal that was toggled closed if it was open.
+    //     pub fn toggle_modal<V, F>(
+    //         &mut self,
+    //         cx: &mut ViewContext<Self>,
+    //         add_view: F,
+    //     ) -> Option<View<V>>
+    //     where
+    //         V: 'static + Modal,
+    //         F: FnOnce(&mut Self, &mut ViewContext<Self>) -> View<V>,
+    //     {
+    //         cx.notify();
+    //         // Whatever modal was visible is getting clobbered. If its the same type as V, then return
+    //         // it. Otherwise, create a new modal and set it as active.
+    //         if let Some(already_open_modal) = self
+    //             .dismiss_modal(cx)
+    //             .and_then(|modal| modal.downcast::<V>())
+    //         {
+    //             cx.focus_self();
+    //             Some(already_open_modal)
+    //         } else {
+    //             let modal = add_view(self, cx);
+    //             cx.subscribe(&modal, |this, _, event, cx| {
+    //                 if V::dismiss_on_event(event) {
+    //                     this.dismiss_modal(cx);
+    //                 }
+    //             })
+    //             .detach();
+    //             let previously_focused_view_id = cx.focused_view_id();
+    //             cx.focus(&modal);
+    //             self.modal = Some(ActiveModal {
+    //                 view: Box::new(modal),
+    //                 previously_focused_view_id,
+    //             });
+    //             None
+    //         }
+    //     }
+
+    //     pub fn modal<V: 'static + View>(&self) -> Option<View<V>> {
+    //         self.modal
+    //             .as_ref()
+    //             .and_then(|modal| modal.view.as_any().clone().downcast::<V>())
+    //     }
+
+    //     pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
+    //         if let Some(modal) = self.modal.take() {
+    //             if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
+    //                 if modal.view.has_focus(cx) {
+    //                     cx.window_context().focus(Some(previously_focused_view_id));
+    //                 }
+    //             }
+    //             cx.notify();
+    //             Some(modal.view.as_any().clone())
+    //         } else {
+    //             None
+    //         }
+    //     }
+
+    //     pub fn items<'a>(
+    //         &'a self,
+    //         cx: &'a AppContext,
+    //     ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
+    //         self.panes.iter().flat_map(|pane| pane.read(cx).items())
+    //     }
+
+    //     pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
+    //         self.items_of_type(cx).max_by_key(|item| item.id())
+    //     }
+
+    //     pub fn items_of_type<'a, T: Item>(
+    //         &'a self,
+    //         cx: &'a AppContext,
+    //     ) -> impl 'a + Iterator<Item = View<T>> {
+    //         self.panes
+    //             .iter()
+    //             .flat_map(|pane| pane.read(cx).items_of_type())
+    //     }
+
+    //     pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
+    //         self.active_pane().read(cx).active_item()
+    //     }
+
+    //     fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
+    //         self.active_item(cx).and_then(|item| item.project_path(cx))
+    //     }
+
+    //     pub fn save_active_item(
+    //         &mut self,
+    //         save_intent: SaveIntent,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<()>> {
+    //         let project = self.project.clone();
+    //         let pane = self.active_pane();
+    //         let item_ix = pane.read(cx).active_item_index();
+    //         let item = pane.read(cx).active_item();
+    //         let pane = pane.downgrade();
+
+    //         cx.spawn(|_, mut cx| async move {
+    //             if let Some(item) = item {
+    //                 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
+    //                     .await
+    //                     .map(|_| ())
+    //             } else {
+    //                 Ok(())
+    //             }
+    //         })
+    //     }
+
+    //     pub fn close_inactive_items_and_panes(
+    //         &mut self,
+    //         _: &CloseInactiveTabsAndPanes,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         self.close_all_internal(true, SaveIntent::Close, cx)
+    //     }
+
+    //     pub fn close_all_items_and_panes(
+    //         &mut self,
+    //         action: &CloseAllItemsAndPanes,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
+    //     }
+
+    //     fn close_all_internal(
+    //         &mut self,
+    //         retain_active_pane: bool,
+    //         save_intent: SaveIntent,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         let current_pane = self.active_pane();
+
+    //         let mut tasks = Vec::new();
+
+    //         if retain_active_pane {
+    //             if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
+    //                 pane.close_inactive_items(&CloseInactiveItems, cx)
+    //             }) {
+    //                 tasks.push(current_pane_close);
+    //             };
+    //         }
+
+    //         for pane in self.panes() {
+    //             if retain_active_pane && pane.id() == current_pane.id() {
+    //                 continue;
+    //             }
+
+    //             if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
+    //                 pane.close_all_items(
+    //                     &CloseAllItems {
+    //                         save_intent: Some(save_intent),
+    //                     },
+    //                     cx,
+    //                 )
+    //             }) {
+    //                 tasks.push(close_pane_items)
+    //             }
+    //         }
+
+    //         if tasks.is_empty() {
+    //             None
+    //         } else {
+    //             Some(cx.spawn(|_, _| async move {
+    //                 for task in tasks {
+    //                     task.await?
+    //                 }
+    //                 Ok(())
+    //             }))
+    //         }
+    //     }
+
+    //     pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
+    //         let dock = match dock_side {
+    //             DockPosition::Left => &self.left_dock,
+    //             DockPosition::Bottom => &self.bottom_dock,
+    //             DockPosition::Right => &self.right_dock,
+    //         };
+    //         let mut focus_center = false;
+    //         let mut reveal_dock = false;
+    //         dock.update(cx, |dock, cx| {
+    //             let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
+    //             let was_visible = dock.is_open() && !other_is_zoomed;
+    //             dock.set_open(!was_visible, cx);
+
+    //             if let Some(active_panel) = dock.active_panel() {
+    //                 if was_visible {
+    //                     if active_panel.has_focus(cx) {
+    //                         focus_center = true;
+    //                     }
+    //                 } else {
+    //                     cx.focus(active_panel.as_any());
+    //                     reveal_dock = true;
+    //                 }
+    //             }
+    //         });
+
+    //         if reveal_dock {
+    //             self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
+    //         }
+
+    //         if focus_center {
+    //             cx.focus_self();
+    //         }
+
+    //         cx.notify();
+    //         self.serialize_workspace(cx);
+    //     }
+
+    //     pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
+    //         let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
+
+    //         for dock in docks {
+    //             dock.update(cx, |dock, cx| {
+    //                 dock.set_open(false, cx);
+    //             });
+    //         }
+
+    //         cx.focus_self();
+    //         cx.notify();
+    //         self.serialize_workspace(cx);
+    //     }
+
+    //     /// Transfer focus to the panel of the given type.
+    //     pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
+    //         self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
+    //             .as_any()
+    //             .clone()
+    //             .downcast()
+    //     }
+
+    //     /// Focus the panel of the given type if it isn't already focused. If it is
+    //     /// already focused, then transfer focus back to the workspace center.
+    //     pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
+    //         self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
+    //     }
+
+    //     /// Focus or unfocus the given panel type, depending on the given callback.
+    //     fn focus_or_unfocus_panel<T: Panel>(
+    //         &mut self,
+    //         cx: &mut ViewContext<Self>,
+    //         should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
+    //     ) -> Option<Rc<dyn PanelHandle>> {
+    //         for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+    //             if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
+    //                 let mut focus_center = false;
+    //                 let mut reveal_dock = false;
+    //                 let panel = dock.update(cx, |dock, cx| {
+    //                     dock.activate_panel(panel_index, cx);
+
+    //                     let panel = dock.active_panel().cloned();
+    //                     if let Some(panel) = panel.as_ref() {
+    //                         if should_focus(&**panel, cx) {
+    //                             dock.set_open(true, cx);
+    //                             cx.focus(panel.as_any());
+    //                             reveal_dock = true;
+    //                         } else {
+    //                             // if panel.is_zoomed(cx) {
+    //                             //     dock.set_open(false, cx);
+    //                             // }
+    //                             focus_center = true;
+    //                         }
+    //                     }
+    //                     panel
+    //                 });
+
+    //                 if focus_center {
+    //                     cx.focus_self();
+    //                 }
+
+    //                 self.serialize_workspace(cx);
+    //                 cx.notify();
+    //                 return panel;
+    //             }
+    //         }
+    //         None
+    //     }
+
+    //     pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
+    //         for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+    //             let dock = dock.read(cx);
+    //             if let Some(panel) = dock.panel::<T>() {
+    //                 return Some(panel);
+    //             }
+    //         }
+    //         None
+    //     }
+
+    //     fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
+    //         for pane in &self.panes {
+    //             pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
+    //         }
+
+    //         self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
+    //         self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
+    //         self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
+    //         self.zoomed = None;
+    //         self.zoomed_position = None;
+
+    //         cx.notify();
+    //     }
+
+    //     #[cfg(any(test, feature = "test-support"))]
+    //     pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
+    //         self.zoomed.and_then(|view| view.upgrade(cx))
+    //     }
+
+    //     fn dismiss_zoomed_items_to_reveal(
+    //         &mut self,
+    //         dock_to_reveal: Option<DockPosition>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         // If a center pane is zoomed, unzoom it.
+    //         for pane in &self.panes {
+    //             if pane != &self.active_pane || dock_to_reveal.is_some() {
+    //                 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
+    //             }
+    //         }
+
+    //         // If another dock is zoomed, hide it.
+    //         let mut focus_center = false;
+    //         for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
+    //             dock.update(cx, |dock, cx| {
+    //                 if Some(dock.position()) != dock_to_reveal {
+    //                     if let Some(panel) = dock.active_panel() {
+    //                         if panel.is_zoomed(cx) {
+    //                             focus_center |= panel.has_focus(cx);
+    //                             dock.set_open(false, cx);
+    //                         }
+    //                     }
+    //                 }
+    //             });
+    //         }
+
+    //         if focus_center {
+    //             cx.focus_self();
+    //         }
+
+    //         if self.zoomed_position != dock_to_reveal {
+    //             self.zoomed = None;
+    //             self.zoomed_position = None;
+    //         }
+
+    //         cx.notify();
+    //     }
+
+    //     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
+    //         let pane = cx.add_view(|cx| {
+    //             Pane::new(
+    //                 self.weak_handle(),
+    //                 self.project.clone(),
+    //                 self.pane_history_timestamp.clone(),
+    //                 cx,
+    //             )
+    //         });
+    //         cx.subscribe(&pane, Self::handle_pane_event).detach();
+    //         self.panes.push(pane.clone());
+    //         cx.focus(&pane);
+    //         cx.emit(Event::PaneAdded(pane.clone()));
+    //         pane
+    //     }
+
+    //     pub fn add_item_to_center(
+    //         &mut self,
+    //         item: Box<dyn ItemHandle>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> bool {
+    //         if let Some(center_pane) = self.last_active_center_pane.clone() {
+    //             if let Some(center_pane) = center_pane.upgrade(cx) {
+    //                 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
+    //                 true
+    //             } else {
+    //                 false
+    //             }
+    //         } else {
+    //             false
+    //         }
+    //     }
+
+    //     pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+    //         self.active_pane
+    //             .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
+    //     }
+
+    //     pub fn split_item(
+    //         &mut self,
+    //         split_direction: SplitDirection,
+    //         item: Box<dyn ItemHandle>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
+    //         new_pane.update(cx, move |new_pane, cx| {
+    //             new_pane.add_item(item, true, true, None, cx)
+    //         })
+    //     }
+
+    //     pub fn open_abs_path(
+    //         &mut self,
+    //         abs_path: PathBuf,
+    //         visible: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+    //         cx.spawn(|workspace, mut cx| async move {
+    //             let open_paths_task_result = workspace
+    //                 .update(&mut cx, |workspace, cx| {
+    //                     workspace.open_paths(vec![abs_path.clone()], visible, cx)
+    //                 })
+    //                 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
+    //                 .await;
+    //             anyhow::ensure!(
+    //                 open_paths_task_result.len() == 1,
+    //                 "open abs path {abs_path:?} task returned incorrect number of results"
+    //             );
+    //             match open_paths_task_result
+    //                 .into_iter()
+    //                 .next()
+    //                 .expect("ensured single task result")
+    //             {
+    //                 Some(open_result) => {
+    //                     open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
+    //                 }
+    //                 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
+    //             }
+    //         })
+    //     }
+
+    //     pub fn split_abs_path(
+    //         &mut self,
+    //         abs_path: PathBuf,
+    //         visible: bool,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+    //         let project_path_task =
+    //             Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
+    //         cx.spawn(|this, mut cx| async move {
+    //             let (_, path) = project_path_task.await?;
+    //             this.update(&mut cx, |this, cx| this.split_path(path, cx))?
+    //                 .await
+    //         })
+    //     }
+
+    pub fn open_path(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        pane: Option<WeakView<Pane>>,
+        focus_item: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
+        let pane = pane.unwrap_or_else(|| {
+            self.last_active_center_pane.clone().unwrap_or_else(|| {
+                self.panes
+                    .first()
+                    .expect("There must be an active pane")
+                    .downgrade()
+            })
+        });
+
+        let task = self.load_path(path.into(), cx);
+        cx.spawn(|_, mut cx| async move {
+            let (project_entry_id, build_item) = task.await?;
+            pane.update(&mut cx, |pane, cx| {
+                pane.open_item(project_entry_id, focus_item, cx, build_item)
+            })
+        })
+    }
+
+    //     pub fn split_path(
+    //         &mut self,
+    //         path: impl Into<ProjectPath>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
+    //         let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
+    //             self.panes
+    //                 .first()
+    //                 .expect("There must be an active pane")
+    //                 .downgrade()
+    //         });
+
+    //         if let Member::Pane(center_pane) = &self.center.root {
+    //             if center_pane.read(cx).items_len() == 0 {
+    //                 return self.open_path(path, Some(pane), true, cx);
+    //             }
+    //         }
+
+    //         let task = self.load_path(path.into(), cx);
+    //         cx.spawn(|this, mut cx| async move {
+    //             let (project_entry_id, build_item) = task.await?;
+    //             this.update(&mut cx, move |this, cx| -> Option<_> {
+    //                 let pane = pane.upgrade(cx)?;
+    //                 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
+    //                 new_pane.update(cx, |new_pane, cx| {
+    //                     Some(new_pane.open_item(project_entry_id, true, cx, build_item))
+    //                 })
+    //             })
+    //             .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
+    //         })
+    //     }
+
+    pub(crate) fn load_path(
+        &mut self,
+        path: ProjectPath,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<
+        Result<(
+            ProjectEntryId,
+            impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
+        )>,
+    > {
+        let project = self.project().clone();
+        let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
+        cx.spawn(|_, mut cx| async move {
+            let (project_entry_id, project_item) = project_item.await?;
+            let build_item = cx.update(|cx| {
+                cx.default_global::<ProjectItemBuilders>()
+                    .get(&project_item.model_type())
+                    .ok_or_else(|| anyhow!("no item builder for project item"))
+                    .cloned()
+            })?;
+            let build_item =
+                move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
+            Ok((project_entry_id, build_item))
+        })
+    }
+
+    //     pub fn open_project_item<T>(
+    //         &mut self,
+    //         project_item: ModelHandle<T::Item>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> View<T>
+    //     where
+    //         T: ProjectItem,
+    //     {
+    //         use project::Item as _;
+
+    //         let entry_id = project_item.read(cx).entry_id(cx);
+    //         if let Some(item) = entry_id
+    //             .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
+    //             .and_then(|item| item.downcast())
+    //         {
+    //             self.activate_item(&item, cx);
+    //             return item;
+    //         }
+
+    //         let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
+    //         self.add_item(Box::new(item.clone()), cx);
+    //         item
+    //     }
+
+    //     pub fn split_project_item<T>(
+    //         &mut self,
+    //         project_item: ModelHandle<T::Item>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> View<T>
+    //     where
+    //         T: ProjectItem,
+    //     {
+    //         use project::Item as _;
+
+    //         let entry_id = project_item.read(cx).entry_id(cx);
+    //         if let Some(item) = entry_id
+    //             .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
+    //             .and_then(|item| item.downcast())
+    //         {
+    //             self.activate_item(&item, cx);
+    //             return item;
+    //         }
+
+    //         let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
+    //         self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
+    //         item
+    //     }
+
+    //     pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
+    //         if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
+    //             self.active_pane.update(cx, |pane, cx| {
+    //                 pane.add_item(Box::new(shared_screen), false, true, None, cx)
+    //             });
+    //         }
+    //     }
+
+    //     pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
+    //         let result = self.panes.iter().find_map(|pane| {
+    //             pane.read(cx)
+    //                 .index_for_item(item)
+    //                 .map(|ix| (pane.clone(), ix))
+    //         });
+    //         if let Some((pane, ix)) = result {
+    //             pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
+    //             true
+    //         } else {
+    //             false
+    //         }
+    //     }
+
+    //     fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
+    //         let panes = self.center.panes();
+    //         if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
+    //             cx.focus(&pane);
+    //         } else {
+    //             self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
+    //         }
+    //     }
+
+    //     pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
+    //         let panes = self.center.panes();
+    //         if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
+    //             let next_ix = (ix + 1) % panes.len();
+    //             let next_pane = panes[next_ix].clone();
+    //             cx.focus(&next_pane);
+    //         }
+    //     }
+
+    //     pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
+    //         let panes = self.center.panes();
+    //         if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
+    //             let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
+    //             let prev_pane = panes[prev_ix].clone();
+    //             cx.focus(&prev_pane);
+    //         }
+    //     }
+
+    //     pub fn activate_pane_in_direction(
+    //         &mut self,
+    //         direction: SplitDirection,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         if let Some(pane) = self.find_pane_in_direction(direction, cx) {
+    //             cx.focus(pane);
+    //         }
+    //     }
+
+    //     pub fn swap_pane_in_direction(
+    //         &mut self,
+    //         direction: SplitDirection,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         if let Some(to) = self
+    //             .find_pane_in_direction(direction, cx)
+    //             .map(|pane| pane.clone())
+    //         {
+    //             self.center.swap(&self.active_pane.clone(), &to);
+    //             cx.notify();
+    //         }
+    //     }
+
+    //     fn find_pane_in_direction(
+    //         &mut self,
+    //         direction: SplitDirection,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<&View<Pane>> {
+    //         let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
+    //             return None;
+    //         };
+    //         let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
+    //         let center = match cursor {
+    //             Some(cursor) if bounding_box.contains_point(cursor) => cursor,
+    //             _ => bounding_box.center(),
+    //         };
+
+    //         let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
+
+    //         let target = match direction {
+    //             SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
+    //             SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
+    //             SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
+    //             SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
+    //         };
+    //         self.center.pane_at_pixel_position(target)
+    //     }
+
+    //     fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
+    //         if self.active_pane != pane {
+    //             self.active_pane = pane.clone();
+    //             self.status_bar.update(cx, |status_bar, cx| {
+    //                 status_bar.set_active_pane(&self.active_pane, cx);
+    //             });
+    //             self.active_item_path_changed(cx);
+    //             self.last_active_center_pane = Some(pane.downgrade());
+    //         }
+
+    //         self.dismiss_zoomed_items_to_reveal(None, cx);
+    //         if pane.read(cx).is_zoomed() {
+    //             self.zoomed = Some(pane.downgrade().into_any());
+    //         } else {
+    //             self.zoomed = None;
+    //         }
+    //         self.zoomed_position = None;
+    //         self.update_active_view_for_followers(cx);
+
+    //         cx.notify();
+    //     }
+
+    //     fn handle_pane_event(
+    //         &mut self,
+    //         pane: View<Pane>,
+    //         event: &pane::Event,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         match event {
+    //             pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
+    //             pane::Event::Split(direction) => {
+    //                 self.split_and_clone(pane, *direction, cx);
+    //             }
+    //             pane::Event::Remove => self.remove_pane(pane, cx),
+    //             pane::Event::ActivateItem { local } => {
+    //                 if *local {
+    //                     self.unfollow(&pane, cx);
+    //                 }
+    //                 if &pane == self.active_pane() {
+    //                     self.active_item_path_changed(cx);
+    //                 }
+    //             }
+    //             pane::Event::ChangeItemTitle => {
+    //                 if pane == self.active_pane {
+    //                     self.active_item_path_changed(cx);
+    //                 }
+    //                 self.update_window_edited(cx);
+    //             }
+    //             pane::Event::RemoveItem { item_id } => {
+    //                 self.update_window_edited(cx);
+    //                 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
+    //                     if entry.get().id() == pane.id() {
+    //                         entry.remove();
+    //                     }
+    //                 }
+    //             }
+    //             pane::Event::Focus => {
+    //                 self.handle_pane_focused(pane.clone(), cx);
+    //             }
+    //             pane::Event::ZoomIn => {
+    //                 if pane == self.active_pane {
+    //                     pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
+    //                     if pane.read(cx).has_focus() {
+    //                         self.zoomed = Some(pane.downgrade().into_any());
+    //                         self.zoomed_position = None;
+    //                     }
+    //                     cx.notify();
+    //                 }
+    //             }
+    //             pane::Event::ZoomOut => {
+    //                 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
+    //                 if self.zoomed_position.is_none() {
+    //                     self.zoomed = None;
+    //                 }
+    //                 cx.notify();
+    //             }
+    //         }
+
+    //         self.serialize_workspace(cx);
+    //     }
+
+    //     pub fn split_pane(
+    //         &mut self,
+    //         pane_to_split: View<Pane>,
+    //         split_direction: SplitDirection,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> View<Pane> {
+    //         let new_pane = self.add_pane(cx);
+    //         self.center
+    //             .split(&pane_to_split, &new_pane, split_direction)
+    //             .unwrap();
+    //         cx.notify();
+    //         new_pane
+    //     }
+
+    //     pub fn split_and_clone(
+    //         &mut self,
+    //         pane: View<Pane>,
+    //         direction: SplitDirection,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<View<Pane>> {
+    //         let item = pane.read(cx).active_item()?;
+    //         let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
+    //             let new_pane = self.add_pane(cx);
+    //             new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
+    //             self.center.split(&pane, &new_pane, direction).unwrap();
+    //             Some(new_pane)
+    //         } else {
+    //             None
+    //         };
+    //         cx.notify();
+    //         maybe_pane_handle
+    //     }
+
+    //     pub fn split_pane_with_item(
+    //         &mut self,
+    //         pane_to_split: WeakViewHandle<Pane>,
+    //         split_direction: SplitDirection,
+    //         from: WeakViewHandle<Pane>,
+    //         item_id_to_move: usize,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         let Some(pane_to_split) = pane_to_split.upgrade(cx) else {
+    //             return;
+    //         };
+    //         let Some(from) = from.upgrade(cx) else {
+    //             return;
+    //         };
+
+    //         let new_pane = self.add_pane(cx);
+    //         self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
+    //         self.center
+    //             .split(&pane_to_split, &new_pane, split_direction)
+    //             .unwrap();
+    //         cx.notify();
+    //     }
+
+    //     pub fn split_pane_with_project_entry(
+    //         &mut self,
+    //         pane_to_split: WeakViewHandle<Pane>,
+    //         split_direction: SplitDirection,
+    //         project_entry: ProjectEntryId,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         let pane_to_split = pane_to_split.upgrade(cx)?;
+    //         let new_pane = self.add_pane(cx);
+    //         self.center
+    //             .split(&pane_to_split, &new_pane, split_direction)
+    //             .unwrap();
+
+    //         let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
+    //         let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
+    //         Some(cx.foreground().spawn(async move {
+    //             task.await?;
+    //             Ok(())
+    //         }))
+    //     }
+
+    //     pub fn move_item(
+    //         &mut self,
+    //         source: View<Pane>,
+    //         destination: View<Pane>,
+    //         item_id_to_move: usize,
+    //         destination_index: usize,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         let item_to_move = source
+    //             .read(cx)
+    //             .items()
+    //             .enumerate()
+    //             .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
+
+    //         if item_to_move.is_none() {
+    //             log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
+    //             return;
+    //         }
+    //         let (item_ix, item_handle) = item_to_move.unwrap();
+    //         let item_handle = item_handle.clone();
+
+    //         if source != destination {
+    //             // Close item from previous pane
+    //             source.update(cx, |source, cx| {
+    //                 source.remove_item(item_ix, false, cx);
+    //             });
+    //         }
+
+    //         // This automatically removes duplicate items in the pane
+    //         destination.update(cx, |destination, cx| {
+    //             destination.add_item(item_handle, true, true, Some(destination_index), cx);
+    //             cx.focus_self();
+    //         });
+    //     }
+
+    //     fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
+    //         if self.center.remove(&pane).unwrap() {
+    //             self.force_remove_pane(&pane, cx);
+    //             self.unfollow(&pane, cx);
+    //             self.last_leaders_by_pane.remove(&pane.downgrade());
+    //             for removed_item in pane.read(cx).items() {
+    //                 self.panes_by_item.remove(&removed_item.id());
+    //             }
+
+    //             cx.notify();
+    //         } else {
+    //             self.active_item_path_changed(cx);
+    //         }
+    //     }
+
+    //     pub fn panes(&self) -> &[View<Pane>] {
+    //         &self.panes
+    //     }
+
+    //     pub fn active_pane(&self) -> &View<Pane> {
+    //         &self.active_pane
+    //     }
+
+    //     fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
+    //         self.follower_states.retain(|_, state| {
+    //             if state.leader_id == peer_id {
+    //                 for item in state.items_by_leader_view_id.values() {
+    //                     item.set_leader_peer_id(None, cx);
+    //                 }
+    //                 false
+    //             } else {
+    //                 true
+    //             }
+    //         });
+    //         cx.notify();
+    //     }
+
+    //     fn start_following(
+    //         &mut self,
+    //         leader_id: PeerId,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         let pane = self.active_pane().clone();
+
+    //         self.last_leaders_by_pane
+    //             .insert(pane.downgrade(), leader_id);
+    //         self.unfollow(&pane, cx);
+    //         self.follower_states.insert(
+    //             pane.clone(),
+    //             FollowerState {
+    //                 leader_id,
+    //                 active_view_id: None,
+    //                 items_by_leader_view_id: Default::default(),
+    //             },
+    //         );
+    //         cx.notify();
+
+    //         let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+    //         let project_id = self.project.read(cx).remote_id();
+    //         let request = self.app_state.client.request(proto::Follow {
+    //             room_id,
+    //             project_id,
+    //             leader_id: Some(leader_id),
+    //         });
+
+    //         Some(cx.spawn(|this, mut cx| async move {
+    //             let response = request.await?;
+    //             this.update(&mut cx, |this, _| {
+    //                 let state = this
+    //                     .follower_states
+    //                     .get_mut(&pane)
+    //                     .ok_or_else(|| anyhow!("following interrupted"))?;
+    //                 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
+    //                     Some(ViewId::from_proto(active_view_id)?)
+    //                 } else {
+    //                     None
+    //                 };
+    //                 Ok::<_, anyhow::Error>(())
+    //             })??;
+    //             Self::add_views_from_leader(
+    //                 this.clone(),
+    //                 leader_id,
+    //                 vec![pane],
+    //                 response.views,
+    //                 &mut cx,
+    //             )
+    //             .await?;
+    //             this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
+    //             Ok(())
+    //         }))
+    //     }
+
+    //     pub fn follow_next_collaborator(
+    //         &mut self,
+    //         _: &FollowNextCollaborator,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         let collaborators = self.project.read(cx).collaborators();
+    //         let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
+    //             let mut collaborators = collaborators.keys().copied();
+    //             for peer_id in collaborators.by_ref() {
+    //                 if peer_id == leader_id {
+    //                     break;
+    //                 }
+    //             }
+    //             collaborators.next()
+    //         } else if let Some(last_leader_id) =
+    //             self.last_leaders_by_pane.get(&self.active_pane.downgrade())
+    //         {
+    //             if collaborators.contains_key(last_leader_id) {
+    //                 Some(*last_leader_id)
+    //             } else {
+    //                 None
+    //             }
+    //         } else {
+    //             None
+    //         };
+
+    //         let pane = self.active_pane.clone();
+    //         let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
+    //         else {
+    //             return None;
+    //         };
+    //         if Some(leader_id) == self.unfollow(&pane, cx) {
+    //             return None;
+    //         }
+    //         self.follow(leader_id, cx)
+    //     }
+
+    //     pub fn follow(
+    //         &mut self,
+    //         leader_id: PeerId,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<Task<Result<()>>> {
+    //         let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
+    //         let project = self.project.read(cx);
+
+    //         let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
+    //             return None;
+    //         };
+
+    //         let other_project_id = match remote_participant.location {
+    //             call::ParticipantLocation::External => None,
+    //             call::ParticipantLocation::UnsharedProject => None,
+    //             call::ParticipantLocation::SharedProject { project_id } => {
+    //                 if Some(project_id) == project.remote_id() {
+    //                     None
+    //                 } else {
+    //                     Some(project_id)
+    //                 }
+    //             }
+    //         };
+
+    //         // if they are active in another project, follow there.
+    //         if let Some(project_id) = other_project_id {
+    //             let app_state = self.app_state.clone();
+    //             return Some(crate::join_remote_project(
+    //                 project_id,
+    //                 remote_participant.user.id,
+    //                 app_state,
+    //                 cx,
+    //             ));
+    //         }
+
+    //         // if you're already following, find the right pane and focus it.
+    //         for (pane, state) in &self.follower_states {
+    //             if leader_id == state.leader_id {
+    //                 cx.focus(pane);
+    //                 return None;
+    //             }
+    //         }
+
+    //         // Otherwise, follow.
+    //         self.start_following(leader_id, cx)
+    //     }
+
+    //     pub fn unfollow(
+    //         &mut self,
+    //         pane: &View<Pane>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<PeerId> {
+    //         let state = self.follower_states.remove(pane)?;
+    //         let leader_id = state.leader_id;
+    //         for (_, item) in state.items_by_leader_view_id {
+    //             item.set_leader_peer_id(None, cx);
+    //         }
+
+    //         if self
+    //             .follower_states
+    //             .values()
+    //             .all(|state| state.leader_id != state.leader_id)
+    //         {
+    //             let project_id = self.project.read(cx).remote_id();
+    //             let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+    //             self.app_state
+    //                 .client
+    //                 .send(proto::Unfollow {
+    //                     room_id,
+    //                     project_id,
+    //                     leader_id: Some(leader_id),
+    //                 })
+    //                 .log_err();
+    //         }
+
+    //         cx.notify();
+    //         Some(leader_id)
+    //     }
+
+    //     pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
+    //         self.follower_states
+    //             .values()
+    //             .any(|state| state.leader_id == peer_id)
+    //     }
+
+    //     fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+    //         // TODO: There should be a better system in place for this
+    //         // (https://github.com/zed-industries/zed/issues/1290)
+    //         let is_fullscreen = cx.window_is_fullscreen();
+    //         let container_theme = if is_fullscreen {
+    //             let mut container_theme = theme.titlebar.container;
+    //             container_theme.padding.left = container_theme.padding.right;
+    //             container_theme
+    //         } else {
+    //             theme.titlebar.container
+    //         };
+
+    //         enum TitleBar {}
+    //         MouseEventHandler::new::<TitleBar, _>(0, cx, |_, cx| {
+    //             Stack::new()
+    //                 .with_children(
+    //                     self.titlebar_item
+    //                         .as_ref()
+    //                         .map(|item| ChildView::new(item, cx)),
+    //                 )
+    //                 .contained()
+    //                 .with_style(container_theme)
+    //         })
+    //         .on_click(MouseButton::Left, |event, _, cx| {
+    //             if event.click_count == 2 {
+    //                 cx.zoom_window();
+    //             }
+    //         })
+    //         .constrained()
+    //         .with_height(theme.titlebar.height)
+    //         .into_any_named("titlebar")
+    //     }
+
+    //     fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
+    //         let active_entry = self.active_project_path(cx);
+    //         self.project
+    //             .update(cx, |project, cx| project.set_active_path(active_entry, cx));
+    //         self.update_window_title(cx);
+    //     }
+
+    //     fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
+    //         let project = self.project().read(cx);
+    //         let mut title = String::new();
+
+    //         if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
+    //             let filename = path
+    //                 .path
+    //                 .file_name()
+    //                 .map(|s| s.to_string_lossy())
+    //                 .or_else(|| {
+    //                     Some(Cow::Borrowed(
+    //                         project
+    //                             .worktree_for_id(path.worktree_id, cx)?
+    //                             .read(cx)
+    //                             .root_name(),
+    //                     ))
+    //                 });
+
+    //             if let Some(filename) = filename {
+    //                 title.push_str(filename.as_ref());
+    //                 title.push_str(" — ");
+    //             }
+    //         }
+
+    //         for (i, name) in project.worktree_root_names(cx).enumerate() {
+    //             if i > 0 {
+    //                 title.push_str(", ");
+    //             }
+    //             title.push_str(name);
+    //         }
+
+    //         if title.is_empty() {
+    //             title = "empty project".to_string();
+    //         }
+
+    //         if project.is_remote() {
+    //             title.push_str(" ↙");
+    //         } else if project.is_shared() {
+    //             title.push_str(" ↗");
+    //         }
+
+    //         cx.set_window_title(&title);
+    //     }
+
+    //     fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
+    //         let is_edited = !self.project.read(cx).is_read_only()
+    //             && self
+    //                 .items(cx)
+    //                 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
+    //         if is_edited != self.window_edited {
+    //             self.window_edited = is_edited;
+    //             cx.set_window_edited(self.window_edited)
+    //         }
+    //     }
+
+    //     fn render_disconnected_overlay(
+    //         &self,
+    //         cx: &mut ViewContext<Workspace>,
+    //     ) -> Option<AnyElement<Workspace>> {
+    //         if self.project.read(cx).is_read_only() {
+    //             enum DisconnectedOverlay {}
+    //             Some(
+    //                 MouseEventHandler::new::<DisconnectedOverlay, _>(0, cx, |_, cx| {
+    //                     let theme = &theme::current(cx);
+    //                     Label::new(
+    //                         "Your connection to the remote project has been lost.",
+    //                         theme.workspace.disconnected_overlay.text.clone(),
+    //                     )
+    //                     .aligned()
+    //                     .contained()
+    //                     .with_style(theme.workspace.disconnected_overlay.container)
+    //                 })
+    //                 .with_cursor_style(CursorStyle::Arrow)
+    //                 .capture_all()
+    //                 .into_any_named("disconnected overlay"),
+    //             )
+    //         } else {
+    //             None
+    //         }
+    //     }
+
+    //     fn render_notifications(
+    //         &self,
+    //         theme: &theme::Workspace,
+    //         cx: &AppContext,
+    //     ) -> Option<AnyElement<Workspace>> {
+    //         if self.notifications.is_empty() {
+    //             None
+    //         } else {
+    //             Some(
+    //                 Flex::column()
+    //                     .with_children(self.notifications.iter().map(|(_, _, notification)| {
+    //                         ChildView::new(notification.as_any(), cx)
+    //                             .contained()
+    //                             .with_style(theme.notification)
+    //                     }))
+    //                     .constrained()
+    //                     .with_width(theme.notifications.width)
+    //                     .contained()
+    //                     .with_style(theme.notifications.container)
+    //                     .aligned()
+    //                     .bottom()
+    //                     .right()
+    //                     .into_any(),
+    //             )
+    //         }
+    //     }
+
+    //     // RPC handlers
+
+    //     fn handle_follow(
+    //         &mut self,
+    //         follower_project_id: Option<u64>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> proto::FollowResponse {
+    //         let client = &self.app_state.client;
+    //         let project_id = self.project.read(cx).remote_id();
+
+    //         let active_view_id = self.active_item(cx).and_then(|i| {
+    //             Some(
+    //                 i.to_followable_item_handle(cx)?
+    //                     .remote_id(client, cx)?
+    //                     .to_proto(),
+    //             )
+    //         });
+
+    //         cx.notify();
+
+    //         self.last_active_view_id = active_view_id.clone();
+    //         proto::FollowResponse {
+    //             active_view_id,
+    //             views: self
+    //                 .panes()
+    //                 .iter()
+    //                 .flat_map(|pane| {
+    //                     let leader_id = self.leader_for_pane(pane);
+    //                     pane.read(cx).items().filter_map({
+    //                         let cx = &cx;
+    //                         move |item| {
+    //                             let item = item.to_followable_item_handle(cx)?;
+    //                             if (project_id.is_none() || project_id != follower_project_id)
+    //                                 && item.is_project_item(cx)
+    //                             {
+    //                                 return None;
+    //                             }
+    //                             let id = item.remote_id(client, cx)?.to_proto();
+    //                             let variant = item.to_state_proto(cx)?;
+    //                             Some(proto::View {
+    //                                 id: Some(id),
+    //                                 leader_id,
+    //                                 variant: Some(variant),
+    //                             })
+    //                         }
+    //                     })
+    //                 })
+    //                 .collect(),
+    //         }
+    //     }
+
+    //     fn handle_update_followers(
+    //         &mut self,
+    //         leader_id: PeerId,
+    //         message: proto::UpdateFollowers,
+    //         _cx: &mut ViewContext<Self>,
+    //     ) {
+    //         self.leader_updates_tx
+    //             .unbounded_send((leader_id, message))
+    //             .ok();
+    //     }
+
+    //     async fn process_leader_update(
+    //         this: &WeakViewHandle<Self>,
+    //         leader_id: PeerId,
+    //         update: proto::UpdateFollowers,
+    //         cx: &mut AsyncAppContext,
+    //     ) -> Result<()> {
+    //         match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
+    //             proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
+    //                 this.update(cx, |this, _| {
+    //                     for (_, state) in &mut this.follower_states {
+    //                         if state.leader_id == leader_id {
+    //                             state.active_view_id =
+    //                                 if let Some(active_view_id) = update_active_view.id.clone() {
+    //                                     Some(ViewId::from_proto(active_view_id)?)
+    //                                 } else {
+    //                                     None
+    //                                 };
+    //                         }
+    //                     }
+    //                     anyhow::Ok(())
+    //                 })??;
+    //             }
+    //             proto::update_followers::Variant::UpdateView(update_view) => {
+    //                 let variant = update_view
+    //                     .variant
+    //                     .ok_or_else(|| anyhow!("missing update view variant"))?;
+    //                 let id = update_view
+    //                     .id
+    //                     .ok_or_else(|| anyhow!("missing update view id"))?;
+    //                 let mut tasks = Vec::new();
+    //                 this.update(cx, |this, cx| {
+    //                     let project = this.project.clone();
+    //                     for (_, state) in &mut this.follower_states {
+    //                         if state.leader_id == leader_id {
+    //                             let view_id = ViewId::from_proto(id.clone())?;
+    //                             if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
+    //                                 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
+    //                             }
+    //                         }
+    //                     }
+    //                     anyhow::Ok(())
+    //                 })??;
+    //                 try_join_all(tasks).await.log_err();
+    //             }
+    //             proto::update_followers::Variant::CreateView(view) => {
+    //                 let panes = this.read_with(cx, |this, _| {
+    //                     this.follower_states
+    //                         .iter()
+    //                         .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
+    //                         .cloned()
+    //                         .collect()
+    //                 })?;
+    //                 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
+    //             }
+    //         }
+    //         this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
+    //         Ok(())
+    //     }
+
+    //     async fn add_views_from_leader(
+    //         this: WeakViewHandle<Self>,
+    //         leader_id: PeerId,
+    //         panes: Vec<View<Pane>>,
+    //         views: Vec<proto::View>,
+    //         cx: &mut AsyncAppContext,
+    //     ) -> Result<()> {
+    //         let this = this
+    //             .upgrade(cx)
+    //             .ok_or_else(|| anyhow!("workspace dropped"))?;
+
+    //         let item_builders = cx.update(|cx| {
+    //             cx.default_global::<FollowableItemBuilders>()
+    //                 .values()
+    //                 .map(|b| b.0)
+    //                 .collect::<Vec<_>>()
+    //         });
+
+    //         let mut item_tasks_by_pane = HashMap::default();
+    //         for pane in panes {
+    //             let mut item_tasks = Vec::new();
+    //             let mut leader_view_ids = Vec::new();
+    //             for view in &views {
+    //                 let Some(id) = &view.id else { continue };
+    //                 let id = ViewId::from_proto(id.clone())?;
+    //                 let mut variant = view.variant.clone();
+    //                 if variant.is_none() {
+    //                     Err(anyhow!("missing view variant"))?;
+    //                 }
+    //                 for build_item in &item_builders {
+    //                     let task = cx
+    //                         .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx));
+    //                     if let Some(task) = task {
+    //                         item_tasks.push(task);
+    //                         leader_view_ids.push(id);
+    //                         break;
+    //                     } else {
+    //                         assert!(variant.is_some());
+    //                     }
+    //                 }
+    //             }
+
+    //             item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
+    //         }
+
+    //         for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
+    //             let items = futures::future::try_join_all(item_tasks).await?;
+    //             this.update(cx, |this, cx| {
+    //                 let state = this.follower_states.get_mut(&pane)?;
+    //                 for (id, item) in leader_view_ids.into_iter().zip(items) {
+    //                     item.set_leader_peer_id(Some(leader_id), cx);
+    //                     state.items_by_leader_view_id.insert(id, item);
+    //                 }
+
+    //                 Some(())
+    //             });
+    //         }
+    //         Ok(())
+    //     }
+
+    //     fn update_active_view_for_followers(&mut self, cx: &AppContext) {
+    //         let mut is_project_item = true;
+    //         let mut update = proto::UpdateActiveView::default();
+    //         if self.active_pane.read(cx).has_focus() {
+    //             let item = self
+    //                 .active_item(cx)
+    //                 .and_then(|item| item.to_followable_item_handle(cx));
+    //             if let Some(item) = item {
+    //                 is_project_item = item.is_project_item(cx);
+    //                 update = proto::UpdateActiveView {
+    //                     id: item
+    //                         .remote_id(&self.app_state.client, cx)
+    //                         .map(|id| id.to_proto()),
+    //                     leader_id: self.leader_for_pane(&self.active_pane),
+    //                 };
+    //             }
+    //         }
+
+    //         if update.id != self.last_active_view_id {
+    //             self.last_active_view_id = update.id.clone();
+    //             self.update_followers(
+    //                 is_project_item,
+    //                 proto::update_followers::Variant::UpdateActiveView(update),
+    //                 cx,
+    //             );
+    //         }
+    //     }
+
+    //     fn update_followers(
+    //         &self,
+    //         project_only: bool,
+    //         update: proto::update_followers::Variant,
+    //         cx: &AppContext,
+    //     ) -> Option<()> {
+    //         let project_id = if project_only {
+    //             self.project.read(cx).remote_id()
+    //         } else {
+    //             None
+    //         };
+    //         self.app_state().workspace_store.read_with(cx, |store, cx| {
+    //             store.update_followers(project_id, update, cx)
+    //         })
+    //     }
+
+    //     pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
+    //         self.follower_states.get(pane).map(|state| state.leader_id)
+    //     }
+
+    //     fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+    //         cx.notify();
+
+    //         let call = self.active_call()?;
+    //         let room = call.read(cx).room()?.read(cx);
+    //         let participant = room.remote_participant_for_peer_id(leader_id)?;
+    //         let mut items_to_activate = Vec::new();
+
+    //         let leader_in_this_app;
+    //         let leader_in_this_project;
+    //         match participant.location {
+    //             call::ParticipantLocation::SharedProject { project_id } => {
+    //                 leader_in_this_app = true;
+    //                 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
+    //             }
+    //             call::ParticipantLocation::UnsharedProject => {
+    //                 leader_in_this_app = true;
+    //                 leader_in_this_project = false;
+    //             }
+    //             call::ParticipantLocation::External => {
+    //                 leader_in_this_app = false;
+    //                 leader_in_this_project = false;
+    //             }
+    //         };
+
+    //         for (pane, state) in &self.follower_states {
+    //             if state.leader_id != leader_id {
+    //                 continue;
+    //             }
+    //             if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
+    //                 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
+    //                     if leader_in_this_project || !item.is_project_item(cx) {
+    //                         items_to_activate.push((pane.clone(), item.boxed_clone()));
+    //                     }
+    //                 } else {
+    //                     log::warn!(
+    //                         "unknown view id {:?} for leader {:?}",
+    //                         active_view_id,
+    //                         leader_id
+    //                     );
+    //                 }
+    //                 continue;
+    //             }
+    //             if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
+    //                 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
+    //             }
+    //         }
+
+    //         for (pane, item) in items_to_activate {
+    //             let pane_was_focused = pane.read(cx).has_focus();
+    //             if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
+    //                 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
+    //             } else {
+    //                 pane.update(cx, |pane, cx| {
+    //                     pane.add_item(item.boxed_clone(), false, false, None, cx)
+    //                 });
+    //             }
+
+    //             if pane_was_focused {
+    //                 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
+    //             }
+    //         }
+
+    //         None
+    //     }
+
+    //     fn shared_screen_for_peer(
+    //         &self,
+    //         peer_id: PeerId,
+    //         pane: &View<Pane>,
+    //         cx: &mut ViewContext<Self>,
+    //     ) -> Option<View<SharedScreen>> {
+    //         let call = self.active_call()?;
+    //         let room = call.read(cx).room()?.read(cx);
+    //         let participant = room.remote_participant_for_peer_id(peer_id)?;
+    //         let track = participant.video_tracks.values().next()?.clone();
+    //         let user = participant.user.clone();
+
+    //         for item in pane.read(cx).items_of_type::<SharedScreen>() {
+    //             if item.read(cx).peer_id == peer_id {
+    //                 return Some(item);
+    //             }
+    //         }
+
+    //         Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
+    //     }
+
+    //     pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+    //         if active {
+    //             self.update_active_view_for_followers(cx);
+    //             cx.background()
+    //                 .spawn(persistence::DB.update_timestamp(self.database_id()))
+    //                 .detach();
+    //         } else {
+    //             for pane in &self.panes {
+    //                 pane.update(cx, |pane, cx| {
+    //                     if let Some(item) = pane.active_item() {
+    //                         item.workspace_deactivated(cx);
+    //                     }
+    //                     if matches!(
+    //                         settings::get::<WorkspaceSettings>(cx).autosave,
+    //                         AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
+    //                     ) {
+    //                         for item in pane.items() {
+    //                             Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
+    //                                 .detach_and_log_err(cx);
+    //                         }
+    //                     }
+    //                 });
+    //             }
+    //         }
+    //     }
+
+    //     fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
+    //         self.active_call.as_ref().map(|(call, _)| call)
+    //     }
+
+    //     fn on_active_call_event(
+    //         &mut self,
+    //         _: ModelHandle<ActiveCall>,
+    //         event: &call::room::Event,
+    //         cx: &mut ViewContext<Self>,
+    //     ) {
+    //         match event {
+    //             call::room::Event::ParticipantLocationChanged { participant_id }
+    //             | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
+    //                 self.leader_updated(*participant_id, cx);
+    //             }
+    //             _ => {}
+    //         }
+    //     }
+
+    //     pub fn database_id(&self) -> WorkspaceId {
+    //         self.database_id
+    //     }
+
+    //     fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
+    //         let project = self.project().read(cx);
+
+    //         if project.is_local() {
+    //             Some(
+    //                 project
+    //                     .visible_worktrees(cx)
+    //                     .map(|worktree| worktree.read(cx).abs_path())
+    //                     .collect::<Vec<_>>()
+    //                     .into(),
+    //             )
+    //         } else {
+    //             None
+    //         }
+    //     }
+
+    //     fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
+    //         match member {
+    //             Member::Axis(PaneAxis { members, .. }) => {
+    //                 for child in members.iter() {
+    //                     self.remove_panes(child.clone(), cx)
+    //                 }
+    //             }
+    //             Member::Pane(pane) => {
+    //                 self.force_remove_pane(&pane, cx);
+    //             }
+    //         }
+    //     }
+
+    //     fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
+    //         self.panes.retain(|p| p != pane);
+    //         cx.focus(self.panes.last().unwrap());
+    //         if self.last_active_center_pane == Some(pane.downgrade()) {
+    //             self.last_active_center_pane = None;
+    //         }
+    //         cx.notify();
+    //     }
+
+    //     fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
+    //         self._schedule_serialize = Some(cx.spawn(|this, cx| async move {
+    //             cx.background().timer(Duration::from_millis(100)).await;
+    //             this.read_with(&cx, |this, cx| this.serialize_workspace(cx))
+    //                 .ok();
+    //         }));
+    //     }
+
+    //     fn serialize_workspace(&self, cx: &ViewContext<Self>) {
+    //         fn serialize_pane_handle(
+    //             pane_handle: &View<Pane>,
+    //             cx: &AppContext,
+    //         ) -> SerializedPane {
+    //             let (items, active) = {
+    //                 let pane = pane_handle.read(cx);
+    //                 let active_item_id = pane.active_item().map(|item| item.id());
+    //                 (
+    //                     pane.items()
+    //                         .filter_map(|item_handle| {
+    //                             Some(SerializedItem {
+    //                                 kind: Arc::from(item_handle.serialized_item_kind()?),
+    //                                 item_id: item_handle.id(),
+    //                                 active: Some(item_handle.id()) == active_item_id,
+    //                             })
+    //                         })
+    //                         .collect::<Vec<_>>(),
+    //                     pane.has_focus(),
+    //                 )
+    //             };
+
+    //             SerializedPane::new(items, active)
+    //         }
+
+    //         fn build_serialized_pane_group(
+    //             pane_group: &Member,
+    //             cx: &AppContext,
+    //         ) -> SerializedPaneGroup {
+    //             match pane_group {
+    //                 Member::Axis(PaneAxis {
+    //                     axis,
+    //                     members,
+    //                     flexes,
+    //                     bounding_boxes: _,
+    //                 }) => SerializedPaneGroup::Group {
+    //                     axis: *axis,
+    //                     children: members
+    //                         .iter()
+    //                         .map(|member| build_serialized_pane_group(member, cx))
+    //                         .collect::<Vec<_>>(),
+    //                     flexes: Some(flexes.borrow().clone()),
+    //                 },
+    //                 Member::Pane(pane_handle) => {
+    //                     SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
+    //                 }
+    //             }
+    //         }
+
+    //         fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
+    //             let left_dock = this.left_dock.read(cx);
+    //             let left_visible = left_dock.is_open();
+    //             let left_active_panel = left_dock.visible_panel().and_then(|panel| {
+    //                 Some(
+    //                     cx.view_ui_name(panel.as_any().window(), panel.id())?
+    //                         .to_string(),
+    //                 )
+    //             });
+    //             let left_dock_zoom = left_dock
+    //                 .visible_panel()
+    //                 .map(|panel| panel.is_zoomed(cx))
+    //                 .unwrap_or(false);
+
+    //             let right_dock = this.right_dock.read(cx);
+    //             let right_visible = right_dock.is_open();
+    //             let right_active_panel = right_dock.visible_panel().and_then(|panel| {
+    //                 Some(
+    //                     cx.view_ui_name(panel.as_any().window(), panel.id())?
+    //                         .to_string(),
+    //                 )
+    //             });
+    //             let right_dock_zoom = right_dock
+    //                 .visible_panel()
+    //                 .map(|panel| panel.is_zoomed(cx))
+    //                 .unwrap_or(false);
+
+    //             let bottom_dock = this.bottom_dock.read(cx);
+    //             let bottom_visible = bottom_dock.is_open();
+    //             let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
+    //                 Some(
+    //                     cx.view_ui_name(panel.as_any().window(), panel.id())?
+    //                         .to_string(),
+    //                 )
+    //             });
+    //             let bottom_dock_zoom = bottom_dock
+    //                 .visible_panel()
+    //                 .map(|panel| panel.is_zoomed(cx))
+    //                 .unwrap_or(false);
+
+    //             DockStructure {
+    //                 left: DockData {
+    //                     visible: left_visible,
+    //                     active_panel: left_active_panel,
+    //                     zoom: left_dock_zoom,
+    //                 },
+    //                 right: DockData {
+    //                     visible: right_visible,
+    //                     active_panel: right_active_panel,
+    //                     zoom: right_dock_zoom,
+    //                 },
+    //                 bottom: DockData {
+    //                     visible: bottom_visible,
+    //                     active_panel: bottom_active_panel,
+    //                     zoom: bottom_dock_zoom,
+    //                 },
+    //             }
+    //         }
+
+    //         if let Some(location) = self.location(cx) {
+    //             // Load bearing special case:
+    //             //  - with_local_workspace() relies on this to not have other stuff open
+    //             //    when you open your log
+    //             if !location.paths().is_empty() {
+    //                 let center_group = build_serialized_pane_group(&self.center.root, cx);
+    //                 let docks = build_serialized_docks(self, cx);
+
+    //                 let serialized_workspace = SerializedWorkspace {
+    //                     id: self.database_id,
+    //                     location,
+    //                     center_group,
+    //                     bounds: Default::default(),
+    //                     display: Default::default(),
+    //                     docks,
+    //                 };
+
+    //                 cx.background()
+    //                     .spawn(persistence::DB.save_workspace(serialized_workspace))
+    //                     .detach();
+    //             }
+    //         }
+    //     }
+
+    //     pub(crate) fn load_workspace(
+    //         workspace: WeakViewHandle<Workspace>,
+    //         serialized_workspace: SerializedWorkspace,
+    //         paths_to_open: Vec<Option<ProjectPath>>,
+    //         cx: &mut AppContext,
+    //     ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
+    //         cx.spawn(|mut cx| async move {
+    //             let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
+    //                 (
+    //                     workspace.project().clone(),
+    //                     workspace.last_active_center_pane.clone(),
+    //                 )
+    //             })?;
+
+    //             let mut center_group = None;
+    //             let mut center_items = None;
+    //             // Traverse the splits tree and add to things
+    //             if let Some((group, active_pane, items)) = serialized_workspace
+    //                 .center_group
+    //                 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
+    //                 .await
+    //             {
+    //                 center_items = Some(items);
+    //                 center_group = Some((group, active_pane))
+    //             }
+
+    //             let mut items_by_project_path = cx.read(|cx| {
+    //                 center_items
+    //                     .unwrap_or_default()
+    //                     .into_iter()
+    //                     .filter_map(|item| {
+    //                         let item = item?;
+    //                         let project_path = item.project_path(cx)?;
+    //                         Some((project_path, item))
+    //                     })
+    //                     .collect::<HashMap<_, _>>()
+    //             });
+
+    //             let opened_items = paths_to_open
+    //                 .into_iter()
+    //                 .map(|path_to_open| {
+    //                     path_to_open
+    //                         .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
+    //                 })
+    //                 .collect::<Vec<_>>();
+
+    //             // Remove old panes from workspace panes list
+    //             workspace.update(&mut cx, |workspace, cx| {
+    //                 if let Some((center_group, active_pane)) = center_group {
+    //                     workspace.remove_panes(workspace.center.root.clone(), cx);
+
+    //                     // Swap workspace center group
+    //                     workspace.center = PaneGroup::with_root(center_group);
+
+    //                     // Change the focus to the workspace first so that we retrigger focus in on the pane.
+    //                     cx.focus_self();
+
+    //                     if let Some(active_pane) = active_pane {
+    //                         cx.focus(&active_pane);
+    //                     } else {
+    //                         cx.focus(workspace.panes.last().unwrap());
+    //                     }
+    //                 } else {
+    //                     let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
+    //                     if let Some(old_center_handle) = old_center_handle {
+    //                         cx.focus(&old_center_handle)
+    //                     } else {
+    //                         cx.focus_self()
+    //                     }
+    //                 }
+
+    //                 let docks = serialized_workspace.docks;
+    //                 workspace.left_dock.update(cx, |dock, cx| {
+    //                     dock.set_open(docks.left.visible, cx);
+    //                     if let Some(active_panel) = docks.left.active_panel {
+    //                         if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+    //                             dock.activate_panel(ix, cx);
+    //                         }
+    //                     }
+    //                     dock.active_panel()
+    //                         .map(|panel| panel.set_zoomed(docks.left.zoom, cx));
+    //                     if docks.left.visible && docks.left.zoom {
+    //                         cx.focus_self()
+    //                     }
+    //                 });
+    //                 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
+    //                 workspace.right_dock.update(cx, |dock, cx| {
+    //                     dock.set_open(docks.right.visible, cx);
+    //                     if let Some(active_panel) = docks.right.active_panel {
+    //                         if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+    //                             dock.activate_panel(ix, cx);
+    //                         }
+    //                     }
+    //                     dock.active_panel()
+    //                         .map(|panel| panel.set_zoomed(docks.right.zoom, cx));
+
+    //                     if docks.right.visible && docks.right.zoom {
+    //                         cx.focus_self()
+    //                     }
+    //                 });
+    //                 workspace.bottom_dock.update(cx, |dock, cx| {
+    //                     dock.set_open(docks.bottom.visible, cx);
+    //                     if let Some(active_panel) = docks.bottom.active_panel {
+    //                         if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+    //                             dock.activate_panel(ix, cx);
+    //                         }
+    //                     }
+
+    //                     dock.active_panel()
+    //                         .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
+
+    //                     if docks.bottom.visible && docks.bottom.zoom {
+    //                         cx.focus_self()
+    //                     }
+    //                 });
+
+    //                 cx.notify();
+    //             })?;
+
+    //             // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
+    //             workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
+
+    //             Ok(opened_items)
+    //         })
+    //     }
+
+    //     #[cfg(any(test, feature = "test-support"))]
+    //     pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+    //         use node_runtime::FakeNodeRuntime;
+
+    //         let client = project.read(cx).client();
+    //         let user_store = project.read(cx).user_store();
+
+    //         let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
+    //         let app_state = Arc::new(AppState {
+    //             languages: project.read(cx).languages().clone(),
+    //             workspace_store,
+    //             client,
+    //             user_store,
+    //             fs: project.read(cx).fs().clone(),
+    //             build_window_options: |_, _, _| Default::default(),
+    //             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
+    //             node_runtime: FakeNodeRuntime::new(),
+    //         });
+    //         Self::new(0, project, app_state, cx)
+    //     }
+
+    //     fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
+    //         let dock = match position {
+    //             DockPosition::Left => &self.left_dock,
+    //             DockPosition::Right => &self.right_dock,
+    //             DockPosition::Bottom => &self.bottom_dock,
+    //         };
+    //         let active_panel = dock.read(cx).visible_panel()?;
+    //         let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
+    //             dock.read(cx).render_placeholder(cx)
+    //         } else {
+    //             ChildView::new(dock, cx).into_any()
+    //         };
+
+    //         Some(
+    //             element
+    //                 .constrained()
+    //                 .dynamically(move |constraint, _, cx| match position {
+    //                     DockPosition::Left | DockPosition::Right => SizeConstraint::new(
+    //                         Vector2F::new(20., constraint.min.y()),
+    //                         Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
+    //                     ),
+    //                     DockPosition::Bottom => SizeConstraint::new(
+    //                         Vector2F::new(constraint.min.x(), 20.),
+    //                         Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
+    //                     ),
+    //                 })
+    //                 .into_any(),
+    //         )
+    //     }
+    // }
+
+    // fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
+    //     ZED_WINDOW_POSITION
+    //         .zip(*ZED_WINDOW_SIZE)
+    //         .map(|(position, size)| {
+    //             WindowBounds::Fixed(RectF::new(
+    //                 cx.platform().screens()[0].bounds().origin() + position,
+    //                 size,
+    //             ))
+    //         })
+    // }
+
+    // async fn open_items(
+    //     serialized_workspace: Option<SerializedWorkspace>,
+    //     workspace: &WeakViewHandle<Workspace>,
+    //     mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
+    //     app_state: Arc<AppState>,
+    //     mut cx: AsyncAppContext,
+    // ) -> Result<Vec<Option<Result<Box<dyn ItemHandle>>>>> {
+    //     let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
+
+    //     if let Some(serialized_workspace) = serialized_workspace {
+    //         let workspace = workspace.clone();
+    //         let restored_items = cx
+    //             .update(|cx| {
+    //                 Workspace::load_workspace(
+    //                     workspace,
+    //                     serialized_workspace,
+    //                     project_paths_to_open
+    //                         .iter()
+    //                         .map(|(_, project_path)| project_path)
+    //                         .cloned()
+    //                         .collect(),
+    //                     cx,
+    //                 )
+    //             })
+    //             .await?;
+
+    //         let restored_project_paths = cx.read(|cx| {
+    //             restored_items
+    //                 .iter()
+    //                 .filter_map(|item| item.as_ref()?.project_path(cx))
+    //                 .collect::<HashSet<_>>()
+    //         });
+
+    //         for restored_item in restored_items {
+    //             opened_items.push(restored_item.map(Ok));
+    //         }
+
+    //         project_paths_to_open
+    //             .iter_mut()
+    //             .for_each(|(_, project_path)| {
+    //                 if let Some(project_path_to_open) = project_path {
+    //                     if restored_project_paths.contains(project_path_to_open) {
+    //                         *project_path = None;
+    //                     }
+    //                 }
+    //             });
+    //     } else {
+    //         for _ in 0..project_paths_to_open.len() {
+    //             opened_items.push(None);
+    //         }
+    //     }
+    //     assert!(opened_items.len() == project_paths_to_open.len());
+
+    //     let tasks =
+    //         project_paths_to_open
+    //             .into_iter()
+    //             .enumerate()
+    //             .map(|(i, (abs_path, project_path))| {
+    //                 let workspace = workspace.clone();
+    //                 cx.spawn(|mut cx| {
+    //                     let fs = app_state.fs.clone();
+    //                     async move {
+    //                         let file_project_path = project_path?;
+    //                         if fs.is_file(&abs_path).await {
+    //                             Some((
+    //                                 i,
+    //                                 workspace
+    //                                     .update(&mut cx, |workspace, cx| {
+    //                                         workspace.open_path(file_project_path, None, true, cx)
+    //                                     })
+    //                                     .log_err()?
+    //                                     .await,
+    //                             ))
+    //                         } else {
+    //                             None
+    //                         }
+    //                     }
+    //                 })
+    //             });
+
+    //     for maybe_opened_path in futures::future::join_all(tasks.into_iter())
+    //         .await
+    //         .into_iter()
+    //     {
+    //         if let Some((i, path_open_result)) = maybe_opened_path {
+    //             opened_items[i] = Some(path_open_result);
+    //         }
+    //     }
+
+    //     Ok(opened_items)
+    // }
+
+    // fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
+    //     const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
+    //     const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
+    //     const MESSAGE_ID: usize = 2;
+
+    //     if workspace
+    //         .read_with(cx, |workspace, cx| {
+    //             workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
+    //         })
+    //         .unwrap_or(false)
+    //     {
+    //         return;
+    //     }
+
+    //     if db::kvp::KEY_VALUE_STORE
+    //         .read_kvp(NEW_DOCK_HINT_KEY)
+    //         .ok()
+    //         .flatten()
+    //         .is_some()
+    //     {
+    //         if !workspace
+    //             .read_with(cx, |workspace, cx| {
+    //                 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
+    //             })
+    //             .unwrap_or(false)
+    //         {
+    //             cx.update(|cx| {
+    //                 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
+    //                     let entry = tracker
+    //                         .entry(TypeId::of::<MessageNotification>())
+    //                         .or_default();
+    //                     if !entry.contains(&MESSAGE_ID) {
+    //                         entry.push(MESSAGE_ID);
+    //                     }
+    //                 });
+    //             });
+    //         }
+
+    //         return;
+    //     }
+
+    //     cx.spawn(|_| async move {
+    //         db::kvp::KEY_VALUE_STORE
+    //             .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
+    //             .await
+    //             .ok();
+    //     })
+    //     .detach();
+
+    //     workspace
+    //         .update(cx, |workspace, cx| {
+    //             workspace.show_notification_once(2, cx, |cx| {
+    //                 cx.add_view(|_| {
+    //                     MessageNotification::new_element(|text, _| {
+    //                         Text::new(
+    //                             "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
+    //                             text,
+    //                         )
+    //                         .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
+    //                             let code_span_background_color = settings::get::<ThemeSettings>(cx)
+    //                                 .theme
+    //                                 .editor
+    //                                 .document_highlight_read_background;
+
+    //                             cx.scene().push_quad(gpui::Quad {
+    //                                 bounds,
+    //                                 background: Some(code_span_background_color),
+    //                                 border: Default::default(),
+    //                                 corner_radii: (2.0).into(),
+    //                             })
+    //                         })
+    //                         .into_any()
+    //                     })
+    //                     .with_click_message("Read more about the new panel system")
+    //                     .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
+    //                 })
+    //             })
+    //         })
+    //         .ok();
+}
+
+// fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
+//     const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
+
+//     workspace
+//         .update(cx, |workspace, cx| {
+//             if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
+//                 workspace.show_notification_once(0, cx, |cx| {
+//                     cx.add_view(|_| {
+//                         MessageNotification::new("Failed to load the database file.")
+//                             .with_click_message("Click to let us know about this error")
+//                             .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
+//                     })
+//                 });
+//             }
+//         })
+//         .log_err();
+// }
+
+// impl Entity for Workspace {
+//     type Event = Event;
+
+//     fn release(&mut self, cx: &mut AppContext) {
+//         self.app_state.workspace_store.update(cx, |store, _| {
+//             store.workspaces.remove(&self.weak_self);
+//         })
+//     }
+// }
+
+// impl View for Workspace {
+//     fn ui_name() -> &'static str {
+//         "Workspace"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let theme = theme::current(cx).clone();
+//         Stack::new()
+//             .with_child(
+//                 Flex::column()
+//                     .with_child(self.render_titlebar(&theme, cx))
+//                     .with_child(
+//                         Stack::new()
+//                             .with_child({
+//                                 let project = self.project.clone();
+//                                 Flex::row()
+//                                     .with_children(self.render_dock(DockPosition::Left, cx))
+//                                     .with_child(
+//                                         Flex::column()
+//                                             .with_child(
+//                                                 FlexItem::new(
+//                                                     self.center.render(
+//                                                         &project,
+//                                                         &theme,
+//                                                         &self.follower_states,
+//                                                         self.active_call(),
+//                                                         self.active_pane(),
+//                                                         self.zoomed
+//                                                             .as_ref()
+//                                                             .and_then(|zoomed| zoomed.upgrade(cx))
+//                                                             .as_ref(),
+//                                                         &self.app_state,
+//                                                         cx,
+//                                                     ),
+//                                                 )
+//                                                 .flex(1., true),
+//                                             )
+//                                             .with_children(
+//                                                 self.render_dock(DockPosition::Bottom, cx),
+//                                             )
+//                                             .flex(1., true),
+//                                     )
+//                                     .with_children(self.render_dock(DockPosition::Right, cx))
+//                             })
+//                             .with_child(Overlay::new(
+//                                 Stack::new()
+//                                     .with_children(self.zoomed.as_ref().and_then(|zoomed| {
+//                                         enum ZoomBackground {}
+//                                         let zoomed = zoomed.upgrade(cx)?;
+
+//                                         let mut foreground_style =
+//                                             theme.workspace.zoomed_pane_foreground;
+//                                         if let Some(zoomed_dock_position) = self.zoomed_position {
+//                                             foreground_style =
+//                                                 theme.workspace.zoomed_panel_foreground;
+//                                             let margin = foreground_style.margin.top;
+//                                             let border = foreground_style.border.top;
+
+//                                             // Only include a margin and border on the opposite side.
+//                                             foreground_style.margin.top = 0.;
+//                                             foreground_style.margin.left = 0.;
+//                                             foreground_style.margin.bottom = 0.;
+//                                             foreground_style.margin.right = 0.;
+//                                             foreground_style.border.top = false;
+//                                             foreground_style.border.left = false;
+//                                             foreground_style.border.bottom = false;
+//                                             foreground_style.border.right = false;
+//                                             match zoomed_dock_position {
+//                                                 DockPosition::Left => {
+//                                                     foreground_style.margin.right = margin;
+//                                                     foreground_style.border.right = border;
+//                                                 }
+//                                                 DockPosition::Right => {
+//                                                     foreground_style.margin.left = margin;
+//                                                     foreground_style.border.left = border;
+//                                                 }
+//                                                 DockPosition::Bottom => {
+//                                                     foreground_style.margin.top = margin;
+//                                                     foreground_style.border.top = border;
+//                                                 }
+//                                             }
+//                                         }
+
+//                                         Some(
+//                                             ChildView::new(&zoomed, cx)
+//                                                 .contained()
+//                                                 .with_style(foreground_style)
+//                                                 .aligned()
+//                                                 .contained()
+//                                                 .with_style(theme.workspace.zoomed_background)
+//                                                 .mouse::<ZoomBackground>(0)
+//                                                 .capture_all()
+//                                                 .on_down(
+//                                                     MouseButton::Left,
+//                                                     |_, this: &mut Self, cx| {
+//                                                         this.zoom_out(cx);
+//                                                     },
+//                                                 ),
+//                                         )
+//                                     }))
+//                                     .with_children(self.modal.as_ref().map(|modal| {
+//                                         // Prevent clicks within the modal from falling
+//                                         // through to the rest of the workspace.
+//                                         enum ModalBackground {}
+//                                         MouseEventHandler::new::<ModalBackground, _>(
+//                                             0,
+//                                             cx,
+//                                             |_, cx| ChildView::new(modal.view.as_any(), cx),
+//                                         )
+//                                         .on_click(MouseButton::Left, |_, _, _| {})
+//                                         .contained()
+//                                         .with_style(theme.workspace.modal)
+//                                         .aligned()
+//                                         .top()
+//                                     }))
+//                                     .with_children(self.render_notifications(&theme.workspace, cx)),
+//                             ))
+//                             .provide_resize_bounds::<WorkspaceBounds>()
+//                             .flex(1.0, true),
+//                     )
+//                     .with_child(ChildView::new(&self.status_bar, cx))
+//                     .contained()
+//                     .with_background_color(theme.workspace.background),
+//             )
+//             .with_children(DragAndDrop::render(cx))
+//             .with_children(self.render_disconnected_overlay(cx))
+//             .into_any_named("workspace")
+//     }
+
+//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         if cx.is_self_focused() {
+//             cx.focus(&self.active_pane);
+//         }
+//     }
+
+//     fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
+//         DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
+//     }
+// }
+
+// impl WorkspaceStore {
+//     pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
+//         Self {
+//             workspaces: Default::default(),
+//             followers: Default::default(),
+//             _subscriptions: vec![
+//                 client.add_request_handler(cx.handle(), Self::handle_follow),
+//                 client.add_message_handler(cx.handle(), Self::handle_unfollow),
+//                 client.add_message_handler(cx.handle(), Self::handle_update_followers),
+//             ],
+//             client,
+//         }
+//     }
+
+//     pub fn update_followers(
+//         &self,
+//         project_id: Option<u64>,
+//         update: proto::update_followers::Variant,
+//         cx: &AppContext,
+//     ) -> Option<()> {
+//         if !cx.has_global::<ModelHandle<ActiveCall>>() {
+//             return None;
+//         }
+
+//         let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
+//         let follower_ids: Vec<_> = self
+//             .followers
+//             .iter()
+//             .filter_map(|follower| {
+//                 if follower.project_id == project_id || project_id.is_none() {
+//                     Some(follower.peer_id.into())
+//                 } else {
+//                     None
+//                 }
+//             })
+//             .collect();
+//         if follower_ids.is_empty() {
+//             return None;
+//         }
+//         self.client
+//             .send(proto::UpdateFollowers {
+//                 room_id,
+//                 project_id,
+//                 follower_ids,
+//                 variant: Some(update),
+//             })
+//             .log_err()
+//     }
+
+//     async fn handle_follow(
+//         this: ModelHandle<Self>,
+//         envelope: TypedEnvelope<proto::Follow>,
+//         _: Arc<Client>,
+//         mut cx: AsyncAppContext,
+//     ) -> Result<proto::FollowResponse> {
+//         this.update(&mut cx, |this, cx| {
+//             let follower = Follower {
+//                 project_id: envelope.payload.project_id,
+//                 peer_id: envelope.original_sender_id()?,
+//             };
+//             let active_project = ActiveCall::global(cx)
+//                 .read(cx)
+//                 .location()
+//                 .map(|project| project.id());
+
+//             let mut response = proto::FollowResponse::default();
+//             for workspace in &this.workspaces {
+//                 let Some(workspace) = workspace.upgrade(cx) else {
+//                     continue;
+//                 };
+
+//                 workspace.update(cx.as_mut(), |workspace, cx| {
+//                     let handler_response = workspace.handle_follow(follower.project_id, cx);
+//                     if response.views.is_empty() {
+//                         response.views = handler_response.views;
+//                     } else {
+//                         response.views.extend_from_slice(&handler_response.views);
+//                     }
+
+//                     if let Some(active_view_id) = handler_response.active_view_id.clone() {
+//                         if response.active_view_id.is_none()
+//                             || Some(workspace.project.id()) == active_project
+//                         {
+//                             response.active_view_id = Some(active_view_id);
+//                         }
+//                     }
+//                 });
+//             }
+
+//             if let Err(ix) = this.followers.binary_search(&follower) {
+//                 this.followers.insert(ix, follower);
+//             }
+
+//             Ok(response)
+//         })
+//     }
+
+//     async fn handle_unfollow(
+//         this: ModelHandle<Self>,
+//         envelope: TypedEnvelope<proto::Unfollow>,
+//         _: Arc<Client>,
+//         mut cx: AsyncAppContext,
+//     ) -> Result<()> {
+//         this.update(&mut cx, |this, _| {
+//             let follower = Follower {
+//                 project_id: envelope.payload.project_id,
+//                 peer_id: envelope.original_sender_id()?,
+//             };
+//             if let Ok(ix) = this.followers.binary_search(&follower) {
+//                 this.followers.remove(ix);
+//             }
+//             Ok(())
+//         })
+//     }
+
+//     async fn handle_update_followers(
+//         this: ModelHandle<Self>,
+//         envelope: TypedEnvelope<proto::UpdateFollowers>,
+//         _: Arc<Client>,
+//         mut cx: AsyncAppContext,
+//     ) -> Result<()> {
+//         let leader_id = envelope.original_sender_id()?;
+//         let update = envelope.payload;
+//         this.update(&mut cx, |this, cx| {
+//             for workspace in &this.workspaces {
+//                 let Some(workspace) = workspace.upgrade(cx) else {
+//                     continue;
+//                 };
+//                 workspace.update(cx.as_mut(), |workspace, cx| {
+//                     let project_id = workspace.project.read(cx).remote_id();
+//                     if update.project_id != project_id && update.project_id.is_some() {
+//                         return;
+//                     }
+//                     workspace.handle_update_followers(leader_id, update.clone(), cx);
+//                 });
+//             }
+//             Ok(())
+//         })
+//     }
+// }
+
+// impl Entity for WorkspaceStore {
+//     type Event = ();
+// }
+
+// impl ViewId {
+//     pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
+//         Ok(Self {
+//             creator: message
+//                 .creator
+//                 .ok_or_else(|| anyhow!("creator is missing"))?,
+//             id: message.id,
+//         })
+//     }
+
+//     pub(crate) fn to_proto(&self) -> proto::ViewId {
+//         proto::ViewId {
+//             creator: Some(self.creator),
+//             id: self.id,
+//         }
+//     }
+// }
+
+// pub trait WorkspaceHandle {
+//     fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
+// }
+
+// impl WorkspaceHandle for View<Workspace> {
+//     fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
+//         self.read(cx)
+//             .worktrees(cx)
+//             .flat_map(|worktree| {
+//                 let worktree_id = worktree.read(cx).id();
+//                 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
+//                     worktree_id,
+//                     path: f.path.clone(),
+//                 })
+//             })
+//             .collect::<Vec<_>>()
+//     }
+// }
+
+// impl std::fmt::Debug for OpenPaths {
+//     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+//         f.debug_struct("OpenPaths")
+//             .field("paths", &self.paths)
+//             .finish()
+//     }
+// }
+
+// pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
+
+pub async fn activate_workspace_for_project(
+    cx: &mut AsyncAppContext,
+    predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
+) -> Option<WindowHandle<Workspace>> {
+    cx.run_on_main(move |cx| {
+        for window in cx.windows() {
+            let Some(workspace) = window.downcast::<Workspace>() else {
+                continue;
+            };
+
+            let predicate = cx
+                .update_window_root(&workspace, |workspace, cx| {
+                    let project = workspace.project.read(cx);
+                    if predicate(project, cx) {
+                        cx.activate_window();
+                        true
+                    } else {
+                        false
+                    }
+                })
+                .log_err()
+                .unwrap_or(false);
+
+            if predicate {
+                return Some(workspace);
+            }
+        }
+
+        None
+    })
+    .ok()?
+    .await
+}
+
+// pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
+//     DB.last_workspace().await.log_err().flatten()
+// }
+
+// async fn join_channel_internal(
+//     channel_id: u64,
+//     app_state: &Arc<AppState>,
+//     requesting_window: Option<WindowHandle<Workspace>>,
+//     active_call: &ModelHandle<ActiveCall>,
+//     cx: &mut AsyncAppContext,
+// ) -> Result<bool> {
+//     let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| {
+//         let Some(room) = active_call.room().map(|room| room.read(cx)) else {
+//             return (false, None);
+//         };
+
+//         let already_in_channel = room.channel_id() == Some(channel_id);
+//         let should_prompt = room.is_sharing_project()
+//             && room.remote_participants().len() > 0
+//             && !already_in_channel;
+//         let open_room = if already_in_channel {
+//             active_call.room().cloned()
+//         } else {
+//             None
+//         };
+//         (should_prompt, open_room)
+//     });
+
+//     if let Some(room) = open_room {
+//         let task = room.update(cx, |room, cx| {
+//             if let Some((project, host)) = room.most_active_project(cx) {
+//                 return Some(join_remote_project(project, host, app_state.clone(), cx));
+//             }
+
+//             None
+//         });
+//         if let Some(task) = task {
+//             task.await?;
+//         }
+//         return anyhow::Ok(true);
+//     }
+
+//     if should_prompt {
+//         if let Some(workspace) = requesting_window {
+//             if let Some(window) = workspace.update(cx, |cx| cx.window()) {
+//                 let answer = window.prompt(
+//                     PromptLevel::Warning,
+//                     "Leaving this call will unshare your current project.\nDo you want to switch channels?",
+//                     &["Yes, Join Channel", "Cancel"],
+//                     cx,
+//                 );
+
+//                 if let Some(mut answer) = answer {
+//                     if answer.next().await == Some(1) {
+//                         return Ok(false);
+//                     }
+//                 }
+//             } else {
+//                 return Ok(false); // unreachable!() hopefully
+//             }
+//         } else {
+//             return Ok(false); // unreachable!() hopefully
+//         }
+//     }
+
+//     let client = cx.read(|cx| active_call.read(cx).client());
+
+//     let mut client_status = client.status();
+
+//     // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
+//     'outer: loop {
+//         let Some(status) = client_status.recv().await else {
+//             return Err(anyhow!("error connecting"));
+//         };
+
+//         match status {
+//             Status::Connecting
+//             | Status::Authenticating
+//             | Status::Reconnecting
+//             | Status::Reauthenticating => continue,
+//             Status::Connected { .. } => break 'outer,
+//             Status::SignedOut => return Err(anyhow!("not signed in")),
+//             Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
+//             Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
+//                 return Err(anyhow!("zed is offline"))
+//             }
+//         }
+//     }
+
+//     let room = active_call
+//         .update(cx, |active_call, cx| {
+//             active_call.join_channel(channel_id, cx)
+//         })
+//         .await?;
+
+//     room.update(cx, |room, _| room.room_update_completed())
+//         .await;
+
+//     let task = room.update(cx, |room, cx| {
+//         if let Some((project, host)) = room.most_active_project(cx) {
+//             return Some(join_remote_project(project, host, app_state.clone(), cx));
+//         }
+
+//         None
+//     });
+//     if let Some(task) = task {
+//         task.await?;
+//         return anyhow::Ok(true);
+//     }
+//     anyhow::Ok(false)
+// }
+
+// pub fn join_channel(
+//     channel_id: u64,
+//     app_state: Arc<AppState>,
+//     requesting_window: Option<WindowHandle<Workspace>>,
+//     cx: &mut AppContext,
+// ) -> Task<Result<()>> {
+//     let active_call = ActiveCall::global(cx);
+//     cx.spawn(|mut cx| async move {
+//         let result = join_channel_internal(
+//             channel_id,
+//             &app_state,
+//             requesting_window,
+//             &active_call,
+//             &mut cx,
+//         )
+//         .await;
+
+//         // join channel succeeded, and opened a window
+//         if matches!(result, Ok(true)) {
+//             return anyhow::Ok(());
+//         }
+
+//         if requesting_window.is_some() {
+//             return anyhow::Ok(());
+//         }
+
+//         // find an existing workspace to focus and show call controls
+//         let mut active_window = activate_any_workspace_window(&mut cx);
+//         if active_window.is_none() {
+//             // no open workspaces, make one to show the error in (blergh)
+//             cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))
+//                 .await;
+//         }
+
+//         active_window = activate_any_workspace_window(&mut cx);
+//         if active_window.is_none() {
+//             return result.map(|_| ()); // unreachable!() assuming new_local always opens a window
+//         }
+
+//         if let Err(err) = result {
+//             let prompt = active_window.unwrap().prompt(
+//                 PromptLevel::Critical,
+//                 &format!("Failed to join channel: {}", err),
+//                 &["Ok"],
+//                 &mut cx,
+//             );
+//             if let Some(mut prompt) = prompt {
+//                 prompt.next().await;
+//             } else {
+//                 return Err(err);
+//             }
+//         }
+
+//         // return ok, we showed the error to the user.
+//         return anyhow::Ok(());
+//     })
+// }
+
+// pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
+//     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);
+//         }
+//     }
+//     None
+// }
+
+use client2::{
+    proto::{self, PeerId, ViewId},
+    Client, UserStore,
+};
+use collections::{HashMap, HashSet};
+use gpui2::{
+    AnyHandle, AnyView, AppContext, AsyncAppContext, DisplayId, Handle, MainThread, Task, View,
+    ViewContext, WeakHandle, WeakView, WindowBounds, WindowHandle, WindowOptions,
+};
+use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
+use language2::LanguageRegistry;
+use node_runtime::NodeRuntime;
+use project2::{Project, ProjectEntryId, ProjectPath, Worktree};
+use std::{
+    any::TypeId,
+    path::{Path, PathBuf},
+    sync::Arc,
+    time::Duration,
+};
+use util::ResultExt;
+
+#[allow(clippy::type_complexity)]
+pub fn open_paths(
+    abs_paths: &[PathBuf],
+    app_state: &Arc<AppState>,
+    requesting_window: Option<WindowHandle<Workspace>>,
+    cx: &mut AppContext,
+) -> Task<
+    anyhow::Result<(
+        WindowHandle<Workspace>,
+        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
+    )>,
+> {
+    let app_state = app_state.clone();
+    let abs_paths = abs_paths.to_vec();
+    cx.spawn(|mut cx| async move {
+        // Open paths in existing workspace if possible
+        let existing = activate_workspace_for_project(&mut cx, |project, cx| {
+            project.contains_paths(&abs_paths, cx)
+        })
+        .await;
+
+        if let Some(existing) = existing {
+            Ok((
+                existing.clone(),
+                cx.update_window_root(&existing, |workspace, cx| {
+                    workspace.open_paths(abs_paths, true, cx)
+                })?
+                .await,
+            ))
+        } else {
+            todo!()
+            // Ok(cx
+            //     .update(|cx| {
+            //         Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
+            //     })
+            //     .await)
+        }
+    })
+}
+
+// pub fn open_new(
+//     app_state: &Arc<AppState>,
+//     cx: &mut AppContext,
+//     init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
+// ) -> Task<()> {
+//     let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
+//     cx.spawn(|mut cx| async move {
+//         let (workspace, opened_paths) = task.await;
+
+//         workspace
+//             .update(&mut cx, |workspace, cx| {
+//                 if opened_paths.is_empty() {
+//                     init(workspace, cx)
+//                 }
+//             })
+//             .log_err();
+//     })
+// }
+
+// pub fn create_and_open_local_file(
+//     path: &'static Path,
+//     cx: &mut ViewContext<Workspace>,
+//     default_content: impl 'static + Send + FnOnce() -> Rope,
+// ) -> Task<Result<Box<dyn ItemHandle>>> {
+//     cx.spawn(|workspace, mut cx| async move {
+//         let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
+//         if !fs.is_file(path).await {
+//             fs.create_file(path, Default::default()).await?;
+//             fs.save(path, &default_content(), Default::default())
+//                 .await?;
+//         }
+
+//         let mut items = workspace
+//             .update(&mut cx, |workspace, cx| {
+//                 workspace.with_local_workspace(cx, |workspace, cx| {
+//                     workspace.open_paths(vec![path.to_path_buf()], false, cx)
+//                 })
+//             })?
+//             .await?
+//             .await;
+
+//         let item = items.pop().flatten();
+//         item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
+//     })
+// }
+
+// pub fn join_remote_project(
+//     project_id: u64,
+//     follow_user_id: u64,
+//     app_state: Arc<AppState>,
+//     cx: &mut AppContext,
+// ) -> Task<Result<()>> {
+//     cx.spawn(|mut cx| async move {
+//         let windows = cx.windows();
+//         let existing_workspace = windows.into_iter().find_map(|window| {
+//             window.downcast::<Workspace>().and_then(|window| {
+//                 window
+//                     .read_root_with(&cx, |workspace, cx| {
+//                         if workspace.project().read(cx).remote_id() == Some(project_id) {
+//                             Some(cx.handle().downgrade())
+//                         } else {
+//                             None
+//                         }
+//                     })
+//                     .unwrap_or(None)
+//             })
+//         });
+
+//         let workspace = if let Some(existing_workspace) = existing_workspace {
+//             existing_workspace
+//         } else {
+//             let active_call = cx.read(ActiveCall::global);
+//             let room = active_call
+//                 .read_with(&cx, |call, _| call.room().cloned())
+//                 .ok_or_else(|| anyhow!("not in a call"))?;
+//             let project = room
+//                 .update(&mut cx, |room, cx| {
+//                     room.join_project(
+//                         project_id,
+//                         app_state.languages.clone(),
+//                         app_state.fs.clone(),
+//                         cx,
+//                     )
+//                 })
+//                 .await?;
+
+//             let window_bounds_override = window_bounds_env_override(&cx);
+//             let window = cx.add_window(
+//                 (app_state.build_window_options)(
+//                     window_bounds_override,
+//                     None,
+//                     cx.platform().as_ref(),
+//                 ),
+//                 |cx| Workspace::new(0, project, app_state.clone(), cx),
+//             );
+//             let workspace = window.root(&cx).unwrap();
+//             (app_state.initialize_workspace)(
+//                 workspace.downgrade(),
+//                 false,
+//                 app_state.clone(),
+//                 cx.clone(),
+//             )
+//             .await
+//             .log_err();
+
+//             workspace.downgrade()
+//         };
+
+//         workspace.window().activate(&mut cx);
+//         cx.platform().activate(true);
+
+//         workspace.update(&mut cx, |workspace, cx| {
+//             if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+//                 let follow_peer_id = room
+//                     .read(cx)
+//                     .remote_participants()
+//                     .iter()
+//                     .find(|(_, participant)| participant.user.id == follow_user_id)
+//                     .map(|(_, p)| p.peer_id)
+//                     .or_else(|| {
+//                         // If we couldn't follow the given user, follow the host instead.
+//                         let collaborator = workspace
+//                             .project()
+//                             .read(cx)
+//                             .collaborators()
+//                             .values()
+//                             .find(|collaborator| collaborator.replica_id == 0)?;
+//                         Some(collaborator.peer_id)
+//                     });
+
+//                 if let Some(follow_peer_id) = follow_peer_id {
+//                     workspace
+//                         .follow(follow_peer_id, cx)
+//                         .map(|follow| follow.detach_and_log_err(cx));
+//                 }
+//             }
+//         })?;
+
+//         anyhow::Ok(())
+//     })
+// }
+
+// pub fn restart(_: &Restart, cx: &mut AppContext) {
+//     let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
+//     cx.spawn(|mut cx| async move {
+//         let mut workspace_windows = cx
+//             .windows()
+//             .into_iter()
+//             .filter_map(|window| window.downcast::<Workspace>())
+//             .collect::<Vec<_>>();
+
+//         // If multiple windows have unsaved changes, and need a save prompt,
+//         // prompt in the active window before switching to a different window.
+//         workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
+
+//         if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
+//             let answer = window.prompt(
+//                 PromptLevel::Info,
+//                 "Are you sure you want to restart?",
+//                 &["Restart", "Cancel"],
+//                 &mut cx,
+//             );
+
+//             if let Some(mut answer) = answer {
+//                 let answer = answer.next().await;
+//                 if answer != Some(0) {
+//                     return Ok(());
+//                 }
+//             }
+//         }
+
+//         // If the user cancels any save prompt, then keep the app open.
+//         for window in workspace_windows {
+//             if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
+//                 workspace.prepare_to_close(true, cx)
+//             }) {
+//                 if !should_close.await? {
+//                     return Ok(());
+//                 }
+//             }
+//         }
+//         cx.platform().restart();
+//         anyhow::Ok(())
+//     })
+//     .detach_and_log_err(cx);
+// }
+
+// fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
+//     let mut parts = value.split(',');
+//     let width: usize = parts.next()?.parse().ok()?;
+//     let height: usize = parts.next()?.parse().ok()?;
+//     Some(vec2f(width as f32, height as f32))
+// }
+
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use crate::{
+//         dock::test::{TestPanel, TestPanelEvent},
+//         item::test::{TestItem, TestItemEvent, TestProjectItem},
+//     };
+//     use fs::FakeFs;
+//     use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
+//     use project::{Project, ProjectEntryId};
+//     use serde_json::json;
+//     use settings::SettingsStore;
+//     use std::{cell::RefCell, rc::Rc};
+
+//     #[gpui::test]
+//     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
+//         init_test(cx);
+
+//         let fs = FakeFs::new(cx.background());
+//         let project = Project::test(fs, [], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+
+//         // Adding an item with no ambiguity renders the tab without detail.
+//         let item1 = window.add_view(cx, |_| {
+//             let mut item = TestItem::new();
+//             item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
+//             item
+//         });
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.add_item(Box::new(item1.clone()), cx);
+//         });
+//         item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
+
+//         // Adding an item that creates ambiguity increases the level of detail on
+//         // both tabs.
+//         let item2 = window.add_view(cx, |_| {
+//             let mut item = TestItem::new();
+//             item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
+//             item
+//         });
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.add_item(Box::new(item2.clone()), cx);
+//         });
+//         item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
+//         item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
+
+//         // Adding an item that creates ambiguity increases the level of detail only
+//         // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
+//         // we stop at the highest detail available.
+//         let item3 = window.add_view(cx, |_| {
+//             let mut item = TestItem::new();
+//             item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
+//             item
+//         });
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.add_item(Box::new(item3.clone()), cx);
+//         });
+//         item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
+//         item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
+//         item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
+//     }
+
+//     #[gpui::test]
+//     async fn test_tracking_active_path(cx: &mut TestAppContext) {
+//         init_test(cx);
+
+//         let fs = FakeFs::new(cx.background());
+//         fs.insert_tree(
+//             "/root1",
+//             json!({
+//                 "one.txt": "",
+//                 "two.txt": "",
+//             }),
+//         )
+//         .await;
+//         fs.insert_tree(
+//             "/root2",
+//             json!({
+//                 "three.txt": "",
+//             }),
+//         )
+//         .await;
+
+//         let project = Project::test(fs, ["root1".as_ref()], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+//         let worktree_id = project.read_with(cx, |project, cx| {
+//             project.worktrees(cx).next().unwrap().read(cx).id()
+//         });
+
+//         let item1 = window.add_view(cx, |cx| {
+//             TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
+//         });
+//         let item2 = window.add_view(cx, |cx| {
+//             TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
+//         });
+
+//         // Add an item to an empty pane
+//         workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
+//         project.read_with(cx, |project, cx| {
+//             assert_eq!(
+//                 project.active_entry(),
+//                 project
+//                     .entry_for_path(&(worktree_id, "one.txt").into(), cx)
+//                     .map(|e| e.id)
+//             );
+//         });
+//         assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1"));
+
+//         // Add a second item to a non-empty pane
+//         workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
+//         assert_eq!(window.current_title(cx).as_deref(), Some("two.txt — root1"));
+//         project.read_with(cx, |project, cx| {
+//             assert_eq!(
+//                 project.active_entry(),
+//                 project
+//                     .entry_for_path(&(worktree_id, "two.txt").into(), cx)
+//                     .map(|e| e.id)
+//             );
+//         });
+
+//         // Close the active item
+//         pane.update(cx, |pane, cx| {
+//             pane.close_active_item(&Default::default(), cx).unwrap()
+//         })
+//         .await
+//         .unwrap();
+//         assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1"));
+//         project.read_with(cx, |project, cx| {
+//             assert_eq!(
+//                 project.active_entry(),
+//                 project
+//                     .entry_for_path(&(worktree_id, "one.txt").into(), cx)
+//                     .map(|e| e.id)
+//             );
+//         });
+
+//         // Add a project folder
+//         project
+//             .update(cx, |project, cx| {
+//                 project.find_or_create_local_worktree("/root2", true, cx)
+//             })
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             window.current_title(cx).as_deref(),
+//             Some("one.txt — root1, root2")
+//         );
+
+//         // Remove a project folder
+//         project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
+//         assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root2"));
+//     }
+
+//     #[gpui::test]
+//     async fn test_close_window(cx: &mut TestAppContext) {
+//         init_test(cx);
+
+//         let fs = FakeFs::new(cx.background());
+//         fs.insert_tree("/root", json!({ "one": "" })).await;
+
+//         let project = Project::test(fs, ["root".as_ref()], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+//         let workspace = window.root(cx);
+
+//         // When there are no dirty items, there's nothing to do.
+//         let item1 = window.add_view(cx, |_| TestItem::new());
+//         workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
+//         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
+//         assert!(task.await.unwrap());
+
+//         // When there are dirty untitled items, prompt to save each one. If the user
+//         // cancels any prompt, then abort.
+//         let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
+//         let item3 = window.add_view(cx, |cx| {
+//             TestItem::new()
+//                 .with_dirty(true)
+//                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
+//         });
+//         workspace.update(cx, |w, cx| {
+//             w.add_item(Box::new(item2.clone()), cx);
+//             w.add_item(Box::new(item3.clone()), cx);
+//         });
+//         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
+//         cx.foreground().run_until_parked();
+//         window.simulate_prompt_answer(2, cx); // cancel save all
+//         cx.foreground().run_until_parked();
+//         window.simulate_prompt_answer(2, cx); // cancel save all
+//         cx.foreground().run_until_parked();
+//         assert!(!window.has_pending_prompt(cx));
+//         assert!(!task.await.unwrap());
+//     }
+
+//     #[gpui::test]
+//     async fn test_close_pane_items(cx: &mut TestAppContext) {
+//         init_test(cx);
+
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, None, cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         let item1 = window.add_view(cx, |cx| {
+//             TestItem::new()
+//                 .with_dirty(true)
+//                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
+//         });
+//         let item2 = window.add_view(cx, |cx| {
+//             TestItem::new()
+//                 .with_dirty(true)
+//                 .with_conflict(true)
+//                 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
+//         });
+//         let item3 = window.add_view(cx, |cx| {
+//             TestItem::new()
+//                 .with_dirty(true)
+//                 .with_conflict(true)
+//                 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
+//         });
+//         let item4 = window.add_view(cx, |cx| {
+//             TestItem::new()
+//                 .with_dirty(true)
+//                 .with_project_items(&[TestProjectItem::new_untitled(cx)])
+//         });
+//         let pane = workspace.update(cx, |workspace, cx| {
+//             workspace.add_item(Box::new(item1.clone()), cx);
+//             workspace.add_item(Box::new(item2.clone()), cx);
+//             workspace.add_item(Box::new(item3.clone()), cx);
+//             workspace.add_item(Box::new(item4.clone()), cx);
+//             workspace.active_pane().clone()
+//         });
+
+//         let close_items = pane.update(cx, |pane, cx| {
+//             pane.activate_item(1, true, true, cx);
+//             assert_eq!(pane.active_item().unwrap().id(), item2.id());
+//             let item1_id = item1.id();
+//             let item3_id = item3.id();
+//             let item4_id = item4.id();
+//             pane.close_items(cx, SaveIntent::Close, move |id| {
+//                 [item1_id, item3_id, item4_id].contains(&id)
+//             })
+//         });
+//         cx.foreground().run_until_parked();
+
+//         assert!(window.has_pending_prompt(cx));
+//         // Ignore "Save all" prompt
+//         window.simulate_prompt_answer(2, cx);
+//         cx.foreground().run_until_parked();
+//         // There's a prompt to save item 1.
+//         pane.read_with(cx, |pane, _| {
+//             assert_eq!(pane.items_len(), 4);
+//             assert_eq!(pane.active_item().unwrap().id(), item1.id());
+//         });
+//         // Confirm saving item 1.
+//         window.simulate_prompt_answer(0, cx);
+//         cx.foreground().run_until_parked();
+
+//         // Item 1 is saved. There's a prompt to save item 3.
+//         pane.read_with(cx, |pane, cx| {
+//             assert_eq!(item1.read(cx).save_count, 1);
+//             assert_eq!(item1.read(cx).save_as_count, 0);
+//             assert_eq!(item1.read(cx).reload_count, 0);
+//             assert_eq!(pane.items_len(), 3);
+//             assert_eq!(pane.active_item().unwrap().id(), item3.id());
+//         });
+//         assert!(window.has_pending_prompt(cx));
+
+//         // Cancel saving item 3.
+//         window.simulate_prompt_answer(1, cx);
+//         cx.foreground().run_until_parked();
+
+//         // Item 3 is reloaded. There's a prompt to save item 4.
+//         pane.read_with(cx, |pane, cx| {
+//             assert_eq!(item3.read(cx).save_count, 0);
+//             assert_eq!(item3.read(cx).save_as_count, 0);
+//             assert_eq!(item3.read(cx).reload_count, 1);
+//             assert_eq!(pane.items_len(), 2);
+//             assert_eq!(pane.active_item().unwrap().id(), item4.id());
+//         });
+//         assert!(window.has_pending_prompt(cx));
+
+//         // Confirm saving item 4.
+//         window.simulate_prompt_answer(0, cx);
+//         cx.foreground().run_until_parked();
+
+//         // There's a prompt for a path for item 4.
+//         cx.simulate_new_path_selection(|_| Some(Default::default()));
+//         close_items.await.unwrap();
+
+//         // The requested items are closed.
+//         pane.read_with(cx, |pane, cx| {
+//             assert_eq!(item4.read(cx).save_count, 0);
+//             assert_eq!(item4.read(cx).save_as_count, 1);
+//             assert_eq!(item4.read(cx).reload_count, 0);
+//             assert_eq!(pane.items_len(), 1);
+//             assert_eq!(pane.active_item().unwrap().id(), item2.id());
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
+//         init_test(cx);
+
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, [], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         // Create several workspace items with single project entries, and two
+//         // workspace items with multiple project entries.
+//         let single_entry_items = (0..=4)
+//             .map(|project_entry_id| {
+//                 window.add_view(cx, |cx| {
+//                     TestItem::new()
+//                         .with_dirty(true)
+//                         .with_project_items(&[TestProjectItem::new(
+//                             project_entry_id,
+//                             &format!("{project_entry_id}.txt"),
+//                             cx,
+//                         )])
+//                 })
+//             })
+//             .collect::<Vec<_>>();
+//         let item_2_3 = window.add_view(cx, |cx| {
+//             TestItem::new()
+//                 .with_dirty(true)
+//                 .with_singleton(false)
+//                 .with_project_items(&[
+//                     single_entry_items[2].read(cx).project_items[0].clone(),
+//                     single_entry_items[3].read(cx).project_items[0].clone(),
+//                 ])
+//         });
+//         let item_3_4 = window.add_view(cx, |cx| {
+//             TestItem::new()
+//                 .with_dirty(true)
+//                 .with_singleton(false)
+//                 .with_project_items(&[
+//                     single_entry_items[3].read(cx).project_items[0].clone(),
+//                     single_entry_items[4].read(cx).project_items[0].clone(),
+//                 ])
+//         });
+
+//         // Create two panes that contain the following project entries:
+//         //   left pane:
+//         //     multi-entry items:   (2, 3)
+//         //     single-entry items:  0, 1, 2, 3, 4
+//         //   right pane:
+//         //     single-entry items:  1
+//         //     multi-entry items:   (3, 4)
+//         let left_pane = workspace.update(cx, |workspace, cx| {
+//             let left_pane = workspace.active_pane().clone();
+//             workspace.add_item(Box::new(item_2_3.clone()), cx);
+//             for item in single_entry_items {
+//                 workspace.add_item(Box::new(item), cx);
+//             }
+//             left_pane.update(cx, |pane, cx| {
+//                 pane.activate_item(2, true, true, cx);
+//             });
+
+//             workspace
+//                 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
+//                 .unwrap();
+
+//             left_pane
+//         });
+
+//         //Need to cause an effect flush in order to respect new focus
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.add_item(Box::new(item_3_4.clone()), cx);
+//             cx.focus(&left_pane);
+//         });
+
+//         // When closing all of the items in the left pane, we should be prompted twice:
+//         // once for project entry 0, and once for project entry 2. After those two
+//         // prompts, the task should complete.
+
+//         let close = left_pane.update(cx, |pane, cx| {
+//             pane.close_items(cx, SaveIntent::Close, move |_| true)
+//         });
+//         cx.foreground().run_until_parked();
+//         // Discard "Save all" prompt
+//         window.simulate_prompt_answer(2, cx);
+
+//         cx.foreground().run_until_parked();
+//         left_pane.read_with(cx, |pane, cx| {
+//             assert_eq!(
+//                 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
+//                 &[ProjectEntryId::from_proto(0)]
+//             );
+//         });
+//         window.simulate_prompt_answer(0, cx);
+
+//         cx.foreground().run_until_parked();
+//         left_pane.read_with(cx, |pane, cx| {
+//             assert_eq!(
+//                 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
+//                 &[ProjectEntryId::from_proto(2)]
+//             );
+//         });
+//         window.simulate_prompt_answer(0, cx);
+
+//         cx.foreground().run_until_parked();
+//         close.await.unwrap();
+//         left_pane.read_with(cx, |pane, _| {
+//             assert_eq!(pane.items_len(), 0);
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+//         init_test(cx);
+
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, [], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         let item = window.add_view(cx, |cx| {
+//             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
+//         });
+//         let item_id = item.id();
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.add_item(Box::new(item.clone()), cx);
+//         });
+
+//         // Autosave on window change.
+//         item.update(cx, |item, cx| {
+//             cx.update_global(|settings: &mut SettingsStore, cx| {
+//                 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+//                     settings.autosave = Some(AutosaveSetting::OnWindowChange);
+//                 })
+//             });
+//             item.is_dirty = true;
+//         });
+
+//         // Deactivating the window saves the file.
+//         window.simulate_deactivation(cx);
+//         deterministic.run_until_parked();
+//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
+
+//         // Autosave on focus change.
+//         item.update(cx, |item, cx| {
+//             cx.focus_self();
+//             cx.update_global(|settings: &mut SettingsStore, cx| {
+//                 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+//                     settings.autosave = Some(AutosaveSetting::OnFocusChange);
+//                 })
+//             });
+//             item.is_dirty = true;
+//         });
+
+//         // Blurring the item saves the file.
+//         item.update(cx, |_, cx| cx.blur());
+//         deterministic.run_until_parked();
+//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
+
+//         // Deactivating the window still saves the file.
+//         window.simulate_activation(cx);
+//         item.update(cx, |item, cx| {
+//             cx.focus_self();
+//             item.is_dirty = true;
+//         });
+//         window.simulate_deactivation(cx);
+
+//         deterministic.run_until_parked();
+//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
+
+//         // Autosave after delay.
+//         item.update(cx, |item, cx| {
+//             cx.update_global(|settings: &mut SettingsStore, cx| {
+//                 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+//                     settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
+//                 })
+//             });
+//             item.is_dirty = true;
+//             cx.emit(TestItemEvent::Edit);
+//         });
+
+//         // Delay hasn't fully expired, so the file is still dirty and unsaved.
+//         deterministic.advance_clock(Duration::from_millis(250));
+//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
+
+//         // After delay expires, the file is saved.
+//         deterministic.advance_clock(Duration::from_millis(250));
+//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
+
+//         // Autosave on focus change, ensuring closing the tab counts as such.
+//         item.update(cx, |item, cx| {
+//             cx.update_global(|settings: &mut SettingsStore, cx| {
+//                 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+//                     settings.autosave = Some(AutosaveSetting::OnFocusChange);
+//                 })
+//             });
+//             item.is_dirty = true;
+//         });
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
+//         })
+//         .await
+//         .unwrap();
+//         assert!(!window.has_pending_prompt(cx));
+//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
+
+//         // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.add_item(Box::new(item.clone()), cx);
+//         });
+//         item.update(cx, |item, cx| {
+//             item.project_items[0].update(cx, |item, _| {
+//                 item.entry_id = None;
+//             });
+//             item.is_dirty = true;
+//             cx.blur();
+//         });
+//         deterministic.run_until_parked();
+//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
+
+//         // Ensure autosave is prevented for deleted files also when closing the buffer.
+//         let _close_items = pane.update(cx, |pane, cx| {
+//             pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
+//         });
+//         deterministic.run_until_parked();
+//         assert!(window.has_pending_prompt(cx));
+//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
+//     }
+
+//     #[gpui::test]
+//     async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
+//         init_test(cx);
+
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, [], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         let item = window.add_view(cx, |cx| {
+//             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
+//         });
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+//         let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
+//         let toolbar_notify_count = Rc::new(RefCell::new(0));
+
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.add_item(Box::new(item.clone()), cx);
+//             let toolbar_notification_count = toolbar_notify_count.clone();
+//             cx.observe(&toolbar, move |_, _, _| {
+//                 *toolbar_notification_count.borrow_mut() += 1
+//             })
+//             .detach();
+//         });
+
+//         pane.read_with(cx, |pane, _| {
+//             assert!(!pane.can_navigate_backward());
+//             assert!(!pane.can_navigate_forward());
+//         });
+
+//         item.update(cx, |item, cx| {
+//             item.set_state("one".to_string(), cx);
+//         });
+
+//         // Toolbar must be notified to re-render the navigation buttons
+//         assert_eq!(*toolbar_notify_count.borrow(), 1);
+
+//         pane.read_with(cx, |pane, _| {
+//             assert!(pane.can_navigate_backward());
+//             assert!(!pane.can_navigate_forward());
+//         });
+
+//         workspace
+//             .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
+//             .await
+//             .unwrap();
+
+//         assert_eq!(*toolbar_notify_count.borrow(), 3);
+//         pane.read_with(cx, |pane, _| {
+//             assert!(!pane.can_navigate_backward());
+//             assert!(pane.can_navigate_forward());
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, [], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         let panel = workspace.update(cx, |workspace, cx| {
+//             let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
+//             workspace.add_panel(panel.clone(), cx);
+
+//             workspace
+//                 .right_dock()
+//                 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
+
+//             panel
+//         });
+
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+//         pane.update(cx, |pane, cx| {
+//             let item = cx.add_view(|_| TestItem::new());
+//             pane.add_item(Box::new(item), true, true, None, cx);
+//         });
+
+//         // Transfer focus from center to panel
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_panel_focus::<TestPanel>(cx);
+//         });
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert!(workspace.right_dock().read(cx).is_open());
+//             assert!(!panel.is_zoomed(cx));
+//             assert!(panel.has_focus(cx));
+//         });
+
+//         // Transfer focus from panel to center
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_panel_focus::<TestPanel>(cx);
+//         });
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert!(workspace.right_dock().read(cx).is_open());
+//             assert!(!panel.is_zoomed(cx));
+//             assert!(!panel.has_focus(cx));
+//         });
+
+//         // Close the dock
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_dock(DockPosition::Right, cx);
+//         });
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert!(!workspace.right_dock().read(cx).is_open());
+//             assert!(!panel.is_zoomed(cx));
+//             assert!(!panel.has_focus(cx));
+//         });
+
+//         // Open the dock
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_dock(DockPosition::Right, cx);
+//         });
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert!(workspace.right_dock().read(cx).is_open());
+//             assert!(!panel.is_zoomed(cx));
+//             assert!(panel.has_focus(cx));
+//         });
+
+//         // Focus and zoom panel
+//         panel.update(cx, |panel, cx| {
+//             cx.focus_self();
+//             panel.set_zoomed(true, cx)
+//         });
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert!(workspace.right_dock().read(cx).is_open());
+//             assert!(panel.is_zoomed(cx));
+//             assert!(panel.has_focus(cx));
+//         });
+
+//         // Transfer focus to the center closes the dock
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_panel_focus::<TestPanel>(cx);
+//         });
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert!(!workspace.right_dock().read(cx).is_open());
+//             assert!(panel.is_zoomed(cx));
+//             assert!(!panel.has_focus(cx));
+//         });
+
+//         // Transferring focus back to the panel keeps it zoomed
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_panel_focus::<TestPanel>(cx);
+//         });
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert!(workspace.right_dock().read(cx).is_open());
+//             assert!(panel.is_zoomed(cx));
+//             assert!(panel.has_focus(cx));
+//         });
+
+//         // Close the dock while it is zoomed
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_dock(DockPosition::Right, cx)
+//         });
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert!(!workspace.right_dock().read(cx).is_open());
+//             assert!(panel.is_zoomed(cx));
+//             assert!(workspace.zoomed.is_none());
+//             assert!(!panel.has_focus(cx));
+//         });
+
+//         // Opening the dock, when it's zoomed, retains focus
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_dock(DockPosition::Right, cx)
+//         });
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert!(workspace.right_dock().read(cx).is_open());
+//             assert!(panel.is_zoomed(cx));
+//             assert!(workspace.zoomed.is_some());
+//             assert!(panel.has_focus(cx));
+//         });
+
+//         // Unzoom and close the panel, zoom the active pane.
+//         panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_dock(DockPosition::Right, cx)
+//         });
+//         pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
+
+//         // Opening a dock unzooms the pane.
+//         workspace.update(cx, |workspace, cx| {
+//             workspace.toggle_dock(DockPosition::Right, cx)
+//         });
+//         workspace.read_with(cx, |workspace, cx| {
+//             let pane = pane.read(cx);
+//             assert!(!pane.is_zoomed());
+//             assert!(!pane.has_focus());
+//             assert!(workspace.right_dock().read(cx).is_open());
+//             assert!(workspace.zoomed.is_none());
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_panels(cx: &mut gpui::TestAppContext) {
+//         init_test(cx);
+//         let fs = FakeFs::new(cx.background());
+
+//         let project = Project::test(fs, [], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
+//             // Add panel_1 on the left, panel_2 on the right.
+//             let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
+//             workspace.add_panel(panel_1.clone(), cx);
+//             workspace
+//                 .left_dock()
+//                 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
+//             let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
+//             workspace.add_panel(panel_2.clone(), cx);
+//             workspace
+//                 .right_dock()
+//                 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
+
+//             let left_dock = workspace.left_dock();
+//             assert_eq!(
+//                 left_dock.read(cx).visible_panel().unwrap().id(),
+//                 panel_1.id()
+//             );
+//             assert_eq!(
+//                 left_dock.read(cx).active_panel_size(cx).unwrap(),
+//                 panel_1.size(cx)
+//             );
+
+//             left_dock.update(cx, |left_dock, cx| {
+//                 left_dock.resize_active_panel(Some(1337.), cx)
+//             });
+//             assert_eq!(
+//                 workspace
+//                     .right_dock()
+//                     .read(cx)
+//                     .visible_panel()
+//                     .unwrap()
+//                     .id(),
+//                 panel_2.id()
+//             );
+
+//             (panel_1, panel_2)
+//         });
+
+//         // Move panel_1 to the right
+//         panel_1.update(cx, |panel_1, cx| {
+//             panel_1.set_position(DockPosition::Right, cx)
+//         });
+
+//         workspace.update(cx, |workspace, cx| {
+//             // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
+//             // Since it was the only panel on the left, the left dock should now be closed.
+//             assert!(!workspace.left_dock().read(cx).is_open());
+//             assert!(workspace.left_dock().read(cx).visible_panel().is_none());
+//             let right_dock = workspace.right_dock();
+//             assert_eq!(
+//                 right_dock.read(cx).visible_panel().unwrap().id(),
+//                 panel_1.id()
+//             );
+//             assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
+
+//             // Now we move panel_2 to the left
+//             panel_2.set_position(DockPosition::Left, cx);
+//         });
+
+//         workspace.update(cx, |workspace, cx| {
+//             // Since panel_2 was not visible on the right, we don't open the left dock.
+//             assert!(!workspace.left_dock().read(cx).is_open());
+//             // And the right dock is unaffected in it's displaying of panel_1
+//             assert!(workspace.right_dock().read(cx).is_open());
+//             assert_eq!(
+//                 workspace
+//                     .right_dock()
+//                     .read(cx)
+//                     .visible_panel()
+//                     .unwrap()
+//                     .id(),
+//                 panel_1.id()
+//             );
+//         });
+
+//         // Move panel_1 back to the left
+//         panel_1.update(cx, |panel_1, cx| {
+//             panel_1.set_position(DockPosition::Left, cx)
+//         });
+
+//         workspace.update(cx, |workspace, cx| {
+//             // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
+//             let left_dock = workspace.left_dock();
+//             assert!(left_dock.read(cx).is_open());
+//             assert_eq!(
+//                 left_dock.read(cx).visible_panel().unwrap().id(),
+//                 panel_1.id()
+//             );
+//             assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
+//             // And right the dock should be closed as it no longer has any panels.
+//             assert!(!workspace.right_dock().read(cx).is_open());
+
+//             // Now we move panel_1 to the bottom
+//             panel_1.set_position(DockPosition::Bottom, cx);
+//         });
+
+//         workspace.update(cx, |workspace, cx| {
+//             // Since panel_1 was visible on the left, we close the left dock.
+//             assert!(!workspace.left_dock().read(cx).is_open());
+//             // The bottom dock is sized based on the panel's default size,
+//             // since the panel orientation changed from vertical to horizontal.
+//             let bottom_dock = workspace.bottom_dock();
+//             assert_eq!(
+//                 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
+//                 panel_1.size(cx),
+//             );
+//             // Close bottom dock and move panel_1 back to the left.
+//             bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
+//             panel_1.set_position(DockPosition::Left, cx);
+//         });
+
+//         // Emit activated event on panel 1
+//         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
+
+//         // Now the left dock is open and panel_1 is active and focused.
+//         workspace.read_with(cx, |workspace, cx| {
+//             let left_dock = workspace.left_dock();
+//             assert!(left_dock.read(cx).is_open());
+//             assert_eq!(
+//                 left_dock.read(cx).visible_panel().unwrap().id(),
+//                 panel_1.id()
+//             );
+//             assert!(panel_1.is_focused(cx));
+//         });
+
+//         // Emit closed event on panel 2, which is not active
+//         panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
+
+//         // Wo don't close the left dock, because panel_2 wasn't the active panel
+//         workspace.read_with(cx, |workspace, cx| {
+//             let left_dock = workspace.left_dock();
+//             assert!(left_dock.read(cx).is_open());
+//             assert_eq!(
+//                 left_dock.read(cx).visible_panel().unwrap().id(),
+//                 panel_1.id()
+//             );
+//         });
+
+//         // Emitting a ZoomIn event shows the panel as zoomed.
+//         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
+//         workspace.read_with(cx, |workspace, _| {
+//             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+//             assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
+//         });
+
+//         // Move panel to another dock while it is zoomed
+//         panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
+//         workspace.read_with(cx, |workspace, _| {
+//             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+//             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+//         });
+
+//         // If focus is transferred to another view that's not a panel or another pane, we still show
+//         // the panel as zoomed.
+//         let focus_receiver = window.add_view(cx, |_| EmptyView);
+//         focus_receiver.update(cx, |_, cx| cx.focus_self());
+//         workspace.read_with(cx, |workspace, _| {
+//             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+//             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+//         });
+
+//         // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
+//         workspace.update(cx, |_, cx| cx.focus_self());
+//         workspace.read_with(cx, |workspace, _| {
+//             assert_eq!(workspace.zoomed, None);
+//             assert_eq!(workspace.zoomed_position, None);
+//         });
+
+//         // If focus is transferred again to another view that's not a panel or a pane, we won't
+//         // show the panel as zoomed because it wasn't zoomed before.
+//         focus_receiver.update(cx, |_, cx| cx.focus_self());
+//         workspace.read_with(cx, |workspace, _| {
+//             assert_eq!(workspace.zoomed, None);
+//             assert_eq!(workspace.zoomed_position, None);
+//         });
+
+//         // When focus is transferred back to the panel, it is zoomed again.
+//         panel_1.update(cx, |_, cx| cx.focus_self());
+//         workspace.read_with(cx, |workspace, _| {
+//             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+//             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+//         });
+
+//         // Emitting a ZoomOut event unzooms the panel.
+//         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
+//         workspace.read_with(cx, |workspace, _| {
+//             assert_eq!(workspace.zoomed, None);
+//             assert_eq!(workspace.zoomed_position, None);
+//         });
+
+//         // Emit closed event on panel 1, which is active
+//         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
+
+//         // Now the left dock is closed, because panel_1 was the active panel
+//         workspace.read_with(cx, |workspace, cx| {
+//             let right_dock = workspace.right_dock();
+//             assert!(!right_dock.read(cx).is_open());
+//         });
+//     }
+
+//     pub fn init_test(cx: &mut TestAppContext) {
+//         cx.foreground().forbid_parking();
+//         cx.update(|cx| {
+//             cx.set_global(SettingsStore::test(cx));
+//             theme::init((), cx);
+//             language::init(cx);
+//             crate::init_settings(cx);
+//             Project::init_settings(cx);
+//         });
+//     }
+// }

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 🔗

@@ -1,4 +1,4 @@
-use ai::embedding::OpenAIEmbeddings;
+use ai::providers::open_ai::OpenAIEmbeddingProvider;
 use anyhow::{anyhow, Result};
 use client::{self, UserStore};
 use gpui::{AsyncAppContext, ModelHandle, Task};
@@ -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,12 +469,13 @@ fn main() {
             .join("embeddings_db");
 
         let languages = languages.clone();
+
         let fs = fs.clone();
         cx.spawn(|mut cx| async move {
             let semantic_index = SemanticIndex::new(
                 fs.clone(),
                 db_file_path,
-                Arc::new(OpenAIEmbeddings::new(http_client, cx.background())),
+                Arc::new(OpenAIEmbeddingProvider::new(http_client, cx.background())),
                 languages.clone(),
                 cx.clone(),
             )

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,
@@ -321,8 +321,8 @@ impl LspAdapter for NextLspAdapter {
             latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
         let version = release.name.clone();
         let platform = match consts::ARCH {
-            "x86_64" => "darwin_arm64",
-            "aarch64" => "darwin_amd64",
+            "x86_64" => "darwin_amd64",
+            "aarch64" => "darwin_arm64",
             other => bail!("Running on unsupported platform: {other}"),
         };
         let asset_name = format!("next_ls_{}", platform);
@@ -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 🔗

@@ -15,6 +15,7 @@ name = "Zed"
 path = "src/main.rs"
 
 [dependencies]
+ai2 = { path = "../ai2"}
 # audio = { path = "../audio" }
 # activity_indicator = { path = "../activity_indicator" }
 # auto_update = { path = "../auto_update" }
@@ -43,10 +44,10 @@ 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" }
+lsp2 = { path = "../lsp2" }
 language_tools = { path = "../language_tools" }
 node_runtime = { path = "../node_runtime" }
 # assistant = { path = "../assistant" }
@@ -59,7 +60,7 @@ project2 = { path = "../project2" }
 # recent_projects = { path = "../recent_projects" }
 rpc2 = { path = "../rpc2" }
 settings2 = { path = "../settings2" }
-feature_flags = { path = "../feature_flags" }
+feature_flags2 = { path = "../feature_flags2" }
 sum_tree = { path = "../sum_tree" }
 shellexpand = "2.1.0"
 text = { path = "../text" }
@@ -134,6 +135,7 @@ 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"

crates/zed2/src/languages.rs 🔗

@@ -0,0 +1,273 @@
+use anyhow::Context;
+use gpui2::AppContext;
+pub use language2::*;
+use node_runtime::NodeRuntime;
+use rust_embed::RustEmbed;
+use settings2::Settings;
+use std::{borrow::Cow, str, sync::Arc};
+use util::asset_str;
+
+use self::elixir::ElixirSettings;
+
+mod c;
+mod css;
+mod elixir;
+mod go;
+mod html;
+mod json;
+#[cfg(feature = "plugin_runtime")]
+mod language_plugin;
+mod lua;
+mod php;
+mod python;
+mod ruby;
+mod rust;
+mod svelte;
+mod tailwind;
+mod typescript;
+mod vue;
+mod yaml;
+
+// 1. Add tree-sitter-{language} parser to zed crate
+// 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below
+// 3. Add config.toml to the newly created language directory using existing languages as a template
+// 4. Copy highlights from tree sitter repo for the language into a highlights.scm file.
+//      Note: github highlights take the last match while zed takes the first
+// 5. Add indents.scm, outline.scm, and brackets.scm to implement indent on newline, outline/breadcrumbs,
+//    and autoclosing brackets respectively
+// 6. If the language has injections add an injections.scm query file
+
+#[derive(RustEmbed)]
+#[folder = "src/languages"]
+#[exclude = "*.rs"]
+struct LanguageDir;
+
+pub fn init(
+    languages: Arc<LanguageRegistry>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    cx: &mut AppContext,
+) {
+    ElixirSettings::register(cx);
+
+    let language = |name, grammar, adapters| {
+        languages.register(name, load_config(name), grammar, adapters, load_queries)
+    };
+
+    language("bash", tree_sitter_bash::language(), vec![]);
+    language(
+        "c",
+        tree_sitter_c::language(),
+        vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>],
+    );
+    language(
+        "cpp",
+        tree_sitter_cpp::language(),
+        vec![Arc::new(c::CLspAdapter)],
+    );
+    language(
+        "css",
+        tree_sitter_css::language(),
+        vec![
+            Arc::new(css::CssLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+
+    match &ElixirSettings::get(None, cx).lsp {
+        elixir::ElixirLspSetting::ElixirLs => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![
+                Arc::new(elixir::ElixirLspAdapter),
+                Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+            ],
+        ),
+        elixir::ElixirLspSetting::NextLs => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir::NextLspAdapter)],
+        ),
+        elixir::ElixirLspSetting::Local { path, arguments } => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir::LocalLspAdapter {
+                path: path.clone(),
+                arguments: arguments.clone(),
+            })],
+        ),
+    }
+
+    language(
+        "go",
+        tree_sitter_go::language(),
+        vec![Arc::new(go::GoLspAdapter)],
+    );
+    language(
+        "heex",
+        tree_sitter_heex::language(),
+        vec![
+            Arc::new(elixir::ElixirLspAdapter),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "json",
+        tree_sitter_json::language(),
+        vec![Arc::new(json::JsonLspAdapter::new(
+            node_runtime.clone(),
+            languages.clone(),
+        ))],
+    );
+    language("markdown", tree_sitter_markdown::language(), vec![]);
+    language(
+        "python",
+        tree_sitter_python::language(),
+        vec![Arc::new(python::PythonLspAdapter::new(
+            node_runtime.clone(),
+        ))],
+    );
+    language(
+        "rust",
+        tree_sitter_rust::language(),
+        vec![Arc::new(rust::RustLspAdapter)],
+    );
+    language("toml", tree_sitter_toml::language(), vec![]);
+    language(
+        "tsx",
+        tree_sitter_typescript::language_tsx(),
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "typescript",
+        tree_sitter_typescript::language_typescript(),
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "javascript",
+        tree_sitter_typescript::language_tsx(),
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "html",
+        tree_sitter_html::language(),
+        vec![
+            Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())),
+            Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+        ],
+    );
+    language(
+        "ruby",
+        tree_sitter_ruby::language(),
+        vec![Arc::new(ruby::RubyLanguageServer)],
+    );
+    language(
+        "erb",
+        tree_sitter_embedded_template::language(),
+        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![]);
+    language(
+        "lua",
+        tree_sitter_lua::language(),
+        vec![Arc::new(lua::LuaLspAdapter)],
+    );
+    language(
+        "yaml",
+        tree_sitter_yaml::language(),
+        vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))],
+    );
+    language(
+        "svelte",
+        tree_sitter_svelte::language(),
+        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.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"))]
+pub async fn language(
+    name: &str,
+    grammar: tree_sitter::Language,
+    lsp_adapter: Option<Arc<dyn LspAdapter>>,
+) -> Arc<Language> {
+    Arc::new(
+        Language::new(load_config(name), Some(grammar))
+            .with_lsp_adapters(lsp_adapter.into_iter().collect())
+            .await
+            .with_queries(load_queries(name))
+            .unwrap(),
+    )
+}
+
+fn load_config(name: &str) -> LanguageConfig {
+    toml::from_slice(
+        &LanguageDir::get(&format!("{}/config.toml", name))
+            .unwrap()
+            .data,
+    )
+    .with_context(|| format!("failed to load config.toml for language {name:?}"))
+    .unwrap()
+}
+
+fn load_queries(name: &str) -> LanguageQueries {
+    LanguageQueries {
+        highlights: load_query(name, "/highlights"),
+        brackets: load_query(name, "/brackets"),
+        indents: load_query(name, "/indents"),
+        outline: load_query(name, "/outline"),
+        embedding: load_query(name, "/embedding"),
+        injections: load_query(name, "/injections"),
+        overrides: load_query(name, "/overrides"),
+    }
+}
+
+fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
+    let mut result = None;
+    for path in LanguageDir::iter() {
+        if let Some(remainder) = path.strip_prefix(name) {
+            if remainder.starts_with(filename_prefix) {
+                let contents = asset_str::<LanguageDir>(path.as_ref());
+                match &mut result {
+                    None => result = Some(contents),
+                    Some(r) => r.to_mut().push_str(contents.as_ref()),
+                }
+            }
+        }
+    }
+    result
+}

crates/zed2/src/languages/bash/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "Shell Script"
+path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile"]
+line_comment = "# "
+first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
+brackets = [
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed2/src/languages/bash/highlights.scm 🔗

@@ -0,0 +1,59 @@
+[
+  (string)
+  (raw_string)
+  (heredoc_body)
+  (heredoc_start)
+  (ansi_c_string)
+] @string
+
+(command_name) @function
+
+(variable_name) @property
+
+[
+  "case"
+  "do"
+  "done"
+  "elif"
+  "else"
+  "esac"
+  "export"
+  "fi"
+  "for"
+  "function"
+  "if"
+  "in"
+  "select"
+  "then"
+  "unset"
+  "until"
+  "while"
+  "local"
+  "declare"
+] @keyword
+
+(comment) @comment
+
+(function_definition name: (word) @function)
+
+(file_descriptor) @number
+
+[
+  (command_substitution)
+  (process_substitution)
+  (expansion)
+]@embedded
+
+[
+  "$"
+  "&&"
+  ">"
+  ">>"
+  "<"
+  "|"
+] @operator
+
+(
+  (command (_) @constant)
+  (#match? @constant "^-")
+)

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

@@ -0,0 +1,321 @@
+use anyhow::{anyhow, Context, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+pub use language2::*;
+use lsp2::LanguageServerBinary;
+use smol::fs::{self, File};
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
+
+pub struct CLspAdapter;
+
+#[async_trait]
+impl super::LspAdapter for CLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("clangd".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "clangd"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release = latest_github_release("clangd/clangd", false, delegate.http_client()).await?;
+        let asset_name = format!("clangd-mac-{}.zip", release.name);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: release.name,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
+        let version_dir = container_dir.join(format!("clangd_{}", version.name));
+        let binary_path = version_dir.join("bin/clangd");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .context("error downloading release")?;
+            let mut file = File::create(&zip_path).await?;
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            futures::io::copy(response.body_mut(), &mut file).await?;
+
+            let unzip_status = smol::process::Command::new("unzip")
+                .current_dir(&container_dir)
+                .arg(&zip_path)
+                .output()
+                .await?
+                .status;
+            if !unzip_status.success() {
+                Err(anyhow!("failed to unzip clangd archive"))?;
+            }
+
+            remove_matching(&container_dir, |entry| entry != version_dir).await;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec![],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let label = completion
+            .label
+            .strip_prefix('•')
+            .unwrap_or(&completion.label)
+            .trim();
+
+        match completion.kind {
+            Some(lsp2::CompletionItemKind::FIELD) if completion.detail.is_some() => {
+                let detail = completion.detail.as_ref().unwrap();
+                let text = format!("{} {}", detail, label);
+                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
+                let runs = language.highlight_text(&source, 11..11 + text.len());
+                return Some(CodeLabel {
+                    filter_range: detail.len() + 1..text.len(),
+                    text,
+                    runs,
+                });
+            }
+            Some(lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE)
+                if completion.detail.is_some() =>
+            {
+                let detail = completion.detail.as_ref().unwrap();
+                let text = format!("{} {}", detail, label);
+                let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
+                return Some(CodeLabel {
+                    filter_range: detail.len() + 1..text.len(),
+                    text,
+                    runs,
+                });
+            }
+            Some(lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD)
+                if completion.detail.is_some() =>
+            {
+                let detail = completion.detail.as_ref().unwrap();
+                let text = format!("{} {}", detail, label);
+                let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
+                return Some(CodeLabel {
+                    filter_range: detail.len() + 1..text.rfind('(').unwrap_or(text.len()),
+                    text,
+                    runs,
+                });
+            }
+            Some(kind) => {
+                let highlight_name = match kind {
+                    lsp2::CompletionItemKind::STRUCT
+                    | lsp2::CompletionItemKind::INTERFACE
+                    | lsp2::CompletionItemKind::CLASS
+                    | lsp2::CompletionItemKind::ENUM => Some("type"),
+                    lsp2::CompletionItemKind::ENUM_MEMBER => Some("variant"),
+                    lsp2::CompletionItemKind::KEYWORD => Some("keyword"),
+                    lsp2::CompletionItemKind::VALUE | lsp2::CompletionItemKind::CONSTANT => {
+                        Some("constant")
+                    }
+                    _ => None,
+                };
+                if let Some(highlight_id) = language
+                    .grammar()
+                    .and_then(|g| g.highlight_id_for_name(highlight_name?))
+                {
+                    let mut label = CodeLabel::plain(label.to_string(), None);
+                    label.runs.push((
+                        0..label.text.rfind('(').unwrap_or(label.text.len()),
+                        highlight_id,
+                    ));
+                    return Some(label);
+                }
+            }
+            _ => {}
+        }
+        Some(CodeLabel::plain(label.to_string(), None))
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => {
+                let text = format!("void {} () {{}}", name);
+                let filter_range = 0..name.len();
+                let display_range = 5..5 + name.len();
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::STRUCT => {
+                let text = format!("struct {} {{}}", name);
+                let filter_range = 7..7 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::ENUM => {
+                let text = format!("enum {} {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::INTERFACE | lsp2::SymbolKind::CLASS => {
+                let text = format!("class {} {{}}", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let text = format!("const int {} = 0;", name);
+                let filter_range = 10..10 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::MODULE => {
+                let text = format!("namespace {} {{}}", name);
+                let filter_range = 10..10 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::TYPE_PARAMETER => {
+                let text = format!("typename {} {{}};", name);
+                let filter_range = 9..9 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_clangd_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_clangd_dir = Some(entry.path());
+            }
+        }
+        let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let clangd_bin = clangd_dir.join("bin/clangd");
+        if clangd_bin.exists() {
+            Ok(LanguageServerBinary {
+                path: clangd_bin,
+                arguments: vec![],
+            })
+        } else {
+            Err(anyhow!(
+                "missing clangd binary in directory {:?}",
+                clangd_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
+#[cfg(test)]
+mod tests {
+    use gpui2::{Context, TestAppContext};
+    use language2::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
+    use settings2::SettingsStore;
+    use std::num::NonZeroU32;
+
+    #[gpui2::test]
+    async fn test_c_autoindent(cx: &mut TestAppContext) {
+        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            language2::init(cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                    s.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
+        });
+        let language = crate::languages::language("c", tree_sitter_c::language(), None).await;
+
+        cx.build_model(|cx| {
+            let mut buffer =
+                Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx);
+
+            // empty function
+            buffer.edit([(0..0, "int main() {}")], None, cx);
+
+            // indent inside braces
+            let ix = buffer.len() - 1;
+            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "int main() {\n  \n}");
+
+            // indent body of single-statement if statement
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "int main() {\n  if (a)\n    b;\n}");
+
+            // indent inside field expression
+            let ix = buffer.len() - 3;
+            buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "int main() {\n  if (a)\n    b\n      .c;\n}");
+
+            buffer
+        });
+    }
+}

crates/zed2/src/languages/c/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "C"
+path_suffixes = ["c"]
+line_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 = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]

crates/zed2/src/languages/c/embedding.scm 🔗

@@ -0,0 +1,43 @@
+(
+    (comment)* @context
+    .
+    (declaration
+        declarator: [
+            (function_declarator
+                declarator: (_) @name)
+            (pointer_declarator
+                "*" @name
+                declarator: (function_declarator
+                    declarator: (_) @name))
+            (pointer_declarator
+                "*" @name
+                declarator: (pointer_declarator
+                    "*" @name
+                    declarator: (function_declarator
+                        declarator: (_) @name)))
+            ]
+        ) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (function_definition
+        declarator: [
+            (function_declarator
+                declarator: (_) @name
+                )
+            (pointer_declarator
+                "*" @name
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    ))
+            (pointer_declarator
+                "*" @name
+                declarator: (pointer_declarator
+                    "*" @name
+                    declarator: (function_declarator
+                        declarator: (_) @name)))
+            ]
+        ) @item
+    )

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

@@ -0,0 +1,109 @@
+[
+  "break"
+  "case"
+  "const"
+  "continue"
+  "default"
+  "do"
+  "else"
+  "enum"
+  "extern"
+  "for"
+  "if"
+  "inline"
+  "return"
+  "sizeof"
+  "static"
+  "struct"
+  "switch"
+  "typedef"
+  "union"
+  "volatile"
+  "while"
+] @keyword
+
+[
+  "#define"
+  "#elif"
+  "#else"
+  "#endif"
+  "#if"
+  "#ifdef"
+  "#ifndef"
+  "#include"
+  (preproc_directive)
+] @keyword
+
+[
+  "--"
+  "-"
+  "-="
+  "->"
+  "="
+  "!="
+  "*"
+  "&"
+  "&&"
+  "+"
+  "++"
+  "+="
+  "<"
+  "=="
+  ">"
+  "||"
+] @operator
+
+[
+  "."
+  ";"
+] @punctuation.delimiter
+
+[
+  "{"
+  "}"
+  "("
+  ")"
+  "["
+  "]"
+] @punctuation.bracket
+
+[
+  (string_literal)
+  (system_lib_string)
+  (char_literal)
+] @string
+
+(comment) @comment
+
+(number_literal) @number
+
+[
+  (true)
+  (false)
+  (null)
+] @constant
+
+(identifier) @variable
+
+((identifier) @constant
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+
+(call_expression
+  function: (identifier) @function)
+(call_expression
+  function: (field_expression
+    field: (field_identifier) @function))
+(function_declarator
+  declarator: (identifier) @function)
+(preproc_function_def
+  name: (identifier) @function.special)
+
+(field_identifier) @property
+(statement_identifier) @label
+
+[
+  (type_identifier)
+  (primitive_type)
+  (sized_type_specifier)
+] @type
+

crates/zed2/src/languages/c/outline.scm 🔗

@@ -0,0 +1,70 @@
+(preproc_def
+    "#define" @context
+    name: (_) @name) @item
+
+(preproc_function_def
+    "#define" @context
+    name: (_) @name
+    parameters: (preproc_params
+        "(" @context
+        ")" @context)) @item
+
+(type_definition
+    "typedef" @context
+    declarator: (_) @name) @item
+
+(declaration
+    (type_qualifier)? @context
+    type: (_)? @context
+    declarator: [
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+    ]
+) @item
+
+(function_definition
+    (type_qualifier)? @context
+    type: (_)? @context
+    declarator: [
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+    ]
+) @item

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

@@ -0,0 +1,12 @@
+name = "C++"
+path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"]
+line_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 = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]

crates/zed2/src/languages/cpp/embedding.scm 🔗

@@ -0,0 +1,61 @@
+(
+    (comment)* @context
+    .
+    (function_definition
+        (type_qualifier)? @name
+        type: (_)? @name
+        declarator: [
+            (function_declarator
+                declarator: (_) @name)
+            (pointer_declarator
+                "*" @name
+                declarator: (function_declarator
+                declarator: (_) @name))
+            (pointer_declarator
+                "*" @name
+                declarator: (pointer_declarator
+                    "*" @name
+                declarator: (function_declarator
+                    declarator: (_) @name)))
+            (reference_declarator
+                ["&" "&&"] @name
+                (function_declarator
+                declarator: (_) @name))
+        ]
+        (type_qualifier)? @name) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (template_declaration
+        (class_specifier
+            "class" @name
+            name: (_) @name)
+            ) @item
+)
+
+(
+    (comment)* @context
+    .
+    (class_specifier
+        "class" @name
+        name: (_) @name) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (enum_specifier
+        "enum" @name
+        name: (_) @name) @item
+    )
+
+(
+    (comment)* @context
+    .
+    (declaration
+        type: (struct_specifier
+        "struct" @name)
+        declarator: (_) @name) @item
+)

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

@@ -0,0 +1,158 @@
+(identifier) @variable
+
+(call_expression
+  function: (qualified_identifier
+    name: (identifier) @function))
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (field_expression
+    field: (field_identifier) @function))
+
+(preproc_function_def
+  name: (identifier) @function.special)
+
+(template_function
+  name: (identifier) @function)
+
+(template_method
+  name: (field_identifier) @function)
+
+(function_declarator
+  declarator: (identifier) @function)
+
+(function_declarator
+  declarator: (qualified_identifier
+    name: (identifier) @function))
+
+(function_declarator
+  declarator: (field_identifier) @function)
+
+((namespace_identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(auto) @type
+(type_identifier) @type
+
+((identifier) @constant
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+
+(field_identifier) @property
+(statement_identifier) @label
+(this) @variable.special
+
+[
+  "break"
+  "case"
+  "catch"
+  "class"
+  "co_await"
+  "co_return"
+  "co_yield"
+  "const"
+  "constexpr"
+  "continue"
+  "default"
+  "delete"
+  "do"
+  "else"
+  "enum"
+  "explicit"
+  "extern"
+  "final"
+  "for"
+  "friend"
+  "if"
+  "if"
+  "inline"
+  "mutable"
+  "namespace"
+  "new"
+  "noexcept"
+  "override"
+  "private"
+  "protected"
+  "public"
+  "return"
+  "sizeof"
+  "static"
+  "struct"
+  "switch"
+  "template"
+  "throw"
+  "try"
+  "typedef"
+  "typename"
+  "union"
+  "using"
+  "virtual"
+  "volatile"
+  "while"
+  (primitive_type)
+  (type_qualifier)
+] @keyword
+
+[
+  "#define"
+  "#elif"
+  "#else"
+  "#endif"
+  "#if"
+  "#ifdef"
+  "#ifndef"
+  "#include"
+  (preproc_directive)
+] @keyword
+
+(comment) @comment
+
+[
+  (true)
+  (false)
+  (null)
+  (nullptr)
+] @constant
+
+(number_literal) @number
+
+[
+  (string_literal)
+  (system_lib_string)
+  (char_literal)
+  (raw_string_literal)
+] @string
+
+[
+  "."
+  ";"
+] @punctuation.delimiter
+
+[
+  "{"
+  "}"
+  "("
+  ")"
+  "["
+  "]"
+] @punctuation.bracket
+
+[
+  "--"
+  "-"
+  "-="
+  "->"
+  "="
+  "!="
+  "*"
+  "&"
+  "&&"
+  "+"
+  "++"
+  "+="
+  "<"
+  "=="
+  ">"
+  "||"
+] @operator

crates/zed2/src/languages/cpp/outline.scm 🔗

@@ -0,0 +1,149 @@
+(preproc_def
+    "#define" @context
+    name: (_) @name) @item
+
+(preproc_function_def
+    "#define" @context
+    name: (_) @name
+    parameters: (preproc_params
+        "(" @context
+        ")" @context)) @item
+
+(type_definition
+    "typedef" @context
+    declarator: (_) @name) @item
+
+(struct_specifier
+    "struct" @context
+    name: (_) @name) @item
+
+(class_specifier
+    "class" @context
+    name: (_) @name) @item
+
+(enum_specifier
+    "enum" @context
+    name: (_) @name) @item
+
+(enumerator
+    name: (_) @name) @item
+
+(declaration
+    (storage_class_specifier) @context
+    (type_qualifier)? @context
+    type: (_) @context
+    declarator: (init_declarator
+      declarator: (_) @name)) @item
+
+(function_definition
+    (type_qualifier)? @context
+    type: (_)? @context
+    declarator: [
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+        (reference_declarator
+            ["&" "&&"] @context
+            (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+    ]
+    (type_qualifier)? @context) @item
+
+(declaration
+    (type_qualifier)? @context
+    type: (_)? @context
+    declarator: [
+        (field_identifier) @name
+        (pointer_declarator
+            "*" @context
+            declarator: (field_identifier) @name)
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+        (reference_declarator
+            ["&" "&&"] @context
+            (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+    ]
+    (type_qualifier)? @context) @item
+
+(field_declaration
+    (type_qualifier)? @context
+    type: (_) @context
+    declarator: [
+        (field_identifier) @name
+        (pointer_declarator
+            "*" @context
+            declarator: (field_identifier) @name)
+        (function_declarator
+            declarator: (_) @name
+            parameters: (parameter_list
+                "(" @context
+                ")" @context))
+        (pointer_declarator
+            "*" @context
+            declarator: (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+        (pointer_declarator
+            "*" @context
+            declarator: (pointer_declarator
+                "*" @context
+                declarator: (function_declarator
+                    declarator: (_) @name
+                    parameters: (parameter_list
+                        "(" @context
+                        ")" @context))))
+        (reference_declarator
+            ["&" "&&"] @context
+            (function_declarator
+                declarator: (_) @name
+                parameters: (parameter_list
+                    "(" @context
+                    ")" @context)))
+    ]
+    (type_qualifier)? @context) @item

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

@@ -0,0 +1,130 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str =
+    "node_modules/vscode-langservers-extracted/bin/vscode-css-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct CssLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl CssLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        CssLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for CssLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("vscode-css-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "css"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("vscode-langservers-extracted")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("vscode-langservers-extracted", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| 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(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/css/config.toml 🔗

@@ -0,0 +1,13 @@
+name = "CSS"
+path_suffixes = ["css"]
+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 = false, not_in = ["string", "comment"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+]
+word_characters = ["-"]
+block_comment = ["/* ", " */"]
+prettier_parser_name = "css"

crates/zed2/src/languages/css/highlights.scm 🔗

@@ -0,0 +1,78 @@
+(comment) @comment
+
+[
+  (tag_name)
+  (nesting_selector)
+  (universal_selector)
+] @tag
+
+[
+  "~"
+  ">"
+  "+"
+  "-"
+  "*"
+  "/"
+  "="
+  "^="
+  "|="
+  "~="
+  "$="
+  "*="
+  "and"
+  "or"
+  "not"
+  "only"
+] @operator
+
+(attribute_selector (plain_value) @string)
+
+(attribute_name) @attribute
+(pseudo_element_selector (tag_name) @attribute)
+(pseudo_class_selector (class_name) @attribute)
+
+[
+  (class_name)
+  (id_name)
+  (namespace_name)
+  (property_name)
+  (feature_name)
+] @property
+
+(function_name) @function
+
+(
+  [
+    (property_name)
+    (plain_value)
+  ] @variable.special
+  (#match? @variable.special "^--")
+)
+
+[
+  "@media"
+  "@import"
+  "@charset"
+  "@namespace"
+  "@supports"
+  "@keyframes"
+  (at_keyword)
+  (to)
+  (from)
+  (important)
+]  @keyword
+
+(string_value) @string
+(color_value) @string.special
+
+[
+  (integer_value)
+  (float_value)
+] @number
+
+(unit) @type
+
+[
+  ","
+  ":"
+] @punctuation.delimiter

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

@@ -0,0 +1,546 @@
+use anyhow::{anyhow, bail, Context, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use gpui2::{AsyncAppContext, Task};
+pub use language2::*;
+use lsp2::{CompletionItemKind, LanguageServerBinary, SymbolKind};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings2::Settings;
+use smol::fs::{self, File};
+use std::{
+    any::Any,
+    env::consts,
+    ops::Deref,
+    path::PathBuf,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+};
+use util::{
+    async_maybe,
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ElixirSettings {
+    pub lsp: ElixirLspSetting,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ElixirLspSetting {
+    ElixirLs,
+    NextLs,
+    Local {
+        path: String,
+        arguments: Vec<String>,
+    },
+}
+
+#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
+pub struct ElixirSettingsContent {
+    lsp: Option<ElixirLspSetting>,
+}
+
+impl Settings for ElixirSettings {
+    const KEY: Option<&'static str> = Some("elixir");
+
+    type FileContent = ElixirSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut gpui2::AppContext,
+    ) -> Result<Self>
+    where
+        Self: Sized,
+    {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
+pub struct ElixirLspAdapter;
+
+#[async_trait]
+impl LspAdapter for ElixirLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("elixir-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "elixir-ls"
+    }
+
+    fn will_start_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
+
+        const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
+
+        let delegate = delegate.clone();
+        Some(cx.spawn(|cx| async move {
+            let elixir_output = smol::process::Command::new("elixir")
+                .args(["--version"])
+                .output()
+                .await;
+            if elixir_output.is_err() {
+                if DID_SHOW_NOTIFICATION
+                    .compare_exchange(false, true, SeqCst, SeqCst)
+                    .is_ok()
+                {
+                    cx.update(|cx| {
+                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
+                    })?
+                }
+                return Err(anyhow!("cannot run elixir-ls"));
+            }
+
+            Ok(())
+        }))
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let http = delegate.http_client();
+        let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
+        let version_name = release
+            .name
+            .strip_prefix("Release ")
+            .context("Elixir-ls release name does not start with prefix")?
+            .to_owned();
+
+        let asset_name = format!("elixir-ls-{}.zip", &version_name);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+
+        let version = GitHubLspBinaryVersion {
+            name: version_name,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
+        let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
+        let binary_path = version_dir.join("language_server.sh");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .context("error downloading release")?;
+            let mut file = File::create(&zip_path)
+                .await
+                .with_context(|| format!("failed to create file {}", zip_path.display()))?;
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            futures::io::copy(response.body_mut(), &mut file).await?;
+
+            fs::create_dir_all(&version_dir)
+                .await
+                .with_context(|| format!("failed to create directory {}", version_dir.display()))?;
+            let unzip_status = smol::process::Command::new("unzip")
+                .arg(&zip_path)
+                .arg("-d")
+                .arg(&version_dir)
+                .output()
+                .await?
+                .status;
+            if !unzip_status.success() {
+                Err(anyhow!("failed to unzip elixir-ls archive"))?;
+            }
+
+            remove_matching(&container_dir, |entry| entry != version_dir).await;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec![],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_elixir_ls(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_elixir_ls(container_dir).await
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        match completion.kind.zip(completion.detail.as_ref()) {
+            Some((_, detail)) if detail.starts_with("(function)") => {
+                let text = detail.strip_prefix("(function) ")?;
+                let filter_range = 0..text.find('(').unwrap_or(text.len());
+                let source = Rope::from(format!("def {text}").as_str());
+                let runs = language.highlight_text(&source, 4..4 + text.len());
+                return Some(CodeLabel {
+                    text: text.to_string(),
+                    runs,
+                    filter_range,
+                });
+            }
+            Some((_, detail)) if detail.starts_with("(macro)") => {
+                let text = detail.strip_prefix("(macro) ")?;
+                let filter_range = 0..text.find('(').unwrap_or(text.len());
+                let source = Rope::from(format!("defmacro {text}").as_str());
+                let runs = language.highlight_text(&source, 9..9 + text.len());
+                return Some(CodeLabel {
+                    text: text.to_string(),
+                    runs,
+                    filter_range,
+                });
+            }
+            Some((
+                CompletionItemKind::CLASS
+                | CompletionItemKind::MODULE
+                | CompletionItemKind::INTERFACE
+                | CompletionItemKind::STRUCT,
+                _,
+            )) => {
+                let filter_range = 0..completion
+                    .label
+                    .find(" (")
+                    .unwrap_or(completion.label.len());
+                let text = &completion.label[filter_range.clone()];
+                let source = Rope::from(format!("defmodule {text}").as_str());
+                let runs = language.highlight_text(&source, 10..10 + text.len());
+                return Some(CodeLabel {
+                    text: completion.label.clone(),
+                    runs,
+                    filter_range,
+                });
+            }
+            _ => {}
+        }
+
+        None
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            SymbolKind::METHOD | SymbolKind::FUNCTION => {
+                let text = format!("def {}", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
+                let text = format!("defmodule {}", name);
+                let filter_range = 10..10 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary_elixir_ls(
+    container_dir: PathBuf,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            last = Some(entry?.path());
+        }
+        last.map(|path| LanguageServerBinary {
+            path,
+            arguments: vec![],
+        })
+        .ok_or_else(|| anyhow!("no cached binary"))
+    })()
+    .await
+    .log_err()
+}
+
+pub struct NextLspAdapter;
+
+#[async_trait]
+impl LspAdapter for NextLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("next-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "next-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
+        let version = release.name.clone();
+        let platform = match consts::ARCH {
+            "x86_64" => "darwin_amd64",
+            "aarch64" => "darwin_arm64",
+            other => bail!("Running on unsupported platform: {other}"),
+        };
+        let asset_name = format!("next_ls_{}", platform);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: version,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+
+        let binary_path = container_dir.join("next-ls");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+
+            let mut file = smol::fs::File::create(&binary_path).await?;
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            futures::io::copy(response.body_mut(), &mut file).await?;
+
+            fs::set_permissions(
+                &binary_path,
+                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+            )
+            .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec!["--stdio".into()],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_next(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--stdio".into()];
+                binary
+            })
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_next(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_completion_elixir(completion, language)
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol_kind: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_elixir(name, symbol_kind, language)
+    }
+}
+
+async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async_maybe!({
+        let mut last_binary_path = 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_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name == "next-ls")
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: Vec::new(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })
+    .await
+    .log_err()
+}
+
+pub struct LocalLspAdapter {
+    pub path: String,
+    pub arguments: Vec<String>,
+}
+
+#[async_trait]
+impl LspAdapter for LocalLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("local-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "local-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(()) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path)?;
+        Ok(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_completion_elixir(completion, language)
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_elixir(name, symbol, language)
+    }
+}
+
+fn label_for_completion_elixir(
+    completion: &lsp2::CompletionItem,
+    language: &Arc<Language>,
+) -> Option<CodeLabel> {
+    return Some(CodeLabel {
+        runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
+        text: completion.label.clone(),
+        filter_range: 0..completion.label.len(),
+    });
+}
+
+fn label_for_symbol_elixir(
+    name: &str,
+    _: SymbolKind,
+    language: &Arc<Language>,
+) -> Option<CodeLabel> {
+    Some(CodeLabel {
+        runs: language.highlight_text(&name.into(), 0..name.len()),
+        text: name.to_string(),
+        filter_range: 0..name.len(),
+    })
+}

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

@@ -0,0 +1,16 @@
+name = "Elixir"
+path_suffixes = ["ex", "exs"]
+line_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 = 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/zed2/src/languages/elixir/embedding.scm 🔗

@@ -0,0 +1,27 @@
+(
+    (unary_operator
+        operator: "@"
+        operand: (call
+            target: (identifier) @unary
+            (#match? @unary "^(doc)$"))
+        ) @context
+    .
+    (call
+        target: (identifier) @name
+        (arguments
+            [
+            (identifier) @name
+            (call
+                target: (identifier) @name)
+                (binary_operator
+                    left: (call
+                    target: (identifier) @name)
+                    operator: "when")
+            ])
+        (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+        )
+
+    (call
+        target: (identifier) @name
+        (arguments (alias) @name)
+        (#match? @name "^(defmodule|defprotocol)$")) @item

crates/zed2/src/languages/elixir/highlights.scm 🔗

@@ -0,0 +1,153 @@
+["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
+
+(unary_operator
+  operator: "&"
+  operand: (integer) @operator)
+
+(operator_identifier) @operator
+
+(unary_operator
+  operator: _ @operator)
+
+(binary_operator
+  operator: _ @operator)
+
+(dot
+  operator: _ @operator)
+
+(stab_clause
+  operator: _ @operator)
+
+[
+  (boolean)
+  (nil)
+] @constant
+
+[
+  (integer)
+  (float)
+] @number
+
+(alias) @type
+
+(call
+  target: (dot
+    left: (atom) @type))
+
+(char) @constant
+
+(escape_sequence) @string.escape
+
+[
+  (atom)
+  (quoted_atom)
+  (keyword)
+  (quoted_keyword)
+] @string.special.symbol
+
+[
+  (string)
+  (charlist)
+] @string
+
+(sigil
+  (sigil_name) @__name__
+  quoted_start: _ @string
+  quoted_end: _ @string
+  (#match? @__name__ "^[sS]$")) @string
+
+(sigil
+  (sigil_name) @__name__
+  quoted_start: _ @string.regex
+  quoted_end: _ @string.regex
+  (#match? @__name__ "^[rR]$")) @string.regex
+
+(sigil
+  (sigil_name) @__name__
+  quoted_start: _ @string.special
+  quoted_end: _ @string.special) @string.special
+
+(
+  (identifier) @comment.unused
+  (#match? @comment.unused "^_")
+)
+
+(call
+  target: [
+    (identifier) @function
+    (dot
+      right: (identifier) @function)
+  ])
+
+(call
+  target: (identifier) @keyword
+  (arguments
+    [
+      (identifier) @function
+      (binary_operator
+        left: (identifier) @function
+        operator: "when")
+      (binary_operator
+        operator: "|>"
+        right: (identifier))
+    ])
+  (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
+
+(binary_operator
+  operator: "|>"
+  right: (identifier) @function)
+
+(call
+  target: (identifier) @keyword
+  (#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$"))
+
+(call
+  target: (identifier) @keyword
+  (#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$"))
+
+(
+  (identifier) @constant.builtin
+  (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
+)
+
+(unary_operator
+  operator: "@" @comment.doc
+  operand: (call
+    target: (identifier) @__attribute__ @comment.doc
+    (arguments
+      [
+        (string)
+        (charlist)
+        (sigil)
+        (boolean)
+      ] @comment.doc))
+  (#match? @__attribute__ "^(moduledoc|typedoc|doc)$"))
+
+(comment) @comment
+
+[
+ "%"
+] @punctuation
+
+[
+ ","
+ ";"
+] @punctuation.delimiter
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+  "<<"
+  ">>"
+] @punctuation.bracket
+
+(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded
+
+((sigil
+  (sigil_name) @_sigil_name
+  (quoted_content) @embedded)
+ (#eq? @_sigil_name "H"))

crates/zed2/src/languages/elixir/outline.scm 🔗

@@ -0,0 +1,26 @@
+(call
+  target: (identifier) @context
+  (arguments (alias) @name)
+  (#match? @context "^(defmodule|defprotocol)$")) @item
+
+(call
+  target: (identifier) @context
+  (arguments
+    [
+      (identifier) @name
+      (call
+          target: (identifier) @name
+          (arguments
+              "(" @context.extra
+              _* @context.extra
+              ")" @context.extra))
+      (binary_operator
+        left: (call
+            target: (identifier) @name
+            (arguments
+                "(" @context.extra
+                _* @context.extra
+                ")" @context.extra))
+        operator: "when")
+    ])
+  (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item

crates/zed2/src/languages/elm/config.toml 🔗

@@ -0,0 +1,11 @@
+name = "Elm"
+path_suffixes = ["elm"]
+line_comment = "-- "
+block_comment = ["{- ", " -}"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
+]

crates/zed2/src/languages/elm/highlights.scm 🔗

@@ -0,0 +1,72 @@
+[
+    "if"
+    "then"
+    "else"
+    "let"
+    "in"
+    (case)
+    (of)
+    (backslash)
+    (as)
+    (port)
+    (exposing)
+    (alias)
+    (import)
+    (module)
+    (type)
+    (arrow)
+ ] @keyword
+
+[
+    (eq)
+    (operator_identifier)
+    (colon)
+] @operator
+
+(type_annotation(lower_case_identifier) @function)
+(port_annotation(lower_case_identifier) @function)
+(function_declaration_left(lower_case_identifier) @function.definition)
+
+(function_call_expr
+    target: (value_expr
+        name: (value_qid (lower_case_identifier) @function)))
+
+(exposed_value(lower_case_identifier) @function)
+(exposed_type(upper_case_identifier) @type)
+
+(field_access_expr(value_expr(value_qid)) @identifier)
+(lower_pattern) @variable
+(record_base_identifier) @identifier
+
+[
+    "("
+    ")"
+] @punctuation.bracket
+
+[
+    "|"
+    ","
+] @punctuation.delimiter
+
+(number_constant_expr) @constant
+
+(type_declaration(upper_case_identifier) @type)
+(type_ref) @type
+(type_alias_declaration name: (upper_case_identifier) @type)
+
+(value_expr(upper_case_qid(upper_case_identifier)) @type)
+
+[
+    (line_comment)
+    (block_comment)
+] @comment
+
+(string_escape) @string.escape
+
+[
+    (open_quote)
+    (close_quote)
+    (regular_string_part)
+    (open_char)
+    (close_char)
+] @string

crates/zed2/src/languages/elm/outline.scm 🔗

@@ -0,0 +1,22 @@
+(type_declaration
+    (type) @context
+    (upper_case_identifier) @name) @item
+
+(type_alias_declaration
+    (type) @context
+    (alias) @context
+    name: (upper_case_identifier) @name) @item
+
+(type_alias_declaration
+    typeExpression:
+        (type_expression
+            part: (record_type
+                (field_type
+                    name: (lower_case_identifier) @name) @item)))
+
+(union_variant
+    name: (upper_case_identifier) @name) @item
+
+(value_declaration
+    functionDeclarationLeft:
+        (function_declaration_left(lower_case_identifier) @name)) @item

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

@@ -0,0 +1,8 @@
+name = "ERB"
+path_suffixes = ["erb"]
+autoclose_before = ">})"
+brackets = [
+    { start = "<", end = ">", close = true, newline = true },
+]
+block_comment = ["<%#", "%>"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/glsl/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "GLSL"
+path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"]
+line_comment = "// "
+block_comment = ["/* ", " */"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+]

crates/zed2/src/languages/glsl/highlights.scm 🔗

@@ -0,0 +1,118 @@
+"break" @keyword
+"case" @keyword
+"const" @keyword
+"continue" @keyword
+"default" @keyword
+"do" @keyword
+"else" @keyword
+"enum" @keyword
+"extern" @keyword
+"for" @keyword
+"if" @keyword
+"inline" @keyword
+"return" @keyword
+"sizeof" @keyword
+"static" @keyword
+"struct" @keyword
+"switch" @keyword
+"typedef" @keyword
+"union" @keyword
+"volatile" @keyword
+"while" @keyword
+
+"#define" @keyword
+"#elif" @keyword
+"#else" @keyword
+"#endif" @keyword
+"#if" @keyword
+"#ifdef" @keyword
+"#ifndef" @keyword
+"#include" @keyword
+(preproc_directive) @keyword
+
+"--" @operator
+"-" @operator
+"-=" @operator
+"->" @operator
+"=" @operator
+"!=" @operator
+"*" @operator
+"&" @operator
+"&&" @operator
+"+" @operator
+"++" @operator
+"+=" @operator
+"<" @operator
+"==" @operator
+">" @operator
+"||" @operator
+
+"." @delimiter
+";" @delimiter
+
+(string_literal) @string
+(system_lib_string) @string
+
+(null) @constant
+(number_literal) @number
+(char_literal) @number
+
+(call_expression
+  function: (identifier) @function)
+(call_expression
+  function: (field_expression
+    field: (field_identifier) @function))
+(function_declarator
+  declarator: (identifier) @function)
+(preproc_function_def
+  name: (identifier) @function.special)
+
+(field_identifier) @property
+(statement_identifier) @label
+(type_identifier) @type
+(primitive_type) @type
+(sized_type_specifier) @type
+
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z\\d_]*$"))
+
+(identifier) @variable
+
+(comment) @comment
+; inherits: c
+
+[
+  "in"
+  "out"
+  "inout"
+  "uniform"
+  "shared"
+  "layout"
+  "attribute"
+  "varying"
+  "buffer"
+  "coherent"
+  "readonly"
+  "writeonly"
+  "precision"
+  "highp"
+  "mediump"
+  "lowp"
+  "centroid"
+  "sample"
+  "patch"
+  "smooth"
+  "flat"
+  "noperspective"
+  "invariant"
+  "precise"
+] @type.qualifier
+
+"subroutine" @keyword.function
+
+(extension_storage_class) @storageclass
+
+(
+  (identifier) @variable.builtin
+  (#match? @variable.builtin "^gl_")
+)

crates/zed2/src/languages/go.rs 🔗

@@ -0,0 +1,464 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use gpui2::{AsyncAppContext, Task};
+pub use language2::*;
+use lazy_static::lazy_static;
+use lsp2::LanguageServerBinary;
+use regex::Regex;
+use smol::{fs, process};
+use std::{
+    any::Any,
+    ffi::{OsStr, OsString},
+    ops::Range,
+    path::PathBuf,
+    str,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+};
+use util::{fs::remove_matching, github::latest_github_release, ResultExt};
+
+fn server_binary_arguments() -> Vec<OsString> {
+    vec!["-mode=stdio".into()]
+}
+
+#[derive(Copy, Clone)]
+pub struct GoLspAdapter;
+
+lazy_static! {
+    static ref GOPLS_VERSION_REGEX: Regex = Regex::new(r"\d+\.\d+\.\d+").unwrap();
+}
+
+#[async_trait]
+impl super::LspAdapter for GoLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("gopls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "gopls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release = latest_github_release("golang/tools", false, delegate.http_client()).await?;
+        let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
+        if version.is_none() {
+            log::warn!(
+                "couldn't infer gopls version from github release name '{}'",
+                release.name
+            );
+        }
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    fn will_fetch_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
+
+        const NOTIFICATION_MESSAGE: &str =
+            "Could not install the Go language server `gopls`, because `go` was not found.";
+
+        let delegate = delegate.clone();
+        Some(cx.spawn(|cx| async move {
+            let install_output = process::Command::new("go").args(["version"]).output().await;
+            if install_output.is_err() {
+                if DID_SHOW_NOTIFICATION
+                    .compare_exchange(false, true, SeqCst, SeqCst)
+                    .is_ok()
+                {
+                    cx.update(|cx| {
+                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
+                    })?
+                }
+                return Err(anyhow!("cannot install gopls"));
+            }
+            Ok(())
+        }))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<Option<String>>().unwrap();
+        let this = *self;
+
+        if let Some(version) = *version {
+            let binary_path = container_dir.join(&format!("gopls_{version}"));
+            if let Ok(metadata) = fs::metadata(&binary_path).await {
+                if metadata.is_file() {
+                    remove_matching(&container_dir, |entry| {
+                        entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
+                    })
+                    .await;
+
+                    return Ok(LanguageServerBinary {
+                        path: binary_path.to_path_buf(),
+                        arguments: server_binary_arguments(),
+                    });
+                }
+            }
+        } else if let Some(path) = this
+            .cached_server_binary(container_dir.clone(), delegate)
+            .await
+        {
+            return Ok(path);
+        }
+
+        let gobin_dir = container_dir.join("gobin");
+        fs::create_dir_all(&gobin_dir).await?;
+        let install_output = process::Command::new("go")
+            .env("GO111MODULE", "on")
+            .env("GOBIN", &gobin_dir)
+            .args(["install", "golang.org/x/tools/gopls@latest"])
+            .output()
+            .await?;
+        if !install_output.status.success() {
+            Err(anyhow!("failed to install gopls. Is go installed?"))?;
+        }
+
+        let installed_binary_path = gobin_dir.join("gopls");
+        let version_output = process::Command::new(&installed_binary_path)
+            .arg("version")
+            .output()
+            .await
+            .map_err(|e| anyhow!("failed to run installed gopls binary {:?}", e))?;
+        let version_stdout = str::from_utf8(&version_output.stdout)
+            .map_err(|_| anyhow!("gopls version produced invalid utf8"))?;
+        let version = GOPLS_VERSION_REGEX
+            .find(version_stdout)
+            .ok_or_else(|| anyhow!("failed to parse gopls version output"))?
+            .as_str();
+        let binary_path = container_dir.join(&format!("gopls_{version}"));
+        fs::rename(&installed_binary_path, &binary_path).await?;
+
+        Ok(LanguageServerBinary {
+            path: binary_path.to_path_buf(),
+            arguments: server_binary_arguments(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let label = &completion.label;
+
+        // Gopls returns nested fields and methods as completions.
+        // To syntax highlight these, combine their final component
+        // with their detail.
+        let name_offset = label.rfind('.').unwrap_or(0);
+
+        match completion.kind.zip(completion.detail.as_ref()) {
+            Some((lsp2::CompletionItemKind::MODULE, detail)) => {
+                let text = format!("{label} {detail}");
+                let source = Rope::from(format!("import {text}").as_str());
+                let runs = language.highlight_text(&source, 7..7 + text.len());
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((
+                lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE,
+                detail,
+            )) => {
+                let text = format!("{label} {detail}");
+                let source =
+                    Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
+                let runs = adjust_runs(
+                    name_offset,
+                    language.highlight_text(&source, 4..4 + text.len()),
+                );
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((lsp2::CompletionItemKind::STRUCT, _)) => {
+                let text = format!("{label} struct {{}}");
+                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
+                let runs = adjust_runs(
+                    name_offset,
+                    language.highlight_text(&source, 5..5 + text.len()),
+                );
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((lsp2::CompletionItemKind::INTERFACE, _)) => {
+                let text = format!("{label} interface {{}}");
+                let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
+                let runs = adjust_runs(
+                    name_offset,
+                    language.highlight_text(&source, 5..5 + text.len()),
+                );
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((lsp2::CompletionItemKind::FIELD, detail)) => {
+                let text = format!("{label} {detail}");
+                let source =
+                    Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
+                let runs = adjust_runs(
+                    name_offset,
+                    language.highlight_text(&source, 16..16 + text.len()),
+                );
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..label.len(),
+                });
+            }
+            Some((
+                lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD,
+                detail,
+            )) => {
+                if let Some(signature) = detail.strip_prefix("func") {
+                    let text = format!("{label}{signature}");
+                    let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
+                    let runs = adjust_runs(
+                        name_offset,
+                        language.highlight_text(&source, 5..5 + text.len()),
+                    );
+                    return Some(CodeLabel {
+                        filter_range: 0..label.len(),
+                        text,
+                        runs,
+                    });
+                }
+            }
+            _ => {}
+        }
+        None
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => {
+                let text = format!("func {} () {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::STRUCT => {
+                let text = format!("type {} struct {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..text.len();
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::INTERFACE => {
+                let text = format!("type {} interface {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..text.len();
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CLASS => {
+                let text = format!("type {} T", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let text = format!("const {} = nil", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::VARIABLE => {
+                let text = format!("var {} = nil", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::MODULE => {
+                let text = format!("package {}", name);
+                let filter_range = 8..8 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_binary_path = 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_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name.starts_with("gopls_"))
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: server_binary_arguments(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })()
+    .await
+    .log_err()
+}
+
+fn adjust_runs(
+    delta: usize,
+    mut runs: Vec<(Range<usize>, HighlightId)>,
+) -> Vec<(Range<usize>, HighlightId)> {
+    for (range, _) in &mut runs {
+        range.start += delta;
+        range.end += delta;
+    }
+    runs
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::languages::language;
+    use gpui2::Hsla;
+    use theme2::SyntaxTheme;
+
+    #[gpui2::test]
+    async fn test_go_label_for_completion() {
+        let language = language(
+            "go",
+            tree_sitter_go::language(),
+            Some(Arc::new(GoLspAdapter)),
+        )
+        .await;
+
+        let theme = SyntaxTheme::new_test([
+            ("type", Hsla::default()),
+            ("keyword", Hsla::default()),
+            ("function", Hsla::default()),
+            ("number", Hsla::default()),
+            ("property", Hsla::default()),
+        ]);
+        language.set_theme(&theme);
+
+        let grammar = language.grammar().unwrap();
+        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
+        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
+        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
+        let highlight_number = grammar.highlight_id_for_name("number").unwrap();
+        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
+
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FUNCTION),
+                    label: "Hello".to_string(),
+                    detail: Some("func(a B) c.D".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "Hello(a B) c.D".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (8..9, highlight_type),
+                    (13..14, highlight_type),
+                ],
+            })
+        );
+
+        // Nested methods
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::METHOD),
+                    label: "one.two.Three".to_string(),
+                    detail: Some("func() [3]interface{}".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "one.two.Three() [3]interface{}".to_string(),
+                filter_range: 0..13,
+                runs: vec![
+                    (8..13, highlight_function),
+                    (17..18, highlight_number),
+                    (19..28, highlight_keyword),
+                ],
+            })
+        );
+
+        // Nested fields
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FIELD),
+                    label: "two.Three".to_string(),
+                    detail: Some("a.Bcd".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "two.Three a.Bcd".to_string(),
+                filter_range: 0..9,
+                runs: vec![(4..9, highlight_field), (12..15, highlight_type)],
+            })
+        );
+    }
+}

crates/zed2/src/languages/go/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "Go"
+path_suffixes = ["go"]
+line_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 = false, not_in = ["comment", "string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed2/src/languages/go/embedding.scm 🔗

@@ -0,0 +1,24 @@
+(
+    (comment)* @context
+    .
+    (type_declaration
+        (type_spec
+            name: (_) @name)
+    ) @item
+)
+
+(
+    (comment)* @context
+    .
+    (function_declaration
+        name: (_) @name
+    ) @item
+)
+
+(
+    (comment)* @context
+    .
+    (method_declaration
+        name: (_) @name
+    ) @item
+)

crates/zed2/src/languages/go/highlights.scm 🔗

@@ -0,0 +1,107 @@
+(identifier) @variable
+(type_identifier) @type
+(field_identifier) @property
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (selector_expression
+    field: (field_identifier) @function.method))
+
+(function_declaration
+  name: (identifier) @function)
+
+(method_declaration
+  name: (field_identifier) @function.method)
+
+[
+  "--"
+  "-"
+  "-="
+  ":="
+  "!"
+  "!="
+  "..."
+  "*"
+  "*"
+  "*="
+  "/"
+  "/="
+  "&"
+  "&&"
+  "&="
+  "%"
+  "%="
+  "^"
+  "^="
+  "+"
+  "++"
+  "+="
+  "<-"
+  "<"
+  "<<"
+  "<<="
+  "<="
+  "="
+  "=="
+  ">"
+  ">="
+  ">>"
+  ">>="
+  "|"
+  "|="
+  "||"
+  "~"
+] @operator
+
+[
+  "break"
+  "case"
+  "chan"
+  "const"
+  "continue"
+  "default"
+  "defer"
+  "else"
+  "fallthrough"
+  "for"
+  "func"
+  "go"
+  "goto"
+  "if"
+  "import"
+  "interface"
+  "map"
+  "package"
+  "range"
+  "return"
+  "select"
+  "struct"
+  "switch"
+  "type"
+  "var"
+] @keyword
+
+[
+  (interpreted_string_literal)
+  (raw_string_literal)
+  (rune_literal)
+] @string
+
+(escape_sequence) @escape
+
+[
+  (int_literal)
+  (float_literal)
+  (imaginary_literal)
+] @number
+
+[
+  (true)
+  (false)
+  (nil)
+  (iota)
+] @constant.builtin
+
+(comment) @comment

crates/zed2/src/languages/go/indents.scm 🔗

@@ -0,0 +1,9 @@
+[
+    (assignment_statement)
+    (call_expression)
+    (selector_expression)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed2/src/languages/go/outline.scm 🔗

@@ -0,0 +1,43 @@
+(type_declaration
+    "type" @context
+    (type_spec
+        name: (_) @name)) @item
+
+(function_declaration
+    "func" @context
+    name: (identifier) @name
+    parameters: (parameter_list
+      "(" @context
+      ")" @context)) @item
+
+(method_declaration
+    "func" @context
+    receiver: (parameter_list
+        "(" @context
+        (parameter_declaration
+            type: (_) @context)
+        ")" @context)
+    name: (field_identifier) @name
+    parameters: (parameter_list
+      "(" @context
+      ")" @context)) @item
+
+(const_declaration
+    "const" @context
+    (const_spec
+        name: (identifier) @name) @item)
+
+(source_file
+    (var_declaration
+        "var" @context
+        (var_spec
+            name: (identifier) @name) @item))
+
+(method_spec
+    name: (_) @name
+    parameters: (parameter_list
+      "(" @context
+      ")" @context)) @item
+
+(field_declaration
+    name: (_) @name) @item

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

@@ -0,0 +1,12 @@
+name = "HEEX"
+path_suffixes = ["heex"]
+autoclose_before = ">})"
+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/zed2/src/languages/heex/highlights.scm 🔗

@@ -0,0 +1,57 @@
+; HEEx delimiters
+[
+  "/>"
+  "<!"
+  "<"
+  "</"
+  "</:"
+  "<:"
+  ">"
+  "{"
+  "}"
+] @punctuation.bracket
+
+[
+  "<%!--"
+  "<%"
+  "<%#"
+  "<%%="
+  "<%="
+  "%>"
+  "--%>"
+  "-->"
+  "<!--"
+] @keyword
+
+; HEEx operators are highlighted as such
+"=" @operator
+
+; HEEx inherits the DOCTYPE tag from HTML
+(doctype) @constant
+
+(comment) @comment
+
+; HEEx tags and slots are highlighted as HTML
+[
+ (tag_name)
+ (slot_name)
+] @tag
+
+; HEEx attributes are highlighted as HTML attributes
+(attribute_name) @attribute
+
+; HEEx special attributes are highlighted as keywords
+(special_attribute_name) @keyword
+
+[
+  (attribute_value)
+  (quoted_attribute_value)
+] @string
+
+; HEEx components are highlighted as Elixir modules and functions
+(component_name
+  [
+    (module) @module
+    (function) @function
+    "." @punctuation.delimiter
+  ])

crates/zed2/src/languages/heex/injections.scm 🔗

@@ -0,0 +1,13 @@
+(
+  (directive
+    [
+      (partial_expression_value)
+      (expression_value)
+      (ending_expression_value)
+    ] @content)
+  (#set! language "elixir")
+  (#set! combined)
+)
+
+((expression (expression_value) @content)
+ (#set! language "elixir"))

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

@@ -0,0 +1,130 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str =
+    "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct HtmlLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl HtmlLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        HtmlLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for HtmlLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("vscode-html-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "html"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("vscode-langservers-extracted")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("vscode-langservers-extracted", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| 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(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

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

@@ -0,0 +1,14 @@
+name = "HTML"
+path_suffixes = ["html"]
+autoclose_before = ">})"
+block_comment = ["<!-- ", " -->"]
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "<", end = ">", close = true, newline = true, not_in = ["comment", "string"] },
+    { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] },
+]
+word_characters = ["-"]
+prettier_parser_name = "html"

crates/zed2/src/languages/html/highlights.scm 🔗

@@ -0,0 +1,15 @@
+(tag_name) @keyword
+(erroneous_end_tag_name) @keyword
+(doctype) @constant
+(attribute_name) @property
+(attribute_value) @string
+(comment) @comment
+
+"=" @operator
+
+[
+  "<"
+  ">"
+  "</"
+  "/>"
+] @punctuation.bracket

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

@@ -0,0 +1,26 @@
+name = "JavaScript"
+path_suffixes = ["js", "jsx", "mjs", "cjs"]
+first_line_pattern = '^#!.*\bnode\b'
+line_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 = false, newline = true, not_in = ["comment", "string"] },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "`", end = "`", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
+]
+word_characters = ["$", "#"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "babel"
+
+[overrides.element]
+line_comment = { remove = true }
+block_comment = ["{/* ", " */}"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/javascript/embedding.scm 🔗

@@ -0,0 +1,71 @@
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (function_declaration
+                "async"? @name
+                "function" @name
+                name: (_) @name))
+        (function_declaration
+            "async"? @name
+            "function" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (class_declaration
+                "class" @name
+                name: (_) @name))
+        (class_declaration
+            "class" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (interface_declaration
+                "interface" @name
+                name: (_) @name))
+        (interface_declaration
+            "interface" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (enum_declaration
+                "enum" @name
+                name: (_) @name))
+        (enum_declaration
+            "enum" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "static"
+            ]* @name
+        name: (_) @name) @item
+)

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

@@ -0,0 +1,217 @@
+; Variables
+
+(identifier) @variable
+
+; Properties
+
+(property_identifier) @property
+
+; Function and method calls
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (member_expression
+    property: (property_identifier) @function.method))
+
+; Function and method definitions
+
+(function
+  name: (identifier) @function)
+(function_declaration
+  name: (identifier) @function)
+(method_definition
+  name: (property_identifier) @function.method)
+
+(pair
+  key: (property_identifier) @function.method
+  value: [(function) (arrow_function)])
+
+(assignment_expression
+  left: (member_expression
+    property: (property_identifier) @function.method)
+  right: [(function) (arrow_function)])
+
+(variable_declarator
+  name: (identifier) @function
+  value: [(function) (arrow_function)])
+
+(assignment_expression
+  left: (identifier) @function
+  right: [(function) (arrow_function)])
+
+; Special identifiers
+
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+(type_identifier) @type
+(predefined_type) @type.builtin
+
+([
+  (identifier)
+  (shorthand_property_identifier)
+  (shorthand_property_identifier_pattern)
+ ] @constant
+ (#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
+
+; Literals
+
+(this) @variable.special
+(super) @variable.special
+
+[
+  (null)
+  (undefined)
+] @constant.builtin
+
+[
+  (true)
+  (false)
+] @boolean
+
+(comment) @comment
+
+[
+  (string)
+  (template_string)
+] @string
+
+(regex) @string.regex
+(number) @number
+
+; Tokens
+
+[
+  ";"
+  "?."
+  "."
+  ","
+  ":"
+] @punctuation.delimiter
+
+[
+  "-"
+  "--"
+  "-="
+  "+"
+  "++"
+  "+="
+  "*"
+  "*="
+  "**"
+  "**="
+  "/"
+  "/="
+  "%"
+  "%="
+  "<"
+  "<="
+  "<<"
+  "<<="
+  "="
+  "=="
+  "==="
+  "!"
+  "!="
+  "!=="
+  "=>"
+  ">"
+  ">="
+  ">>"
+  ">>="
+  ">>>"
+  ">>>="
+  "~"
+  "^"
+  "&"
+  "|"
+  "^="
+  "&="
+  "|="
+  "&&"
+  "||"
+  "??"
+  "&&="
+  "||="
+  "??="
+] @operator
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+]  @punctuation.bracket
+
+[
+  "as"
+  "async"
+  "await"
+  "break"
+  "case"
+  "catch"
+  "class"
+  "const"
+  "continue"
+  "debugger"
+  "default"
+  "delete"
+  "do"
+  "else"
+  "export"
+  "extends"
+  "finally"
+  "for"
+  "from"
+  "function"
+  "get"
+  "if"
+  "import"
+  "in"
+  "instanceof"
+  "let"
+  "new"
+  "of"
+  "return"
+  "set"
+  "static"
+  "switch"
+  "target"
+  "throw"
+  "try"
+  "typeof"
+  "var"
+  "void"
+  "while"
+  "with"
+  "yield"
+] @keyword
+
+(template_substitution
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+(type_arguments
+  "<" @punctuation.bracket
+  ">" @punctuation.bracket)
+
+; Keywords
+
+[ "abstract"
+  "declare"
+  "enum"
+  "export"
+  "implements"
+  "interface"
+  "keyof"
+  "namespace"
+  "private"
+  "protected"
+  "public"
+  "type"
+  "readonly"
+  "override"
+] @keyword

crates/zed2/src/languages/javascript/indents.scm 🔗

@@ -0,0 +1,15 @@
+[
+    (call_expression)
+    (assignment_expression)
+    (member_expression)
+    (lexical_declaration)
+    (variable_declaration)
+    (assignment_expression)
+    (if_statement)
+    (for_statement)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

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

@@ -0,0 +1,62 @@
+(internal_module
+    "namespace" @context
+    name: (_) @name) @item
+
+(enum_declaration
+    "enum" @context
+    name: (_) @name) @item
+
+(function_declaration
+    "async"? @context
+    "function" @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item
+
+(interface_declaration
+    "interface" @context
+    name: (_) @name) @item
+
+(program
+    (export_statement
+        (lexical_declaration
+            ["let" "const"] @context
+            (variable_declarator
+                name: (_) @name) @item)))
+
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (_) @name) @item))
+
+(class_declaration
+    "class" @context
+    name: (_) @name) @item
+
+(method_definition
+    [
+        "get"
+        "set"
+        "async"
+        "*"
+        "readonly"
+        "static"
+        (override_modifier)
+        (accessibility_modifier)
+    ]* @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item
+
+(public_field_definition
+    [
+        "declare"
+        "readonly"
+        "abstract"
+        "static"
+        (accessibility_modifier)
+    ]* @context
+    name: (_) @name) @item

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

@@ -0,0 +1,184 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use feature_flags2::FeatureFlagAppExt;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use gpui2::AppContext;
+use language2::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use settings2::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    future,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{paths, ResultExt};
+
+const SERVER_PATH: &'static str =
+    "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct JsonLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+    languages: Arc<LanguageRegistry>,
+}
+
+impl JsonLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>, languages: Arc<LanguageRegistry>) -> Self {
+        JsonLspAdapter { node, languages }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for JsonLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("json-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "json"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("vscode-json-languageserver")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("vscode-json-languageserver", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+
+    fn workspace_configuration(
+        &self,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, serde_json::Value> {
+        let action_names = cx.all_action_names().collect::<Vec<_>>();
+        let staff_mode = cx.is_staff();
+        let language_names = &self.languages.language_names();
+        let settings_schema = cx.global::<SettingsStore>().json_schema(
+            &SettingsJsonSchemaParams {
+                language_names,
+                staff_mode,
+            },
+            cx,
+        );
+
+        future::ready(serde_json::json!({
+            "json": {
+                "format": {
+                    "enable": true,
+                },
+                "schemas": [
+                    {
+                        "fileMatch": [
+                            schema_file_match(&paths::SETTINGS),
+                            &*paths::LOCAL_SETTINGS_RELATIVE_PATH,
+                        ],
+                        "schema": settings_schema,
+                    },
+                    {
+                        "fileMatch": [schema_file_match(&paths::KEYMAP)],
+                        "schema": KeymapFile::generate_json_schema(&action_names),
+                    }
+                ]
+            }
+        }))
+        .boxed()
+    }
+
+    async fn language_ids(&self) -> HashMap<String, String> {
+        [("JSON".into(), "jsonc".into())].into_iter().collect()
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| 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(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
+fn schema_file_match(path: &Path) -> &Path {
+    path.strip_prefix(path.parent().unwrap().parent().unwrap())
+        .unwrap()
+}

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

@@ -0,0 +1,10 @@
+name = "JSON"
+path_suffixes = ["json"]
+line_comment = "// "
+autoclose_before = ",]}"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]
+prettier_parser_name = "json"

crates/zed2/src/languages/json/embedding.scm 🔗

@@ -0,0 +1,14 @@
+; Only produce one embedding for the entire file.
+(document) @item
+
+; Collapse arrays, except for the first object.
+(array
+  "[" @keep
+  .
+  (object)? @keep
+  "]" @keep) @collapse
+
+; Collapse string values (but not keys).
+(pair value: (string
+  "\"" @keep
+  "\"" @keep) @collapse)

crates/zed2/src/languages/json/highlights.scm 🔗

@@ -0,0 +1,21 @@
+(comment) @comment
+
+(string) @string
+
+(pair
+  key: (string) @property)
+
+(number) @number
+
+[
+  (true)
+  (false)
+  (null)
+] @constant
+
+[
+  "{"
+  "}"
+  "["
+  "]"
+] @punctuation.bracket

crates/zed2/src/languages/language_plugin.rs 🔗

@@ -0,0 +1,168 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::lock::Mutex;
+use gpui2::executor::Background;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::ResultExt;
+
+#[allow(dead_code)]
+pub async fn new_json(executor: Arc<Background>) -> Result<PluginLspAdapter> {
+    let plugin = PluginBuilder::new_default()?
+        .host_function_async("command", |command: String| async move {
+            let mut args = command.split(' ');
+            let command = args.next().unwrap();
+            smol::process::Command::new(command)
+                .args(args)
+                .output()
+                .await
+                .log_err()
+                .map(|output| output.stdout)
+        })?
+        .init(PluginBinary::Precompiled(include_bytes!(
+            "../../../../plugins/bin/json_language.wasm.pre",
+        )))
+        .await?;
+
+    PluginLspAdapter::new(plugin, executor).await
+}
+
+pub struct PluginLspAdapter {
+    name: WasiFn<(), String>,
+    fetch_latest_server_version: WasiFn<(), Option<String>>,
+    fetch_server_binary: WasiFn<(PathBuf, String), Result<LanguageServerBinary, String>>,
+    cached_server_binary: WasiFn<PathBuf, Option<LanguageServerBinary>>,
+    initialization_options: WasiFn<(), String>,
+    language_ids: WasiFn<(), Vec<(String, String)>>,
+    executor: Arc<Background>,
+    runtime: Arc<Mutex<Plugin>>,
+}
+
+impl PluginLspAdapter {
+    #[allow(unused)]
+    pub async fn new(mut plugin: Plugin, executor: Arc<Background>) -> Result<Self> {
+        Ok(Self {
+            name: plugin.function("name")?,
+            fetch_latest_server_version: plugin.function("fetch_latest_server_version")?,
+            fetch_server_binary: plugin.function("fetch_server_binary")?,
+            cached_server_binary: plugin.function("cached_server_binary")?,
+            initialization_options: plugin.function("initialization_options")?,
+            language_ids: plugin.function("language_ids")?,
+            executor,
+            runtime: Arc::new(Mutex::new(plugin)),
+        })
+    }
+}
+
+#[async_trait]
+impl LspAdapter for PluginLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        let name: String = self
+            .runtime
+            .lock()
+            .await
+            .call(&self.name, ())
+            .await
+            .unwrap();
+        LanguageServerName(name.into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "PluginLspAdapter"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let runtime = self.runtime.clone();
+        let function = self.fetch_latest_server_version;
+        self.executor
+            .spawn(async move {
+                let mut runtime = runtime.lock().await;
+                let versions: Result<Option<String>> =
+                    runtime.call::<_, Option<String>>(&function, ()).await;
+                versions
+                    .map_err(|e| anyhow!("{}", e))?
+                    .ok_or_else(|| anyhow!("Could not fetch latest server version"))
+                    .map(|v| Box::new(v) as Box<_>)
+            })
+            .await
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = *version.downcast::<String>().unwrap();
+        let runtime = self.runtime.clone();
+        let function = self.fetch_server_binary;
+        self.executor
+            .spawn(async move {
+                let mut runtime = runtime.lock().await;
+                let handle = runtime.attach_path(&container_dir)?;
+                let result: Result<LanguageServerBinary, String> =
+                    runtime.call(&function, (container_dir, version)).await?;
+                runtime.remove_resource(handle)?;
+                result.map_err(|e| anyhow!("{}", e))
+            })
+            .await
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let runtime = self.runtime.clone();
+        let function = self.cached_server_binary;
+
+        self.executor
+            .spawn(async move {
+                let mut runtime = runtime.lock().await;
+                let handle = runtime.attach_path(&container_dir).ok()?;
+                let result: Option<LanguageServerBinary> =
+                    runtime.call(&function, container_dir).await.ok()?;
+                runtime.remove_resource(handle).ok()?;
+                result
+            })
+            .await
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        let string: String = self
+            .runtime
+            .lock()
+            .await
+            .call(&self.initialization_options, ())
+            .await
+            .log_err()?;
+
+        serde_json::from_str(&string).ok()
+    }
+
+    async fn language_ids(&self) -> HashMap<String, String> {
+        self.runtime
+            .lock()
+            .await
+            .call(&self.language_ids, ())
+            .await
+            .log_err()
+            .unwrap_or_default()
+            .into_iter()
+            .collect()
+    }
+}

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

@@ -0,0 +1,135 @@
+use anyhow::{anyhow, bail, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use async_trait::async_trait;
+use futures::{io::BufReader, StreamExt};
+use language2::{LanguageServerName, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use smol::fs;
+use std::{any::Any, env::consts, path::PathBuf};
+use util::{
+    async_maybe,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
+
+#[derive(Copy, Clone)]
+pub struct LuaLspAdapter;
+
+#[async_trait]
+impl super::LspAdapter for LuaLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("lua-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "lua"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("LuaLS/lua-language-server", false, delegate.http_client())
+                .await?;
+        let version = release.name.clone();
+        let platform = match consts::ARCH {
+            "x86_64" => "x64",
+            "aarch64" => "arm64",
+            other => bail!("Running on unsupported platform: {other}"),
+        };
+        let asset_name = format!("lua-language-server-{version}-darwin-{platform}.tar.gz");
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: release.name.clone(),
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+
+        let binary_path = container_dir.join("bin/lua-language-server");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(container_dir).await?;
+        }
+
+        fs::set_permissions(
+            &binary_path,
+            <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+        )
+        .await?;
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: Vec::new(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--version".into()];
+                binary
+            })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async_maybe!({
+        let mut last_binary_path = 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_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name == "lua-language-server")
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: Vec::new(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })
+    .await
+    .log_err()
+}

crates/zed2/src/languages/lua/config.toml 🔗

@@ -0,0 +1,10 @@
+name = "Lua"
+path_suffixes = ["lua"]
+line_comment = "-- "
+autoclose_before = ",]}"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]
+collapsed_placeholder = "--[ ... ]--"

crates/zed2/src/languages/lua/highlights.scm 🔗

@@ -0,0 +1,198 @@
+;; Keywords
+
+"return" @keyword
+
+[
+ "goto"
+ "in"
+ "local"
+] @keyword
+
+(break_statement) @keyword
+
+(do_statement
+[
+  "do"
+  "end"
+] @keyword)
+
+(while_statement
+[
+  "while"
+  "do"
+  "end"
+] @keyword)
+
+(repeat_statement
+[
+  "repeat"
+  "until"
+] @keyword)
+
+(if_statement
+[
+  "if"
+  "elseif"
+  "else"
+  "then"
+  "end"
+] @keyword)
+
+(elseif_statement
+[
+  "elseif"
+  "then"
+  "end"
+] @keyword)
+
+(else_statement
+[
+  "else"
+  "end"
+] @keyword)
+
+(for_statement
+[
+  "for"
+  "do"
+  "end"
+] @keyword)
+
+(function_declaration
+[
+  "function"
+  "end"
+] @keyword)
+
+(function_definition
+[
+  "function"
+  "end"
+] @keyword)
+
+;; Operators
+
+[
+ "and"
+ "not"
+ "or"
+] @operator
+
+[
+  "+"
+  "-"
+  "*"
+  "/"
+  "%"
+  "^"
+  "#"
+  "=="
+  "~="
+  "<="
+  ">="
+  "<"
+  ">"
+  "="
+  "&"
+  "~"
+  "|"
+  "<<"
+  ">>"
+  "//"
+  ".."
+] @operator
+
+;; Punctuations
+
+[
+  ";"
+  ":"
+  ","
+  "."
+] @punctuation.delimiter
+
+;; Brackets
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+;; Variables
+
+(identifier) @variable
+
+((identifier) @variable.special
+ (#eq? @variable.special "self"))
+
+(variable_list
+   attribute: (attribute
+     (["<" ">"] @punctuation.bracket
+      (identifier) @attribute)))
+
+;; Constants
+
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z_0-9]*$"))
+
+(vararg_expression) @constant
+
+(nil) @constant.builtin
+
+[
+  (false)
+  (true)
+] @boolean
+
+;; Tables
+
+(field name: (identifier) @field)
+
+(dot_index_expression field: (identifier) @field)
+
+(table_constructor
+[
+  "{"
+  "}"
+] @constructor)
+
+;; Functions
+
+(parameters (identifier) @parameter)
+
+(function_call
+  name: [
+    (identifier) @function
+    (dot_index_expression field: (identifier) @function)
+  ])
+
+(function_declaration
+  name: [
+    (identifier) @function.definition
+    (dot_index_expression field: (identifier) @function.definition)
+  ])
+
+(method_index_expression method: (identifier) @method)
+
+(function_call
+  (identifier) @function.builtin
+  (#any-of? @function.builtin
+    ;; built-in functions in Lua 5.1
+    "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs"
+    "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print"
+    "rawequal" "rawget" "rawset" "require" "select" "setfenv" "setmetatable"
+    "tonumber" "tostring" "type" "unpack" "xpcall"))
+
+;; Others
+
+(comment) @comment
+
+(hash_bang_line) @preproc
+
+(number) @number
+
+(string) @string

crates/zed2/src/languages/lua/indents.scm 🔗

@@ -0,0 +1,10 @@
+(if_statement "end" @end) @indent
+(do_statement "end" @end) @indent
+(while_statement "end" @end) @indent
+(for_statement "end" @end) @indent
+(repeat_statement "until" @end) @indent
+(function_declaration "end" @end) @indent
+
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed2/src/languages/markdown/config.toml 🔗

@@ -0,0 +1,11 @@
+name = "Markdown"
+path_suffixes = ["md", "mdx"]
+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 },
+    { start = "\"", end = "\"", close = false, newline = false },
+    { start = "'", end = "'", close = false, newline = false },
+    { start = "`", end = "`", close = false, newline = false },
+]

crates/zed2/src/languages/markdown/highlights.scm 🔗

@@ -0,0 +1,24 @@
+(emphasis) @emphasis
+(strong_emphasis) @emphasis.strong
+
+[
+  (atx_heading)
+  (setext_heading)
+] @title
+
+[
+  (list_marker_plus)
+  (list_marker_minus)
+  (list_marker_star)
+  (list_marker_dot)
+  (list_marker_parenthesis)
+] @punctuation.list_marker
+
+(code_span) @text.literal
+
+(fenced_code_block
+  (info_string
+    (language) @text.literal))
+
+(link_destination) @link_uri
+(link_text) @link_text

crates/zed2/src/languages/nix/config.toml 🔗

@@ -0,0 +1,11 @@
+name = "Nix"
+path_suffixes = ["nix"]
+line_comment = "# "
+block_comment = ["/* ", " */"]
+autoclose_before = ";:.,=}])>` \n\t\""
+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 },
+]

crates/zed2/src/languages/nix/highlights.scm 🔗

@@ -0,0 +1,95 @@
+(comment) @comment
+
+[
+  "if"
+  "then"
+  "else"
+  "let"
+  "inherit"
+  "in"
+  "rec"
+  "with"
+  "assert"
+  "or"
+] @keyword
+
+[
+ (string_expression)
+ (indented_string_expression)
+] @string
+
+[
+  (path_expression)
+  (hpath_expression)
+  (spath_expression)
+] @string.special.path
+
+(uri_expression) @link_uri
+
+[
+  (integer_expression)
+  (float_expression)
+] @number
+
+(interpolation
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+(escape_sequence) @escape
+(dollar_escape) @escape
+
+(function_expression
+  universal: (identifier) @parameter
+)
+
+(formal
+  name: (identifier) @parameter
+  "?"? @punctuation.delimiter)
+
+(select_expression
+  attrpath: (attrpath (identifier)) @property)
+
+(apply_expression
+  function: [
+    (variable_expression (identifier)) @function
+    (select_expression
+      attrpath: (attrpath
+        attr: (identifier) @function .))])
+
+(unary_expression
+  operator: _ @operator)
+
+(binary_expression
+  operator: _ @operator)
+
+(variable_expression (identifier) @variable)
+
+(binding
+  attrpath: (attrpath (identifier)) @property)
+
+"=" @operator
+
+[
+  ";"
+  "."
+  ","
+] @punctuation.delimiter
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+] @punctuation.bracket
+
+(identifier) @variable
+
+((identifier) @function.builtin

crates/zed2/src/languages/nu/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "Nu"
+path_suffixes = ["nu"]
+line_comment = "# "
+autoclose_before = ";:.,=}])>` \n\t\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+]

crates/zed2/src/languages/nu/highlights.scm 🔗

@@ -0,0 +1,302 @@
+;;; ---
+;;; keywords
+[
+    "def"
+    "def-env"
+    "alias"
+    "export-env"
+    "export"
+    "extern"
+    "module"
+
+    "let"
+    "let-env"
+    "mut"
+    "const"
+
+    "hide-env"
+
+    "source"
+    "source-env"
+
+    "overlay"
+    "register"
+
+    "loop"
+    "while"
+    "error"
+
+    "do"
+    "if"
+    "else"
+    "try"
+    "catch"
+    "match"
+
+    "break"
+    "continue"
+    "return"
+
+] @keyword
+
+(hide_mod "hide" @keyword)
+(decl_use "use" @keyword)
+
+(ctrl_for
+    "for" @keyword
+    "in" @keyword
+)
+(overlay_list "list" @keyword)
+(overlay_hide "hide" @keyword)
+(overlay_new "new" @keyword)
+(overlay_use
+    "use" @keyword
+    "as" @keyword
+)
+(ctrl_error "make" @keyword)
+
+;;; ---
+;;; literals
+(val_number) @constant
+(val_duration
+    unit: [
+        "ns" "µs" "us" "ms" "sec" "min" "hr" "day" "wk"
+    ] @variable
+)
+(val_filesize
+    unit: [
+        "b" "B"
+
+        "kb" "kB" "Kb" "KB"
+        "mb" "mB" "Mb" "MB"
+        "gb" "gB" "Gb" "GB"
+        "tb" "tB" "Tb" "TB"
+        "pb" "pB" "Pb" "PB"
+        "eb" "eB" "Eb" "EB"
+        "zb" "zB" "Zb" "ZB"
+
+        "kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB"
+        "mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB"
+        "gib" "giB" "gIB" "gIb" "Gib" "GIb" "GIB"
+        "tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB"
+        "pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB"
+        "eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB"
+        "zib" "ziB" "zIB" "zIb" "Zib" "ZIb" "ZIB"
+    ] @variable
+)
+(val_binary
+    [
+       "0b"
+       "0o"
+       "0x"
+    ] @constant
+    "[" @punctuation.bracket
+    digit: [
+        "," @punctuation.delimiter
+        (hex_digit) @constant
+    ]
+    "]" @punctuation.bracket
+) @constant
+(val_bool) @constant.builtin
+(val_nothing) @constant.builtin
+(val_string) @string
+(val_date) @constant
+(inter_escape_sequence) @constant
+(escape_sequence) @constant
+(val_interpolated [
+    "$\""
+    "$\'"
+    "\""
+    "\'"
+] @string)
+(unescaped_interpolated_content) @string
+(escaped_interpolated_content) @string
+(expr_interpolated ["(" ")"] @variable)
+
+;;; ---
+;;; operators
+(expr_binary [
+    "+"
+    "-"
+    "*"
+    "/"
+    "mod"
+    "//"
+    "++"
+    "**"
+    "=="
+    "!="
+    "<"
+    "<="
+    ">"
+    ">="
+    "=~"
+    "!~"
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+] @operator)
+
+(expr_binary opr: ([
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+]) @keyword)
+
+(where_command [
+    "+"
+    "-"
+    "*"
+    "/"
+    "mod"
+    "//"
+    "++"
+    "**"
+    "=="
+    "!="
+    "<"
+    "<="
+    ">"
+    ">="
+    "=~"
+    "!~"
+    "and"
+    "or"
+    "xor"
+    "bit-or"
+    "bit-xor"
+    "bit-and"
+    "bit-shl"
+    "bit-shr"
+    "in"
+    "not-in"
+    "starts-with"
+    "ends-with"
+] @operator)
+
+(assignment [
+    "="
+    "+="
+    "-="
+    "*="
+    "/="
+    "++="
+] @operator)
+
+(expr_unary ["not" "-"] @operator)
+
+(val_range [
+    ".."
+    "..="
+    "..<"
+] @operator)
+
+["=>" "=" "|"] @operator
+
+[
+    "o>"   "out>"
+    "e>"   "err>"
+    "e+o>" "err+out>"
+    "o+e>" "out+err>"
+] @special
+
+;;; ---
+;;; punctuation
+[
+    ","
+    ";"
+] @punctuation.delimiter
+
+(param_short_flag "-" @punctuation.delimiter)
+(param_long_flag ["--"] @punctuation.delimiter)
+(long_flag ["--"] @punctuation.delimiter)
+(param_rest "..." @punctuation.delimiter)
+(param_type [":"] @punctuation.special)
+(param_value ["="] @punctuation.special)
+(param_cmd ["@"] @punctuation.special)
+(param_opt ["?"] @punctuation.special)
+
+[
+    "(" ")"
+    "{" "}"
+    "[" "]"
+] @punctuation.bracket
+
+(val_record
+  (record_entry ":" @punctuation.delimiter))
+;;; ---
+;;; identifiers
+(param_rest
+    name: (_) @variable)
+(param_opt
+    name: (_) @variable)
+(parameter
+    param_name: (_) @variable)
+(param_cmd
+    (cmd_identifier) @string)
+(param_long_flag) @variable
+(param_short_flag) @variable
+
+(short_flag) @variable
+(long_flag) @variable
+
+(scope_pattern [(wild_card) @function])
+
+(cmd_identifier) @function
+
+(command
+    "^" @punctuation.delimiter
+    head: (_) @function
+)
+
+"where" @function
+
+(path
+  ["." "?"] @punctuation.delimiter
+) @variable
+
+(val_variable
+  "$" @operator
+  [
+   (identifier) @variable
+   "in" @type.builtin
+   "nu" @type.builtin
+   "env" @type.builtin
+   "nothing" @type.builtin
+   ]  ; If we have a special styling, use it here
+)
+;;; ---
+;;; types
+(flat_type) @type.builtin
+(list_type
+    "list" @type
+    ["<" ">"] @punctuation.bracket
+)
+(collection_type
+    ["record" "table"] @type
+    "<" @punctuation.bracket
+    key: (_) @variable
+    ["," ":"] @punctuation.delimiter
+    ">" @punctuation.bracket
+)
+
+(shebang) @comment
+(comment) @comment

crates/zed2/src/languages/php.rs 🔗

@@ -0,0 +1,137 @@
+use anyhow::{anyhow, Result};
+
+use async_trait::async_trait;
+use collections::HashMap;
+
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+
+use smol::{fs, stream::StreamExt};
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+fn intelephense_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct IntelephenseVersion(String);
+
+pub struct IntelephenseLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl IntelephenseLspAdapter {
+    const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
+
+    #[allow(unused)]
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        Self { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for IntelephenseLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("intelephense".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "php"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(IntelephenseVersion(
+            self.node.npm_package_latest_version("intelephense").await?,
+        )) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<IntelephenseVersion>().unwrap();
+        let server_path = container_dir.join(Self::SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(&container_dir, &[("intelephense", version.0.as_str())])
+                .await?;
+        }
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: intelephense_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn label_for_completion(
+        &self,
+        _item: &lsp2::CompletionItem,
+        _language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        None
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        None
+    }
+    async fn language_ids(&self) -> HashMap<String, String> {
+        HashMap::from_iter([("PHP".into(), "php".into())])
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| 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(IntelephenseLspAdapter::SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: intelephense_server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

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

@@ -0,0 +1,14 @@
+name = "PHP"
+path_suffixes = ["php"]
+first_line_pattern = '^#!.*php'
+line_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 = false, not_in = ["string"] },
+]
+collapsed_placeholder = "/* ... */"
+word_characters = ["$"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/php/embedding.scm 🔗

@@ -0,0 +1,36 @@
+(
+    (comment)* @context
+    .
+    [
+        (function_definition
+            "function" @name
+            name: (_) @name
+            body: (_
+                "{" @keep
+                "}" @keep) @collapse
+            )
+
+        (trait_declaration
+            "trait" @name
+            name: (_) @name)
+
+        (method_declaration
+            "function" @name
+            name: (_) @name
+            body: (_
+                "{" @keep
+                "}" @keep) @collapse
+            )
+
+        (interface_declaration
+            "interface" @name
+            name: (_) @name
+            )
+
+        (enum_declaration
+            "enum" @name
+            name: (_) @name
+            )
+
+        ] @item
+    )

crates/zed2/src/languages/php/highlights.scm 🔗

@@ -0,0 +1,123 @@
+(php_tag) @tag
+"?>" @tag
+
+; Types
+
+(primitive_type) @type.builtin
+(cast_type) @type.builtin
+(named_type (name) @type) @type
+(named_type (qualified_name) @type) @type
+
+; Functions
+
+(array_creation_expression "array" @function.builtin)
+(list_literal "list" @function.builtin)
+
+(method_declaration
+  name: (name) @function.method)
+
+(function_call_expression
+  function: [(qualified_name (name)) (name)] @function)
+
+(scoped_call_expression
+  name: (name) @function)
+
+(member_call_expression
+  name: (name) @function.method)
+
+(function_definition
+  name: (name) @function)
+
+; Member
+
+(property_element
+  (variable_name) @property)
+
+(member_access_expression
+  name: (variable_name (name)) @property)
+(member_access_expression
+  name: (name) @property)
+
+; Variables
+
+(relative_scope) @variable.builtin
+
+((name) @constant
+ (#match? @constant "^_?[A-Z][A-Z\\d_]+$"))
+((name) @constant.builtin
+ (#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$"))
+
+((name) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+((name) @variable.builtin
+ (#eq? @variable.builtin "this"))
+
+(variable_name) @variable
+
+; Basic tokens
+[
+  (string)
+  (string_value)
+  (encapsed_string)
+  (heredoc)
+  (heredoc_body)
+  (nowdoc_body)
+] @string
+(boolean) @constant.builtin
+(null) @constant.builtin
+(integer) @number
+(float) @number
+(comment) @comment
+
+"$" @operator
+
+; Keywords
+
+"abstract" @keyword
+"as" @keyword
+"break" @keyword
+"case" @keyword
+"catch" @keyword
+"class" @keyword
+"const" @keyword
+"continue" @keyword
+"declare" @keyword
+"default" @keyword
+"do" @keyword
+"echo" @keyword
+"else" @keyword
+"elseif" @keyword
+"enum" @keyword
+"enddeclare" @keyword
+"endforeach" @keyword
+"endif" @keyword
+"endswitch" @keyword
+"endwhile" @keyword
+"extends" @keyword
+"final" @keyword
+"finally" @keyword
+"foreach" @keyword
+"function" @keyword
+"global" @keyword
+"if" @keyword
+"implements" @keyword
+"include_once" @keyword
+"include" @keyword
+"insteadof" @keyword
+"interface" @keyword
+"namespace" @keyword
+"new" @keyword
+"private" @keyword
+"protected" @keyword
+"public" @keyword
+"require_once" @keyword
+"require" @keyword
+"return" @keyword
+"static" @keyword
+"switch" @keyword
+"throw" @keyword
+"trait" @keyword
+"try" @keyword
+"use" @keyword
+"while" @keyword

crates/zed2/src/languages/php/outline.scm 🔗

@@ -0,0 +1,29 @@
+(class_declaration
+    "class" @context
+    name: (name) @name
+    ) @item
+
+(function_definition
+    "function" @context
+    name: (_) @name
+    ) @item
+
+(method_declaration
+    "function" @context
+    name: (_) @name
+    ) @item
+
+(interface_declaration
+    "interface" @context
+    name: (_) @name
+    ) @item
+
+(enum_declaration
+    "enum" @context
+    name: (_) @name
+    ) @item
+
+(trait_declaration
+    "trait" @context
+    name: (_) @name
+    ) @item

crates/zed2/src/languages/php/tags.scm 🔗

@@ -0,0 +1,40 @@
+(namespace_definition
+  name: (namespace_name) @name) @module
+
+(interface_declaration
+  name: (name) @name) @definition.interface
+
+(trait_declaration
+  name: (name) @name) @definition.interface
+
+(class_declaration
+  name: (name) @name) @definition.class
+
+(class_interface_clause [(name) (qualified_name)] @name) @impl
+
+(property_declaration
+  (property_element (variable_name (name) @name))) @definition.field
+
+(function_definition
+  name: (name) @name) @definition.function
+
+(method_declaration
+  name: (name) @name) @definition.function
+
+(object_creation_expression
+  [
+    (qualified_name (name) @name)
+    (variable_name (name) @name)
+  ]) @reference.class
+
+(function_call_expression
+  function: [
+    (qualified_name (name) @name)
+    (variable_name (name)) @name
+  ]) @reference.call
+
+(scoped_call_expression
+  name: (name) @name) @reference.call
+
+(member_call_expression
+  name: (name) @name) @reference.call

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

@@ -0,0 +1,296 @@
+use anyhow::Result;
+use async_trait::async_trait;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct PythonLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl PythonLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        PythonLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for PythonLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("pyright".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "pyright"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(&container_dir, &[("pyright", version.as_str())])
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn process_completion(&self, item: &mut lsp2::CompletionItem) {
+        // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
+        // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
+        // and `name` is the symbol name itself.
+        //
+        // Because the the symbol name is included, there generally are not ties when
+        // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
+        // into account. Here, we remove the symbol name from the sortText in order
+        // to allow our own fuzzy score to be used to break ties.
+        //
+        // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
+        let Some(sort_text) = &mut item.sort_text else {
+            return;
+        };
+        let mut parts = sort_text.split('.');
+        let Some(first) = parts.next() else { return };
+        let Some(second) = parts.next() else { return };
+        let Some(_) = parts.next() else { return };
+        sort_text.replace_range(first.len() + second.len() + 1.., "");
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp2::CompletionItem,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        let label = &item.label;
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            lsp2::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
+            lsp2::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
+            lsp2::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
+            lsp2::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
+            _ => return None,
+        };
+        Some(language2::CodeLabel {
+            text: label.clone(),
+            runs: vec![(0..label.len(), highlight_id)],
+            filter_range: 0..label.len(),
+        })
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => {
+                let text = format!("def {}():\n", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CLASS => {
+                let text = format!("class {}:", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let text = format!("{} = 0", name);
+                let filter_range = 0..name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(language2::CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    let server_path = container_dir.join(SERVER_PATH);
+    if server_path.exists() {
+        Some(LanguageServerBinary {
+            path: node.binary_path().await.log_err()?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    } else {
+        log::error!("missing executable in directory {:?}", server_path);
+        None
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use gpui2::{Context, ModelContext, TestAppContext};
+    use language2::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
+    use settings2::SettingsStore;
+    use std::num::NonZeroU32;
+
+    #[gpui2::test]
+    async fn test_python_autoindent(cx: &mut TestAppContext) {
+        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
+        let language =
+            crate::languages::language("python", tree_sitter_python::language(), None).await;
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            language2::init(cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                    s.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
+        });
+
+        cx.build_model(|cx| {
+            let mut buffer =
+                Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx);
+            let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
+                let ix = buffer.len();
+                buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
+            };
+
+            // indent after "def():"
+            append(&mut buffer, "def a():\n", cx);
+            assert_eq!(buffer.text(), "def a():\n  ");
+
+            // preserve indent after blank line
+            append(&mut buffer, "\n  ", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  ");
+
+            // indent after "if"
+            append(&mut buffer, "if a:\n  ", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    ");
+
+            // preserve indent after statement
+            append(&mut buffer, "b()\n", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    ");
+
+            // preserve indent after statement
+            append(&mut buffer, "else", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    else");
+
+            // dedent "else""
+            append(&mut buffer, ":", cx);
+            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n  else:");
+
+            // indent lines after else
+            append(&mut buffer, "\n", cx);
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    "
+            );
+
+            // indent after an open paren. the closing  paren is not indented
+            // because there is another token before it on the same line.
+            append(&mut buffer, "foo(\n1)", cx);
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n      1)"
+            );
+
+            // dedent the closing paren if it is shifted to the beginning of the line
+            let argument_ix = buffer.text().find('1').unwrap();
+            buffer.edit(
+                [(argument_ix..argument_ix + 1, "")],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )"
+            );
+
+            // preserve indent after the close paren
+            append(&mut buffer, "\n", cx);
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n    "
+            );
+
+            // manually outdent the last line
+            let end_whitespace_ix = buffer.len() - 4;
+            buffer.edit(
+                [(end_whitespace_ix..buffer.len(), "")],
+                Some(AutoindentMode::EachLine),
+                cx,
+            );
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n"
+            );
+
+            // preserve the newly reduced indentation on the next newline
+            append(&mut buffer, "\n", cx);
+            assert_eq!(
+                buffer.text(),
+                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n\n"
+            );
+
+            // reset to a simple if statement
+            buffer.edit([(0..buffer.len(), "if a:\n  b(\n  )")], None, cx);
+
+            // dedent "else" on the line after a closing paren
+            append(&mut buffer, "\n  else:\n", cx);
+            assert_eq!(buffer.text(), "if a:\n  b(\n  )\nelse:\n  ");
+
+            buffer
+        });
+    }
+}

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

@@ -0,0 +1,16 @@
+name = "Python"
+path_suffixes = ["py", "pyi", "mpy"]
+first_line_pattern = '^#!.*\bpython[0-9.]*\b'
+line_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 = false, not_in = ["string"] },
+    { start = "'", end = "'", close = false, newline = false, not_in = ["string"] },
+]
+
+auto_indent_using_last_non_empty_line = false
+increase_indent_pattern = ":\\s*$"
+decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"

crates/zed2/src/languages/python/highlights.scm 🔗

@@ -0,0 +1,125 @@
+(attribute attribute: (identifier) @property)
+(type (identifier) @type)
+
+; Function calls
+
+(decorator) @function
+
+(call
+  function: (attribute attribute: (identifier) @function.method))
+(call
+  function: (identifier) @function)
+
+; Function definitions
+
+(function_definition
+  name: (identifier) @function)
+
+; Identifier naming conventions
+
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+((identifier) @constant
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+
+; Builtin functions
+
+((call
+  function: (identifier) @function.builtin)
+ (#match?
+   @function.builtin
+   "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$"))
+
+; Literals
+
+[
+  (none)
+  (true)
+  (false)
+] @constant.builtin
+
+[
+  (integer)
+  (float)
+] @number
+
+(comment) @comment
+(string) @string
+(escape_sequence) @escape
+
+(interpolation
+  "{" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+[
+  "-"
+  "-="
+  "!="
+  "*"
+  "**"
+  "**="
+  "*="
+  "/"
+  "//"
+  "//="
+  "/="
+  "&"
+  "%"
+  "%="
+  "^"
+  "+"
+  "->"
+  "+="
+  "<"
+  "<<"
+  "<="
+  "<>"
+  "="
+  ":="
+  "=="
+  ">"
+  ">="
+  ">>"
+  "|"
+  "~"
+  "and"
+  "in"
+  "is"
+  "not"
+  "or"
+] @operator
+
+[
+  "as"
+  "assert"
+  "async"
+  "await"
+  "break"
+  "class"
+  "continue"
+  "def"
+  "del"
+  "elif"
+  "else"
+  "except"
+  "exec"
+  "finally"
+  "for"
+  "from"
+  "global"
+  "if"
+  "import"
+  "lambda"
+  "nonlocal"
+  "pass"
+  "print"
+  "raise"
+  "return"
+  "try"
+  "while"
+  "with"
+  "yield"
+  "match"
+  "case"
+] @keyword

crates/zed2/src/languages/racket/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "Racket"
+path_suffixes = ["rkt"]
+line_comment = "; "
+autoclose_before = "])"
+brackets = [
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false },
+]

crates/zed2/src/languages/racket/highlights.scm 🔗

@@ -0,0 +1,40 @@
+["(" ")" "[" "]" "{" "}"] @punctuation.bracket
+
+[(string)
+ (here_string)
+ (byte_string)] @string
+(regex) @string.regex
+(escape_sequence) @escape
+
+[(comment)
+ (block_comment)
+ (sexp_comment)] @comment
+
+(symbol) @variable
+
+(number) @number
+(character) @constant.builtin
+(boolean) @constant.builtin
+(keyword) @constant
+(quote . (symbol)) @constant
+
+(extension) @keyword
+(lang_name) @variable.special
+
+((symbol) @operator
+ (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
+
+(list
+  .
+  (symbol) @function)
+
+(list
+  .
+  (symbol) @keyword
+  (#match? @keyword

crates/zed2/src/languages/ruby.rs 🔗

@@ -0,0 +1,160 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use std::{any::Any, path::PathBuf, sync::Arc};
+
+pub struct RubyLanguageServer;
+
+#[async_trait]
+impl LspAdapter for RubyLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("solargraph".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "solargraph"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!("solargraph must be installed manually"))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "solargraph".into(),
+            arguments: vec!["stdio".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp2::CompletionItem,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        let label = &item.label;
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            lsp2::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
+            lsp2::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
+            lsp2::CompletionItemKind::CLASS | lsp2::CompletionItemKind::MODULE => {
+                grammar.highlight_id_for_name("type")?
+            }
+            lsp2::CompletionItemKind::KEYWORD => {
+                if label.starts_with(':') {
+                    grammar.highlight_id_for_name("string.special.symbol")?
+                } else {
+                    grammar.highlight_id_for_name("keyword")?
+                }
+            }
+            lsp2::CompletionItemKind::VARIABLE => {
+                if label.starts_with('@') {
+                    grammar.highlight_id_for_name("property")?
+                } else {
+                    return None;
+                }
+            }
+            _ => return None,
+        };
+        Some(language2::CodeLabel {
+            text: label.clone(),
+            runs: vec![(0..label.len(), highlight_id)],
+            filter_range: 0..label.len(),
+        })
+    }
+
+    async fn label_for_symbol(
+        &self,
+        label: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        let grammar = language.grammar()?;
+        match kind {
+            lsp2::SymbolKind::METHOD => {
+                let mut parts = label.split('#');
+                let classes = parts.next()?;
+                let method = parts.next()?;
+                if parts.next().is_some() {
+                    return None;
+                }
+
+                let class_id = grammar.highlight_id_for_name("type")?;
+                let method_id = grammar.highlight_id_for_name("function.method")?;
+
+                let mut ix = 0;
+                let mut runs = Vec::new();
+                for (i, class) in classes.split("::").enumerate() {
+                    if i > 0 {
+                        ix += 2;
+                    }
+                    let end_ix = ix + class.len();
+                    runs.push((ix..end_ix, class_id));
+                    ix = end_ix;
+                }
+
+                ix += 1;
+                let end_ix = ix + method.len();
+                runs.push((ix..end_ix, method_id));
+                Some(language2::CodeLabel {
+                    text: label.to_string(),
+                    runs,
+                    filter_range: 0..label.len(),
+                })
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let constant_id = grammar.highlight_id_for_name("constant")?;
+                Some(language2::CodeLabel {
+                    text: label.to_string(),
+                    runs: vec![(0..label.len(), constant_id)],
+                    filter_range: 0..label.len(),
+                })
+            }
+            lsp2::SymbolKind::CLASS | lsp2::SymbolKind::MODULE => {
+                let class_id = grammar.highlight_id_for_name("type")?;
+
+                let mut ix = 0;
+                let mut runs = Vec::new();
+                for (i, class) in label.split("::").enumerate() {
+                    if i > 0 {
+                        ix += "::".len();
+                    }
+                    let end_ix = ix + class.len();
+                    runs.push((ix..end_ix, class_id));
+                    ix = end_ix;
+                }
+
+                Some(language2::CodeLabel {
+                    text: label.to_string(),
+                    runs,
+                    filter_range: 0..label.len(),
+                })
+            }
+            _ => return None,
+        }
+    }
+}

crates/zed2/src/languages/ruby/brackets.scm 🔗

@@ -0,0 +1,14 @@
+("[" @open "]" @close)
+("{" @open "}" @close)
+("\"" @open "\"" @close)
+("do" @open "end" @close)
+
+(block_parameters "|" @open "|" @close)
+(interpolation "#{" @open "}" @close)
+
+(if "if" @open "end" @close)
+(unless "unless" @open "end" @close)
+(begin "begin" @open "end" @close)
+(module "module" @open "end" @close)
+(_ . "def" @open "end" @close)
+(_ . "class" @open "end" @close)

crates/zed2/src/languages/ruby/config.toml 🔗

@@ -0,0 +1,13 @@
+name = "Ruby"
+path_suffixes = ["rb", "Gemfile"]
+first_line_pattern = '^#!.*\bruby\b'
+line_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 = false, not_in = ["comment", "string"] },
+  { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+]
+collapsed_placeholder = "# ..."

crates/zed2/src/languages/ruby/embedding.scm 🔗

@@ -0,0 +1,22 @@
+(
+    (comment)* @context
+    .
+    [
+        (module
+            "module" @name
+            name: (_) @name)
+        (method
+            "def" @name
+            name: (_) @name
+            body: (body_statement) @collapse)
+        (class
+            "class" @name
+            name: (_) @name)
+        (singleton_method
+            "def" @name
+            object: (_) @name
+            "." @name
+            name: (_) @name
+            body: (body_statement) @collapse)
+        ] @item
+    )

crates/zed2/src/languages/ruby/highlights.scm 🔗

@@ -0,0 +1,181 @@
+; Keywords
+
+[
+  "alias"
+  "and"
+  "begin"
+  "break"
+  "case"
+  "class"
+  "def"
+  "do"
+  "else"
+  "elsif"
+  "end"
+  "ensure"
+  "for"
+  "if"
+  "in"
+  "module"
+  "next"
+  "or"
+  "rescue"
+  "retry"
+  "return"
+  "then"
+  "unless"
+  "until"
+  "when"
+  "while"
+  "yield"
+] @keyword
+
+(identifier) @variable
+
+((identifier) @keyword
+ (#match? @keyword "^(private|protected|public)$"))
+
+; Function calls
+
+((identifier) @function.method.builtin
+ (#eq? @function.method.builtin "require"))
+
+"defined?" @function.method.builtin
+
+(call
+  method: [(identifier) (constant)] @function.method)
+
+; Function definitions
+
+(alias (identifier) @function.method)
+(setter (identifier) @function.method)
+(method name: [(identifier) (constant)] @function.method)
+(singleton_method name: [(identifier) (constant)] @function.method)
+
+; Identifiers
+
+[
+  (class_variable)
+  (instance_variable)
+] @property
+
+((identifier) @constant.builtin
+ (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
+
+(file) @constant.builtin
+(line) @constant.builtin
+(encoding) @constant.builtin
+
+(hash_splat_nil
+  "**" @operator
+) @constant.builtin
+
+((constant) @constant
+ (#match? @constant "^[A-Z\\d_]+$"))
+
+(constant) @type
+
+(self) @variable.special
+(super) @variable.special
+
+; Literals
+
+[
+  (string)
+  (bare_string)
+  (subshell)
+  (heredoc_body)
+  (heredoc_beginning)
+] @string
+
+[
+  (simple_symbol)
+  (delimited_symbol)
+  (hash_key_symbol)
+  (bare_symbol)
+] @string.special.symbol
+
+(regex) @string.regex
+(escape_sequence) @escape
+
+[
+  (integer)
+  (float)
+] @number
+
+[
+  (nil)
+  (true)
+  (false)
+] @constant.builtin
+
+(comment) @comment
+
+; Operators
+
+[
+  "!"
+  "~"
+  "+"
+  "-"
+  "**"
+  "*"
+  "/"
+  "%"
+  "<<"
+  ">>"
+  "&"
+  "|"
+  "^"
+  ">"
+  "<"
+  "<="
+  ">="
+  "=="
+  "!="
+  "=~"
+  "!~"
+  "<=>"
+  "||"
+  "&&"
+  ".."
+  "..."
+  "="
+  "**="
+  "*="
+  "/="
+  "%="
+  "+="
+  "-="
+  "<<="
+  ">>="
+  "&&="
+  "&="
+  "||="
+  "|="
+  "^="
+  "=>"
+  "->"
+  (operator)
+] @operator
+
+[
+  ","
+  ";"
+  "."
+] @punctuation.delimiter
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+  "%w("
+  "%i("
+] @punctuation.bracket
+
+(interpolation
+  "#{" @punctuation.special
+  "}" @punctuation.special) @embedded

crates/zed2/src/languages/ruby/indents.scm 🔗

@@ -0,0 +1,17 @@
+(method "end" @end) @indent
+(class "end" @end) @indent
+(module "end" @end) @indent
+(begin "end" @end) @indent
+(do_block "end" @end) @indent
+
+(then) @indent
+(call) @indent
+
+(ensure) @outdent
+(rescue) @outdent
+(else) @outdent
+
+
+(_ "[" "]" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed2/src/languages/ruby/outline.scm 🔗

@@ -0,0 +1,17 @@
+(class
+    "class" @context
+    name: (_) @name) @item
+
+(method
+    "def" @context
+    name: (_) @name) @item
+
+(singleton_method
+    "def" @context
+    object: (_) @context
+    "." @context
+    name: (_) @name) @item
+
+(module
+    "module" @context
+    name: (_) @name) @item

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

@@ -0,0 +1,568 @@
+use anyhow::{anyhow, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_trait::async_trait;
+use futures::{io::BufReader, StreamExt};
+pub use language2::*;
+use lazy_static::lazy_static;
+use lsp2::LanguageServerBinary;
+use regex::Regex;
+use smol::fs::{self, File};
+use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
+
+pub struct RustLspAdapter;
+
+#[async_trait]
+impl LspAdapter for RustLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("rust-analyzer".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "rust"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("rust-analyzer/rust-analyzer", false, delegate.http_client())
+                .await?;
+        let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        Ok(Box::new(GitHubLspBinaryVersion {
+            name: release.name,
+            url: asset.browser_download_url.clone(),
+        }))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
+
+        if fs::metadata(&destination_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let mut file = File::create(&destination_path).await?;
+            futures::io::copy(decompressed_bytes, &mut file).await?;
+            fs::set_permissions(
+                &destination_path,
+                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+            )
+            .await?;
+
+            remove_matching(&container_dir, |entry| entry != destination_path).await;
+        }
+
+        Ok(LanguageServerBinary {
+            path: destination_path,
+            arguments: Default::default(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
+        vec!["rustc".into()]
+    }
+
+    async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
+        Some("rust-analyzer/flycheck".into())
+    }
+
+    fn process_diagnostics(&self, params: &mut lsp2::PublishDiagnosticsParams) {
+        lazy_static! {
+            static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
+        }
+
+        for diagnostic in &mut params.diagnostics {
+            for message in diagnostic
+                .related_information
+                .iter_mut()
+                .flatten()
+                .map(|info| &mut info.message)
+                .chain([&mut diagnostic.message])
+            {
+                if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") {
+                    *message = sanitized;
+                }
+            }
+        }
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        match completion.kind {
+            Some(lsp2::CompletionItemKind::FIELD) if completion.detail.is_some() => {
+                let detail = completion.detail.as_ref().unwrap();
+                let name = &completion.label;
+                let text = format!("{}: {}", name, detail);
+                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
+                let runs = language.highlight_text(&source, 11..11 + text.len());
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                });
+            }
+            Some(lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE)
+                if completion.detail.is_some()
+                    && completion.insert_text_format != Some(lsp2::InsertTextFormat::SNIPPET) =>
+            {
+                let detail = completion.detail.as_ref().unwrap();
+                let name = &completion.label;
+                let text = format!("{}: {}", name, detail);
+                let source = Rope::from(format!("let {} = ();", text).as_str());
+                let runs = language.highlight_text(&source, 4..4 + text.len());
+                return Some(CodeLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                });
+            }
+            Some(lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD)
+                if completion.detail.is_some() =>
+            {
+                lazy_static! {
+                    static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
+                }
+                let detail = completion.detail.as_ref().unwrap();
+                const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
+                let prefix = FUNCTION_PREFIXES
+                    .iter()
+                    .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix)));
+                // fn keyword should be followed by opening parenthesis.
+                if let Some((prefix, suffix)) = prefix {
+                    if suffix.starts_with('(') {
+                        let text = REGEX.replace(&completion.label, suffix).to_string();
+                        let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
+                        let run_start = prefix.len() + 1;
+                        let runs =
+                            language.highlight_text(&source, run_start..run_start + text.len());
+                        return Some(CodeLabel {
+                            filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
+                            text,
+                            runs,
+                        });
+                    }
+                }
+            }
+            Some(kind) => {
+                let highlight_name = match kind {
+                    lsp2::CompletionItemKind::STRUCT
+                    | lsp2::CompletionItemKind::INTERFACE
+                    | lsp2::CompletionItemKind::ENUM => Some("type"),
+                    lsp2::CompletionItemKind::ENUM_MEMBER => Some("variant"),
+                    lsp2::CompletionItemKind::KEYWORD => Some("keyword"),
+                    lsp2::CompletionItemKind::VALUE | lsp2::CompletionItemKind::CONSTANT => {
+                        Some("constant")
+                    }
+                    _ => None,
+                };
+                let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
+                let mut label = CodeLabel::plain(completion.label.clone(), None);
+                label.runs.push((
+                    0..label.text.rfind('(').unwrap_or(label.text.len()),
+                    highlight_id,
+                ));
+                return Some(label);
+            }
+            _ => {}
+        }
+        None
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => {
+                let text = format!("fn {} () {{}}", name);
+                let filter_range = 3..3 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::STRUCT => {
+                let text = format!("struct {} {{}}", name);
+                let filter_range = 7..7 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::ENUM => {
+                let text = format!("enum {} {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::INTERFACE => {
+                let text = format!("trait {} {{}}", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::CONSTANT => {
+                let text = format!("const {}: () = ();", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::MODULE => {
+                let text = format!("mod {} {{}}", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp2::SymbolKind::TYPE_PARAMETER => {
+                let text = format!("type {} {{}}", name);
+                let filter_range = 5..5 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            last = Some(entry?.path());
+        }
+
+        anyhow::Ok(LanguageServerBinary {
+            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
+            arguments: Default::default(),
+        })
+    })()
+    .await
+    .log_err()
+}
+
+#[cfg(test)]
+mod tests {
+    use std::num::NonZeroU32;
+
+    use super::*;
+    use crate::languages::language;
+    use gpui2::{Context, Hsla, TestAppContext};
+    use language2::language_settings::AllLanguageSettings;
+    use settings2::SettingsStore;
+    use theme2::SyntaxTheme;
+
+    #[gpui2::test]
+    async fn test_process_rust_diagnostics() {
+        let mut params = lsp2::PublishDiagnosticsParams {
+            uri: lsp2::Url::from_file_path("/a").unwrap(),
+            version: None,
+            diagnostics: vec![
+                // no newlines
+                lsp2::Diagnostic {
+                    message: "use of moved value `a`".to_string(),
+                    ..Default::default()
+                },
+                // newline at the end of a code span
+                lsp2::Diagnostic {
+                    message: "consider importing this struct: `use b::c;\n`".to_string(),
+                    ..Default::default()
+                },
+                // code span starting right after a newline
+                lsp2::Diagnostic {
+                    message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
+                        .to_string(),
+                    ..Default::default()
+                },
+            ],
+        };
+        RustLspAdapter.process_diagnostics(&mut params);
+
+        assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
+
+        // remove trailing newline from code span
+        assert_eq!(
+            params.diagnostics[1].message,
+            "consider importing this struct: `use b::c;`"
+        );
+
+        // do not remove newline before the start of code span
+        assert_eq!(
+            params.diagnostics[2].message,
+            "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
+        );
+    }
+
+    #[gpui2::test]
+    async fn test_rust_label_for_completion() {
+        let language = language(
+            "rust",
+            tree_sitter_rust::language(),
+            Some(Arc::new(RustLspAdapter)),
+        )
+        .await;
+        let grammar = language.grammar().unwrap();
+        let theme = SyntaxTheme::new_test([
+            ("type", Hsla::default()),
+            ("keyword", Hsla::default()),
+            ("function", Hsla::default()),
+            ("property", Hsla::default()),
+        ]);
+
+        language.set_theme(&theme);
+
+        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
+        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
+        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
+        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
+
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FUNCTION),
+                    label: "hello(…)".to_string(),
+                    detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+            })
+        );
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FUNCTION),
+                    label: "hello(…)".to_string(),
+                    detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+            })
+        );
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FIELD),
+                    label: "len".to_string(),
+                    detail: Some("usize".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "len: usize".to_string(),
+                filter_range: 0..3,
+                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
+            })
+        );
+
+        assert_eq!(
+            language
+                .label_for_completion(&lsp2::CompletionItem {
+                    kind: Some(lsp2::CompletionItemKind::FUNCTION),
+                    label: "hello(…)".to_string(),
+                    detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+            })
+        );
+    }
+
+    #[gpui2::test]
+    async fn test_rust_label_for_symbol() {
+        let language = language(
+            "rust",
+            tree_sitter_rust::language(),
+            Some(Arc::new(RustLspAdapter)),
+        )
+        .await;
+        let grammar = language.grammar().unwrap();
+        let theme = SyntaxTheme::new_test([
+            ("type", Hsla::default()),
+            ("keyword", Hsla::default()),
+            ("function", Hsla::default()),
+            ("property", Hsla::default()),
+        ]);
+
+        language.set_theme(&theme);
+
+        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
+        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
+        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
+
+        assert_eq!(
+            language
+                .label_for_symbol("hello", lsp2::SymbolKind::FUNCTION)
+                .await,
+            Some(CodeLabel {
+                text: "fn hello".to_string(),
+                filter_range: 3..8,
+                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
+            })
+        );
+
+        assert_eq!(
+            language
+                .label_for_symbol("World", lsp2::SymbolKind::TYPE_PARAMETER)
+                .await,
+            Some(CodeLabel {
+                text: "type World".to_string(),
+                filter_range: 5..10,
+                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
+            })
+        );
+    }
+
+    #[gpui2::test]
+    async fn test_rust_autoindent(cx: &mut TestAppContext) {
+        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            language2::init(cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                    s.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
+        });
+
+        let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await;
+
+        cx.build_model(|cx| {
+            let mut buffer =
+                Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx);
+
+            // indent between braces
+            buffer.set_text("fn a() {}", cx);
+            let ix = buffer.len() - 1;
+            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "fn a() {\n  \n}");
+
+            // indent between braces, even after empty lines
+            buffer.set_text("fn a() {\n\n\n}", cx);
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "fn a() {\n\n\n  \n}");
+
+            // indent a line that continues a field expression
+            buffer.set_text("fn a() {\n  \n}", cx);
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n}");
+
+            // indent further lines that continue the field expression, even after empty lines
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "fn a() {\n  b\n    .c\n    \n    .d\n}");
+
+            // dedent the line after the field expression
+            let ix = buffer.len() - 2;
+            buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(
+                buffer.text(),
+                "fn a() {\n  b\n    .c\n    \n    .d;\n  e\n}"
+            );
+
+            // indent inside a struct within a call
+            buffer.set_text("const a: B = c(D {});", cx);
+            let ix = buffer.len() - 3;
+            buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "const a: B = c(D {\n  \n});");
+
+            // indent further inside a nested call
+            let ix = buffer.len() - 4;
+            buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(buffer.text(), "const a: B = c(D {\n  e: f(\n    \n  )\n});");
+
+            // keep that indent after an empty line
+            let ix = buffer.len() - 8;
+            buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx);
+            assert_eq!(
+                buffer.text(),
+                "const a: B = c(D {\n  e: f(\n    \n    \n  )\n});"
+            );
+
+            buffer
+        });
+    }
+}

crates/zed2/src/languages/rust/config.toml 🔗

@@ -0,0 +1,13 @@
+name = "Rust"
+path_suffixes = ["rs"]
+line_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 = false, 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"] },
+]
+collapsed_placeholder = " /* ... */ "

crates/zed2/src/languages/rust/embedding.scm 🔗

@@ -0,0 +1,32 @@
+(
+    [(line_comment) (attribute_item)]* @context
+    .
+    [
+
+        (struct_item
+            name: (_) @name)
+
+        (enum_item
+            name: (_) @name)
+
+        (impl_item
+            trait: (_)? @name
+            "for"? @name
+            type: (_) @name)
+
+        (trait_item
+            name: (_) @name)
+
+        (function_item
+            name: (_) @name
+            body: (block
+                "{" @keep
+                "}" @keep) @collapse)
+
+        (macro_definition
+            name: (_) @name)
+        ] @item
+    )
+
+(attribute_item) @collapse
+(use_declaration) @collapse

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

@@ -0,0 +1,116 @@
+(type_identifier) @type
+(primitive_type) @type.builtin
+(self) @variable.special
+(field_identifier) @property
+
+(call_expression
+  function: [
+    (identifier) @function
+    (scoped_identifier
+      name: (identifier) @function)
+    (field_expression
+      field: (field_identifier) @function.method)
+  ])
+
+(generic_function
+  function: [
+    (identifier) @function
+    (scoped_identifier
+      name: (identifier) @function)
+    (field_expression
+      field: (field_identifier) @function.method)
+  ])
+
+(function_item name: (identifier) @function.definition)
+(function_signature_item name: (identifier) @function.definition)
+
+(macro_invocation
+  macro: [
+    (identifier) @function.special
+    (scoped_identifier
+      name: (identifier) @function.special)
+  ])
+
+(macro_definition
+  name: (identifier) @function.special.definition)
+
+; Identifier conventions
+
+; Assume uppercase names are types/enum-constructors
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+; Assume all-caps names are constants
+((identifier) @constant
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+
+[
+  "("
+  ")"
+  "{"
+  "}"
+  "["
+  "]"
+] @punctuation.bracket
+
+(_
+  .
+  "<" @punctuation.bracket
+  ">" @punctuation.bracket)
+
+[
+  "as"
+  "async"
+  "await"
+  "break"
+  "const"
+  "continue"
+  "default"
+  "dyn"
+  "else"
+  "enum"
+  "extern"
+  "for"
+  "fn"
+  "if"
+  "in"
+  "impl"
+  "let"
+  "loop"
+  "macro_rules!"
+  "match"
+  "mod"
+  "move"
+  "pub"
+  "ref"
+  "return"
+  "static"
+  "struct"
+  "trait"
+  "type"
+  "use"
+  "where"
+  "while"
+  "union"
+  "unsafe"
+  (mutable_specifier)
+  (super)
+] @keyword
+
+[
+  (string_literal)
+  (raw_string_literal)
+  (char_literal)
+] @string
+
+[
+  (integer_literal)
+  (float_literal)
+] @number
+
+(boolean_literal) @constant
+
+[
+  (line_comment)
+  (block_comment)
+] @comment

crates/zed2/src/languages/rust/indents.scm 🔗

@@ -0,0 +1,14 @@
+[
+    ((where_clause) _ @end)
+    (field_expression)
+    (call_expression)
+    (assignment_expression)
+    (let_declaration)
+    (let_chain)
+    (await_expression)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed2/src/languages/rust/outline.scm 🔗

@@ -0,0 +1,63 @@
+(struct_item
+    (visibility_modifier)? @context
+    "struct" @context
+    name: (_) @name) @item
+
+(enum_item
+    (visibility_modifier)? @context
+    "enum" @context
+    name: (_) @name) @item
+
+(enum_variant
+    (visibility_modifier)? @context
+    name: (_) @name) @item
+
+(impl_item
+    "impl" @context
+    trait: (_)? @name
+    "for"? @context
+    type: (_) @name) @item
+
+(trait_item
+    (visibility_modifier)? @context
+    "trait" @context
+    name: (_) @name) @item
+
+(function_item
+    (visibility_modifier)? @context
+    (function_modifiers)? @context
+    "fn" @context
+    name: (_) @name) @item
+
+(function_signature_item
+    (visibility_modifier)? @context
+    (function_modifiers)? @context
+    "fn" @context
+    name: (_) @name) @item
+
+(macro_definition
+    . "macro_rules!" @context
+    name: (_) @name) @item
+
+(mod_item
+    (visibility_modifier)? @context
+    "mod" @context
+    name: (_) @name) @item
+
+(type_item
+    (visibility_modifier)? @context
+    "type" @context
+    name: (_) @name) @item
+
+(associated_type
+    "type" @context
+    name: (_) @name) @item
+
+(const_item
+    (visibility_modifier)? @context
+    "const" @context
+    name: (_) @name) @item
+
+(field_declaration
+    (visibility_modifier)? @context
+    name: (_) @name) @item

crates/zed2/src/languages/scheme/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "Scheme"
+path_suffixes = ["scm", "ss"]
+line_comment = "; "
+autoclose_before = "])"
+brackets = [
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed2/src/languages/scheme/highlights.scm 🔗

@@ -0,0 +1,28 @@
+["(" ")" "[" "]" "{" "}"] @punctuation.bracket
+
+(number) @number
+(character) @constant.builtin
+(boolean) @constant.builtin
+
+(symbol) @variable
+(string) @string
+
+(escape_sequence) @escape
+
+[(comment)
+ (block_comment)
+ (directive)] @comment
+
+((symbol) @operator
+ (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
+
+(list
+  .
+  (symbol) @function)
+
+(list
+  .
+  (symbol) @keyword
+  (#match? @keyword
+   "^(define-syntax|let\\*|lambda|λ|case|=>|quote-splicing|unquote-splicing|set!|let|letrec|letrec-syntax|let-values|let\\*-values|do|else|define|cond|syntax-rules|unquote|begin|quote|let-syntax|and|if|quasiquote|letrec|delay|or|when|unless|identifier-syntax|assert|library|export|import|rename|only|except|prefix)$"
+   ))

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

@@ -0,0 +1,133 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str = "node_modules/svelte-language-server/bin/server.js";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct SvelteLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl SvelteLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        SvelteLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for SvelteLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("svelte-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "svelte"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("svelte-language-server")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("svelte-language-server", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &["prettier-plugin-svelte"]
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| 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(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

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

@@ -0,0 +1,20 @@
+name = "Svelte"
+path_suffixes = ["svelte"]
+line_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 = false, 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"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "svelte"
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/svelte/highlights.scm 🔗

@@ -0,0 +1,42 @@
+; Special identifiers
+;--------------------
+
+; TODO:
+(tag_name) @tag
+(attribute_name) @property
+(erroneous_end_tag_name) @keyword
+(comment) @comment
+
+[
+  (attribute_value)
+  (quoted_attribute_value)
+] @string
+
+[
+  (text)
+  (raw_text_expr)
+] @none
+
+[
+  (special_block_keyword)
+  (then)
+  (as)
+] @keyword
+
+[
+  "{"
+  "}"
+] @punctuation.bracket
+
+"=" @operator
+
+[
+  "<"
+  ">"
+  "</"
+  "/>"
+  "#"
+  ":"
+  "/"
+  "@"
+] @tag.delimiter

crates/zed2/src/languages/svelte/injections.scm 🔗

@@ -0,0 +1,28 @@
+; injections.scm
+; --------------
+(script_element
+  (raw_text) @content
+  (#set! "language" "javascript"))
+
+ ((script_element
+     (start_tag
+       (attribute
+         (quoted_attribute_value (attribute_value) @_language)))
+      (raw_text) @content)
+    (#eq? @_language "ts")
+    (#set! "language" "typescript"))
+
+((script_element
+    (start_tag
+        (attribute
+        (quoted_attribute_value (attribute_value) @_language)))
+    (raw_text) @content)
+  (#eq? @_language "typescript")
+  (#set! "language" "typescript"))
+
+(style_element
+  (raw_text) @content
+  (#set! "language" "css"))
+
+((raw_text_expr) @content
+  (#set! "language" "javascript"))

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

@@ -0,0 +1,167 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::{
+    future::{self, BoxFuture},
+    FutureExt, StreamExt,
+};
+use gpui2::AppContext;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::{json, Value};
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str = "node_modules/.bin/tailwindcss-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct TailwindLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl TailwindLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        TailwindLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for TailwindLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("tailwindcss-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "tailwind"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("@tailwindcss/language-server")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("@tailwindcss/language-server", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true,
+            "userLanguages": {
+                "html": "html",
+                "css": "css",
+                "javascript": "javascript",
+                "typescriptreact": "typescriptreact",
+            },
+        }))
+    }
+
+    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+        future::ready(json!({
+            "tailwindCSS": {
+                "emmetCompletions": true,
+            }
+        }))
+        .boxed()
+    }
+
+    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()),
+            ("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 prettier_plugins(&self) -> &[&'static str] {
+        &["prettier-plugin-tailwindcss"]
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| 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(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/toml/config.toml 🔗

@@ -0,0 +1,10 @@
+name = "TOML"
+path_suffixes = ["Cargo.lock", "toml"]
+line_comment = "# "
+autoclose_before = ",]}"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
+    { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
+]

crates/zed2/src/languages/toml/highlights.scm 🔗

@@ -0,0 +1,37 @@
+; Properties
+;-----------
+
+(bare_key) @property
+(quoted_key) @property
+
+; Literals
+;---------
+
+(boolean) @constant
+(comment) @comment
+(string) @string
+(integer) @number
+(float) @number
+(offset_date_time) @string.special
+(local_date_time) @string.special
+(local_date) @string.special
+(local_time) @string.special
+
+; Punctuation
+;------------
+
+[
+  "."
+  ","
+] @punctuation.delimiter
+
+"=" @operator
+
+[
+  "["
+  "]"
+  "[["
+  "]]"
+  "{"
+  "}"
+]  @punctuation.bracket

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

@@ -0,0 +1,25 @@
+name = "TSX"
+path_suffixes = ["tsx"]
+line_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 = false, 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"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]
+word_characters = ["#", "$"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
+prettier_parser_name = "typescript"
+
+[overrides.element]
+line_comment = { remove = true }
+block_comment = ["{/* ", " */}"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]

crates/zed2/src/languages/tsx/embedding.scm 🔗

@@ -0,0 +1,85 @@
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (function_declaration
+                "async"? @name
+                "function" @name
+                name: (_) @name))
+        (function_declaration
+            "async"? @name
+            "function" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (class_declaration
+                "class" @name
+                name: (_) @name))
+        (class_declaration
+            "class" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (interface_declaration
+                "interface" @name
+                name: (_) @name))
+        (interface_declaration
+            "interface" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (enum_declaration
+                "enum" @name
+                name: (_) @name))
+        (enum_declaration
+            "enum" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (type_alias_declaration
+                "type" @name
+                name: (_) @name))
+        (type_alias_declaration
+            "type" @name
+            name: (_) @name)
+        ] @item
+    )
+
+(
+    (comment)* @context
+    .
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "static"
+            ]* @name
+        name: (_) @name) @item
+    )

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

@@ -0,0 +1,384 @@
+use anyhow::{anyhow, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use async_trait::async_trait;
+use futures::{future::BoxFuture, FutureExt};
+use gpui2::AppContext;
+use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp2::{CodeActionKind, LanguageServerBinary};
+use node_runtime::NodeRuntime;
+use serde_json::{json, Value};
+use smol::{fs, io::BufReader, stream::StreamExt};
+use std::{
+    any::Any,
+    ffi::OsString,
+    future,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{fs::remove_matching, github::latest_github_release};
+use util::{github::GitHubLspBinaryVersion, ResultExt};
+
+fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![
+        server_path.into(),
+        "--stdio".into(),
+        "--tsserver-path".into(),
+        "node_modules/typescript/lib".into(),
+    ]
+}
+
+fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct TypeScriptLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl TypeScriptLspAdapter {
+    const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
+    const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
+
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        TypeScriptLspAdapter { node }
+    }
+}
+
+struct TypeScriptVersions {
+    typescript_version: String,
+    server_version: String,
+}
+
+#[async_trait]
+impl LspAdapter for TypeScriptLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("typescript-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "tsserver"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(TypeScriptVersions {
+            typescript_version: self.node.npm_package_latest_version("typescript").await?,
+            server_version: self
+                .node
+                .npm_package_latest_version("typescript-language-server")
+                .await?,
+        }) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<TypeScriptVersions>().unwrap();
+        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[
+                        ("typescript", version.typescript_version.as_str()),
+                        (
+                            "typescript-language-server",
+                            version.server_version.as_str(),
+                        ),
+                    ],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: typescript_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_ts_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_ts_server_binary(container_dir, &*self.node).await
+    }
+
+    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        Some(vec![
+            CodeActionKind::QUICKFIX,
+            CodeActionKind::REFACTOR,
+            CodeActionKind::REFACTOR_EXTRACT,
+            CodeActionKind::SOURCE,
+        ])
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp2::CompletionItem,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        use lsp2::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("property"),
+            _ => None,
+        }?;
+
+        let text = match &item.detail {
+            Some(detail) => format!("{} {}", item.label, detail),
+            None => item.label.clone(),
+        };
+
+        Some(language2::CodeLabel {
+            text,
+            runs: vec![(0..len, highlight_id)],
+            filter_range: 0..len,
+        })
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}
+
+async fn get_cached_ts_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
+        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
+        if new_server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: typescript_server_binary_arguments(&new_server_path),
+            })
+        } else if old_server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: typescript_server_binary_arguments(&old_server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                container_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
+pub struct EsLintLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl EsLintLspAdapter {
+    const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
+
+    #[allow(unused)]
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        EsLintLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for EsLintLspAdapter {
+    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+        future::ready(json!({
+            "": {
+                "validate": "on",
+                "rulesCustomizations": [],
+                "run": "onType",
+                "nodePath": null,
+            }
+        }))
+        .boxed()
+    }
+
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("eslint".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "eslint"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        // At the time of writing the latest vscode-eslint release was released in 2020 and requires
+        // special custom LSP protocol extensions be handled to fully initialize. Download the latest
+        // prerelease instead to sidestep this issue
+        let release =
+            latest_github_release("microsoft/vscode-eslint", true, delegate.http_client()).await?;
+        Ok(Box::new(GitHubLspBinaryVersion {
+            name: release.name,
+            url: release.tarball_url,
+        }))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
+        let server_path = destination_path.join(Self::SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            remove_matching(&container_dir, |entry| entry != destination_path).await;
+
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(&destination_path).await?;
+
+            let mut dir = fs::read_dir(&destination_path).await?;
+            let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
+            let repo_root = destination_path.join("vscode-eslint");
+            fs::rename(first.path(), &repo_root).await?;
+
+            self.node
+                .run_npm_subcommand(Some(&repo_root), "install", &[])
+                .await?;
+
+            self.node
+                .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: eslint_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_eslint_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_eslint_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn label_for_completion(
+        &self,
+        _item: &lsp2::CompletionItem,
+        _language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        None
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        None
+    }
+}
+
+async fn get_cached_eslint_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        // This is unfortunate but we don't know what the version is to build a path directly
+        let mut dir = fs::read_dir(&container_dir).await?;
+        let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
+        if !first.file_type().await?.is_dir() {
+            return Err(anyhow!("First entry is not a directory"));
+        }
+        let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
+
+        Ok(LanguageServerBinary {
+            path: node.binary_path().await?,
+            arguments: eslint_server_binary_arguments(&server_path),
+        })
+    })()
+    .await
+    .log_err()
+}
+
+#[cfg(test)]
+mod tests {
+    use gpui2::{Context, TestAppContext};
+    use unindent::Unindent;
+
+    #[gpui2::test]
+    async fn test_outline(cx: &mut TestAppContext) {
+        let language = crate::languages::language(
+            "typescript",
+            tree_sitter_typescript::language_typescript(),
+            None,
+        )
+        .await;
+
+        let text = r#"
+            function a() {
+              // local variables are omitted
+              let a1 = 1;
+              // all functions are included
+              async function a2() {}
+            }
+            // top-level variables are included
+            let b: C
+            function getB() {}
+            // exported variables are included
+            export const d = e;
+        "#
+        .unindent();
+
+        let buffer = cx.build_model(|cx| {
+            language2::Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, 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<_>>(),
+            &[
+                ("function a()", 0),
+                ("async function a2()", 1),
+                ("let b", 0),
+                ("function getB()", 0),
+                ("const d", 0),
+            ]
+        );
+    }
+}

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

@@ -0,0 +1,16 @@
+name = "TypeScript"
+path_suffixes = ["ts", "cts", "d.cts", "d.mts", "mts"]
+line_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 = false, 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"] },
+    { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
+]
+word_characters = ["#", "$"]
+prettier_parser_name = "typescript"

crates/zed2/src/languages/typescript/embedding.scm 🔗

@@ -0,0 +1,85 @@
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (function_declaration
+                "async"? @name
+                "function" @name
+                name: (_) @name))
+        (function_declaration
+            "async"? @name
+            "function" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (class_declaration
+                "class" @name
+                name: (_) @name))
+        (class_declaration
+            "class" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (interface_declaration
+                "interface" @name
+                name: (_) @name))
+        (interface_declaration
+            "interface" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (enum_declaration
+                "enum" @name
+                name: (_) @name))
+        (enum_declaration
+            "enum" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    [
+        (export_statement
+            (type_alias_declaration
+                "type" @name
+                name: (_) @name))
+        (type_alias_declaration
+            "type" @name
+            name: (_) @name)
+    ] @item
+)
+
+(
+    (comment)* @context
+    .
+    (method_definition
+        [
+            "get"
+            "set"
+            "async"
+            "*"
+            "static"
+            ]* @name
+        name: (_) @name) @item
+)

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

@@ -0,0 +1,221 @@
+; Variables
+
+(identifier) @variable
+
+; Properties
+
+(property_identifier) @property
+
+; Function and method calls
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (member_expression
+    property: (property_identifier) @function.method))
+
+; Function and method definitions
+
+(function
+  name: (identifier) @function)
+(function_declaration
+  name: (identifier) @function)
+(method_definition
+  name: (property_identifier) @function.method)
+
+(pair
+  key: (property_identifier) @function.method
+  value: [(function) (arrow_function)])
+
+(assignment_expression
+  left: (member_expression
+    property: (property_identifier) @function.method)
+  right: [(function) (arrow_function)])
+
+(variable_declarator
+  name: (identifier) @function
+  value: [(function) (arrow_function)])
+
+(assignment_expression
+  left: (identifier) @function
+  right: [(function) (arrow_function)])
+
+; Special identifiers
+
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+(type_identifier) @type
+(predefined_type) @type.builtin
+
+([
+  (identifier)
+  (shorthand_property_identifier)
+  (shorthand_property_identifier_pattern)
+ ] @constant
+ (#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
+
+; Literals
+
+(this) @variable.special
+(super) @variable.special
+
+[
+  (null)
+  (undefined)
+] @constant.builtin
+
+[
+  (true)
+  (false)
+] @boolean
+
+(comment) @comment
+
+[
+  (string)
+  (template_string)
+] @string
+
+(regex) @string.regex
+(number) @number
+
+; Tokens
+
+[
+  ";"
+  "?."
+  "."
+  ","
+  ":"
+] @punctuation.delimiter
+
+[
+  "-"
+  "--"
+  "-="
+  "+"
+  "++"
+  "+="
+  "*"
+  "*="
+  "**"
+  "**="
+  "/"
+  "/="
+  "%"
+  "%="
+  "<"
+  "<="
+  "<<"
+  "<<="
+  "="
+  "=="
+  "==="
+  "!"
+  "!="
+  "!=="
+  "=>"
+  ">"
+  ">="
+  ">>"
+  ">>="
+  ">>>"
+  ">>>="
+  "~"
+  "^"
+  "&"
+  "|"
+  "^="
+  "&="
+  "|="
+  "&&"
+  "||"
+  "??"
+  "&&="
+  "||="
+  "??="
+] @operator
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+]  @punctuation.bracket
+
+[
+  "as"
+  "async"
+  "await"
+  "break"
+  "case"
+  "catch"
+  "class"
+  "const"
+  "continue"
+  "debugger"
+  "default"
+  "delete"
+  "do"
+  "else"
+  "export"
+  "extends"
+  "finally"
+  "for"
+  "from"
+  "function"
+  "get"
+  "if"
+  "import"
+  "in"
+  "instanceof"
+  "let"
+  "new"
+  "of"
+  "return"
+  "satisfies"
+  "set"
+  "static"
+  "switch"
+  "target"
+  "throw"
+  "try"
+  "typeof"
+  "var"
+  "void"
+  "while"
+  "with"
+  "yield"
+] @keyword
+
+(template_substitution
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+(type_arguments
+  "<" @punctuation.bracket
+  ">" @punctuation.bracket)
+
+; Keywords
+
+[ "abstract"
+  "declare"
+  "enum"
+  "export"
+  "implements"
+  "interface"
+  "keyof"
+  "namespace"
+  "private"
+  "protected"
+  "public"
+  "type"
+  "readonly"
+  "override"
+] @keyword

crates/zed2/src/languages/typescript/indents.scm 🔗

@@ -0,0 +1,15 @@
+[
+    (call_expression)
+    (assignment_expression)
+    (member_expression)
+    (lexical_declaration)
+    (variable_declaration)
+    (assignment_expression)
+    (if_statement)
+    (for_statement)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

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

@@ -0,0 +1,65 @@
+(internal_module
+    "namespace" @context
+    name: (_) @name) @item
+
+(enum_declaration
+    "enum" @context
+    name: (_) @name) @item
+
+(type_alias_declaration
+    "type" @context
+    name: (_) @name) @item
+
+(function_declaration
+    "async"? @context
+    "function" @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item
+
+(interface_declaration
+    "interface" @context
+    name: (_) @name) @item
+
+(export_statement
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (_) @name) @item))
+
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (_) @name) @item))
+
+(class_declaration
+    "class" @context
+    name: (_) @name) @item
+
+(method_definition
+    [
+        "get"
+        "set"
+        "async"
+        "*"
+        "readonly"
+        "static"
+        (override_modifier)
+        (accessibility_modifier)
+    ]* @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item
+
+(public_field_definition
+    [
+        "declare"
+        "readonly"
+        "abstract"
+        "static"
+        (accessibility_modifier)
+    ]* @context
+    name: (_) @name) @item

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

@@ -0,0 +1,220 @@
+use anyhow::{anyhow, ensure, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+pub use language2::*;
+use lsp2::{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: &lsp2::CompletionItem,
+        language: &Arc<language2::Language>,
+    ) -> Option<language2::CodeLabel> {
+        use lsp2::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(language2::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/zed2/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/zed2/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/zed2/src/languages/yaml.rs 🔗

@@ -0,0 +1,142 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use gpui2::AppContext;
+use language2::{
+    language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
+};
+use lsp2::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::Value;
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    future,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct YamlLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl YamlLspAdapter {
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        YamlLspAdapter { node }
+    }
+}
+
+#[async_trait]
+impl LspAdapter for YamlLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("yaml-language-server".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "yaml"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("yaml-language-server")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[("yaml-language-server", version.as_str())],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &*self.node).await
+    }
+    fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
+        let tab_size = all_language_settings(None, cx)
+            .language(Some("YAML"))
+            .tab_size;
+
+        future::ready(serde_json::json!({
+            "yaml": {
+                "keyOrdering": false
+            },
+            "[yaml]": {
+                "editor.tabSize": tab_size,
+            }
+        }))
+        .boxed()
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| 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(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed2/src/languages/yaml/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "YAML"
+path_suffixes = ["yml", "yaml"]
+line_comment = "# "
+autoclose_before = ",]}"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]
+
+increase_indent_pattern = ":\\s*[|>]?\\s*$"
+prettier_parser_name = "yaml"

crates/zed2/src/languages/yaml/highlights.scm 🔗

@@ -0,0 +1,49 @@
+(boolean_scalar) @boolean
+(null_scalar) @constant.builtin
+
+[
+  (double_quote_scalar)
+  (single_quote_scalar)
+  (block_scalar)
+  (string_scalar)
+] @string
+
+(escape_sequence) @string.escape
+
+[
+  (integer_scalar)
+  (float_scalar)
+] @number
+
+(comment) @comment
+
+[
+  (anchor_name)
+  (alias_name)
+  (tag) 
+] @type
+
+key: (flow_node (plain_scalar (string_scalar) @property)) 
+
+[
+ ","
+ "-"
+ ":"
+ ">"
+ "?"
+ "|"
+] @punctuation.delimiter
+
+[
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "*"
+ "&"
+ "---"
+ "..."
+] @punctuation.special

crates/zed2/src/main.rs 🔗

@@ -45,12 +45,8 @@ use util::{
     paths, ResultExt,
 };
 use uuid::Uuid;
+use zed2::languages;
 use zed2::{ensure_only_instance, AppState, Assets, IsOnlyInstance};
-// use zed2::{
-//     assets::Assets,
-//     build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
-//     only_instance::{ensure_only_instance, IsOnlyInstance},
-// };
 
 mod open_listener;
 
@@ -117,10 +113,12 @@ fn main() {
         let copilot_language_server_id = languages.next_language_server_id();
         languages.set_executor(cx.executor().clone());
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
+        let languages = Arc::new(languages);
         let node_runtime = RealNodeRuntime::new(http.clone());
 
         language2::init(cx);
-        let user_store = cx.entity(|cx| UserStore::new(client.clone(), http.clone(), cx));
+        languages::init(languages.clone(), node_runtime.clone(), cx);
+        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
         // let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
         cx.set_global(client.clone());
@@ -188,7 +186,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);

crates/zed2/src/zed2.rs 🔗

@@ -1,10 +1,11 @@
 mod assets;
+pub mod languages;
 mod only_instance;
 mod open_listener;
 
 pub use assets::*;
 use client2::{Client, UserStore};
-use gpui2::{AsyncAppContext, Handle};
+use gpui2::{AsyncAppContext, Model};
 pub use only_instance::*;
 pub use open_listener::*;
 
@@ -47,7 +48,7 @@ pub fn connect_to_cli(
 
 pub struct AppState {
     pub client: Arc<Client>,
-    pub user_store: Handle<UserStore>,
+    pub user_store: Model<UserStore>,
 }
 
 pub async fn handle_cli_connection(

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-2-progress-report.py 🔗

@@ -0,0 +1,27 @@
+import os
+from pathlib import Path
+
+THIS_SCRIPT_PATH: Path = Path(__file__)
+CRATES_DIR: Path = THIS_SCRIPT_PATH.parent.parent / "crates"
+
+zed_1_crate_count: int = 0
+zed_2_crate_count: int = 0
+
+for child in os.listdir(CRATES_DIR):
+    child_path: str = os.path.join(CRATES_DIR, child)
+
+    if not os.path.isdir(child_path):
+        continue
+
+    if child.endswith("2"):
+        zed_2_crate_count += 1
+    else:
+        zed_1_crate_count += 1
+
+print(f"crates ported: {zed_2_crate_count}")
+print(f"crates in total: {zed_1_crate_count}")
+
+percent_complete: float = (zed_2_crate_count / zed_1_crate_count) * 100
+percent_complete_rounded: float = round(percent_complete, 2)
+
+print(f"progress: {percent_complete_rounded}%")

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: {