zed2 (#3174)

Max Brunsfeld created

PR to get zed2 into main.

Because we have taken the approach of porting crates by renaming them to
`-2` we will need to manually reapply any changes that were made to
ported crates since the `zed2` branch diverged from master.

I think this is the list of PRs that may need changes ported manually.
Any changes to the following crates may need to be moved from crate `x`
to `x2` for each of the following crates: `audio call client copilot db
feature_flags fs fuzzy gpui install_cli language lsp prettier project
rpc settings storybook terminal theme ui zed`.

- [x] f75eb3f62 Conrad Irwin (origin/main, origin/HEAD, main) Merge
branch 'more-signing' (17 hours ago)
- [x] 832026a0a Julia Limit language server reinstallation attempts
(#3177) (18 hours ago)
- [x] 4539cef6d Julia Capture language server stderr during startup/init
and log if failure (#3175) (21 hours ago)
- [x] e6f2288a0 Conrad Irwin Don't use function_name in vim tests
(#3171) (2 days ago)
- [x] f67f42779 Mikayla Maki Rename IIFE to maybe (#3165) (2 days ago)
- [ ] 90f65ec9f Max Brunsfeld Remove logic for multiple channel parents
(#3162) (2 days ago)
- [ ] 4f859e025 Conrad Irwin link to channel notes (#3167) (2 days ago)
- [ ] b8bd070a8 Conrad Irwin Fix panic by disallowing multiple room
joins (#3149) (3 days ago)
- [ ] cc9e92857 Max Brunsfeld Guest roles (#3140) (3 days ago)
- [x] b090cefdd Kirill Bulatov Rework prettier tests (#3160) (3 days
ago)
- [ ] ff497810d Kyle Caverly move keychain access into semantic index as
opposed to on init (#3158) (3 days ago)
- [x] 2b95db087 Conrad Irwin Fix infinite loop in select all (#3154) (3
days ago)
- [ ] a5836b033 Max Brunsfeld Add chat mentions and a notifications
panel (#3121) (4 days ago)
- [ ] ef1a69156 Kyle Caverly update semantic search to use keychain as
fallback (#3151) (6 days ago)
- [x] 26638748b Kirill Bulatov Move prettier parsers data into languages
from LSP adapters (#3150) (6 days ago)
- [ ] 0dae0f602 Conrad Irwin pixel columns (#3052) (7 days ago)
- [x] cc7df91cc Julia Whoops (#3146) (7 days ago)
- [x] 808976ee2 Julia Magic incantations for Tailwind autocomplete in
more languages (#3141) (7 days ago)
- [ ] cc390ba86 Conrad Irwin Start writing role to database (#3120) (10
days ago)
- [ ] 2795091f0 Kyle Caverly Introduce Context Retrieval in Inline
Assistant (#3097) (10 days ago)
- [x] b168bded1 Conrad Irwin New entitlements: (#3118) (10 days ago)
- [x] 247cdb1e1 Joseph T. Lyons Fix telemetry-related crash on start up
(#3131) (11 days ago)
- [ ] 2323fd17b Julia Autocomplete docs (#3126) (2 weeks ago)
- [x] 16d9d77d8 Kirill Bulatov Update diagnostics indicator when
diagnostics are udpated (#3128) (2 weeks ago)
- [ ] 634202340 Kirill Bulatov Remove zed -> ... -> semantic_index ->
zed Cargo dependency cycle (#3127) (2 weeks ago)

Note: this list does not include any PRs that did not change crates that
have been converted; it also does not include any commits that were
pushed directly to master.

### To figure out what needs migrating, run:

```
git diff COMMIT^..COMMIT -- crates/audio crates/call crates/client crates/copilot crates/db crates/feature_flags crates/fs crates/fuzzy crates/gpui crates/install_cli crates/language crates/lsp crates/prettier crates/project crates/rpc crates/settings crates/storybook crates/terminal crates/theme crates/ui crates/zed
```

Change summary

Cargo.lock                                                   |  780 
Cargo.toml                                                   |   25 
crates/Cargo.toml                                            |   38 
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/audio2/Cargo.toml                                     |   24 
crates/audio2/audio/Cargo.toml                               |   23 
crates/audio2/audio/src/assets.rs                            |   44 
crates/audio2/audio/src/audio.rs                             |   81 
crates/audio2/src/assets.rs                                  |   44 
crates/audio2/src/audio2.rs                                  |  111 
crates/call/src/room.rs                                      |    4 
crates/call2/Cargo.toml                                      |   52 
crates/call2/src/call2.rs                                    |  461 
crates/call2/src/call_settings.rs                            |   32 
crates/call2/src/participant.rs                              |   71 
crates/call2/src/room.rs                                     | 1622 +
crates/client2/Cargo.toml                                    |   52 
crates/client2/src/client2.rs                                | 1651 +
crates/client2/src/telemetry.rs                              |  333 
crates/client2/src/test.rs                                   |  216 
crates/client2/src/user.rs                                   |  739 
crates/copilot2/Cargo.toml                                   |   50 
crates/copilot2/src/copilot2.rs                              | 1234 
crates/copilot2/src/request.rs                               |  225 
crates/copilot2/src/sign_in.rs                               |  376 
crates/db2/Cargo.toml                                        |   33 
crates/db2/README.md                                         |    5 
crates/db2/src/db2.rs                                        |  327 
crates/db2/src/kvp.rs                                        |   62 
crates/db2/src/query.rs                                      |  314 
crates/feature_flags2/Cargo.toml                             |   12 
crates/feature_flags2/src/feature_flags2.rs                  |   80 
crates/fs2/Cargo.toml                                        |   40 
crates/fs2/src/fs2.rs                                        | 1278 
crates/fs2/src/repository.rs                                 |  417 
crates/fuzzy2/Cargo.toml                                     |   13 
crates/fuzzy2/src/char_bag.rs                                |   63 
crates/fuzzy2/src/fuzzy2.rs                                  |   10 
crates/fuzzy2/src/matcher.rs                                 |  464 
crates/fuzzy2/src/paths.rs                                   |  257 
crates/fuzzy2/src/strings.rs                                 |  159 
crates/gpui/src/app.rs                                       |    2 
crates/gpui/src/fonts.rs                                     |    5 
crates/gpui/src/image_cache.rs                               |    1 
crates/gpui/src/platform/mac/atlas.rs                        |    1 
crates/gpui/src/platform/mac/renderer.rs                     |    1 
crates/gpui/src/platform/mac/window.rs                       |    1 
crates/gpui/src/text_layout.rs                               |   34 
crates/gpui2/Cargo.toml                                      |   83 
crates/gpui2/build.rs                                        |  134 
crates/gpui2/src/action.rs                                   |  432 
crates/gpui2/src/adapter.rs                                  |   76 
crates/gpui2/src/app.rs                                      |  919 
crates/gpui2/src/app/async_context.rs                        |  252 
crates/gpui2/src/app/entity_map.rs                           |  501 
crates/gpui2/src/app/model_context.rs                        |  266 
crates/gpui2/src/app/test_context.rs                         |  152 
crates/gpui2/src/assets.rs                                   |   64 
crates/gpui2/src/color.rs                                    |  267 
crates/gpui2/src/element.rs                                  |  372 
crates/gpui2/src/elements.rs                                 |   15 
crates/gpui2/src/elements/div.rs                             |  530 
crates/gpui2/src/elements/hoverable.rs                       |  105 
crates/gpui2/src/elements/img.rs                             |  206 
crates/gpui2/src/elements/pressable.rs                       |  113 
crates/gpui2/src/elements/svg.rs                             |  169 
crates/gpui2/src/elements/text.rs                            |  176 
crates/gpui2/src/executor.rs                                 |  290 
crates/gpui2/src/focusable.rs                                |  252 
crates/gpui2/src/geometry.rs                                 | 1244 
crates/gpui2/src/gpui2.rs                                    |  376 
crates/gpui2/src/image_cache.rs                              |   99 
crates/gpui2/src/interactive.rs                              | 1116 
crates/gpui2/src/keymap/binding.rs                           |   80 
crates/gpui2/src/keymap/keymap.rs                            |  398 
crates/gpui2/src/keymap/matcher.rs                           |  473 
crates/gpui2/src/keymap/mod.rs                               |    7 
crates/gpui2/src/platform.rs                                 |  497 
crates/gpui2/src/platform/keystroke.rs                       |  151 
crates/gpui2/src/platform/mac.rs                             |  165 
crates/gpui2/src/platform/mac/dispatch.h                     |    1 
crates/gpui2/src/platform/mac/dispatcher.rs                  |  100 
crates/gpui2/src/platform/mac/display.rs                     |  101 
crates/gpui2/src/platform/mac/display_linker.rs              |  274 
crates/gpui2/src/platform/mac/events.rs                      |  357 
crates/gpui2/src/platform/mac/metal_atlas.rs                 |  256 
crates/gpui2/src/platform/mac/metal_renderer.rs              |  880 
crates/gpui2/src/platform/mac/open_type.rs                   |  394 
crates/gpui2/src/platform/mac/platform.rs                    | 1149 
crates/gpui2/src/platform/mac/shaders.metal                  |  553 
crates/gpui2/src/platform/mac/text_system.rs                 |  752 
crates/gpui2/src/platform/mac/window.rs                      | 1755 +
crates/gpui2/src/platform/mac/window_appearence.rs           |   35 
crates/gpui2/src/platform/test.rs                            |    5 
crates/gpui2/src/platform/test/dispatcher.rs                 |  244 
crates/gpui2/src/platform/test/platform.rs                   |  186 
crates/gpui2/src/scene.rs                                    |  829 
crates/gpui2/src/style.rs                                    |  558 
crates/gpui2/src/styled.rs                                   |  576 
crates/gpui2/src/subscription.rs                             |  114 
crates/gpui2/src/svg_renderer.rs                             |   46 
crates/gpui2/src/taffy.rs                                    |  435 
crates/gpui2/src/test.rs                                     |   51 
crates/gpui2/src/text_system.rs                              |  537 
crates/gpui2/src/text_system/font_features.rs                |  162 
crates/gpui2/src/text_system/line.rs                         |  154 
crates/gpui2/src/text_system/line_layout.rs                  |  295 
crates/gpui2/src/text_system/line_wrapper.rs                 |  282 
crates/gpui2/src/util.rs                                     |   41 
crates/gpui2/src/view.rs                                     |  412 
crates/gpui2/src/view_context.rs                             |   79 
crates/gpui2/src/window.rs                                   | 2022 +
crates/gpui2_macros/src/derive_component.rs                  |   66 
crates/gpui2_macros/src/derive_element.rs                    |   93 
crates/gpui2_macros/src/derive_into_element.rs               |   69 
crates/gpui2_macros/src/gpui2_macros.rs                      |   22 
crates/gpui2_macros/src/style_helpers.rs                     |  561 
crates/gpui2_macros/src/styleable_helpers.rs                 |  408 
crates/gpui2_macros/src/test.rs                              |  239 
crates/install_cli2/Cargo.toml                               |   18 
crates/install_cli2/src/install_cli2.rs                      |   57 
crates/journal2/Cargo.toml                                   |   27 
crates/journal2/src/journal2.rs                              |  176 
crates/language2/Cargo.toml                                  |   85 
crates/language2/build.rs                                    |    5 
crates/language2/src/buffer.rs                               | 3098 ++
crates/language2/src/buffer_tests.rs                         | 2446 +
crates/language2/src/diagnostic_set.rs                       |  236 
crates/language2/src/highlight_map.rs                        |  111 
crates/language2/src/language2.rs                            | 1959 +
crates/language2/src/language_settings.rs                    |  431 
crates/language2/src/outline.rs                              |  138 
crates/language2/src/proto.rs                                |  589 
crates/language2/src/syntax_map.rs                           | 1813 +
crates/language2/src/syntax_map/syntax_map_tests.rs          | 1323 
crates/live_kit_client/examples/test_app.rs                  |    8 
crates/live_kit_client/src/live_kit_client.rs                |    1 
crates/live_kit_client/src/prod.rs                           |   19 
crates/live_kit_client/src/test.rs                           |    4 
crates/lsp2/Cargo.toml                                       |   38 
crates/lsp2/src/lsp2.rs                                      | 1179 
crates/menu2/Cargo.toml                                      |   12 
crates/menu2/src/menu2.rs                                    |   25 
crates/node_runtime/Cargo.toml                               |    1 
crates/prettier2/Cargo.toml                                  |   35 
crates/prettier2/src/prettier2.rs                            |  468 
crates/prettier2/src/prettier_server.js                      |  217 
crates/project2/Cargo.toml                                   |   84 
crates/project2/src/ignore.rs                                |   57 
crates/project2/src/lsp_command.rs                           | 2350 +
crates/project2/src/project2.rs                              | 8975 ++++++
crates/project2/src/project_settings.rs                      |   48 
crates/project2/src/project_tests.rs                         | 4077 ++
crates/project2/src/search.rs                                |  458 
crates/project2/src/terminals.rs                             |  129 
crates/project2/src/worktree.rs                              | 4387 ++
crates/project2/src/worktree_tests.rs                        | 2141 +
crates/refineable/derive_refineable/src/derive_refineable.rs |  131 
crates/refineable/src/refineable.rs                          |   18 
crates/rpc2/Cargo.toml                                       |   44 
crates/rpc2/build.rs                                         |    8 
crates/rpc2/proto/zed.proto                                  | 1559 +
crates/rpc2/src/auth.rs                                      |  136 
crates/rpc2/src/conn.rs                                      |  108 
crates/rpc2/src/macros.rs                                    |   70 
crates/rpc2/src/peer.rs                                      |  933 
crates/rpc2/src/proto.rs                                     |  674 
crates/rpc2/src/rpc.rs                                       |    9 
crates/settings2/Cargo.toml                                  |   42 
crates/settings2/src/keymap_file.rs                          |  163 
crates/settings2/src/settings2.rs                            |   38 
crates/settings2/src/settings_file.rs                        |  116 
crates/settings2/src/settings_store.rs                       | 1301 
crates/storybook2/Cargo.lock                                 | 2919 +
crates/storybook2/Cargo.toml                                 |   32 
crates/storybook2/build.rs                                   |    5 
crates/storybook2/docs/thoughts.md                           |   72 
crates/storybook2/src/assets.rs                              |   30 
crates/storybook2/src/components.rs                          |   97 
crates/storybook2/src/stories.rs                             |   13 
crates/storybook2/src/stories/colors.rs                      |   38 
crates/storybook2/src/stories/focus.rs                       |  116 
crates/storybook2/src/stories/kitchen_sink.rs                |   40 
crates/storybook2/src/stories/scroll.rs                      |   54 
crates/storybook2/src/stories/text.rs                        |   21 
crates/storybook2/src/stories/z_index.rs                     |  175 
crates/storybook2/src/story.rs                               |    1 
crates/storybook2/src/story_selector.rs                      |  184 
crates/storybook2/src/storybook2.rs                          |  126 
crates/terminal2/Cargo.toml                                  |   38 
crates/terminal2/src/mappings/colors.rs                      |  137 
crates/terminal2/src/mappings/keys.rs                        |  464 
crates/terminal2/src/mappings/mod.rs                         |    3 
crates/terminal2/src/mappings/mouse.rs                       |  277 
crates/terminal2/src/terminal2.rs                            | 1528 +
crates/terminal2/src/terminal_settings.rs                    |  164 
crates/theme2/Cargo.toml                                     |   36 
crates/theme2/src/default.rs                                 | 2118 +
crates/theme2/src/registry.rs                                |   84 
crates/theme2/src/scale.rs                                   |  164 
crates/theme2/src/settings.rs                                |  194 
crates/theme2/src/theme2.rs                                  |  155 
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                              |   79 
crates/theme2/src/themes/one_dark.rs                         |  131 
crates/theme2/src/themes/one_light.rs                        |  131 
crates/theme2/src/themes/rose_pine.rs                        |  132 
crates/theme2/src/themes/rose_pine_dawn.rs                   |  132 
crates/theme2/src/themes/rose_pine_moon.rs                   |  132 
crates/theme2/src/themes/sandcastle.rs                       |  130 
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                            |   18 
crates/theme_converter/src/main.rs                           |  390 
crates/theme_converter/src/theme_printer.rs                  |  174 
crates/ui2/Cargo.toml                                        |   20 
crates/ui2/docs/elevation.md                                 |   57 
crates/ui2/src/components.rs                                 |   71 
crates/ui2/src/components/assistant_panel.rs                 |   93 
crates/ui2/src/components/breadcrumb.rs                      |  119 
crates/ui2/src/components/buffer.rs                          |  271 
crates/ui2/src/components/buffer_search.rs                   |   45 
crates/ui2/src/components/chat_panel.rs                      |  151 
crates/ui2/src/components/collab_panel.rs                    |  108 
crates/ui2/src/components/command_palette.rs                 |   48 
crates/ui2/src/components/context_menu.rs                    |   91 
crates/ui2/src/components/copilot.rs                         |   46 
crates/ui2/src/components/editor_pane.rs                     |   77 
crates/ui2/src/components/facepile.rs                        |   59 
crates/ui2/src/components/icon_button.rs                     |  108 
crates/ui2/src/components/keybinding.rs                      |  224 
crates/ui2/src/components/language_selector.rs               |   57 
crates/ui2/src/components/list.rs                            |  583 
crates/ui2/src/components/modal.rs                           |   83 
crates/ui2/src/components/multi_buffer.rs                    |   67 
crates/ui2/src/components/notification_toast.rs              |   41 
crates/ui2/src/components/notifications_panel.rs             |   69 
crates/ui2/src/components/palette.rs                         |  222 
crates/ui2/src/components/panel.rs                           |  154 
crates/ui2/src/components/panes.rs                           |  130 
crates/ui2/src/components/player_stack.rs                    |   63 
crates/ui2/src/components/project_panel.rs                   |   79 
crates/ui2/src/components/recent_projects.rs                 |   53 
crates/ui2/src/components/status_bar.rs                      |  205 
crates/ui2/src/components/tab.rs                             |  275 
crates/ui2/src/components/tab_bar.rs                         |  144 
crates/ui2/src/components/terminal.rs                        |  101 
crates/ui2/src/components/theme_selector.rs                  |   60 
crates/ui2/src/components/title_bar.rs                       |  215 
crates/ui2/src/components/toast.rs                           |   93 
crates/ui2/src/components/toolbar.rs                         |  130 
crates/ui2/src/components/traffic_lights.rs                  |  100 
crates/ui2/src/components/workspace.rs                       |  380 
crates/ui2/src/elements.rs                                   |   19 
crates/ui2/src/elements/avatar.rs                            |   69 
crates/ui2/src/elements/button.rs                            |  383 
crates/ui2/src/elements/details.rs                           |   79 
crates/ui2/src/elements/icon.rs                              |  215 
crates/ui2/src/elements/input.rs                             |  131 
crates/ui2/src/elements/label.rs                             |  221 
crates/ui2/src/elements/player.rs                            |  158 
crates/ui2/src/elements/stack.rs                             |   31 
crates/ui2/src/elements/tool_divider.rs                      |   16 
crates/ui2/src/elevation.md                                  |   85 
crates/ui2/src/elevation.rs                                  |   70 
crates/ui2/src/lib.rs                                        |   44 
crates/ui2/src/prelude.rs                                    |  232 
crates/ui2/src/settings.rs                                   |   76 
crates/ui2/src/static_data.rs                                | 1019 
crates/ui2/src/story.rs                                      |   44 
crates/util/src/arc_cow.rs                                   |   48 
crates/util/src/util.rs                                      |   15 
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/src/lib.rs                                |   26 
crates/zed2/Cargo.toml                                       |  182 
crates/zed2/build.rs                                         |   24 
crates/zed2/contents/dev/embedded.provisionprofile           |    0 
crates/zed2/contents/preview/embedded.provisionprofile       |    0 
crates/zed2/contents/stable/embedded.provisionprofile        |    0 
crates/zed2/resources/app-icon-preview.png                   |    0 
crates/zed2/resources/app-icon-preview@2x.png                |    0 
crates/zed2/resources/app-icon.png                           |    0 
crates/zed2/resources/app-icon@2x.png                        |    0 
crates/zed2/resources/info/DocumentTypes.plist               |   62 
crates/zed2/resources/info/Permissions.plist                 |   24 
crates/zed2/resources/zed.entitlements                       |   24 
crates/zed2/src/assets.rs                                    |   33 
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                                      |  923 
crates/zed2/src/only_instance.rs                             |  104 
crates/zed2/src/open_listener.rs                             |   98 
crates/zed2/src/zed2.rs                                      |  208 
script/zed-2-progress-report.py                              |   27 
theme.txt                                                    |  254 
526 files changed, 124,095 insertions(+), 2,234 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -108,6 +108,33 @@ dependencies = [
  "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",
+]
+
 [[package]]
 name = "alacritty_config"
 version = "0.1.2-dev"
@@ -659,6 +686,20 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "audio2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "futures 0.3.28",
+ "gpui2",
+ "log",
+ "parking_lot 0.11.2",
+ "rodio",
+ "util",
+]
+
 [[package]]
 name = "auto_update"
 version = "0.1.0"
@@ -776,6 +817,17 @@ dependencies = [
  "rustc-demangle",
 ]
 
+[[package]]
+name = "backtrace-on-stack-overflow"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b"
+dependencies = [
+ "backtrace",
+ "libc",
+ "nix 0.23.2",
+]
+
 [[package]]
 name = "base64"
 version = "0.13.1"
@@ -1104,6 +1156,32 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "call2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-broadcast",
+ "audio2",
+ "client2",
+ "collections",
+ "fs2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "live_kit_client",
+ "log",
+ "media",
+ "postage",
+ "project2",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "util",
+]
+
 [[package]]
 name = "cap-fs-ext"
 version = "0.24.4"
@@ -1176,6 +1254,25 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
 
+[[package]]
+name = "cbindgen"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49"
+dependencies = [
+ "clap 3.2.25",
+ "heck 0.4.1",
+ "indexmap 1.9.3",
+ "log",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn 1.0.109",
+ "tempfile",
+ "toml 0.5.11",
+]
+
 [[package]]
 name = "cc"
 version = "1.0.83"
@@ -1422,6 +1519,43 @@ dependencies = [
  "uuid 1.4.1",
 ]
 
+[[package]]
+name = "client2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-recursion 0.3.2",
+ "async-tungstenite",
+ "collections",
+ "db2",
+ "feature_flags2",
+ "futures 0.3.28",
+ "gpui2",
+ "image",
+ "lazy_static",
+ "log",
+ "parking_lot 0.11.2",
+ "postage",
+ "rand 0.8.5",
+ "rpc2",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "settings",
+ "settings2",
+ "smol",
+ "sum_tree",
+ "sysinfo",
+ "tempfile",
+ "text",
+ "thiserror",
+ "time",
+ "tiny_http",
+ "url",
+ "util",
+ "uuid 1.4.1",
+]
+
 [[package]]
 name = "clock"
 version = "0.1.0"
@@ -1724,6 +1858,33 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "copilot2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-compression",
+ "async-tar",
+ "clock",
+ "collections",
+ "context_menu",
+ "fs",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "log",
+ "lsp2",
+ "node_runtime",
+ "parking_lot 0.11.2",
+ "rpc",
+ "serde",
+ "serde_derive",
+ "settings2",
+ "smol",
+ "theme",
+ "util",
+]
+
 [[package]]
 name = "copilot_button"
 version = "0.1.0"
@@ -2154,6 +2315,28 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "db2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "collections",
+ "env_logger 0.9.3",
+ "gpui2",
+ "indoc",
+ "lazy_static",
+ "log",
+ "parking_lot 0.11.2",
+ "serde",
+ "serde_derive",
+ "smol",
+ "sqlez",
+ "sqlez_macros",
+ "tempdir",
+ "util",
+]
+
 [[package]]
 name = "deflate"
 version = "0.8.6"
@@ -2621,6 +2804,14 @@ dependencies = [
  "gpui",
 ]
 
+[[package]]
+name = "feature_flags2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui2",
+]
+
 [[package]]
 name = "feedback"
 version = "0.1.0"
@@ -2866,6 +3057,34 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "fs2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "collections",
+ "fsevent",
+ "futures 0.3.28",
+ "git2",
+ "gpui2",
+ "lazy_static",
+ "libc",
+ "log",
+ "parking_lot 0.11.2",
+ "regex",
+ "rope",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smol",
+ "sum_tree",
+ "tempfile",
+ "text",
+ "time",
+ "util",
+]
+
 [[package]]
 name = "fsevent"
 version = "2.0.2"
@@ -3044,6 +3263,14 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "fuzzy2"
+version = "0.1.0"
+dependencies = [
+ "gpui2",
+ "util",
+]
+
 [[package]]
 name = "fxhash"
 version = "0.2.1"
@@ -3258,20 +3485,64 @@ name = "gpui2"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "async-task",
+ "backtrace",
+ "bindgen 0.65.1",
+ "bitflags 2.4.0",
+ "block",
+ "cbindgen",
+ "cocoa",
+ "collections",
+ "core-foundation",
+ "core-graphics",
+ "core-text",
+ "ctor",
  "derive_more",
+ "dhat",
+ "env_logger 0.9.3",
+ "etagere",
+ "font-kit",
+ "foreign-types",
  "futures 0.3.28",
- "gpui",
  "gpui2_macros",
+ "gpui_macros",
+ "image",
+ "itertools 0.10.5",
+ "lazy_static",
  "log",
+ "media",
+ "metal",
+ "num_cpus",
+ "objc",
+ "ordered-float 2.10.0",
+ "parking",
  "parking_lot 0.11.2",
+ "pathfinder_geometry",
+ "plane-split",
+ "png",
+ "postage",
+ "rand 0.8.5",
  "refineable",
- "rust-embed",
+ "resvg",
+ "schemars",
+ "seahash",
  "serde",
- "settings",
+ "serde_derive",
+ "serde_json",
  "simplelog",
+ "slotmap",
  "smallvec",
- "theme",
+ "smol",
+ "sqlez",
+ "sum_tree",
+ "taffy",
+ "thiserror",
+ "time",
+ "tiny-skia",
+ "usvg",
  "util",
+ "uuid 1.4.1",
+ "waker-fn",
 ]
 
 [[package]]
@@ -3698,6 +3969,17 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "install_cli2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui2",
+ "log",
+ "smol",
+ "util",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -3916,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"
@@ -4032,6 +4332,59 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "language2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-broadcast",
+ "async-trait",
+ "client2",
+ "clock",
+ "collections",
+ "ctor",
+ "env_logger 0.9.3",
+ "futures 0.3.28",
+ "fuzzy2",
+ "git",
+ "globset",
+ "gpui2",
+ "indoc",
+ "lazy_static",
+ "log",
+ "lsp2",
+ "parking_lot 0.11.2",
+ "postage",
+ "rand 0.8.5",
+ "regex",
+ "rpc2",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "similar",
+ "smallvec",
+ "smol",
+ "sum_tree",
+ "text",
+ "theme2",
+ "tree-sitter",
+ "tree-sitter-elixir",
+ "tree-sitter-embedded-template",
+ "tree-sitter-heex",
+ "tree-sitter-html",
+ "tree-sitter-json 0.20.0",
+ "tree-sitter-markdown",
+ "tree-sitter-python",
+ "tree-sitter-ruby",
+ "tree-sitter-rust",
+ "tree-sitter-typescript",
+ "unicase",
+ "unindent",
+ "util",
+]
+
 [[package]]
 name = "language_selector"
 version = "0.1.0"
@@ -4310,6 +4663,29 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "lsp2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-pipe",
+ "collections",
+ "ctor",
+ "env_logger 0.9.3",
+ "futures 0.3.28",
+ "gpui2",
+ "log",
+ "lsp-types",
+ "parking_lot 0.11.2",
+ "postage",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smol",
+ "unindent",
+ "util",
+]
+
 [[package]]
 name = "mach"
 version = "0.3.2"
@@ -4446,6 +4822,13 @@ dependencies = [
  "gpui",
 ]
 
+[[package]]
+name = "menu2"
+version = "0.1.0"
+dependencies = [
+ "gpui2",
+]
+
 [[package]]
 name = "metal"
 version = "0.21.0"
@@ -4738,6 +5121,19 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "nix"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
+dependencies = [
+ "bitflags 1.3.2",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "memoffset 0.6.5",
+]
+
 [[package]]
 name = "nix"
 version = "0.24.3"
@@ -4769,7 +5165,6 @@ dependencies = [
  "async-tar",
  "async-trait",
  "futures 0.3.28",
- "gpui",
  "log",
  "parking_lot 0.11.2",
  "serde",
@@ -5491,6 +5886,17 @@ version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
+[[package]]
+name = "plane-split"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c1f7d82649829ecdef8e258790b0587acf0a8403f0ce963473d8e918acc1643"
+dependencies = [
+ "euclid",
+ "log",
+ "smallvec",
+]
+
 [[package]]
 name = "plist"
 version = "1.5.0"
@@ -5621,6 +6027,27 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "prettier2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "collections",
+ "fs2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "log",
+ "lsp2",
+ "node_runtime",
+ "parking_lot 0.11.2",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "util",
+]
+
 [[package]]
 name = "pretty_assertions"
 version = "1.4.0"
@@ -5700,58 +6127,113 @@ source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6
 dependencies = [
  "libc",
  "log",
- "ntapi 0.3.7",
- "winapi 0.3.9",
+ "ntapi 0.3.7",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "project"
+version = "0.1.0"
+dependencies = [
+ "aho-corasick",
+ "anyhow",
+ "async-trait",
+ "backtrace",
+ "client",
+ "clock",
+ "collections",
+ "copilot",
+ "ctor",
+ "db",
+ "env_logger 0.9.3",
+ "fs",
+ "fsevent",
+ "futures 0.3.28",
+ "fuzzy",
+ "git",
+ "git2",
+ "globset",
+ "gpui",
+ "ignore",
+ "itertools 0.10.5",
+ "language",
+ "lazy_static",
+ "log",
+ "lsp",
+ "node_runtime",
+ "parking_lot 0.11.2",
+ "postage",
+ "prettier",
+ "pretty_assertions",
+ "rand 0.8.5",
+ "regex",
+ "rpc",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings",
+ "sha2 0.10.7",
+ "similar",
+ "smol",
+ "sum_tree",
+ "tempdir",
+ "terminal",
+ "text",
+ "thiserror",
+ "toml 0.5.11",
+ "unindent",
+ "util",
 ]
 
 [[package]]
-name = "project"
+name = "project2"
 version = "0.1.0"
 dependencies = [
  "aho-corasick",
  "anyhow",
  "async-trait",
  "backtrace",
- "client",
+ "client2",
  "clock",
  "collections",
- "copilot",
+ "copilot2",
  "ctor",
- "db",
+ "db2",
  "env_logger 0.9.3",
- "fs",
+ "fs2",
  "fsevent",
  "futures 0.3.28",
- "fuzzy",
+ "fuzzy2",
  "git",
  "git2",
  "globset",
- "gpui",
+ "gpui2",
  "ignore",
  "itertools 0.10.5",
- "language",
+ "language2",
  "lazy_static",
  "log",
- "lsp",
+ "lsp2",
  "node_runtime",
  "parking_lot 0.11.2",
  "postage",
- "prettier",
+ "prettier2",
  "pretty_assertions",
  "rand 0.8.5",
  "regex",
- "rpc",
+ "rpc2",
  "schemars",
  "serde",
  "serde_derive",
  "serde_json",
- "settings",
+ "settings2",
  "sha2 0.10.7",
  "similar",
  "smol",
  "sum_tree",
  "tempdir",
- "terminal",
+ "terminal2",
  "text",
  "thiserror",
  "toml 0.5.11",
@@ -6494,6 +6976,35 @@ dependencies = [
  "zstd",
 ]
 
+[[package]]
+name = "rpc2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-lock",
+ "async-tungstenite",
+ "base64 0.13.1",
+ "clock",
+ "collections",
+ "ctor",
+ "env_logger 0.9.3",
+ "futures 0.3.28",
+ "gpui2",
+ "parking_lot 0.11.2",
+ "prost 0.8.0",
+ "prost-build",
+ "rand 0.8.5",
+ "rsa 0.4.0",
+ "serde",
+ "serde_derive",
+ "smol",
+ "smol-timeout",
+ "tempdir",
+ "tracing",
+ "util",
+ "zstd",
+]
+
 [[package]]
 name = "rsa"
 version = "0.4.0"
@@ -7198,6 +7709,36 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "settings2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "feature_flags2",
+ "fs",
+ "fs2",
+ "futures 0.3.28",
+ "gpui2",
+ "indoc",
+ "lazy_static",
+ "postage",
+ "pretty_assertions",
+ "rust-embed",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_json_lenient",
+ "smallvec",
+ "sqlez",
+ "toml 0.5.11",
+ "tree-sitter",
+ "tree-sitter-json 0.19.0",
+ "unindent",
+ "util",
+]
+
 [[package]]
 name = "sha-1"
 version = "0.9.8"
@@ -7766,6 +8307,29 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
+[[package]]
+name = "storybook2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "backtrace-on-stack-overflow",
+ "chrono",
+ "clap 4.4.4",
+ "gpui2",
+ "itertools 0.11.0",
+ "log",
+ "rust-embed",
+ "serde",
+ "settings2",
+ "simplelog",
+ "smallvec",
+ "strum",
+ "theme",
+ "theme2",
+ "ui2",
+ "util",
+]
+
 [[package]]
 name = "stringprep"
 version = "0.1.4"
@@ -8075,6 +8639,35 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "terminal2"
+version = "0.1.0"
+dependencies = [
+ "alacritty_terminal",
+ "anyhow",
+ "db2",
+ "dirs 4.0.0",
+ "futures 0.3.28",
+ "gpui2",
+ "itertools 0.10.5",
+ "lazy_static",
+ "libc",
+ "mio-extras",
+ "ordered-float 2.10.0",
+ "procinfo",
+ "rand 0.8.5",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "settings2",
+ "shellexpand",
+ "smallvec",
+ "smol",
+ "theme2",
+ "thiserror",
+ "util",
+]
+
 [[package]]
 name = "terminal_view"
 version = "0.1.0"
@@ -8157,6 +8750,39 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "theme2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "fs",
+ "gpui2",
+ "indexmap 1.9.3",
+ "parking_lot 0.11.2",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "toml 0.5.11",
+ "util",
+]
+
+[[package]]
+name = "theme_converter"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap 4.4.4",
+ "convert_case 0.6.0",
+ "gpui2",
+ "log",
+ "rust-embed",
+ "serde",
+ "simplelog",
+ "theme2",
+]
+
 [[package]]
 name = "theme_selector"
 version = "0.1.0"
@@ -8964,6 +9590,21 @@ version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 
+[[package]]
+name = "ui2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "gpui2",
+ "itertools 0.11.0",
+ "rand 0.8.5",
+ "serde",
+ "smallvec",
+ "strum",
+ "theme2",
+]
+
 [[package]]
 name = "unicase"
 version = "2.7.0"
@@ -10285,6 +10926,105 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "zed2"
+version = "0.109.0"
+dependencies = [
+ "ai2",
+ "anyhow",
+ "async-compression",
+ "async-recursion 0.3.2",
+ "async-tar",
+ "async-trait",
+ "backtrace",
+ "call2",
+ "chrono",
+ "cli",
+ "client2",
+ "collections",
+ "copilot2",
+ "ctor",
+ "db2",
+ "env_logger 0.9.3",
+ "feature_flags2",
+ "fs2",
+ "fsevent",
+ "futures 0.3.28",
+ "fuzzy",
+ "gpui2",
+ "ignore",
+ "image",
+ "indexmap 1.9.3",
+ "install_cli",
+ "isahc",
+ "journal2",
+ "language2",
+ "language_tools",
+ "lazy_static",
+ "libc",
+ "log",
+ "lsp2",
+ "node_runtime",
+ "num_cpus",
+ "parking_lot 0.11.2",
+ "postage",
+ "project2",
+ "rand 0.8.5",
+ "regex",
+ "rpc2",
+ "rsa 0.4.0",
+ "rust-embed",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "shellexpand",
+ "simplelog",
+ "smallvec",
+ "smol",
+ "sum_tree",
+ "tempdir",
+ "text",
+ "theme2",
+ "thiserror",
+ "tiny_http",
+ "toml 0.5.11",
+ "tree-sitter",
+ "tree-sitter-bash",
+ "tree-sitter-c",
+ "tree-sitter-cpp",
+ "tree-sitter-css",
+ "tree-sitter-elixir",
+ "tree-sitter-elm",
+ "tree-sitter-embedded-template",
+ "tree-sitter-glsl",
+ "tree-sitter-go",
+ "tree-sitter-heex",
+ "tree-sitter-html",
+ "tree-sitter-json 0.20.0",
+ "tree-sitter-lua",
+ "tree-sitter-markdown",
+ "tree-sitter-nix",
+ "tree-sitter-nu",
+ "tree-sitter-php",
+ "tree-sitter-python",
+ "tree-sitter-racket",
+ "tree-sitter-ruby",
+ "tree-sitter-rust",
+ "tree-sitter-scheme",
+ "tree-sitter-svelte",
+ "tree-sitter-toml",
+ "tree-sitter-typescript",
+ "tree-sitter-vue",
+ "tree-sitter-yaml",
+ "unindent",
+ "url",
+ "urlencoding",
+ "util",
+ "uuid 1.4.1",
+]
+
 [[package]]
 name = "zeroize"
 version = "1.6.0"

Cargo.toml 🔗

@@ -4,12 +4,15 @@ members = [
     "crates/ai",
     "crates/assistant",
     "crates/audio",
+    "crates/audio2",
     "crates/auto_update",
     "crates/breadcrumbs",
     "crates/call",
+    "crates/call2",
     "crates/channel",
     "crates/cli",
     "crates/client",
+    "crates/client2",
     "crates/clock",
     "crates/collab",
     "crates/collab_ui",
@@ -18,18 +21,24 @@ members = [
     "crates/component_test",
     "crates/context_menu",
     "crates/copilot",
+    "crates/copilot2",
     "crates/copilot_button",
     "crates/db",
+    "crates/db2",
     "crates/refineable",
     "crates/refineable/derive_refineable",
     "crates/diagnostics",
     "crates/drag_and_drop",
     "crates/editor",
+    "crates/feature_flags",
+    "crates/feature_flags2",
     "crates/feedback",
     "crates/file_finder",
     "crates/fs",
+    "crates/fs2",
     "crates/fsevent",
     "crates/fuzzy",
+    "crates/fuzzy2",
     "crates/git",
     "crates/go_to_line",
     "crates/gpui",
@@ -37,15 +46,20 @@ members = [
     "crates/gpui2",
     "crates/gpui2_macros",
     "crates/install_cli",
+    "crates/install_cli2",
     "crates/journal",
+    "crates/journal2",
     "crates/language",
+    "crates/language2",
     "crates/language_selector",
     "crates/language_tools",
     "crates/live_kit_client",
     "crates/live_kit_server",
     "crates/lsp",
+    "crates/lsp2",
     "crates/media",
     "crates/menu",
+    "crates/menu2",
     "crates/multi_buffer",
     "crates/node_runtime",
     "crates/notifications",
@@ -55,24 +69,32 @@ members = [
     "crates/plugin_macros",
     "crates/plugin_runtime",
     "crates/prettier",
+    "crates/prettier2",
     "crates/project",
+    "crates/project2",
     "crates/project_panel",
     "crates/project_symbols",
     "crates/recent_projects",
     "crates/rope",
     "crates/rpc",
+    "crates/rpc2",
     "crates/search",
     "crates/settings",
+    "crates/settings2",
     "crates/snippet",
     "crates/sqlez",
     "crates/sqlez_macros",
-    "crates/feature_flags",
     "crates/rich_text",
+    "crates/storybook2",
     "crates/sum_tree",
     "crates/terminal",
+    "crates/terminal2",
     "crates/text",
     "crates/theme",
+    "crates/theme2",
+    "crates/theme_converter",
     "crates/theme_selector",
+    "crates/ui2",
     "crates/util",
     "crates/semantic_index",
     "crates/vim",
@@ -81,6 +103,7 @@ members = [
     "crates/welcome",
     "crates/xtask",
     "crates/zed",
+    "crates/zed2",
     "crates/zed-actions"
 ]
 default-members = ["crates/zed"]

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

@@ -0,0 +1,24 @@
+[package]
+name = "audio2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/audio2.rs"
+doctest = false
+
+[dependencies]
+gpui2 = { path = "../gpui2" }
+collections = { path = "../collections" }
+util = { path = "../util" }
+
+
+rodio ={version = "0.17.1", default-features=false, features = ["wav"]}
+
+log.workspace = true
+futures.workspace = true
+anyhow.workspace = true
+parking_lot.workspace = true
+
+[dev-dependencies]

crates/audio2/audio/Cargo.toml 🔗

@@ -0,0 +1,23 @@
+[package]
+name = "audio"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/audio.rs"
+doctest = false
+
+[dependencies]
+gpui = { path = "../gpui" }
+collections = { path = "../collections" }
+util = { path = "../util" }
+
+rodio ={version = "0.17.1", default-features=false, features = ["wav"]}
+
+log.workspace = true
+
+anyhow.workspace = true
+parking_lot.workspace = true
+
+[dev-dependencies]

crates/audio2/audio/src/assets.rs 🔗

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

crates/audio2/audio/src/audio.rs 🔗

@@ -0,0 +1,81 @@
+use assets::SoundRegistry;
+use gpui::{AppContext, AssetSource};
+use rodio::{OutputStream, OutputStreamHandle};
+use util::ResultExt;
+
+mod assets;
+
+pub fn init(source: impl AssetSource, cx: &mut AppContext) {
+    cx.set_global(SoundRegistry::new(source));
+    cx.set_global(Audio::new());
+}
+
+pub enum Sound {
+    Joined,
+    Leave,
+    Mute,
+    Unmute,
+    StartScreenshare,
+    StopScreenshare,
+}
+
+impl Sound {
+    fn file(&self) -> &'static str {
+        match self {
+            Self::Joined => "joined_call",
+            Self::Leave => "leave_call",
+            Self::Mute => "mute",
+            Self::Unmute => "unmute",
+            Self::StartScreenshare => "start_screenshare",
+            Self::StopScreenshare => "stop_screenshare",
+        }
+    }
+}
+
+pub struct Audio {
+    _output_stream: Option<OutputStream>,
+    output_handle: Option<OutputStreamHandle>,
+}
+
+impl Audio {
+    pub fn new() -> Self {
+        Self {
+            _output_stream: None,
+            output_handle: None,
+        }
+    }
+
+    fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
+        if self.output_handle.is_none() {
+            let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
+            self.output_handle = output_handle;
+            self._output_stream = _output_stream;
+        }
+
+        self.output_handle.as_ref()
+    }
+
+    pub fn play_sound(sound: Sound, cx: &mut AppContext) {
+        if !cx.has_global::<Self>() {
+            return;
+        }
+
+        cx.update_global::<Self, _, _>(|this, cx| {
+            let output_handle = this.ensure_output_exists()?;
+            let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
+            output_handle.play_raw(source).log_err()?;
+            Some(())
+        });
+    }
+
+    pub fn end_call(cx: &mut AppContext) {
+        if !cx.has_global::<Self>() {
+            return;
+        }
+
+        cx.update_global::<Self, _, _>(|this, _| {
+            this._output_stream.take();
+            this.output_handle.take();
+        });
+    }
+}

crates/audio2/src/assets.rs 🔗

@@ -0,0 +1,44 @@
+use std::{io::Cursor, sync::Arc};
+
+use anyhow::Result;
+use collections::HashMap;
+use gpui2::{AppContext, AssetSource};
+use rodio::{
+    source::{Buffered, SamplesConverter},
+    Decoder, Source,
+};
+
+type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
+
+pub struct SoundRegistry {
+    cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
+    assets: Box<dyn AssetSource>,
+}
+
+impl SoundRegistry {
+    pub fn new(source: impl AssetSource) -> Arc<Self> {
+        Arc::new(Self {
+            cache: Default::default(),
+            assets: Box::new(source),
+        })
+    }
+
+    pub fn global(cx: &AppContext) -> Arc<Self> {
+        cx.global::<Arc<Self>>().clone()
+    }
+
+    pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {
+        if let Some(wav) = self.cache.lock().get(name) {
+            return Ok(wav.clone());
+        }
+
+        let path = format!("sounds/{}.wav", name);
+        let bytes = self.assets.load(&path)?.into_owned();
+        let cursor = Cursor::new(bytes);
+        let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
+
+        self.cache.lock().insert(name.to_string(), source.clone());
+
+        Ok(source)
+    }
+}

crates/audio2/src/audio2.rs 🔗

@@ -0,0 +1,111 @@
+use assets::SoundRegistry;
+use futures::{channel::mpsc, StreamExt};
+use gpui2::{AppContext, AssetSource, Executor};
+use rodio::{OutputStream, OutputStreamHandle};
+use util::ResultExt;
+
+mod assets;
+
+pub fn init(source: impl AssetSource, cx: &mut AppContext) {
+    cx.set_global(Audio::new(cx.executor()));
+    cx.set_global(SoundRegistry::new(source));
+}
+
+pub enum Sound {
+    Joined,
+    Leave,
+    Mute,
+    Unmute,
+    StartScreenshare,
+    StopScreenshare,
+}
+
+impl Sound {
+    fn file(&self) -> &'static str {
+        match self {
+            Self::Joined => "joined_call",
+            Self::Leave => "leave_call",
+            Self::Mute => "mute",
+            Self::Unmute => "unmute",
+            Self::StartScreenshare => "start_screenshare",
+            Self::StopScreenshare => "stop_screenshare",
+        }
+    }
+}
+
+pub struct Audio {
+    tx: mpsc::UnboundedSender<Box<dyn FnOnce(&mut AudioState) + Send>>,
+}
+
+struct AudioState {
+    _output_stream: Option<OutputStream>,
+    output_handle: Option<OutputStreamHandle>,
+}
+
+impl AudioState {
+    fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
+        if self.output_handle.is_none() {
+            let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
+            self.output_handle = output_handle;
+            self._output_stream = _output_stream;
+        }
+
+        self.output_handle.as_ref()
+    }
+
+    fn take(&mut self) {
+        self._output_stream.take();
+        self.output_handle.take();
+    }
+}
+
+impl Audio {
+    pub fn new(executor: &Executor) -> Self {
+        let (tx, mut rx) = mpsc::unbounded::<Box<dyn FnOnce(&mut AudioState) + Send>>();
+        executor
+            .spawn_on_main(|| async move {
+                let mut audio = AudioState {
+                    _output_stream: None,
+                    output_handle: None,
+                };
+
+                while let Some(f) = rx.next().await {
+                    (f)(&mut audio);
+                }
+            })
+            .detach();
+
+        Self { tx }
+    }
+
+    pub fn play_sound(sound: Sound, cx: &mut AppContext) {
+        if !cx.has_global::<Self>() {
+            return;
+        }
+
+        let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
+            return;
+        };
+
+        let this = cx.global::<Self>();
+        this.tx
+            .unbounded_send(Box::new(move |state| {
+                if let Some(output_handle) = state.ensure_output_exists() {
+                    output_handle.play_raw(source).log_err();
+                }
+            }))
+            .ok();
+    }
+
+    pub fn end_call(cx: &AppContext) {
+        if !cx.has_global::<Self>() {
+            return;
+        }
+
+        let this = cx.global::<Self>();
+
+        this.tx
+            .unbounded_send(Box::new(move |state| state.take()))
+            .ok();
+    }
+}

crates/call/src/room.rs 🔗

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

crates/call2/Cargo.toml 🔗

@@ -0,0 +1,52 @@
+[package]
+name = "call2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/call2.rs"
+doctest = false
+
+[features]
+test-support = [
+    "client2/test-support",
+    "collections/test-support",
+    "gpui2/test-support",
+    "live_kit_client/test-support",
+    "project2/test-support",
+    "util/test-support"
+]
+
+[dependencies]
+audio2 = { path = "../audio2" }
+client2 = { path = "../client2" }
+collections = { path = "../collections" }
+gpui2 = { path = "../gpui2" }
+log.workspace = true
+live_kit_client = { path = "../live_kit_client" }
+fs2 = { path = "../fs2" }
+language2 = { path = "../language2" }
+media = { path = "../media" }
+project2 = { path = "../project2" }
+settings2 = { path = "../settings2" }
+util = { path = "../util" }
+
+anyhow.workspace = true
+async-broadcast = "0.4"
+futures.workspace = true
+postage.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+serde_derive.workspace = true
+
+[dev-dependencies]
+client2 = { path = "../client2", features = ["test-support"] }
+fs2 = { path = "../fs2", features = ["test-support"] }
+language2 = { path = "../language2", features = ["test-support"] }
+collections = { path = "../collections", features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
+project2 = { path = "../project2", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }

crates/call2/src/call2.rs 🔗

@@ -0,0 +1,461 @@
+pub mod call_settings;
+pub mod participant;
+pub mod room;
+
+use anyhow::{anyhow, Result};
+use audio2::Audio;
+use call_settings::CallSettings;
+use client2::{
+    proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
+    ZED_ALWAYS_ACTIVE,
+};
+use collections::HashSet;
+use futures::{future::Shared, FutureExt};
+use gpui2::{
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
+    WeakModel,
+};
+use postage::watch;
+use project2::Project;
+use settings2::Settings;
+use std::sync::Arc;
+
+pub use participant::ParticipantLocation;
+pub use room::Room;
+
+pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
+    CallSettings::register(cx);
+
+    let active_call = cx.build_model(|cx| ActiveCall::new(client, user_store, cx));
+    cx.set_global(active_call);
+}
+
+#[derive(Clone)]
+pub struct IncomingCall {
+    pub room_id: u64,
+    pub calling_user: Arc<User>,
+    pub participants: Vec<Arc<User>>,
+    pub initial_project: Option<proto::ParticipantProject>,
+}
+
+/// Singleton global maintaining the user's participation in a room across workspaces.
+pub struct ActiveCall {
+    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: Model<UserStore>,
+    _subscriptions: Vec<client2::Subscription>,
+}
+
+impl EventEmitter for ActiveCall {
+    type Event = room::Event;
+}
+
+impl ActiveCall {
+    fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
+        Self {
+            room: None,
+            pending_room_creation: None,
+            location: None,
+            pending_invites: Default::default(),
+            incoming_call: watch::channel(),
+
+            _subscriptions: vec![
+                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,
+        }
+    }
+
+    pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
+        self.room()?.read(cx).channel_id()
+    }
+
+    async fn handle_incoming_call(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::IncomingCall>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::Ack> {
+        let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
+        let call = IncomingCall {
+            room_id: envelope.payload.room_id,
+            participants: user_store
+                .update(&mut cx, |user_store, cx| {
+                    user_store.get_users(envelope.payload.participant_user_ids, cx)
+                })?
+                .await?,
+            calling_user: user_store
+                .update(&mut cx, |user_store, cx| {
+                    user_store.get_user(envelope.payload.calling_user_id, cx)
+                })?
+                .await?,
+            initial_project: envelope.payload.initial_project,
+        };
+        this.update(&mut cx, |this, _| {
+            *this.incoming_call.0.borrow_mut() = Some(call);
+        })?;
+
+        Ok(proto::Ack {})
+    }
+
+    async fn handle_call_canceled(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::CallCanceled>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, _| {
+            let mut incoming_call = this.incoming_call.0.borrow_mut();
+            if incoming_call
+                .as_ref()
+                .map_or(false, |call| call.room_id == envelope.payload.room_id)
+            {
+                incoming_call.take();
+            }
+        })?;
+        Ok(())
+    }
+
+    pub fn global(cx: &AppContext) -> Model<Self> {
+        cx.global::<Model<Self>>().clone()
+    }
+
+    pub fn invite(
+        &mut self,
+        called_user_id: u64,
+        initial_project: Option<Model<Project>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if !self.pending_invites.insert(called_user_id) {
+            return Task::ready(Err(anyhow!("user was already invited")));
+        }
+        cx.notify();
+
+        let room = if let Some(room) = self.room().cloned() {
+            Some(Task::ready(Ok(room)).shared())
+        } else {
+            self.pending_room_creation.clone()
+        };
+
+        let invite = if let Some(room) = room {
+            cx.spawn(move |_, mut cx| async move {
+                let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
+
+                let initial_project_id = if let Some(initial_project) = initial_project {
+                    Some(
+                        room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
+                            .await?,
+                    )
+                } else {
+                    None
+                };
+
+                room.update(&mut cx, move |room, cx| {
+                    room.call(called_user_id, initial_project_id, cx)
+                })?
+                .await?;
+
+                anyhow::Ok(())
+            })
+        } else {
+            let client = self.client.clone();
+            let user_store = self.user_store.clone();
+            let room = cx
+                .spawn(move |this, mut cx| async move {
+                    let create_room = async {
+                        let room = cx
+                            .update(|cx| {
+                                Room::create(
+                                    called_user_id,
+                                    initial_project,
+                                    client,
+                                    user_store,
+                                    cx,
+                                )
+                            })?
+                            .await?;
+
+                        this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
+                            .await?;
+
+                        anyhow::Ok(room)
+                    };
+
+                    let room = create_room.await;
+                    this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
+                    room.map_err(Arc::new)
+                })
+                .shared();
+            self.pending_room_creation = Some(room.clone());
+            cx.executor().spawn(async move {
+                room.await.map_err(|err| anyhow!("{:?}", err))?;
+                anyhow::Ok(())
+            })
+        };
+
+        cx.spawn(move |this, mut cx| async move {
+            let result = invite.await;
+            if result.is_ok() {
+                this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
+            } else {
+                // TODO: Resport collaboration error
+            }
+
+            this.update(&mut cx, |this, cx| {
+                this.pending_invites.remove(&called_user_id);
+                cx.notify();
+            })?;
+            result
+        })
+    }
+
+    pub fn cancel_invite(
+        &mut self,
+        called_user_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let room_id = if let Some(room) = self.room() {
+            room.read(cx).id()
+        } else {
+            return Task::ready(Err(anyhow!("no active call")));
+        };
+
+        let client = self.client.clone();
+        cx.executor().spawn(async move {
+            client
+                .request(proto::CancelCall {
+                    room_id,
+                    called_user_id,
+                })
+                .await?;
+            anyhow::Ok(())
+        })
+    }
+
+    pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
+        self.incoming_call.1.clone()
+    }
+
+    pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        if self.room.is_some() {
+            return Task::ready(Err(anyhow!("cannot join while on another call")));
+        }
+
+        let call = if let Some(call) = self.incoming_call.1.borrow().clone() {
+            call
+        } else {
+            return Task::ready(Err(anyhow!("no incoming call")));
+        };
+
+        let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
+
+        cx.spawn(|this, mut cx| async move {
+            let room = join.await?;
+            this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
+                .await?;
+            this.update(&mut cx, |this, cx| {
+                this.report_call_event("accept incoming", cx)
+            })?;
+            Ok(())
+        })
+    }
+
+    pub fn decline_incoming(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
+        let call = self
+            .incoming_call
+            .0
+            .borrow_mut()
+            .take()
+            .ok_or_else(|| anyhow!("no incoming call"))?;
+        report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
+        self.client.send(proto::DeclineCall {
+            room_id: call.room_id,
+        })?;
+        Ok(())
+    }
+
+    pub fn join_channel(
+        &mut self,
+        channel_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Model<Room>>> {
+        if let Some(room) = self.room().cloned() {
+            if room.read(cx).channel_id() == Some(channel_id) {
+                return Task::ready(Ok(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);
+
+        cx.spawn(|this, mut cx| async move {
+            let room = join.await?;
+            this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
+                .await?;
+            this.update(&mut cx, |this, cx| {
+                this.report_call_event("join channel", cx)
+            })?;
+            Ok(room)
+        })
+    }
+
+    pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        cx.notify();
+        self.report_call_event("hang up", cx);
+
+        Audio::end_call(cx);
+        if let Some((room, _)) = self.room.take() {
+            room.update(cx, |room, cx| room.leave(cx))
+        } else {
+            Task::ready(Ok(()))
+        }
+    }
+
+    pub fn share_project(
+        &mut self,
+        project: Model<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<u64>> {
+        if let Some((room, _)) = self.room.as_ref() {
+            self.report_call_event("share project", cx);
+            room.update(cx, |room, cx| room.share_project(project, cx))
+        } else {
+            Task::ready(Err(anyhow!("no active call")))
+        }
+    }
+
+    pub fn unshare_project(
+        &mut self,
+        project: Model<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        if let Some((room, _)) = self.room.as_ref() {
+            self.report_call_event("unshare project", cx);
+            room.update(cx, |room, cx| room.unshare_project(project, cx))
+        } else {
+            Err(anyhow!("no active call"))
+        }
+    }
+
+    pub fn location(&self) -> Option<&WeakModel<Project>> {
+        self.location.as_ref()
+    }
+
+    pub fn set_location(
+        &mut self,
+        project: Option<&Model<Project>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if project.is_some() || !*ZED_ALWAYS_ACTIVE {
+            self.location = project.map(|project| project.downgrade());
+            if let Some((room, _)) = self.room.as_ref() {
+                return room.update(cx, |room, cx| room.set_location(project, cx));
+            }
+        }
+        Task::ready(Ok(()))
+    }
+
+    fn set_room(
+        &mut self,
+        room: Option<Model<Room>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
+            cx.notify();
+            if let Some(room) = room {
+                if room.read(cx).status().is_offline() {
+                    self.room = None;
+                    Task::ready(Ok(()))
+                } else {
+                    let subscriptions = vec![
+                        cx.observe(&room, |this, room, cx| {
+                            if room.read(cx).status().is_offline() {
+                                this.set_room(None, cx).detach_and_log_err(cx);
+                            }
+
+                            cx.notify();
+                        }),
+                        cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
+                    ];
+                    self.room = Some((room.clone(), subscriptions));
+                    let location = self
+                        .location
+                        .as_ref()
+                        .and_then(|location| location.upgrade());
+                    room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
+                }
+            } else {
+                self.room = None;
+                Task::ready(Ok(()))
+            }
+        } else {
+            Task::ready(Ok(()))
+        }
+    }
+
+    pub fn room(&self) -> Option<&Model<Room>> {
+        self.room.as_ref().map(|(room, _)| room)
+    }
+
+    pub fn client(&self) -> Arc<Client> {
+        self.client.clone()
+    }
+
+    pub fn pending_invites(&self) -> &HashSet<u64> {
+        &self.pending_invites
+    }
+
+    pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
+        if let Some(room) = self.room() {
+            let room = room.read(cx);
+            report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
+        }
+    }
+}
+
+pub fn report_call_event_for_room(
+    operation: &'static str,
+    room_id: u64,
+    channel_id: Option<u64>,
+    client: &Arc<Client>,
+    cx: &AppContext,
+) {
+    let telemetry = client.telemetry();
+    let telemetry_settings = *TelemetrySettings::get_global(cx);
+    let event = ClickhouseEvent::Call {
+        operation,
+        room_id: Some(room_id),
+        channel_id,
+    };
+    telemetry.report_clickhouse_event(event, telemetry_settings);
+}
+
+pub fn report_call_event_for_channel(
+    operation: &'static str,
+    channel_id: u64,
+    client: &Arc<Client>,
+    cx: &AppContext,
+) {
+    let room = ActiveCall::global(cx).read(cx).room();
+
+    let telemetry = client.telemetry();
+
+    let telemetry_settings = *TelemetrySettings::get_global(cx);
+
+    let event = ClickhouseEvent::Call {
+        operation,
+        room_id: room.map(|r| r.read(cx).id()),
+        channel_id: Some(channel_id),
+    };
+    telemetry.report_clickhouse_event(event, telemetry_settings);
+}

crates/call2/src/call_settings.rs 🔗

@@ -0,0 +1,32 @@
+use anyhow::Result;
+use gpui2::AppContext;
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings2::Settings;
+
+#[derive(Deserialize, Debug)]
+pub struct CallSettings {
+    pub mute_on_join: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct CallSettingsContent {
+    pub mute_on_join: Option<bool>,
+}
+
+impl Settings for CallSettings {
+    const KEY: Option<&'static str> = Some("calls");
+
+    type FileContent = CallSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _cx: &mut AppContext,
+    ) -> Result<Self>
+    where
+        Self: Sized,
+    {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/call2/src/participant.rs 🔗

@@ -0,0 +1,71 @@
+use anyhow::{anyhow, Result};
+use client2::ParticipantIndex;
+use client2::{proto, User};
+use gpui2::WeakModel;
+pub use live_kit_client::Frame;
+use project2::Project;
+use std::{fmt, sync::Arc};
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum ParticipantLocation {
+    SharedProject { project_id: u64 },
+    UnsharedProject,
+    External,
+}
+
+impl ParticipantLocation {
+    pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
+        match location.and_then(|l| l.variant) {
+            Some(proto::participant_location::Variant::SharedProject(project)) => {
+                Ok(Self::SharedProject {
+                    project_id: project.id,
+                })
+            }
+            Some(proto::participant_location::Variant::UnsharedProject(_)) => {
+                Ok(Self::UnsharedProject)
+            }
+            Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
+            None => Err(anyhow!("participant location was not provided")),
+        }
+    }
+}
+
+#[derive(Clone, Default)]
+pub struct LocalParticipant {
+    pub projects: Vec<proto::ParticipantProject>,
+    pub active_project: Option<WeakModel<Project>>,
+}
+
+#[derive(Clone, Debug)]
+pub struct RemoteParticipant {
+    pub user: Arc<User>,
+    pub peer_id: proto::PeerId,
+    pub projects: Vec<proto::ParticipantProject>,
+    pub location: ParticipantLocation,
+    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>>,
+}
+
+#[derive(Clone)]
+pub struct RemoteVideoTrack {
+    pub(crate) live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
+}
+
+unsafe impl Send for RemoteVideoTrack {}
+// todo!("remove this sync because it's not legit")
+unsafe impl Sync for RemoteVideoTrack {}
+
+impl fmt::Debug for RemoteVideoTrack {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("RemoteVideoTrack").finish()
+    }
+}
+
+impl RemoteVideoTrack {
+    pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
+        self.live_kit_track.frames()
+    }
+}

crates/call2/src/room.rs 🔗

@@ -0,0 +1,1622 @@
+#![allow(dead_code, unused)]
+// todo!()
+
+use crate::{
+    call_settings::CallSettings,
+    participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
+    IncomingCall,
+};
+use anyhow::{anyhow, Result};
+use audio2::{Audio, Sound};
+use client2::{
+    proto::{self, PeerId},
+    Client, ParticipantIndex, TypedEnvelope, User, UserStore,
+};
+use collections::{BTreeMap, HashMap, HashSet};
+use fs2::Fs;
+use futures::{FutureExt, StreamExt};
+use gpui2::{
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
+};
+use language2::LanguageRegistry;
+use live_kit_client::{LocalTrackPublication, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate};
+use postage::{sink::Sink, stream::Stream, watch};
+use project2::Project;
+use settings2::Settings;
+use std::{future::Future, sync::Arc, time::Duration};
+use util::{ResultExt, TryFutureExt};
+
+pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Event {
+    ParticipantLocationChanged {
+        participant_id: proto::PeerId,
+    },
+    RemoteVideoTracksChanged {
+        participant_id: proto::PeerId,
+    },
+    RemoteAudioTracksChanged {
+        participant_id: proto::PeerId,
+    },
+    RemoteProjectShared {
+        owner: Arc<User>,
+        project_id: u64,
+        worktree_root_names: Vec<String>,
+    },
+    RemoteProjectUnshared {
+        project_id: u64,
+    },
+    RemoteProjectJoined {
+        project_id: u64,
+    },
+    RemoteProjectInvitationDiscarded {
+        project_id: u64,
+    },
+    Left,
+}
+
+pub struct Room {
+    id: u64,
+    channel_id: Option<u64>,
+    // live_kit: Option<LiveKitRoom>,
+    status: RoomStatus,
+    shared_projects: HashSet<WeakModel<Project>>,
+    joined_projects: HashSet<WeakModel<Project>>,
+    local_participant: LocalParticipant,
+    remote_participants: BTreeMap<u64, RemoteParticipant>,
+    pending_participants: Vec<Arc<User>>,
+    participant_user_ids: HashSet<u64>,
+    pending_call_count: usize,
+    leave_when_empty: bool,
+    client: Arc<Client>,
+    user_store: Model<UserStore>,
+    follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
+    client_subscriptions: Vec<client2::Subscription>,
+    _subscriptions: Vec<gpui2::Subscription>,
+    room_update_completed_tx: watch::Sender<Option<()>>,
+    room_update_completed_rx: watch::Receiver<Option<()>>,
+    pending_room_update: Option<Task<()>>,
+    maintain_connection: Option<Task<Option<()>>>,
+}
+
+impl EventEmitter for Room {
+    type Event = Event;
+}
+
+impl Room {
+    pub fn channel_id(&self) -> Option<u64> {
+        self.channel_id
+    }
+
+    pub fn is_sharing_project(&self) -> bool {
+        !self.shared_projects.is_empty()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn is_connected(&self) -> bool {
+        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(
+        id: u64,
+        channel_id: Option<u64>,
+        live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
+        client: Arc<Client>,
+        user_store: Model<UserStore>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        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<Model<Project>>,
+        client: Arc<Client>,
+        user_store: Model<UserStore>,
+        cx: &mut AppContext,
+    ) -> 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.build_model(|cx| {
+                Self::new(
+                    room_proto.id,
+                    None,
+                    response.live_kit_connection_info,
+                    client,
+                    user_store,
+                    cx,
+                )
+            })?;
+
+            let initial_project_id = if let Some(initial_project) = initial_project {
+                let initial_project_id = room
+                    .update(&mut cx, |room, cx| {
+                        room.share_project(initial_project.clone(), cx)
+                    })?
+                    .await?;
+                Some(initial_project_id)
+            } else {
+                None
+            };
+
+            match room
+                .update(&mut cx, |room, cx| {
+                    room.leave_when_empty = true;
+                    room.call(called_user_id, initial_project_id, cx)
+                })?
+                .await
+            {
+                Ok(()) => Ok(room),
+                Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
+            }
+        })
+    }
+
+    pub(crate) fn join_channel(
+        channel_id: u64,
+        client: Arc<Client>,
+        user_store: Model<UserStore>,
+        cx: &mut AppContext,
+    ) -> Task<Result<Model<Self>>> {
+        cx.spawn(move |cx| async move {
+            Self::from_join_response(
+                client.request(proto::JoinChannel { channel_id }).await?,
+                client,
+                user_store,
+                cx,
+            )
+        })
+    }
+
+    pub(crate) fn join(
+        call: &IncomingCall,
+        client: Arc<Client>,
+        user_store: Model<UserStore>,
+        cx: &mut AppContext,
+    ) -> Task<Result<Model<Self>>> {
+        let id = call.room_id;
+        cx.spawn(move |cx| async move {
+            Self::from_join_response(
+                client.request(proto::JoinRoom { id }).await?,
+                client,
+                user_store,
+                cx,
+            )
+        })
+    }
+
+    fn released(&mut self, cx: &mut AppContext) {
+        if self.status.is_online() {
+            self.leave_internal(cx).detach_and_log_err(cx);
+        }
+    }
+
+    fn app_will_quit(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
+        let task = if self.status.is_online() {
+            let leave = self.leave_internal(cx);
+            Some(cx.executor().spawn(async move {
+                leave.await.log_err();
+            }))
+        } else {
+            None
+        };
+
+        async move {
+            if let Some(task) = task {
+                task.await;
+            }
+        }
+    }
+
+    pub fn mute_on_join(cx: &AppContext) -> bool {
+        CallSettings::get_global(cx).mute_on_join || client2::IMPERSONATE_LOGIN.is_some()
+    }
+
+    fn from_join_response(
+        response: proto::JoinRoomResponse,
+        client: Arc<Client>,
+        user_store: Model<UserStore>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Model<Self>> {
+        let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
+        let room = cx.build_model(|cx| {
+            Self::new(
+                room_proto.id,
+                response.channel_id,
+                response.live_kit_connection_info,
+                client,
+                user_store,
+                cx,
+            )
+        })?;
+        room.update(&mut cx, |room, cx| {
+            room.leave_when_empty = room.channel_id.is_none();
+            room.apply_room_update(room_proto, cx)?;
+            anyhow::Ok(())
+        })??;
+        Ok(room)
+    }
+
+    fn should_leave(&self) -> bool {
+        self.leave_when_empty
+            && self.pending_room_update.is_none()
+            && self.pending_participants.is_empty()
+            && self.remote_participants.is_empty()
+            && self.pending_call_count == 0
+    }
+
+    pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        cx.notify();
+        cx.emit(Event::Left);
+        self.leave_internal(cx)
+    }
+
+    fn leave_internal(&mut self, cx: &mut AppContext) -> Task<Result<()>> {
+        if self.status.is_offline() {
+            return Task::ready(Err(anyhow!("room is offline")));
+        }
+
+        log::info!("leaving room");
+        Audio::play_sound(Sound::Leave, cx);
+
+        self.clear_state(cx);
+
+        let leave_room = self.client.request(proto::LeaveRoom {});
+        cx.executor().spawn(async move {
+            leave_room.await?;
+            anyhow::Ok(())
+        })
+    }
+
+    pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
+        for project in self.shared_projects.drain() {
+            if let Some(project) = project.upgrade() {
+                project.update(cx, |project, cx| {
+                    project.unshare(cx).log_err();
+                });
+            }
+        }
+        for project in self.joined_projects.drain() {
+            if let Some(project) = project.upgrade() {
+                project.update(cx, |project, cx| {
+                    project.disconnected_from_host(cx);
+                    project.close(cx);
+                });
+            }
+        }
+
+        self.status = RoomStatus::Offline;
+        self.remote_participants.clear();
+        self.pending_participants.clear();
+        self.participant_user_ids.clear();
+        self.client_subscriptions.clear();
+        // self.live_kit.take();
+        self.pending_room_update.take();
+        self.maintain_connection.take();
+    }
+
+    async fn maintain_connection(
+        this: WeakModel<Self>,
+        client: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let mut client_status = client.status();
+        loop {
+            let _ = client_status.try_recv();
+            let is_connected = client_status.borrow().is_connected();
+            // Even if we're initially connected, any future change of the status means we momentarily disconnected.
+            if !is_connected || client_status.next().await.is_some() {
+                log::info!("detected client disconnection");
+
+                this.upgrade()
+                    .ok_or_else(|| anyhow!("room was dropped"))?
+                    .update(&mut cx, |this, cx| {
+                        this.status = RoomStatus::Rejoining;
+                        cx.notify();
+                    })?;
+
+                // Wait for client to re-establish a connection to the server.
+                {
+                    let mut reconnection_timeout = cx.executor().timer(RECONNECT_TIMEOUT).fuse();
+                    let client_reconnection = async {
+                        let mut remaining_attempts = 3;
+                        while remaining_attempts > 0 {
+                            if client_status.borrow().is_connected() {
+                                log::info!("client reconnected, attempting to rejoin room");
+
+                                let Some(this) = this.upgrade() else { break };
+                                match this.update(&mut cx, |this, cx| this.rejoin(cx)) {
+                                    Ok(task) => {
+                                        if task.await.log_err().is_some() {
+                                            return true;
+                                        } else {
+                                            remaining_attempts -= 1;
+                                        }
+                                    }
+                                    Err(_app_dropped) => return false,
+                                }
+                            } else if client_status.borrow().is_signed_out() {
+                                return false;
+                            }
+
+                            log::info!(
+                                "waiting for client status change, remaining attempts {}",
+                                remaining_attempts
+                            );
+                            client_status.next().await;
+                        }
+                        false
+                    }
+                    .fuse();
+                    futures::pin_mut!(client_reconnection);
+
+                    futures::select_biased! {
+                        reconnected = client_reconnection => {
+                            if reconnected {
+                                log::info!("successfully reconnected to room");
+                                // If we successfully joined the room, go back around the loop
+                                // waiting for future connection status changes.
+                                continue;
+                            }
+                        }
+                        _ = reconnection_timeout => {
+                            log::info!("room reconnection timeout expired");
+                        }
+                    }
+                }
+
+                break;
+            }
+        }
+
+        // The client failed to re-establish a connection to the server
+        // or an error occurred while trying to re-join the room. Either way
+        // we leave the room and return an error.
+        if let Some(this) = this.upgrade() {
+            log::info!("reconnection failed, leaving room");
+            let _ = this.update(&mut cx, |this, cx| this.leave(cx))?;
+        }
+        Err(anyhow!(
+            "can't reconnect to room: client failed to re-establish connection"
+        ))
+    }
+
+    fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        let mut projects = HashMap::default();
+        let mut reshared_projects = Vec::new();
+        let mut rejoined_projects = Vec::new();
+        self.shared_projects.retain(|project| {
+            if let Some(handle) = project.upgrade() {
+                let project = handle.read(cx);
+                if let Some(project_id) = project.remote_id() {
+                    projects.insert(project_id, handle.clone());
+                    reshared_projects.push(proto::UpdateProject {
+                        project_id,
+                        worktrees: project.worktree_metadata_protos(cx),
+                    });
+                    return true;
+                }
+            }
+            false
+        });
+        self.joined_projects.retain(|project| {
+            if let Some(handle) = project.upgrade() {
+                let project = handle.read(cx);
+                if let Some(project_id) = project.remote_id() {
+                    projects.insert(project_id, handle.clone());
+                    rejoined_projects.push(proto::RejoinProject {
+                        id: project_id,
+                        worktrees: project
+                            .worktrees()
+                            .map(|worktree| {
+                                let worktree = worktree.read(cx);
+                                proto::RejoinWorktree {
+                                    id: worktree.id().to_proto(),
+                                    scan_id: worktree.completed_scan_id() as u64,
+                                }
+                            })
+                            .collect(),
+                    });
+                }
+                return true;
+            }
+            false
+        });
+
+        let response = self.client.request_envelope(proto::RejoinRoom {
+            id: self.id,
+            reshared_projects,
+            rejoined_projects,
+        });
+
+        cx.spawn(|this, mut cx| async move {
+            let response = response.await?;
+            let message_id = response.message_id;
+            let response = response.payload;
+            let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
+            this.update(&mut cx, |this, cx| {
+                this.status = RoomStatus::Online;
+                this.apply_room_update(room_proto, cx)?;
+
+                for reshared_project in response.reshared_projects {
+                    if let Some(project) = projects.get(&reshared_project.id) {
+                        project.update(cx, |project, cx| {
+                            project.reshared(reshared_project, cx).log_err();
+                        });
+                    }
+                }
+
+                for rejoined_project in response.rejoined_projects {
+                    if let Some(project) = projects.get(&rejoined_project.id) {
+                        project.update(cx, |project, cx| {
+                            project.rejoined(rejoined_project, message_id, cx).log_err();
+                        });
+                    }
+                }
+
+                anyhow::Ok(())
+            })?
+        })
+    }
+
+    pub fn id(&self) -> u64 {
+        self.id
+    }
+
+    pub fn status(&self) -> RoomStatus {
+        self.status
+    }
+
+    pub fn local_participant(&self) -> &LocalParticipant {
+        &self.local_participant
+    }
+
+    pub fn remote_participants(&self) -> &BTreeMap<u64, RemoteParticipant> {
+        &self.remote_participants
+    }
+
+    pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> {
+        self.remote_participants
+            .values()
+            .find(|p| p.peer_id == peer_id)
+    }
+
+    pub fn pending_participants(&self) -> &[Arc<User>] {
+        &self.pending_participants
+    }
+
+    pub fn contains_participant(&self, user_id: u64) -> bool {
+        self.participant_user_ids.contains(&user_id)
+    }
+
+    pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] {
+        self.follows_by_leader_id_project_id
+            .get(&(leader_id, project_id))
+            .map_or(&[], |v| v.as_slice())
+    }
+
+    /// Returns the most 'active' projects, defined as most people in the project
+    pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> {
+        let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
+        for participant in self.remote_participants.values() {
+            match participant.location {
+                ParticipantLocation::SharedProject { project_id } => {
+                    project_hosts_and_guest_counts
+                        .entry(project_id)
+                        .or_default()
+                        .1 += 1;
+                }
+                ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
+            }
+            for project in &participant.projects {
+                project_hosts_and_guest_counts
+                    .entry(project.id)
+                    .or_default()
+                    .0 = Some(participant.user.id);
+            }
+        }
+
+        if let Some(user) = self.user_store.read(cx).current_user() {
+            for project in &self.local_participant.projects {
+                project_hosts_and_guest_counts
+                    .entry(project.id)
+                    .or_default()
+                    .0 = Some(user.id);
+            }
+        }
+
+        project_hosts_and_guest_counts
+            .into_iter()
+            .filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
+            .max_by_key(|(_, _, guest_count)| *guest_count)
+            .map(|(id, host, _)| (id, host))
+    }
+
+    async fn handle_room_updated(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::RoomUpdated>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let room = envelope
+            .payload
+            .room
+            .ok_or_else(|| anyhow!("invalid room"))?;
+        this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
+    }
+
+    fn apply_room_update(
+        &mut self,
+        mut room: proto::Room,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        // Filter ourselves out from the room's participants.
+        let local_participant_ix = room
+            .participants
+            .iter()
+            .position(|participant| Some(participant.user_id) == self.client.user_id());
+        let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix));
+
+        let pending_participant_user_ids = room
+            .pending_participants
+            .iter()
+            .map(|p| p.user_id)
+            .collect::<Vec<_>>();
+
+        let remote_participant_user_ids = room
+            .participants
+            .iter()
+            .map(|p| p.user_id)
+            .collect::<Vec<_>>();
+
+        let (remote_participants, pending_participants) =
+            self.user_store.update(cx, move |user_store, cx| {
+                (
+                    user_store.get_users(remote_participant_user_ids, cx),
+                    user_store.get_users(pending_participant_user_ids, cx),
+                )
+            });
+
+        self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
+            let (remote_participants, pending_participants) =
+                futures::join!(remote_participants, pending_participants);
+
+            this.update(&mut cx, |this, cx| {
+                this.participant_user_ids.clear();
+
+                if let Some(participant) = local_participant {
+                    this.local_participant.projects = participant.projects;
+                } else {
+                    this.local_participant.projects.clear();
+                }
+
+                if let Some(participants) = remote_participants.log_err() {
+                    for (participant, user) in room.participants.into_iter().zip(participants) {
+                        let Some(peer_id) = participant.peer_id else {
+                            continue;
+                        };
+                        let participant_index = ParticipantIndex(participant.participant_index);
+                        this.participant_user_ids.insert(participant.user_id);
+
+                        let old_projects = this
+                            .remote_participants
+                            .get(&participant.user_id)
+                            .into_iter()
+                            .flat_map(|existing| &existing.projects)
+                            .map(|project| project.id)
+                            .collect::<HashSet<_>>();
+                        let new_projects = participant
+                            .projects
+                            .iter()
+                            .map(|project| project.id)
+                            .collect::<HashSet<_>>();
+
+                        for project in &participant.projects {
+                            if !old_projects.contains(&project.id) {
+                                cx.emit(Event::RemoteProjectShared {
+                                    owner: user.clone(),
+                                    project_id: project.id,
+                                    worktree_root_names: project.worktree_root_names.clone(),
+                                });
+                            }
+                        }
+
+                        for unshared_project_id in old_projects.difference(&new_projects) {
+                            this.joined_projects.retain(|project| {
+                                if let Some(project) = project.upgrade() {
+                                    project.update(cx, |project, cx| {
+                                        if project.remote_id() == Some(*unshared_project_id) {
+                                            project.disconnected_from_host(cx);
+                                            false
+                                        } else {
+                                            true
+                                        }
+                                    })
+                                } else {
+                                    false
+                                }
+                            });
+                            cx.emit(Event::RemoteProjectUnshared {
+                                project_id: *unshared_project_id,
+                            });
+                        }
+
+                        let location = ParticipantLocation::from_proto(participant.location)
+                            .unwrap_or(ParticipantLocation::External);
+                        if let Some(remote_participant) =
+                            this.remote_participants.get_mut(&participant.user_id)
+                        {
+                            remote_participant.peer_id = peer_id;
+                            remote_participant.projects = participant.projects;
+                            remote_participant.participant_index = participant_index;
+                            if location != remote_participant.location {
+                                remote_participant.location = location;
+                                cx.emit(Event::ParticipantLocationChanged {
+                                    participant_id: peer_id,
+                                });
+                            }
+                        } else {
+                            this.remote_participants.insert(
+                                participant.user_id,
+                                RemoteParticipant {
+                                    user: user.clone(),
+                                    participant_index,
+                                    peer_id,
+                                    projects: participant.projects,
+                                    location,
+                                    muted: true,
+                                    speaking: false,
+                                    // 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();
+                            //     }
+                            // }
+                        }
+                    }
+
+                    this.remote_participants.retain(|user_id, participant| {
+                        if this.participant_user_ids.contains(user_id) {
+                            true
+                        } else {
+                            for project in &participant.projects {
+                                cx.emit(Event::RemoteProjectUnshared {
+                                    project_id: project.id,
+                                });
+                            }
+                            false
+                        }
+                    });
+                }
+
+                if let Some(pending_participants) = pending_participants.log_err() {
+                    this.pending_participants = pending_participants;
+                    for participant in &this.pending_participants {
+                        this.participant_user_ids.insert(participant.id);
+                    }
+                }
+
+                this.follows_by_leader_id_project_id.clear();
+                for follower in room.followers {
+                    let project_id = follower.project_id;
+                    let (leader, follower) = match (follower.leader_id, follower.follower_id) {
+                        (Some(leader), Some(follower)) => (leader, follower),
+
+                        _ => {
+                            log::error!("Follower message {follower:?} missing some state");
+                            continue;
+                        }
+                    };
+
+                    let list = this
+                        .follows_by_leader_id_project_id
+                        .entry((leader, project_id))
+                        .or_insert(Vec::new());
+                    if !list.contains(&follower) {
+                        list.push(follower);
+                    }
+                }
+
+                this.pending_room_update.take();
+                if this.should_leave() {
+                    log::info!("room is empty, leaving");
+                    let _ = this.leave(cx);
+                }
+
+                this.user_store.update(cx, |user_store, cx| {
+                    let participant_indices_by_user_id = this
+                        .remote_participants
+                        .iter()
+                        .map(|(user_id, participant)| (*user_id, participant.participant_index))
+                        .collect();
+                    user_store.set_participant_indices(participant_indices_by_user_id, cx);
+                });
+
+                this.check_invariants();
+                this.room_update_completed_tx.try_send(Some(())).ok();
+                cx.notify();
+            })
+            .ok();
+        }));
+
+        cx.notify();
+        Ok(())
+    }
+
+    pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
+        let mut done_rx = self.room_update_completed_rx.clone();
+        async move {
+            while let Some(result) = done_rx.next().await {
+                if result.is_some() {
+                    break;
+                }
+            }
+        }
+    }
+
+    fn remote_video_track_updated(
+        &mut self,
+        change: RemoteVideoTrackUpdate,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        todo!();
+        match change {
+            RemoteVideoTrackUpdate::Subscribed(track) => {
+                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.video_tracks.insert(
+                //     track_id.clone(),
+                //     Arc::new(RemoteVideoTrack {
+                //         live_kit_track: track,
+                //     }),
+                // );
+                cx.emit(Event::RemoteVideoTracksChanged {
+                    participant_id: participant.peer_id,
+                });
+            }
+            RemoteVideoTrackUpdate::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.video_tracks.remove(&track_id);
+                cx.emit(Event::RemoteVideoTracksChanged {
+                    participant_id: participant.peer_id,
+                });
+            }
+        }
+
+        cx.notify();
+        Ok(())
+    }
+
+    fn remote_audio_track_updated(
+        &mut self,
+        change: RemoteAudioTrackUpdate,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        match change {
+            RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } => {
+                let mut speaker_ids = speakers
+                    .into_iter()
+                    .filter_map(|speaker_sid| speaker_sid.parse().ok())
+                    .collect::<Vec<u64>>();
+                speaker_ids.sort_unstable();
+                for (sid, participant) in &mut self.remote_participants {
+                    if let Ok(_) = speaker_ids.binary_search(sid) {
+                        participant.speaking = true;
+                    } else {
+                        participant.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 } => {
+                // 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) => {
+                // 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,
+            } => {
+                // 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,
+                // });
+            }
+        }
+
+        cx.notify();
+        Ok(())
+    }
+
+    fn check_invariants(&self) {
+        #[cfg(any(test, feature = "test-support"))]
+        {
+            for participant in self.remote_participants.values() {
+                assert!(self.participant_user_ids.contains(&participant.user.id));
+                assert_ne!(participant.user.id, self.client.user_id().unwrap());
+            }
+
+            for participant in &self.pending_participants {
+                assert!(self.participant_user_ids.contains(&participant.id));
+                assert_ne!(participant.id, self.client.user_id().unwrap());
+            }
+
+            assert_eq!(
+                self.participant_user_ids.len(),
+                self.remote_participants.len() + self.pending_participants.len()
+            );
+        }
+    }
+
+    pub(crate) fn call(
+        &mut self,
+        called_user_id: u64,
+        initial_project_id: Option<u64>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if self.status.is_offline() {
+            return Task::ready(Err(anyhow!("room is offline")));
+        }
+
+        cx.notify();
+        let client = self.client.clone();
+        let room_id = self.id;
+        self.pending_call_count += 1;
+        cx.spawn(move |this, mut cx| async move {
+            let result = client
+                .request(proto::Call {
+                    room_id,
+                    called_user_id,
+                    initial_project_id,
+                })
+                .await;
+            this.update(&mut cx, |this, cx| {
+                this.pending_call_count -= 1;
+                if this.should_leave() {
+                    this.leave(cx).detach_and_log_err(cx);
+                }
+            })?;
+            result?;
+            Ok(())
+        })
+    }
+
+    pub fn join_project(
+        &mut self,
+        id: u64,
+        language_registry: Arc<LanguageRegistry>,
+        fs: Arc<dyn Fs>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Model<Project>>> {
+        let client = self.client.clone();
+        let user_store = self.user_store.clone();
+        cx.emit(Event::RemoteProjectJoined { project_id: id });
+        cx.spawn(move |this, mut cx| async move {
+            let project =
+                Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
+
+            this.update(&mut cx, |this, cx| {
+                this.joined_projects.retain(|project| {
+                    if let Some(project) = project.upgrade() {
+                        !project.read(cx).is_read_only()
+                    } else {
+                        false
+                    }
+                });
+                this.joined_projects.insert(project.downgrade());
+            })?;
+            Ok(project)
+        })
+    }
+
+    pub(crate) fn share_project(
+        &mut self,
+        project: Model<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<u64>> {
+        if let Some(project_id) = project.read(cx).remote_id() {
+            return Task::ready(Ok(project_id));
+        }
+
+        let request = self.client.request(proto::ShareProject {
+            room_id: self.id(),
+            worktrees: project.read(cx).worktree_metadata_protos(cx),
+        });
+        cx.spawn(|this, mut cx| async move {
+            let response = request.await?;
+
+            project.update(&mut cx, |project, cx| {
+                project.shared(response.project_id, cx)
+            })??;
+
+            // If the user's location is in this project, it changes from UnsharedProject to SharedProject.
+            this.update(&mut cx, |this, cx| {
+                this.shared_projects.insert(project.downgrade());
+                let active_project = this.local_participant.active_project.as_ref();
+                if active_project.map_or(false, |location| *location == project) {
+                    this.set_location(Some(&project), cx)
+                } else {
+                    Task::ready(Ok(()))
+                }
+            })?
+            .await?;
+
+            Ok(response.project_id)
+        })
+    }
+
+    pub(crate) fn unshare_project(
+        &mut self,
+        project: Model<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let project_id = match project.read(cx).remote_id() {
+            Some(project_id) => project_id,
+            None => return Ok(()),
+        };
+
+        self.client.send(proto::UnshareProject { project_id })?;
+        project.update(cx, |this, cx| this.unshare(cx))
+    }
+
+    pub(crate) fn set_location(
+        &mut self,
+        project: Option<&Model<Project>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if self.status.is_offline() {
+            return Task::ready(Err(anyhow!("room is offline")));
+        }
+
+        let client = self.client.clone();
+        let room_id = self.id;
+        let location = if let Some(project) = project {
+            self.local_participant.active_project = Some(project.downgrade());
+            if let Some(project_id) = project.read(cx).remote_id() {
+                proto::participant_location::Variant::SharedProject(
+                    proto::participant_location::SharedProject { id: project_id },
+                )
+            } else {
+                proto::participant_location::Variant::UnsharedProject(
+                    proto::participant_location::UnsharedProject {},
+                )
+            }
+        } else {
+            self.local_participant.active_project = None;
+            proto::participant_location::Variant::External(proto::participant_location::External {})
+        };
+
+        cx.notify();
+        cx.executor().spawn_on_main(move || async move {
+            client
+                .request(proto::UpdateParticipantLocation {
+                    room_id,
+                    location: Some(proto::ParticipantLocation {
+                        variant: Some(location),
+                    }),
+                })
+                .await?;
+            Ok(())
+        })
+    }
+
+    pub fn is_screen_sharing(&self) -> bool {
+        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 {
+        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 {
+        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 {
+        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)
+        todo!()
+    }
+
+    #[track_caller]
+    pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        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<()>> {
+        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<()>>> {
+        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<()>>> {
+        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<()> {
+        if self.status.is_offline() {
+            return Err(anyhow!("room is offline"));
+        }
+
+        todo!()
+        // let live_kit = self
+        //     .live_kit
+        //     .as_mut()
+        //     .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
+        // match mem::take(&mut live_kit.screen_track) {
+        //     LocalTrack::None => Err(anyhow!("screen was not shared")),
+        //     LocalTrack::Pending { .. } => {
+        //         cx.notify();
+        //         Ok(())
+        //     }
+        //     LocalTrack::Published {
+        //         track_publication, ..
+        //     } => {
+        //         live_kit.room.unpublish_track(track_publication);
+        //         cx.notify();
+
+        //         Audio::play_sound(Sound::StopScreenshare, cx);
+        //         Ok(())
+        //     }
+        // }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn set_display_sources(&self, sources: Vec<live_kit_client::MacOSDisplay>) {
+        todo!()
+        // self.live_kit
+        //     .as_ref()
+        //     .unwrap()
+        //     .room
+        //     .set_display_sources(sources);
+    }
+}
+
+struct LiveKitRoom {
+    room: Arc<live_kit_client::Room>,
+    screen_track: LocalTrack,
+    microphone_track: LocalTrack,
+    /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
+    muted_by_user: bool,
+    deafened: bool,
+    speaking: bool,
+    next_publish_id: usize,
+    _maintain_room: Task<()>,
+    _maintain_tracks: [Task<()>; 2],
+}
+
+impl LiveKitRoom {
+    fn set_mute(
+        self: &mut LiveKitRoom,
+        should_mute: bool,
+        cx: &mut ModelContext<Room>,
+    ) -> Result<(Task<Result<()>>, bool)> {
+        if !should_mute {
+            // clear user muting state.
+            self.muted_by_user = false;
+        }
+
+        let (result, old_muted) = match &mut self.microphone_track {
+            LocalTrack::None => Err(anyhow!("microphone was not shared")),
+            LocalTrack::Pending { muted, .. } => {
+                let old_muted = *muted;
+                *muted = should_mute;
+                cx.notify();
+                Ok((Task::Ready(Some(Ok(()))), old_muted))
+            }
+            LocalTrack::Published {
+                track_publication,
+                muted,
+            } => {
+                let old_muted = *muted;
+                *muted = should_mute;
+                cx.notify();
+                Ok((
+                    cx.executor().spawn(track_publication.set_mute(*muted)),
+                    old_muted,
+                ))
+            }
+        }?;
+
+        if old_muted != should_mute {
+            if should_mute {
+                Audio::play_sound(Sound::Mute, cx);
+            } else {
+                Audio::play_sound(Sound::Unmute, cx);
+            }
+        }
+
+        Ok((result, old_muted))
+    }
+}
+
+enum LocalTrack {
+    None,
+    Pending {
+        publish_id: usize,
+        muted: bool,
+    },
+    Published {
+        track_publication: LocalTrackPublication,
+        muted: bool,
+    },
+}
+
+impl Default for LocalTrack {
+    fn default() -> Self {
+        Self::None
+    }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum RoomStatus {
+    Online,
+    Rejoining,
+    Offline,
+}
+
+impl RoomStatus {
+    pub fn is_offline(&self) -> bool {
+        matches!(self, RoomStatus::Offline)
+    }
+
+    pub fn is_online(&self) -> bool {
+        matches!(self, RoomStatus::Online)
+    }
+}

crates/client2/Cargo.toml 🔗

@@ -0,0 +1,52 @@
+[package]
+name = "client2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/client2.rs"
+doctest = false
+
+[features]
+test-support = ["collections/test-support", "gpui2/test-support", "rpc2/test-support"]
+
+[dependencies]
+collections = { path = "../collections" }
+db2 = { path = "../db2" }
+gpui2 = { path = "../gpui2" }
+util = { path = "../util" }
+rpc2 = { path = "../rpc2" }
+text = { path = "../text" }
+settings2 = { path = "../settings2" }
+feature_flags2 = { path = "../feature_flags2" }
+sum_tree = { path = "../sum_tree" }
+
+anyhow.workspace = true
+async-recursion = "0.3"
+async-tungstenite = { version = "0.16", features = ["async-tls"] }
+futures.workspace = true
+image = "0.23"
+lazy_static.workspace = true
+log.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+rand.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smol.workspace = true
+sysinfo.workspace = true
+tempfile = "3"
+thiserror.workspace = true
+time.workspace = true
+tiny_http = "0.8"
+uuid.workspace = true
+url = "2.2"
+
+[dev-dependencies]
+collections = { path = "../collections", features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+rpc2 = { path = "../rpc2", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }

crates/client2/src/client2.rs 🔗

@@ -0,0 +1,1651 @@
+#[cfg(any(test, feature = "test-support"))]
+pub mod test;
+
+pub mod telemetry;
+pub mod user;
+
+use anyhow::{anyhow, Context as _, Result};
+use async_recursion::async_recursion;
+use async_tungstenite::tungstenite::{
+    error::Error as WebsocketError,
+    http::{Request, StatusCode},
+};
+use futures::{
+    future::BoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _, TryStreamExt,
+};
+use gpui2::{
+    serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, SemanticVersion, Task,
+    WeakModel,
+};
+use lazy_static::lazy_static;
+use parking_lot::RwLock;
+use postage::watch;
+use rand::prelude::*;
+use rpc2::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings2::Settings;
+use std::{
+    any::TypeId,
+    collections::HashMap,
+    convert::TryFrom,
+    fmt::Write as _,
+    future::Future,
+    marker::PhantomData,
+    path::PathBuf,
+    sync::{atomic::AtomicU64, Arc, Weak},
+    time::{Duration, Instant},
+};
+use telemetry::Telemetry;
+use thiserror::Error;
+use url::Url;
+use util::channel::ReleaseChannel;
+use util::http::HttpClient;
+use util::{ResultExt, TryFutureExt};
+
+pub use rpc2::*;
+pub use telemetry::ClickhouseEvent;
+pub use user::*;
+
+lazy_static! {
+    pub static ref ZED_SERVER_URL: String =
+        std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string());
+    pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
+        .ok()
+        .and_then(|s| if s.is_empty() { None } else { Some(s) });
+    pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
+        .ok()
+        .and_then(|s| if s.is_empty() { None } else { Some(s) });
+    pub static ref ZED_APP_VERSION: Option<SemanticVersion> = std::env::var("ZED_APP_VERSION")
+        .ok()
+        .and_then(|v| v.parse().ok());
+    pub static ref ZED_APP_PATH: Option<PathBuf> =
+        std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
+    pub static ref ZED_ALWAYS_ACTIVE: bool =
+        std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0);
+}
+
+pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
+pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
+pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
+
+#[derive(Clone, Default, PartialEq, Deserialize)]
+pub struct SignIn;
+
+#[derive(Clone, Default, PartialEq, Deserialize)]
+pub struct SignOut;
+
+#[derive(Clone, Default, PartialEq, Deserialize)]
+pub struct Reconnect;
+
+pub fn init_settings(cx: &mut AppContext) {
+    TelemetrySettings::register(cx);
+}
+
+pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
+    init_settings(cx);
+
+    let client = Arc::downgrade(client);
+    cx.register_action_type::<SignIn>();
+    cx.on_action({
+        let client = client.clone();
+        move |_: &SignIn, cx| {
+            if let Some(client) = client.upgrade() {
+                cx.spawn(
+                    |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
+                )
+                .detach();
+            }
+        }
+    });
+
+    cx.register_action_type::<SignOut>();
+    cx.on_action({
+        let client = client.clone();
+        move |_: &SignOut, cx| {
+            if let Some(client) = client.upgrade() {
+                cx.spawn(|cx| async move {
+                    client.disconnect(&cx);
+                })
+                .detach();
+            }
+        }
+    });
+
+    cx.register_action_type::<Reconnect>();
+    cx.on_action({
+        let client = client.clone();
+        move |_: &Reconnect, cx| {
+            if let Some(client) = client.upgrade() {
+                cx.spawn(|cx| async move {
+                    client.reconnect(&cx);
+                })
+                .detach();
+            }
+        }
+    });
+}
+
+pub struct Client {
+    id: AtomicU64,
+    peer: Arc<Peer>,
+    http: Arc<dyn HttpClient>,
+    telemetry: Arc<Telemetry>,
+    state: RwLock<ClientState>,
+
+    #[allow(clippy::type_complexity)]
+    #[cfg(any(test, feature = "test-support"))]
+    authenticate: RwLock<
+        Option<Box<dyn 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>>>,
+    >,
+
+    #[allow(clippy::type_complexity)]
+    #[cfg(any(test, feature = "test-support"))]
+    establish_connection: RwLock<
+        Option<
+            Box<
+                dyn 'static
+                    + Send
+                    + Sync
+                    + Fn(
+                        &Credentials,
+                        &AsyncAppContext,
+                    ) -> Task<Result<Connection, EstablishConnectionError>>,
+            >,
+        >,
+    >,
+}
+
+#[derive(Error, Debug)]
+pub enum EstablishConnectionError {
+    #[error("upgrade required")]
+    UpgradeRequired,
+    #[error("unauthorized")]
+    Unauthorized,
+    #[error("{0}")]
+    Other(#[from] anyhow::Error),
+    #[error("{0}")]
+    Http(#[from] util::http::Error),
+    #[error("{0}")]
+    Io(#[from] std::io::Error),
+    #[error("{0}")]
+    Websocket(#[from] async_tungstenite::tungstenite::http::Error),
+}
+
+impl From<WebsocketError> for EstablishConnectionError {
+    fn from(error: WebsocketError) -> Self {
+        if let WebsocketError::Http(response) = &error {
+            match response.status() {
+                StatusCode::UNAUTHORIZED => return EstablishConnectionError::Unauthorized,
+                StatusCode::UPGRADE_REQUIRED => return EstablishConnectionError::UpgradeRequired,
+                _ => {}
+            }
+        }
+        EstablishConnectionError::Other(error.into())
+    }
+}
+
+impl EstablishConnectionError {
+    pub fn other(error: impl Into<anyhow::Error> + Send + Sync) -> Self {
+        Self::Other(error.into())
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum Status {
+    SignedOut,
+    UpgradeRequired,
+    Authenticating,
+    Connecting,
+    ConnectionError,
+    Connected {
+        peer_id: PeerId,
+        connection_id: ConnectionId,
+    },
+    ConnectionLost,
+    Reauthenticating,
+    Reconnecting,
+    ReconnectionError {
+        next_reconnection: Instant,
+    },
+}
+
+impl Status {
+    pub fn is_connected(&self) -> bool {
+        matches!(self, Self::Connected { .. })
+    }
+
+    pub fn is_signed_out(&self) -> bool {
+        matches!(self, Self::SignedOut | Self::UpgradeRequired)
+    }
+}
+
+struct ClientState {
+    credentials: Option<Credentials>,
+    status: (watch::Sender<Status>, watch::Receiver<Status>),
+    entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
+    _reconnect_task: Option<Task<()>>,
+    reconnect_interval: Duration,
+    entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>,
+    models_by_message_type: HashMap<TypeId, AnyWeakModel>,
+    entity_types_by_message_type: HashMap<TypeId, TypeId>,
+    #[allow(clippy::type_complexity)]
+    message_handlers: HashMap<
+        TypeId,
+        Arc<
+            dyn Send
+                + Sync
+                + Fn(
+                    AnyModel,
+                    Box<dyn AnyTypedEnvelope>,
+                    &Arc<Client>,
+                    AsyncAppContext,
+                ) -> BoxFuture<'static, Result<()>>,
+        >,
+    >,
+}
+
+enum WeakSubscriber {
+    Entity { handle: AnyWeakModel },
+    Pending(Vec<Box<dyn AnyTypedEnvelope>>),
+}
+
+#[derive(Clone, Debug)]
+pub struct Credentials {
+    pub user_id: u64,
+    pub access_token: String,
+}
+
+impl Default for ClientState {
+    fn default() -> Self {
+        Self {
+            credentials: None,
+            status: watch::channel_with(Status::SignedOut),
+            entity_id_extractors: Default::default(),
+            _reconnect_task: None,
+            reconnect_interval: Duration::from_secs(5),
+            models_by_message_type: Default::default(),
+            entities_by_type_and_remote_id: Default::default(),
+            entity_types_by_message_type: Default::default(),
+            message_handlers: Default::default(),
+        }
+    }
+}
+
+pub enum Subscription {
+    Entity {
+        client: Weak<Client>,
+        id: (TypeId, u64),
+    },
+    Message {
+        client: Weak<Client>,
+        id: TypeId,
+    },
+}
+
+impl Drop for Subscription {
+    fn drop(&mut self) {
+        match self {
+            Subscription::Entity { client, id } => {
+                if let Some(client) = client.upgrade() {
+                    let mut state = client.state.write();
+                    let _ = state.entities_by_type_and_remote_id.remove(id);
+                }
+            }
+            Subscription::Message { client, id } => {
+                if let Some(client) = client.upgrade() {
+                    let mut state = client.state.write();
+                    let _ = state.entity_types_by_message_type.remove(id);
+                    let _ = state.message_handlers.remove(id);
+                }
+            }
+        }
+    }
+}
+
+pub struct PendingEntitySubscription<T: 'static> {
+    client: Arc<Client>,
+    remote_id: u64,
+    _entity_type: PhantomData<T>,
+    consumed: bool,
+}
+
+impl<T> PendingEntitySubscription<T>
+where
+    T: 'static + Send,
+{
+    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);
+        let Some(WeakSubscriber::Pending(messages)) =
+            state.entities_by_type_and_remote_id.remove(&id)
+        else {
+            unreachable!()
+        };
+
+        state.entities_by_type_and_remote_id.insert(
+            id,
+            WeakSubscriber::Entity {
+                handle: model.downgrade().into(),
+            },
+        );
+        drop(state);
+        for message in messages {
+            self.client.handle_message(message, cx);
+        }
+        Subscription::Entity {
+            client: Arc::downgrade(&self.client),
+            id,
+        }
+    }
+}
+
+impl<T> Drop for PendingEntitySubscription<T>
+where
+    T: 'static,
+{
+    fn drop(&mut self) {
+        if !self.consumed {
+            let mut state = self.client.state.write();
+            if let Some(WeakSubscriber::Pending(messages)) = state
+                .entities_by_type_and_remote_id
+                .remove(&(TypeId::of::<T>(), self.remote_id))
+            {
+                for message in messages {
+                    log::info!("unhandled message {}", message.payload_type_name());
+                }
+            }
+        }
+    }
+}
+
+#[derive(Copy, Clone)]
+pub struct TelemetrySettings {
+    pub diagnostics: bool,
+    pub metrics: bool,
+}
+
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct TelemetrySettingsContent {
+    pub diagnostics: Option<bool>,
+    pub metrics: Option<bool>,
+}
+
+impl settings2::Settings for TelemetrySettings {
+    const KEY: Option<&'static str> = Some("telemetry");
+
+    type FileContent = TelemetrySettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut AppContext,
+    ) -> Result<Self> {
+        Ok(Self {
+            diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or(
+                default_value
+                    .diagnostics
+                    .ok_or_else(Self::missing_default)?,
+            ),
+            metrics: user_values
+                .first()
+                .and_then(|v| v.metrics)
+                .unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?),
+        })
+    }
+}
+
+impl Client {
+    pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
+        Arc::new(Self {
+            id: AtomicU64::new(0),
+            peer: Peer::new(0),
+            telemetry: Telemetry::new(http.clone(), cx),
+            http,
+            state: Default::default(),
+
+            #[cfg(any(test, feature = "test-support"))]
+            authenticate: Default::default(),
+            #[cfg(any(test, feature = "test-support"))]
+            establish_connection: Default::default(),
+        })
+    }
+
+    pub fn id(&self) -> u64 {
+        self.id.load(std::sync::atomic::Ordering::SeqCst)
+    }
+
+    pub fn http_client(&self) -> Arc<dyn HttpClient> {
+        self.http.clone()
+    }
+
+    pub fn set_id(&self, id: u64) -> &Self {
+        self.id.store(id, std::sync::atomic::Ordering::SeqCst);
+        self
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn teardown(&self) {
+        let mut state = self.state.write();
+        state._reconnect_task.take();
+        state.message_handlers.clear();
+        state.models_by_message_type.clear();
+        state.entities_by_type_and_remote_id.clear();
+        state.entity_id_extractors.clear();
+        self.peer.teardown();
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn override_authenticate<F>(&self, authenticate: F) -> &Self
+    where
+        F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task<Result<Credentials>>,
+    {
+        *self.authenticate.write() = Some(Box::new(authenticate));
+        self
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn override_establish_connection<F>(&self, connect: F) -> &Self
+    where
+        F: 'static
+            + Send
+            + Sync
+            + Fn(&Credentials, &AsyncAppContext) -> Task<Result<Connection, EstablishConnectionError>>,
+    {
+        *self.establish_connection.write() = Some(Box::new(connect));
+        self
+    }
+
+    pub fn user_id(&self) -> Option<u64> {
+        self.state
+            .read()
+            .credentials
+            .as_ref()
+            .map(|credentials| credentials.user_id)
+    }
+
+    pub fn peer_id(&self) -> Option<PeerId> {
+        if let Status::Connected { peer_id, .. } = &*self.status().borrow() {
+            Some(*peer_id)
+        } else {
+            None
+        }
+    }
+
+    pub fn status(&self) -> watch::Receiver<Status> {
+        self.state.read().status.1.clone()
+    }
+
+    fn set_status(self: &Arc<Self>, status: Status, cx: &AsyncAppContext) {
+        log::info!("set status on client {}: {:?}", self.id(), status);
+        let mut state = self.state.write();
+        *state.status.0.borrow_mut() = status;
+
+        match status {
+            Status::Connected { .. } => {
+                state._reconnect_task = None;
+            }
+            Status::ConnectionLost => {
+                let this = self.clone();
+                let reconnect_interval = state.reconnect_interval;
+                state._reconnect_task = Some(cx.spawn(move |cx| async move {
+                    #[cfg(any(test, feature = "test-support"))]
+                    let mut rng = StdRng::seed_from_u64(0);
+                    #[cfg(not(any(test, feature = "test-support")))]
+                    let mut rng = StdRng::from_entropy();
+
+                    let mut delay = INITIAL_RECONNECTION_DELAY;
+                    while let Err(error) = this.authenticate_and_connect(true, &cx).await {
+                        log::error!("failed to connect {}", error);
+                        if matches!(*this.status().borrow(), Status::ConnectionError) {
+                            this.set_status(
+                                Status::ReconnectionError {
+                                    next_reconnection: Instant::now() + delay,
+                                },
+                                &cx,
+                            );
+                            cx.executor().timer(delay).await;
+                            delay = delay
+                                .mul_f32(rng.gen_range(1.0..=2.0))
+                                .min(reconnect_interval);
+                        } else {
+                            break;
+                        }
+                    }
+                }));
+            }
+            Status::SignedOut | Status::UpgradeRequired => {
+                cx.update(|cx| self.telemetry.set_authenticated_user_info(None, false, cx))
+                    .log_err();
+                state._reconnect_task.take();
+            }
+            _ => {}
+        }
+    }
+
+    pub fn subscribe_to_entity<T>(
+        self: &Arc<Self>,
+        remote_id: u64,
+    ) -> Result<PendingEntitySubscription<T>>
+    where
+        T: 'static + Send,
+    {
+        let id = (TypeId::of::<T>(), remote_id);
+
+        let mut state = self.state.write();
+        if state.entities_by_type_and_remote_id.contains_key(&id) {
+            return Err(anyhow!("already subscribed to entity"));
+        } else {
+            state
+                .entities_by_type_and_remote_id
+                .insert(id, WeakSubscriber::Pending(Default::default()));
+            Ok(PendingEntitySubscription {
+                client: self.clone(),
+                remote_id,
+                consumed: false,
+                _entity_type: PhantomData,
+            })
+        }
+    }
+
+    #[track_caller]
+    pub fn add_message_handler<M, E, H, F>(
+        self: &Arc<Self>,
+        entity: WeakModel<E>,
+        handler: H,
+    ) -> Subscription
+    where
+        M: EnvelopedMessage,
+        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>();
+
+        let mut state = self.state.write();
+        state
+            .models_by_message_type
+            .insert(message_type_id, entity.into());
+
+        let prev_handler = state.message_handlers.insert(
+            message_type_id,
+            Arc::new(move |subscriber, envelope, client, cx| {
+                let subscriber = subscriber.downcast::<E>().unwrap();
+                let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
+                handler(subscriber, *envelope, client.clone(), cx).boxed()
+            }),
+        );
+        if prev_handler.is_some() {
+            let location = std::panic::Location::caller();
+            panic!(
+                "{}:{} registered handler for the same message {} twice",
+                location.file(),
+                location.line(),
+                std::any::type_name::<M>()
+            );
+        }
+
+        Subscription::Message {
+            client: Arc::downgrade(self),
+            id: message_type_id,
+        }
+    }
+
+    pub fn add_request_handler<M, E, H, F>(
+        self: &Arc<Self>,
+        model: WeakModel<E>,
+        handler: H,
+    ) -> Subscription
+    where
+        M: RequestMessage,
+        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| {
+            Self::respond_to_request(
+                envelope.receipt(),
+                handler(handle, envelope, this.clone(), cx),
+                this,
+            )
+        })
+    }
+
+    pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage,
+        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| {
+            handler(subscriber.downcast::<E>().unwrap(), message, client, cx)
+        })
+    }
+
+    fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage,
+        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>();
+        let message_type_id = TypeId::of::<M>();
+
+        let mut state = self.state.write();
+        state
+            .entity_types_by_message_type
+            .insert(message_type_id, model_type_id);
+        state
+            .entity_id_extractors
+            .entry(message_type_id)
+            .or_insert_with(|| {
+                |envelope| {
+                    envelope
+                        .as_any()
+                        .downcast_ref::<TypedEnvelope<M>>()
+                        .unwrap()
+                        .payload
+                        .remote_entity_id()
+                }
+            });
+        let prev_handler = state.message_handlers.insert(
+            message_type_id,
+            Arc::new(move |handle, envelope, client, cx| {
+                let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
+                handler(handle, *envelope, client.clone(), cx).boxed()
+            }),
+        );
+        if prev_handler.is_some() {
+            panic!("registered handler for the same message twice");
+        }
+    }
+
+    pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage + RequestMessage,
+        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| {
+            Self::respond_to_request::<M, _>(
+                envelope.receipt(),
+                handler(entity, envelope, client.clone(), cx),
+                client,
+            )
+        })
+    }
+
+    async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
+        receipt: Receipt<T>,
+        response: F,
+        client: Arc<Self>,
+    ) -> Result<()> {
+        match response.await {
+            Ok(response) => {
+                client.respond(receipt, response)?;
+                Ok(())
+            }
+            Err(error) => {
+                client.respond_with_error(
+                    receipt,
+                    proto::Error {
+                        message: format!("{:?}", error),
+                    },
+                )?;
+                Err(error)
+            }
+        }
+    }
+
+    pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
+        read_credentials_from_keychain(cx).await.is_some()
+    }
+
+    #[async_recursion]
+    pub async fn authenticate_and_connect(
+        self: &Arc<Self>,
+        try_keychain: bool,
+        cx: &AsyncAppContext,
+    ) -> anyhow::Result<()> {
+        let was_disconnected = match *self.status().borrow() {
+            Status::SignedOut => true,
+            Status::ConnectionError
+            | Status::ConnectionLost
+            | Status::Authenticating { .. }
+            | Status::Reauthenticating { .. }
+            | Status::ReconnectionError { .. } => false,
+            Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
+                return Ok(())
+            }
+            Status::UpgradeRequired => return Err(EstablishConnectionError::UpgradeRequired)?,
+        };
+
+        if was_disconnected {
+            self.set_status(Status::Authenticating, cx);
+        } else {
+            self.set_status(Status::Reauthenticating, cx)
+        }
+
+        let mut read_from_keychain = false;
+        let mut credentials = self.state.read().credentials.clone();
+        if credentials.is_none() && try_keychain {
+            credentials = read_credentials_from_keychain(cx).await;
+            read_from_keychain = credentials.is_some();
+        }
+        if credentials.is_none() {
+            let mut status_rx = self.status();
+            let _ = status_rx.next().await;
+            futures::select_biased! {
+                authenticate = self.authenticate(cx).fuse() => {
+                    match authenticate {
+                        Ok(creds) => credentials = Some(creds),
+                        Err(err) => {
+                            self.set_status(Status::ConnectionError, cx);
+                            return Err(err);
+                        }
+                    }
+                }
+                _ = status_rx.next().fuse() => {
+                    return Err(anyhow!("authentication canceled"));
+                }
+            }
+        }
+        let credentials = credentials.unwrap();
+        self.set_id(credentials.user_id);
+
+        if was_disconnected {
+            self.set_status(Status::Connecting, cx);
+        } else {
+            self.set_status(Status::Reconnecting, cx);
+        }
+
+        let mut timeout = futures::FutureExt::fuse(cx.executor().timer(CONNECTION_TIMEOUT));
+        futures::select_biased! {
+            connection = self.establish_connection(&credentials, cx).fuse() => {
+                match connection {
+                    Ok(conn) => {
+                        self.state.write().credentials = Some(credentials.clone());
+                        if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
+                            write_credentials_to_keychain(credentials, cx).log_err();
+                        }
+
+                        futures::select_biased! {
+                            result = self.set_connection(conn, cx).fuse() => result,
+                            _ = timeout => {
+                                self.set_status(Status::ConnectionError, cx);
+                                Err(anyhow!("timed out waiting on hello message from server"))
+                            }
+                        }
+                    }
+                    Err(EstablishConnectionError::Unauthorized) => {
+                        self.state.write().credentials.take();
+                        if read_from_keychain {
+                            delete_credentials_from_keychain(cx).log_err();
+                            self.set_status(Status::SignedOut, cx);
+                            self.authenticate_and_connect(false, cx).await
+                        } else {
+                            self.set_status(Status::ConnectionError, cx);
+                            Err(EstablishConnectionError::Unauthorized)?
+                        }
+                    }
+                    Err(EstablishConnectionError::UpgradeRequired) => {
+                        self.set_status(Status::UpgradeRequired, cx);
+                        Err(EstablishConnectionError::UpgradeRequired)?
+                    }
+                    Err(error) => {
+                        self.set_status(Status::ConnectionError, cx);
+                        Err(error)?
+                    }
+                }
+            }
+            _ = &mut timeout => {
+                self.set_status(Status::ConnectionError, cx);
+                Err(anyhow!("timed out trying to establish connection"))
+            }
+        }
+    }
+
+    async fn set_connection(
+        self: &Arc<Self>,
+        conn: Connection,
+        cx: &AsyncAppContext,
+    ) -> Result<()> {
+        let executor = cx.executor();
+        log::info!("add connection to peer");
+        let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn, {
+            let executor = executor.clone();
+            move |duration| executor.timer(duration)
+        });
+        let handle_io = executor.spawn(handle_io);
+
+        let peer_id = async {
+            log::info!("waiting for server hello");
+            let message = incoming
+                .next()
+                .await
+                .ok_or_else(|| anyhow!("no hello message received"))?;
+            log::info!("got server hello");
+            let hello_message_type_name = message.payload_type_name().to_string();
+            let hello = message
+                .into_any()
+                .downcast::<TypedEnvelope<proto::Hello>>()
+                .map_err(|_| {
+                    anyhow!(
+                        "invalid hello message received: {:?}",
+                        hello_message_type_name
+                    )
+                })?;
+            let peer_id = hello
+                .payload
+                .peer_id
+                .ok_or_else(|| anyhow!("invalid peer id"))?;
+            Ok(peer_id)
+        };
+
+        let peer_id = match peer_id.await {
+            Ok(peer_id) => peer_id,
+            Err(error) => {
+                self.peer.disconnect(connection_id);
+                return Err(error);
+            }
+        };
+
+        log::info!(
+            "set status to connected (connection id: {:?}, peer id: {:?})",
+            connection_id,
+            peer_id
+        );
+        self.set_status(
+            Status::Connected {
+                peer_id,
+                connection_id,
+            },
+            cx,
+        );
+
+        cx.spawn({
+            let this = self.clone();
+            |cx| {
+                async move {
+                    while let Some(message) = incoming.next().await {
+                        this.handle_message(message, &cx);
+                        // Don't starve the main thread when receiving lots of messages at once.
+                        smol::future::yield_now().await;
+                    }
+                }
+            }
+        })
+        .detach();
+
+        cx.spawn({
+            let this = self.clone();
+            move |cx| async move {
+                match handle_io.await {
+                    Ok(()) => {
+                        if this.status().borrow().clone()
+                            == (Status::Connected {
+                                connection_id,
+                                peer_id,
+                            })
+                        {
+                            this.set_status(Status::SignedOut, &cx);
+                        }
+                    }
+                    Err(err) => {
+                        log::error!("connection error: {:?}", err);
+                        this.set_status(Status::ConnectionLost, &cx);
+                    }
+                }
+            }
+        })
+        .detach();
+
+        Ok(())
+    }
+
+    fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<Credentials>> {
+        #[cfg(any(test, feature = "test-support"))]
+        if let Some(callback) = self.authenticate.read().as_ref() {
+            return callback(cx);
+        }
+
+        self.authenticate_with_browser(cx)
+    }
+
+    fn establish_connection(
+        self: &Arc<Self>,
+        credentials: &Credentials,
+        cx: &AsyncAppContext,
+    ) -> Task<Result<Connection, EstablishConnectionError>> {
+        #[cfg(any(test, feature = "test-support"))]
+        if let Some(callback) = self.establish_connection.read().as_ref() {
+            return callback(credentials, cx);
+        }
+
+        self.establish_websocket_connection(credentials, cx)
+    }
+
+    async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
+        let preview_param = if is_preview { "?preview=1" } else { "" };
+        let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
+        let response = http.get(&url, Default::default(), false).await?;
+
+        // Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
+        // The website's /rpc endpoint redirects to a collab server's /rpc endpoint,
+        // which requires authorization via an HTTP header.
+        //
+        // For testing purposes, ZED_SERVER_URL can also set to the direct URL of
+        // of a collab server. In that case, a request to the /rpc endpoint will
+        // return an 'unauthorized' response.
+        let collab_url = if response.status().is_redirection() {
+            response
+                .headers()
+                .get("Location")
+                .ok_or_else(|| anyhow!("missing location header in /rpc response"))?
+                .to_str()
+                .map_err(EstablishConnectionError::other)?
+                .to_string()
+        } else if response.status() == StatusCode::UNAUTHORIZED {
+            url
+        } else {
+            Err(anyhow!(
+                "unexpected /rpc response status {}",
+                response.status()
+            ))?
+        };
+
+        Url::parse(&collab_url).context("invalid rpc url")
+    }
+
+    fn establish_websocket_connection(
+        self: &Arc<Self>,
+        credentials: &Credentials,
+        cx: &AsyncAppContext,
+    ) -> Task<Result<Connection, EstablishConnectionError>> {
+        let use_preview_server = cx
+            .try_read_global(|channel: &ReleaseChannel, _| *channel != ReleaseChannel::Stable)
+            .unwrap_or(false);
+
+        let request = Request::builder()
+            .header(
+                "Authorization",
+                format!("{} {}", credentials.user_id, credentials.access_token),
+            )
+            .header("x-zed-protocol-version", rpc2::PROTOCOL_VERSION);
+
+        let http = self.http.clone();
+        cx.executor().spawn(async move {
+            let mut rpc_url = Self::get_rpc_url(http, use_preview_server).await?;
+            let rpc_host = rpc_url
+                .host_str()
+                .zip(rpc_url.port_or_known_default())
+                .ok_or_else(|| anyhow!("missing host in rpc url"))?;
+            let stream = smol::net::TcpStream::connect(rpc_host).await?;
+
+            log::info!("connected to rpc endpoint {}", rpc_url);
+
+            match rpc_url.scheme() {
+                "https" => {
+                    rpc_url.set_scheme("wss").unwrap();
+                    let request = request.uri(rpc_url.as_str()).body(())?;
+                    let (stream, _) =
+                        async_tungstenite::async_tls::client_async_tls(request, stream).await?;
+                    Ok(Connection::new(
+                        stream
+                            .map_err(|error| anyhow!(error))
+                            .sink_map_err(|error| anyhow!(error)),
+                    ))
+                }
+                "http" => {
+                    rpc_url.set_scheme("ws").unwrap();
+                    let request = request.uri(rpc_url.as_str()).body(())?;
+                    let (stream, _) = async_tungstenite::client_async(request, stream).await?;
+                    Ok(Connection::new(
+                        stream
+                            .map_err(|error| anyhow!(error))
+                            .sink_map_err(|error| anyhow!(error)),
+                    ))
+                }
+                _ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
+            }
+        })
+    }
+
+    pub fn authenticate_with_browser(
+        self: &Arc<Self>,
+        cx: &AsyncAppContext,
+    ) -> Task<Result<Credentials>> {
+        let http = self.http.clone();
+        cx.spawn(|cx| async move {
+            // Generate a pair of asymmetric encryption keys. The public key will be used by the
+            // zed server to encrypt the user's access token, so that it can'be intercepted by
+            // any other app running on the user's device.
+            let (public_key, private_key) =
+                rpc2::auth::keypair().expect("failed to generate keypair for auth");
+            let public_key_string =
+                String::try_from(public_key).expect("failed to serialize public key for auth");
+
+            if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
+                return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
+            }
+
+            // Start an HTTP server to receive the redirect from Zed's sign-in page.
+            let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
+            let port = server.server_addr().port();
+
+            // Open the Zed sign-in page in the user's browser, with query parameters that indicate
+            // that the user is signing in from a Zed app running on the same device.
+            let mut url = format!(
+                "{}/native_app_signin?native_app_port={}&native_app_public_key={}",
+                *ZED_SERVER_URL, port, public_key_string
+            );
+
+            if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
+                log::info!("impersonating user @{}", impersonate_login);
+                write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
+            }
+
+            cx.run_on_main(move |cx| cx.open_url(&url))?.await;
+
+            // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
+            // access token from the query params.
+            //
+            // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
+            // custom URL scheme instead of this local HTTP server.
+            let (user_id, access_token) = cx
+                .spawn(|_| async move {
+                    for _ in 0..100 {
+                        if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
+                            let path = req.url();
+                            let mut user_id = None;
+                            let mut access_token = None;
+                            let url = Url::parse(&format!("http://example.com{}", path))
+                                .context("failed to parse login notification url")?;
+                            for (key, value) in url.query_pairs() {
+                                if key == "access_token" {
+                                    access_token = Some(value.to_string());
+                                } else if key == "user_id" {
+                                    user_id = Some(value.to_string());
+                                }
+                            }
+
+                            let post_auth_url =
+                                format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
+                            req.respond(
+                                tiny_http::Response::empty(302).with_header(
+                                    tiny_http::Header::from_bytes(
+                                        &b"Location"[..],
+                                        post_auth_url.as_bytes(),
+                                    )
+                                    .unwrap(),
+                                ),
+                            )
+                            .context("failed to respond to login http request")?;
+                            return Ok((
+                                user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?,
+                                access_token
+                                    .ok_or_else(|| anyhow!("missing access_token parameter"))?,
+                            ));
+                        }
+                    }
+
+                    Err(anyhow!("didn't receive login redirect"))
+                })
+                .await?;
+
+            let access_token = private_key
+                .decrypt_string(&access_token)
+                .context("failed to decrypt access token")?;
+            cx.run_on_main(|cx| cx.activate(true))?.await;
+
+            Ok(Credentials {
+                user_id: user_id.parse()?,
+                access_token,
+            })
+        })
+    }
+
+    async fn authenticate_as_admin(
+        http: Arc<dyn HttpClient>,
+        login: String,
+        mut api_token: String,
+    ) -> Result<Credentials> {
+        #[derive(Deserialize)]
+        struct AuthenticatedUserResponse {
+            user: User,
+        }
+
+        #[derive(Deserialize)]
+        struct User {
+            id: u64,
+        }
+
+        // Use the collab server's admin API to retrieve the id
+        // of the impersonated user.
+        let mut url = Self::get_rpc_url(http.clone(), false).await?;
+        url.set_path("/user");
+        url.set_query(Some(&format!("github_login={login}")));
+        let request = Request::get(url.as_str())
+            .header("Authorization", format!("token {api_token}"))
+            .body("".into())?;
+
+        let mut response = http.send(request).await?;
+        let mut body = String::new();
+        response.body_mut().read_to_string(&mut body).await?;
+        if !response.status().is_success() {
+            Err(anyhow!(
+                "admin user request failed {} - {}",
+                response.status().as_u16(),
+                body,
+            ))?;
+        }
+        let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
+
+        // Use the admin API token to authenticate as the impersonated user.
+        api_token.insert_str(0, "ADMIN_TOKEN:");
+        Ok(Credentials {
+            user_id: response.user.id,
+            access_token: api_token,
+        })
+    }
+
+    pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
+        self.peer.teardown();
+        self.set_status(Status::SignedOut, cx);
+    }
+
+    pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
+        self.peer.teardown();
+        self.set_status(Status::ConnectionLost, cx);
+    }
+
+    fn connection_id(&self) -> Result<ConnectionId> {
+        if let Status::Connected { connection_id, .. } = *self.status().borrow() {
+            Ok(connection_id)
+        } else {
+            Err(anyhow!("not connected"))
+        }
+    }
+
+    pub fn send<T: EnvelopedMessage>(&self, message: T) -> Result<()> {
+        log::debug!("rpc send. client_id:{}, name:{}", self.id(), T::NAME);
+        self.peer.send(self.connection_id()?, message)
+    }
+
+    pub fn request<T: RequestMessage>(
+        &self,
+        request: T,
+    ) -> impl Future<Output = Result<T::Response>> {
+        self.request_envelope(request)
+            .map_ok(|envelope| envelope.payload)
+    }
+
+    pub fn request_envelope<T: RequestMessage>(
+        &self,
+        request: T,
+    ) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
+        let client_id = self.id();
+        log::debug!(
+            "rpc request start. client_id:{}. name:{}",
+            client_id,
+            T::NAME
+        );
+        let response = self
+            .connection_id()
+            .map(|conn_id| self.peer.request_envelope(conn_id, request));
+        async move {
+            let response = response?.await;
+            log::debug!(
+                "rpc request finish. client_id:{}. name:{}",
+                client_id,
+                T::NAME
+            );
+            response
+        }
+    }
+
+    fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
+        log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
+        self.peer.respond(receipt, response)
+    }
+
+    fn respond_with_error<T: RequestMessage>(
+        &self,
+        receipt: Receipt<T>,
+        error: proto::Error,
+    ) -> Result<()> {
+        log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
+        self.peer.respond_with_error(receipt, error)
+    }
+
+    fn handle_message(
+        self: &Arc<Client>,
+        message: Box<dyn AnyTypedEnvelope>,
+        cx: &AsyncAppContext,
+    ) {
+        let mut state = self.state.write();
+        let type_name = message.payload_type_name();
+        let payload_type_id = message.payload_type_id();
+        let sender_id = message.original_sender_id();
+
+        let mut subscriber = None;
+
+        if let Some(handle) = state
+            .models_by_message_type
+            .get(&payload_type_id)
+            .and_then(|handle| handle.upgrade())
+        {
+            subscriber = Some(handle);
+        } else if let Some((extract_entity_id, entity_type_id)) =
+            state.entity_id_extractors.get(&payload_type_id).zip(
+                state
+                    .entity_types_by_message_type
+                    .get(&payload_type_id)
+                    .copied(),
+            )
+        {
+            let entity_id = (extract_entity_id)(message.as_ref());
+
+            match state
+                .entities_by_type_and_remote_id
+                .get_mut(&(entity_type_id, entity_id))
+            {
+                Some(WeakSubscriber::Pending(pending)) => {
+                    pending.push(message);
+                    return;
+                }
+                Some(weak_subscriber @ _) => match weak_subscriber {
+                    WeakSubscriber::Entity { handle } => {
+                        subscriber = handle.upgrade();
+                    }
+
+                    WeakSubscriber::Pending(_) => {}
+                },
+                _ => {}
+            }
+        }
+
+        let subscriber = if let Some(subscriber) = subscriber {
+            subscriber
+        } else {
+            log::info!("unhandled message {}", type_name);
+            self.peer.respond_with_unhandled_message(message).log_err();
+            return;
+        };
+
+        let handler = state.message_handlers.get(&payload_type_id).cloned();
+        // Dropping the state prevents deadlocks if the handler interacts with rpc::Client.
+        // It also ensures we don't hold the lock while yielding back to the executor, as
+        // that might cause the executor thread driving this future to block indefinitely.
+        drop(state);
+
+        if let Some(handler) = handler {
+            let future = handler(subscriber, message, &self, cx.clone());
+            let client_id = self.id();
+            log::debug!(
+                "rpc message received. client_id:{}, sender_id:{:?}, type:{}",
+                client_id,
+                sender_id,
+                type_name
+            );
+            cx.spawn_on_main(move |_| async move {
+                    match future.await {
+                        Ok(()) => {
+                            log::debug!(
+                                "rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
+                                client_id,
+                                sender_id,
+                                type_name
+                            );
+                        }
+                        Err(error) => {
+                            log::error!(
+                                "error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
+                                client_id,
+                                sender_id,
+                                type_name,
+                                error
+                            );
+                        }
+                    }
+                })
+                .detach();
+        } else {
+            log::info!("unhandled message {}", type_name);
+            self.peer.respond_with_unhandled_message(message).log_err();
+        }
+    }
+
+    pub fn telemetry(&self) -> &Arc<Telemetry> {
+        &self.telemetry
+    }
+}
+
+async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
+    if IMPERSONATE_LOGIN.is_some() {
+        return None;
+    }
+
+    let (user_id, access_token) = cx
+        .run_on_main(|cx| cx.read_credentials(&ZED_SERVER_URL).log_err().flatten())
+        .ok()?
+        .await?;
+
+    Some(Credentials {
+        user_id: user_id.parse().ok()?,
+        access_token: String::from_utf8(access_token).ok()?,
+    })
+}
+
+async fn write_credentials_to_keychain(
+    credentials: Credentials,
+    cx: &AsyncAppContext,
+) -> Result<()> {
+    cx.run_on_main(move |cx| {
+        cx.write_credentials(
+            &ZED_SERVER_URL,
+            &credentials.user_id.to_string(),
+            credentials.access_token.as_bytes(),
+        )
+    })?
+    .await
+}
+
+async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
+    cx.run_on_main(move |cx| cx.delete_credentials(&ZED_SERVER_URL))?
+        .await
+}
+
+const WORKTREE_URL_PREFIX: &str = "zed://worktrees/";
+
+pub fn encode_worktree_url(id: u64, access_token: &str) -> String {
+    format!("{}{}/{}", WORKTREE_URL_PREFIX, id, access_token)
+}
+
+pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> {
+    let path = url.trim().strip_prefix(WORKTREE_URL_PREFIX)?;
+    let mut parts = path.split('/');
+    let id = parts.next()?.parse::<u64>().ok()?;
+    let access_token = parts.next()?;
+    if access_token.is_empty() {
+        return None;
+    }
+    Some((id, access_token.to_string()))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test::FakeServer;
+
+    use gpui2::{Context, Executor, TestAppContext};
+    use parking_lot::Mutex;
+    use std::future;
+    use util::http::FakeHttpClient;
+
+    #[gpui2::test(iterations = 10)]
+    async fn test_reconnection(cx: &mut TestAppContext) {
+        let user_id = 5;
+        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        let server = FakeServer::for_client(user_id, &client, cx).await;
+        let mut status = client.status();
+        assert!(matches!(
+            status.next().await,
+            Some(Status::Connected { .. })
+        ));
+        assert_eq!(server.auth_count(), 1);
+
+        server.forbid_connections();
+        server.disconnect();
+        while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
+
+        server.allow_connections();
+        cx.executor().advance_clock(Duration::from_secs(10));
+        while !matches!(status.next().await, Some(Status::Connected { .. })) {}
+        assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting
+
+        server.forbid_connections();
+        server.disconnect();
+        while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
+
+        // Clear cached credentials after authentication fails
+        server.roll_access_token();
+        server.allow_connections();
+        cx.executor().run_until_parked();
+        cx.executor().advance_clock(Duration::from_secs(10));
+        while !matches!(status.next().await, Some(Status::Connected { .. })) {}
+        assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
+    }
+
+    #[gpui2::test(iterations = 10)]
+    async fn test_connection_timeout(executor: Executor, cx: &mut TestAppContext) {
+        let user_id = 5;
+        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        let mut status = client.status();
+
+        // Time out when client tries to connect.
+        client.override_authenticate(move |cx| {
+            cx.executor().spawn(async move {
+                Ok(Credentials {
+                    user_id,
+                    access_token: "token".into(),
+                })
+            })
+        });
+        client.override_establish_connection(|_, cx| {
+            cx.executor().spawn(async move {
+                future::pending::<()>().await;
+                unreachable!()
+            })
+        });
+        let auth_and_connect = cx.spawn({
+            let client = client.clone();
+            |cx| async move { client.authenticate_and_connect(false, &cx).await }
+        });
+        executor.run_until_parked();
+        assert!(matches!(status.next().await, Some(Status::Connecting)));
+
+        executor.advance_clock(CONNECTION_TIMEOUT);
+        assert!(matches!(
+            status.next().await,
+            Some(Status::ConnectionError { .. })
+        ));
+        auth_and_connect.await.unwrap_err();
+
+        // Allow the connection to be established.
+        let server = FakeServer::for_client(user_id, &client, cx).await;
+        assert!(matches!(
+            status.next().await,
+            Some(Status::Connected { .. })
+        ));
+
+        // Disconnect client.
+        server.forbid_connections();
+        server.disconnect();
+        while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
+
+        // Time out when re-establishing the connection.
+        server.allow_connections();
+        client.override_establish_connection(|_, cx| {
+            cx.executor().spawn(async move {
+                future::pending::<()>().await;
+                unreachable!()
+            })
+        });
+        executor.advance_clock(2 * INITIAL_RECONNECTION_DELAY);
+        assert!(matches!(
+            status.next().await,
+            Some(Status::Reconnecting { .. })
+        ));
+
+        executor.advance_clock(CONNECTION_TIMEOUT);
+        assert!(matches!(
+            status.next().await,
+            Some(Status::ReconnectionError { .. })
+        ));
+    }
+
+    #[gpui2::test(iterations = 10)]
+    async fn test_authenticating_more_than_once(cx: &mut TestAppContext, executor: Executor) {
+        let auth_count = Arc::new(Mutex::new(0));
+        let dropped_auth_count = Arc::new(Mutex::new(0));
+        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        client.override_authenticate({
+            let auth_count = auth_count.clone();
+            let dropped_auth_count = dropped_auth_count.clone();
+            move |cx| {
+                let auth_count = auth_count.clone();
+                let dropped_auth_count = dropped_auth_count.clone();
+                cx.executor().spawn(async move {
+                    *auth_count.lock() += 1;
+                    let _drop = util::defer(move || *dropped_auth_count.lock() += 1);
+                    future::pending::<()>().await;
+                    unreachable!()
+                })
+            }
+        });
+
+        let _authenticate = cx.spawn({
+            let client = client.clone();
+            move |cx| async move { client.authenticate_and_connect(false, &cx).await }
+        });
+        executor.run_until_parked();
+        assert_eq!(*auth_count.lock(), 1);
+        assert_eq!(*dropped_auth_count.lock(), 0);
+
+        let _authenticate = cx.spawn({
+            let client = client.clone();
+            |cx| async move { client.authenticate_and_connect(false, &cx).await }
+        });
+        executor.run_until_parked();
+        assert_eq!(*auth_count.lock(), 2);
+        assert_eq!(*dropped_auth_count.lock(), 1);
+    }
+
+    #[test]
+    fn test_encode_and_decode_worktree_url() {
+        let url = encode_worktree_url(5, "deadbeef");
+        assert_eq!(decode_worktree_url(&url), Some((5, "deadbeef".to_string())));
+        assert_eq!(
+            decode_worktree_url(&format!("\n {}\t", url)),
+            Some((5, "deadbeef".to_string()))
+        );
+        assert_eq!(decode_worktree_url("not://the-right-format"), None);
+    }
+
+    #[gpui2::test]
+    async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
+        let user_id = 5;
+        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        let server = FakeServer::for_client(user_id, &client, cx).await;
+
+        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: 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(),
+                    _ => unreachable!(),
+                }
+                async { Ok(()) }
+            },
+        );
+        let model1 = cx.build_model(|_| TestModel {
+            id: 1,
+            subscription: None,
+        });
+        let model2 = cx.build_model(|_| TestModel {
+            id: 2,
+            subscription: None,
+        });
+        let model3 = cx.build_model(|_| TestModel {
+            id: 3,
+            subscription: None,
+        });
+
+        let _subscription1 = client
+            .subscribe_to_entity(1)
+            .unwrap()
+            .set_model(&model1, &mut cx.to_async());
+        let _subscription2 = client
+            .subscribe_to_entity(2)
+            .unwrap()
+            .set_model(&model2, &mut cx.to_async());
+        // Ensure dropping a subscription for the same entity type still allows receiving of
+        // messages for other entity IDs of the same type.
+        let subscription3 = client
+            .subscribe_to_entity(3)
+            .unwrap()
+            .set_model(&model3, &mut cx.to_async());
+        drop(subscription3);
+
+        server.send(proto::JoinProject { project_id: 1 });
+        server.send(proto::JoinProject { project_id: 2 });
+        done_rx1.next().await.unwrap();
+        done_rx2.next().await.unwrap();
+    }
+
+    #[gpui2::test]
+    async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) {
+        let user_id = 5;
+        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.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(
+            model.downgrade(),
+            move |_, _: TypedEnvelope<proto::Ping>, _, _| {
+                done_tx1.try_send(()).unwrap();
+                async { Ok(()) }
+            },
+        );
+        drop(subscription1);
+        let _subscription2 = client.add_message_handler(
+            model.downgrade(),
+            move |_, _: TypedEnvelope<proto::Ping>, _, _| {
+                done_tx2.try_send(()).unwrap();
+                async { Ok(()) }
+            },
+        );
+        server.send(proto::Ping {});
+        done_rx2.next().await.unwrap();
+    }
+
+    #[gpui2::test]
+    async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) {
+        let user_id = 5;
+        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.build_model(|_| TestModel::default());
+        let (done_tx, mut done_rx) = smol::channel::unbounded();
+        let subscription = client.add_message_handler(
+            model.clone().downgrade(),
+            move |model: Model<TestModel>, _: TypedEnvelope<proto::Ping>, _, mut cx| {
+                model
+                    .update(&mut cx, |model, _| model.subscription.take())
+                    .unwrap();
+                done_tx.try_send(()).unwrap();
+                async { Ok(()) }
+            },
+        );
+        model.update(cx, |model, _| {
+            model.subscription = Some(subscription);
+        });
+        server.send(proto::Ping {});
+        done_rx.next().await.unwrap();
+    }
+
+    #[derive(Default)]
+    struct TestModel {
+        id: usize,
+        subscription: Option<Subscription>,
+    }
+}

crates/client2/src/telemetry.rs 🔗

@@ -0,0 +1,333 @@
+use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
+use gpui2::{serde_json, AppContext, AppMetadata, Executor, Task};
+use lazy_static::lazy_static;
+use parking_lot::Mutex;
+use serde::Serialize;
+use settings2::Settings;
+use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
+use sysinfo::{
+    CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
+};
+use tempfile::NamedTempFile;
+use util::http::HttpClient;
+use util::{channel::ReleaseChannel, TryFutureExt};
+
+pub struct Telemetry {
+    http_client: Arc<dyn HttpClient>,
+    executor: Executor,
+    state: Mutex<TelemetryState>,
+}
+
+struct TelemetryState {
+    metrics_id: Option<Arc<str>>,      // Per logged-in user
+    installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
+    session_id: Option<Arc<str>>,      // Per app launch
+    release_channel: Option<&'static str>,
+    app_metadata: AppMetadata,
+    architecture: &'static str,
+    clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
+    flush_clickhouse_events_task: Option<Task<()>>,
+    log_file: Option<NamedTempFile>,
+    is_staff: Option<bool>,
+}
+
+const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
+
+lazy_static! {
+    static ref CLICKHOUSE_EVENTS_URL: String =
+        format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
+}
+
+#[derive(Serialize, Debug)]
+struct ClickhouseEventRequestBody {
+    token: &'static str,
+    installation_id: Option<Arc<str>>,
+    session_id: Option<Arc<str>>,
+    is_staff: Option<bool>,
+    app_version: Option<String>,
+    os_name: &'static str,
+    os_version: Option<String>,
+    architecture: &'static str,
+    release_channel: Option<&'static str>,
+    events: Vec<ClickhouseEventWrapper>,
+}
+
+#[derive(Serialize, Debug)]
+struct ClickhouseEventWrapper {
+    signed_in: bool,
+    #[serde(flatten)]
+    event: ClickhouseEvent,
+}
+
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum AssistantKind {
+    Panel,
+    Inline,
+}
+
+#[derive(Serialize, Debug)]
+#[serde(tag = "type")]
+pub enum ClickhouseEvent {
+    Editor {
+        operation: &'static str,
+        file_extension: Option<String>,
+        vim_mode: bool,
+        copilot_enabled: bool,
+        copilot_enabled_for_language: bool,
+    },
+    Copilot {
+        suggestion_id: Option<String>,
+        suggestion_accepted: bool,
+        file_extension: Option<String>,
+    },
+    Call {
+        operation: &'static str,
+        room_id: Option<u64>,
+        channel_id: Option<u64>,
+    },
+    Assistant {
+        conversation_id: Option<String>,
+        kind: AssistantKind,
+        model: &'static str,
+    },
+    Cpu {
+        usage_as_percentage: f32,
+        core_count: u32,
+    },
+    Memory {
+        memory_in_bytes: u64,
+        virtual_memory_in_bytes: u64,
+    },
+}
+
+#[cfg(debug_assertions)]
+const MAX_QUEUE_LEN: usize = 1;
+
+#[cfg(not(debug_assertions))]
+const MAX_QUEUE_LEN: usize = 10;
+
+#[cfg(debug_assertions)]
+const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
+
+#[cfg(not(debug_assertions))]
+const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
+
+impl Telemetry {
+    pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
+        let release_channel = if cx.has_global::<ReleaseChannel>() {
+            Some(cx.global::<ReleaseChannel>().display_name())
+        } else {
+            None
+        };
+        // TODO: Replace all hardware stuff with nested SystemSpecs json
+        let this = Arc::new(Self {
+            http_client: client,
+            executor: cx.executor().clone(),
+            state: Mutex::new(TelemetryState {
+                app_metadata: cx.app_metadata(),
+                architecture: env::consts::ARCH,
+                release_channel,
+                installation_id: None,
+                metrics_id: None,
+                session_id: None,
+                clickhouse_events_queue: Default::default(),
+                flush_clickhouse_events_task: Default::default(),
+                log_file: None,
+                is_staff: None,
+            }),
+        });
+
+        this
+    }
+
+    pub fn log_file_path(&self) -> Option<PathBuf> {
+        Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
+    }
+
+    pub fn start(
+        self: &Arc<Self>,
+        installation_id: Option<String>,
+        session_id: String,
+        cx: &mut AppContext,
+    ) {
+        let mut state = self.state.lock();
+        state.installation_id = installation_id.map(|id| id.into());
+        state.session_id = Some(session_id.into());
+        let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
+        drop(state);
+
+        if has_clickhouse_events {
+            self.flush_clickhouse_events();
+        }
+
+        let this = self.clone();
+        cx.spawn(|cx| async move {
+            // 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
+                // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
+                const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
+                smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
+
+                system.refresh_specifics(refresh_kind);
+
+                let current_process = Pid::from_u32(std::process::id());
+                let Some(process) = system.processes().get(&current_process) else {
+                    let process = current_process;
+                    log::error!("Failed to find own process {process:?} in system process table");
+                    // TODO: Fire an error telemetry event
+                    return;
+                };
+
+                let memory_event = ClickhouseEvent::Memory {
+                    memory_in_bytes: process.memory(),
+                    virtual_memory_in_bytes: process.virtual_memory(),
+                };
+
+                let cpu_event = ClickhouseEvent::Cpu {
+                    usage_as_percentage: process.cpu_usage(),
+                    core_count: system.cpus().len() as u32,
+                };
+
+                let telemetry_settings = if let Ok(telemetry_settings) =
+                    cx.update(|cx| *TelemetrySettings::get_global(cx))
+                {
+                    telemetry_settings
+                } else {
+                    break;
+                };
+
+                this.report_clickhouse_event(memory_event, telemetry_settings);
+                this.report_clickhouse_event(cpu_event, telemetry_settings);
+            }
+        })
+        .detach();
+    }
+
+    pub fn set_authenticated_user_info(
+        self: &Arc<Self>,
+        metrics_id: Option<String>,
+        is_staff: bool,
+        cx: &AppContext,
+    ) {
+        if !TelemetrySettings::get_global(cx).metrics {
+            return;
+        }
+
+        let mut state = self.state.lock();
+        let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
+        state.metrics_id = metrics_id.clone();
+        state.is_staff = Some(is_staff);
+        drop(state);
+    }
+
+    pub fn report_clickhouse_event(
+        self: &Arc<Self>,
+        event: ClickhouseEvent,
+        telemetry_settings: TelemetrySettings,
+    ) {
+        if !telemetry_settings.metrics {
+            return;
+        }
+
+        let mut state = self.state.lock();
+        let signed_in = state.metrics_id.is_some();
+        state
+            .clickhouse_events_queue
+            .push(ClickhouseEventWrapper { signed_in, event });
+
+        if state.installation_id.is_some() {
+            if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
+                drop(state);
+                self.flush_clickhouse_events();
+            } else {
+                let this = self.clone();
+                let executor = self.executor.clone();
+                state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
+                    executor.timer(DEBOUNCE_INTERVAL).await;
+                    this.flush_clickhouse_events();
+                }));
+            }
+        }
+    }
+
+    pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
+        self.state.lock().metrics_id.clone()
+    }
+
+    pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
+        self.state.lock().installation_id.clone()
+    }
+
+    pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
+        self.state.lock().is_staff
+    }
+
+    fn flush_clickhouse_events(self: &Arc<Self>) {
+        let mut state = self.state.lock();
+        let mut events = mem::take(&mut state.clickhouse_events_queue);
+        state.flush_clickhouse_events_task.take();
+        drop(state);
+
+        let this = self.clone();
+        self.executor
+            .spawn(
+                async move {
+                    let mut json_bytes = Vec::new();
+
+                    if let Some(file) = &mut this.state.lock().log_file {
+                        let file = file.as_file_mut();
+                        for event in &mut events {
+                            json_bytes.clear();
+                            serde_json::to_writer(&mut json_bytes, event)?;
+                            file.write_all(&json_bytes)?;
+                            file.write(b"\n")?;
+                        }
+                    }
+
+                    {
+                        let state = this.state.lock();
+                        let request_body = ClickhouseEventRequestBody {
+                            token: ZED_SECRET_CLIENT_TOKEN,
+                            installation_id: state.installation_id.clone(),
+                            session_id: state.session_id.clone(),
+                            is_staff: state.is_staff.clone(),
+                            app_version: state
+                                .app_metadata
+                                .app_version
+                                .map(|version| version.to_string()),
+                            os_name: state.app_metadata.os_name,
+                            os_version: state
+                                .app_metadata
+                                .os_version
+                                .map(|version| version.to_string()),
+                            architecture: state.architecture,
+
+                            release_channel: state.release_channel,
+                            events,
+                        };
+                        json_bytes.clear();
+                        serde_json::to_writer(&mut json_bytes, &request_body)?;
+                    }
+
+                    this.http_client
+                        .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
+                        .await?;
+                    anyhow::Ok(())
+                }
+                .log_err(),
+            )
+            .detach();
+    }
+}

crates/client2/src/test.rs 🔗

@@ -0,0 +1,216 @@
+use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
+use anyhow::{anyhow, Result};
+use futures::{stream::BoxStream, StreamExt};
+use gpui2::{Context, Executor, Model, TestAppContext};
+use parking_lot::Mutex;
+use rpc2::{
+    proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
+    ConnectionId, Peer, Receipt, TypedEnvelope,
+};
+use std::sync::Arc;
+use util::http::FakeHttpClient;
+
+pub struct FakeServer {
+    peer: Arc<Peer>,
+    state: Arc<Mutex<FakeServerState>>,
+    user_id: u64,
+    executor: Executor,
+}
+
+#[derive(Default)]
+struct FakeServerState {
+    incoming: Option<BoxStream<'static, Box<dyn proto::AnyTypedEnvelope>>>,
+    connection_id: Option<ConnectionId>,
+    forbid_connections: bool,
+    auth_count: usize,
+    access_token: usize,
+}
+
+impl FakeServer {
+    pub async fn for_client(
+        client_user_id: u64,
+        client: &Arc<Client>,
+        cx: &TestAppContext,
+    ) -> Self {
+        let server = Self {
+            peer: Peer::new(0),
+            state: Default::default(),
+            user_id: client_user_id,
+            executor: cx.executor().clone(),
+        };
+
+        client
+            .override_authenticate({
+                let state = Arc::downgrade(&server.state);
+                move |cx| {
+                    let state = state.clone();
+                    cx.spawn(move |_| async move {
+                        let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
+                        let mut state = state.lock();
+                        state.auth_count += 1;
+                        let access_token = state.access_token.to_string();
+                        Ok(Credentials {
+                            user_id: client_user_id,
+                            access_token,
+                        })
+                    })
+                }
+            })
+            .override_establish_connection({
+                let peer = Arc::downgrade(&server.peer);
+                let state = Arc::downgrade(&server.state);
+                move |credentials, cx| {
+                    let peer = peer.clone();
+                    let state = state.clone();
+                    let credentials = credentials.clone();
+                    cx.spawn(move |cx| async move {
+                        let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
+                        let peer = peer.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
+                        if state.lock().forbid_connections {
+                            Err(EstablishConnectionError::Other(anyhow!(
+                                "server is forbidding connections"
+                            )))?
+                        }
+
+                        assert_eq!(credentials.user_id, client_user_id);
+
+                        if credentials.access_token != state.lock().access_token.to_string() {
+                            Err(EstablishConnectionError::Unauthorized)?
+                        }
+
+                        let (client_conn, server_conn, _) =
+                            Connection::in_memory(cx.executor().clone());
+                        let (connection_id, io, incoming) =
+                            peer.add_test_connection(server_conn, cx.executor().clone());
+                        cx.executor().spawn(io).detach();
+                        {
+                            let mut state = state.lock();
+                            state.connection_id = Some(connection_id);
+                            state.incoming = Some(incoming);
+                        }
+                        peer.send(
+                            connection_id,
+                            proto::Hello {
+                                peer_id: Some(connection_id.into()),
+                            },
+                        )
+                        .unwrap();
+
+                        Ok(client_conn)
+                    })
+                }
+            });
+
+        client
+            .authenticate_and_connect(false, &cx.to_async())
+            .await
+            .unwrap();
+
+        server
+    }
+
+    pub fn disconnect(&self) {
+        if self.state.lock().connection_id.is_some() {
+            self.peer.disconnect(self.connection_id());
+            let mut state = self.state.lock();
+            state.connection_id.take();
+            state.incoming.take();
+        }
+    }
+
+    pub fn auth_count(&self) -> usize {
+        self.state.lock().auth_count
+    }
+
+    pub fn roll_access_token(&self) {
+        self.state.lock().access_token += 1;
+    }
+
+    pub fn forbid_connections(&self) {
+        self.state.lock().forbid_connections = true;
+    }
+
+    pub fn allow_connections(&self) {
+        self.state.lock().forbid_connections = false;
+    }
+
+    pub fn send<T: proto::EnvelopedMessage>(&self, message: T) {
+        self.peer.send(self.connection_id(), message).unwrap();
+    }
+
+    #[allow(clippy::await_holding_lock)]
+    pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
+        self.executor.start_waiting();
+
+        loop {
+            let message = self
+                .state
+                .lock()
+                .incoming
+                .as_mut()
+                .expect("not connected")
+                .next()
+                .await
+                .ok_or_else(|| anyhow!("other half hung up"))?;
+            self.executor.finish_waiting();
+            let type_name = message.payload_type_name();
+            let message = message.into_any();
+
+            if message.is::<TypedEnvelope<M>>() {
+                return Ok(*message.downcast().unwrap());
+            }
+
+            if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
+                self.respond(
+                    message
+                        .downcast::<TypedEnvelope<GetPrivateUserInfo>>()
+                        .unwrap()
+                        .receipt(),
+                    GetPrivateUserInfoResponse {
+                        metrics_id: "the-metrics-id".into(),
+                        staff: false,
+                        flags: Default::default(),
+                    },
+                );
+                continue;
+            }
+
+            panic!(
+                "fake server received unexpected message type: {:?}",
+                type_name
+            );
+        }
+    }
+
+    pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {
+        self.peer.respond(receipt, response).unwrap()
+    }
+
+    fn connection_id(&self) -> ConnectionId {
+        self.state.lock().connection_id.expect("not connected")
+    }
+
+    pub async fn build_user_store(
+        &self,
+        client: Arc<Client>,
+        cx: &mut TestAppContext,
+    ) -> Model<UserStore> {
+        let http_client = FakeHttpClient::with_404_response();
+        let user_store = cx.build_model(|cx| UserStore::new(client, http_client, cx));
+        assert_eq!(
+            self.receive::<proto::GetUsers>()
+                .await
+                .unwrap()
+                .payload
+                .user_ids,
+            &[self.user_id]
+        );
+        user_store
+    }
+}
+
+impl Drop for FakeServer {
+    fn drop(&mut self) {
+        self.disconnect();
+    }
+}

crates/client2/src/user.rs 🔗

@@ -0,0 +1,739 @@
+use super::{proto, Client, Status, TypedEnvelope};
+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, ImageData, Model, ModelContext, Task};
+use postage::{sink::Sink, watch};
+use rpc2::proto::{RequestMessage, UsersResponse};
+use std::sync::{Arc, Weak};
+use text::ReplicaId;
+use util::http::HttpClient;
+use util::TryFutureExt as _;
+
+pub type UserId = u64;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct ParticipantIndex(pub u32);
+
+#[derive(Default, Debug)]
+pub struct User {
+    pub id: UserId,
+    pub github_login: String,
+    pub avatar: Option<Arc<ImageData>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Collaborator {
+    pub peer_id: proto::PeerId,
+    pub replica_id: ReplicaId,
+    pub user_id: UserId,
+}
+
+impl PartialOrd for User {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for User {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.github_login.cmp(&other.github_login)
+    }
+}
+
+impl PartialEq for User {
+    fn eq(&self, other: &Self) -> bool {
+        self.id == other.id && self.github_login == other.github_login
+    }
+}
+
+impl Eq for User {}
+
+#[derive(Debug, PartialEq)]
+pub struct Contact {
+    pub user: Arc<User>,
+    pub online: bool,
+    pub busy: bool,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ContactRequestStatus {
+    None,
+    RequestSent,
+    RequestReceived,
+    RequestAccepted,
+}
+
+pub struct UserStore {
+    users: HashMap<u64, Arc<User>>,
+    participant_indices: HashMap<u64, ParticipantIndex>,
+    update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
+    current_user: watch::Receiver<Option<Arc<User>>>,
+    contacts: Vec<Arc<Contact>>,
+    incoming_contact_requests: Vec<Arc<User>>,
+    outgoing_contact_requests: Vec<Arc<User>>,
+    pending_contact_requests: HashMap<u64, usize>,
+    invite_info: Option<InviteInfo>,
+    client: Weak<Client>,
+    http: Arc<dyn HttpClient>,
+    _maintain_contacts: Task<()>,
+    _maintain_current_user: Task<Result<()>>,
+}
+
+#[derive(Clone)]
+pub struct InviteInfo {
+    pub count: u32,
+    pub url: Arc<str>,
+}
+
+pub enum Event {
+    Contact {
+        user: Arc<User>,
+        kind: ContactEventKind,
+    },
+    ShowContacts,
+    ParticipantIndicesChanged,
+}
+
+#[derive(Clone, Copy)]
+pub enum ContactEventKind {
+    Requested,
+    Accepted,
+    Cancelled,
+}
+
+impl EventEmitter for UserStore {
+    type Event = Event;
+}
+
+enum UpdateContacts {
+    Update(proto::UpdateContacts),
+    Wait(postage::barrier::Sender),
+    Clear(postage::barrier::Sender),
+}
+
+impl UserStore {
+    pub fn new(
+        client: Arc<Client>,
+        http: Arc<dyn HttpClient>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        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_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(),
+            current_user: current_user_rx,
+            contacts: Default::default(),
+            incoming_contact_requests: Default::default(),
+            participant_indices: Default::default(),
+            outgoing_contact_requests: Default::default(),
+            invite_info: None,
+            client: Arc::downgrade(&client),
+            update_contacts_tx,
+            http,
+            _maintain_contacts: cx.spawn(|this, mut cx| async move {
+                let _subscriptions = rpc_subscriptions;
+                while let Some(message) = update_contacts_rx.next().await {
+                    if let Ok(task) =
+                        this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
+                    {
+                        task.log_err().await;
+                    } else {
+                        break;
+                    }
+                }
+            }),
+            _maintain_current_user: cx.spawn(|this, mut cx| async move {
+                let mut status = client.status();
+                while let Some(status) = status.next().await {
+                    match status {
+                        Status::Connected { .. } => {
+                            if let Some(user_id) = client.user_id() {
+                                let fetch_user = if let Ok(fetch_user) = this
+                                    .update(&mut cx, |this, cx| {
+                                        this.get_user(user_id, cx).log_err()
+                                    }) {
+                                    fetch_user
+                                } else {
+                                    break;
+                                };
+                                let fetch_metrics_id =
+                                    client.request(proto::GetPrivateUserInfo {}).log_err();
+                                let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
+
+                                cx.update(|cx| {
+                                    if let Some(info) = info {
+                                        cx.update_flags(info.staff, info.flags);
+                                        client.telemetry.set_authenticated_user_info(
+                                            Some(info.metrics_id.clone()),
+                                            info.staff,
+                                            cx,
+                                        )
+                                    }
+                                })?;
+
+                                current_user_tx.send(user).await.ok();
+
+                                this.update(&mut cx, |_, cx| cx.notify())?;
+                            }
+                        }
+                        Status::SignedOut => {
+                            current_user_tx.send(None).await.ok();
+                            this.update(&mut cx, |this, cx| {
+                                cx.notify();
+                                this.clear_contacts()
+                            })?
+                            .await;
+                        }
+                        Status::ConnectionLost => {
+                            this.update(&mut cx, |this, cx| {
+                                cx.notify();
+                                this.clear_contacts()
+                            })?
+                            .await;
+                        }
+                        _ => {}
+                    }
+                }
+                Ok(())
+            }),
+            pending_contact_requests: Default::default(),
+        }
+    }
+
+    #[cfg(feature = "test-support")]
+    pub fn clear_cache(&mut self) {
+        self.users.clear();
+    }
+
+    async fn handle_update_invite_info(
+        this: Model<Self>,
+        message: TypedEnvelope<proto::UpdateInviteInfo>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            this.invite_info = Some(InviteInfo {
+                url: Arc::from(message.payload.url),
+                count: message.payload.count,
+            });
+            cx.notify();
+        })?;
+        Ok(())
+    }
+
+    async fn handle_show_contacts(
+        this: Model<Self>,
+        _: TypedEnvelope<proto::ShowContacts>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?;
+        Ok(())
+    }
+
+    pub fn invite_info(&self) -> Option<&InviteInfo> {
+        self.invite_info.as_ref()
+    }
+
+    async fn handle_update_contacts(
+        this: Model<Self>,
+        message: TypedEnvelope<proto::UpdateContacts>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, _| {
+            this.update_contacts_tx
+                .unbounded_send(UpdateContacts::Update(message.payload))
+                .unwrap();
+        })?;
+        Ok(())
+    }
+
+    fn update_contacts(
+        &mut self,
+        message: UpdateContacts,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        match message {
+            UpdateContacts::Wait(barrier) => {
+                drop(barrier);
+                Task::ready(Ok(()))
+            }
+            UpdateContacts::Clear(barrier) => {
+                self.contacts.clear();
+                self.incoming_contact_requests.clear();
+                self.outgoing_contact_requests.clear();
+                drop(barrier);
+                Task::ready(Ok(()))
+            }
+            UpdateContacts::Update(message) => {
+                let mut user_ids = HashSet::default();
+                for contact in &message.contacts {
+                    user_ids.insert(contact.user_id);
+                }
+                user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
+                user_ids.extend(message.outgoing_requests.iter());
+
+                let load_users = self.get_users(user_ids.into_iter().collect(), cx);
+                cx.spawn(|this, mut cx| async move {
+                    load_users.await?;
+
+                    // Users are fetched in parallel above and cached in call to get_users
+                    // No need to paralellize here
+                    let mut updated_contacts = Vec::new();
+                    let this = this
+                        .upgrade()
+                        .ok_or_else(|| anyhow!("can't upgrade user store handle"))?;
+                    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,
+                        ));
+                    }
+
+                    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)
+                        });
+                    }
+
+                    let mut outgoing_requests = Vec::new();
+                    for requested_user_id in message.outgoing_requests {
+                        outgoing_requests.push(
+                            this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))?
+                                .await?,
+                        );
+                    }
+
+                    let removed_contacts =
+                        HashSet::<u64>::from_iter(message.remove_contacts.iter().copied());
+                    let removed_incoming_requests =
+                        HashSet::<u64>::from_iter(message.remove_incoming_requests.iter().copied());
+                    let removed_outgoing_requests =
+                        HashSet::<u64>::from_iter(message.remove_outgoing_requests.iter().copied());
+
+                    this.update(&mut cx, |this, cx| {
+                        // Remove contacts
+                        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,
+                                });
+                            }
+                            match this.contacts.binary_search_by_key(
+                                &&updated_contact.user.github_login,
+                                |contact| &contact.user.github_login,
+                            ) {
+                                Ok(ix) => this.contacts[ix] = updated_contact,
+                                Err(ix) => this.contacts.insert(ix, updated_contact),
+                            }
+                        }
+
+                        // Remove incoming contact requests
+                        this.incoming_contact_requests.retain(|user| {
+                            if removed_incoming_requests.contains(&user.id) {
+                                cx.emit(Event::Contact {
+                                    user: user.clone(),
+                                    kind: ContactEventKind::Cancelled,
+                                });
+                                false
+                            } else {
+                                true
+                            }
+                        });
+                        // 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,
+                                });
+                            }
+
+                            match this
+                                .incoming_contact_requests
+                                .binary_search_by_key(&&user.github_login, |contact| {
+                                    &contact.github_login
+                                }) {
+                                Ok(ix) => this.incoming_contact_requests[ix] = user,
+                                Err(ix) => this.incoming_contact_requests.insert(ix, user),
+                            }
+                        }
+
+                        // Remove outgoing contact requests
+                        this.outgoing_contact_requests
+                            .retain(|user| !removed_outgoing_requests.contains(&user.id));
+                        // Update existing incoming requests and insert new ones
+                        for request in outgoing_requests {
+                            match this
+                                .outgoing_contact_requests
+                                .binary_search_by_key(&&request.github_login, |contact| {
+                                    &contact.github_login
+                                }) {
+                                Ok(ix) => this.outgoing_contact_requests[ix] = request,
+                                Err(ix) => this.outgoing_contact_requests.insert(ix, request),
+                            }
+                        }
+
+                        cx.notify();
+                    })?;
+
+                    Ok(())
+                })
+            }
+        }
+    }
+
+    pub fn contacts(&self) -> &[Arc<Contact>] {
+        &self.contacts
+    }
+
+    pub fn has_contact(&self, user: &Arc<User>) -> bool {
+        self.contacts
+            .binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login)
+            .is_ok()
+    }
+
+    pub fn incoming_contact_requests(&self) -> &[Arc<User>] {
+        &self.incoming_contact_requests
+    }
+
+    pub fn outgoing_contact_requests(&self) -> &[Arc<User>] {
+        &self.outgoing_contact_requests
+    }
+
+    pub fn is_contact_request_pending(&self, user: &User) -> bool {
+        self.pending_contact_requests.contains_key(&user.id)
+    }
+
+    pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus {
+        if self
+            .contacts
+            .binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login)
+            .is_ok()
+        {
+            ContactRequestStatus::RequestAccepted
+        } else if self
+            .outgoing_contact_requests
+            .binary_search_by_key(&&user.github_login, |user| &user.github_login)
+            .is_ok()
+        {
+            ContactRequestStatus::RequestSent
+        } else if self
+            .incoming_contact_requests
+            .binary_search_by_key(&&user.github_login, |user| &user.github_login)
+            .is_ok()
+        {
+            ContactRequestStatus::RequestReceived
+        } else {
+            ContactRequestStatus::None
+        }
+    }
+
+    pub fn request_contact(
+        &mut self,
+        responder_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx)
+    }
+
+    pub fn remove_contact(
+        &mut self,
+        user_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
+    }
+
+    pub fn respond_to_contact_request(
+        &mut self,
+        requester_id: u64,
+        accept: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        self.perform_contact_request(
+            requester_id,
+            proto::RespondToContactRequest {
+                requester_id,
+                response: if accept {
+                    proto::ContactRequestResponse::Accept
+                } else {
+                    proto::ContactRequestResponse::Decline
+                } as i32,
+            },
+            cx,
+        )
+    }
+
+    pub fn dismiss_contact_request(
+        &mut self,
+        requester_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let client = self.client.upgrade();
+        cx.spawn(move |_, _| async move {
+            client
+                .ok_or_else(|| anyhow!("can't upgrade client reference"))?
+                .request(proto::RespondToContactRequest {
+                    requester_id,
+                    response: proto::ContactRequestResponse::Dismiss as i32,
+                })
+                .await?;
+            Ok(())
+        })
+    }
+
+    fn perform_contact_request<T: RequestMessage>(
+        &mut self,
+        user_id: u64,
+        request: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let client = self.client.upgrade();
+        *self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
+        cx.notify();
+
+        cx.spawn(move |this, mut cx| async move {
+            let response = client
+                .ok_or_else(|| anyhow!("can't upgrade client reference"))?
+                .request(request)
+                .await;
+            this.update(&mut cx, |this, cx| {
+                if let Entry::Occupied(mut request_count) =
+                    this.pending_contact_requests.entry(user_id)
+                {
+                    *request_count.get_mut() -= 1;
+                    if *request_count.get() == 0 {
+                        request_count.remove();
+                    }
+                }
+                cx.notify();
+            })?;
+            response?;
+            Ok(())
+        })
+    }
+
+    pub fn clear_contacts(&mut self) -> impl Future<Output = ()> {
+        let (tx, mut rx) = postage::barrier::channel();
+        self.update_contacts_tx
+            .unbounded_send(UpdateContacts::Clear(tx))
+            .unwrap();
+        async move {
+            rx.next().await;
+        }
+    }
+
+    pub fn contact_updates_done(&mut self) -> impl Future<Output = ()> {
+        let (tx, mut rx) = postage::barrier::channel();
+        self.update_contacts_tx
+            .unbounded_send(UpdateContacts::Wait(tx))
+            .unwrap();
+        async move {
+            rx.next().await;
+        }
+    }
+
+    pub fn get_users(
+        &mut self,
+        user_ids: Vec<u64>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Arc<User>>>> {
+        let mut user_ids_to_fetch = user_ids.clone();
+        user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
+
+        cx.spawn(|this, mut cx| async move {
+            if !user_ids_to_fetch.is_empty() {
+                this.update(&mut cx, |this, cx| {
+                    this.load_users(
+                        proto::GetUsers {
+                            user_ids: user_ids_to_fetch,
+                        },
+                        cx,
+                    )
+                })?
+                .await?;
+            }
+
+            this.update(&mut cx, |this, _| {
+                user_ids
+                    .iter()
+                    .map(|user_id| {
+                        this.users
+                            .get(user_id)
+                            .cloned()
+                            .ok_or_else(|| anyhow!("user {} not found", user_id))
+                    })
+                    .collect()
+            })?
+        })
+    }
+
+    pub fn fuzzy_search_users(
+        &mut self,
+        query: String,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Arc<User>>>> {
+        self.load_users(proto::FuzzySearchUsers { query }, cx)
+    }
+
+    pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
+        self.users.get(&user_id).cloned()
+    }
+
+    pub fn get_user(
+        &mut self,
+        user_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Arc<User>>> {
+        if let Some(user) = self.users.get(&user_id).cloned() {
+            return Task::ready(Ok(user));
+        }
+
+        let load_users = self.get_users(vec![user_id], cx);
+        cx.spawn(move |this, mut cx| async move {
+            load_users.await?;
+            this.update(&mut cx, |this, _| {
+                this.users
+                    .get(&user_id)
+                    .cloned()
+                    .ok_or_else(|| anyhow!("server responded with no users"))
+            })?
+        })
+    }
+
+    pub fn current_user(&self) -> Option<Arc<User>> {
+        self.current_user.borrow().clone()
+    }
+
+    pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
+        self.current_user.clone()
+    }
+
+    fn load_users(
+        &mut self,
+        request: impl RequestMessage<Response = UsersResponse>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Arc<User>>>> {
+        let client = self.client.clone();
+        let http = self.http.clone();
+        cx.spawn(|this, mut cx| async move {
+            if let Some(rpc) = client.upgrade() {
+                let response = rpc.request(request).await.context("error loading users")?;
+                let users = future::join_all(
+                    response
+                        .users
+                        .into_iter()
+                        .map(|user| User::new(user, http.as_ref())),
+                )
+                .await;
+
+                this.update(&mut cx, |this, _| {
+                    for user in &users {
+                        this.users.insert(user.id, user.clone());
+                    }
+                })
+                .ok();
+
+                Ok(users)
+            } else {
+                Ok(Vec::new())
+            }
+        })
+    }
+
+    pub fn set_participant_indices(
+        &mut self,
+        participant_indices: HashMap<u64, ParticipantIndex>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if participant_indices != self.participant_indices {
+            self.participant_indices = participant_indices;
+            cx.emit(Event::ParticipantIndicesChanged);
+        }
+    }
+
+    pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
+        &self.participant_indices
+    }
+}
+
+impl User {
+    async fn new(message: proto::User, http: &dyn HttpClient) -> Arc<Self> {
+        Arc::new(User {
+            id: message.id,
+            github_login: message.github_login,
+            avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await,
+        })
+    }
+}
+
+impl Contact {
+    async fn from_proto(
+        contact: proto::Contact,
+        user_store: &Model<UserStore>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Self> {
+        let user = user_store
+            .update(cx, |user_store, cx| {
+                user_store.get_user(contact.user_id, cx)
+            })?
+            .await?;
+        Ok(Self {
+            user,
+            online: contact.online,
+            busy: contact.busy,
+        })
+    }
+}
+
+impl Collaborator {
+    pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
+        Ok(Self {
+            peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
+            replica_id: message.replica_id as ReplicaId,
+            user_id: message.user_id as UserId,
+        })
+    }
+}
+
+// todo!("we probably don't need this now that we fetch")
+async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
+    let mut response = http
+        .get(url, Default::default(), true)
+        .await
+        .map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
+
+    if !response.status().is_success() {
+        return Err(anyhow!("avatar request failed {:?}", response.status()));
+    }
+
+    let mut body = Vec::new();
+    response
+        .body_mut()
+        .read_to_end(&mut body)
+        .await
+        .map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
+    let format = image::guess_format(&body)?;
+    let image = image::load_from_memory_with_format(&body, format)?.into_bgra8();
+    Ok(Arc::new(ImageData::new(image)))
+}

crates/copilot2/Cargo.toml 🔗

@@ -0,0 +1,50 @@
+[package]
+name = "copilot2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/copilot2.rs"
+doctest = false
+
+[features]
+test-support = [
+    "collections/test-support",
+    "gpui2/test-support",
+    "language2/test-support",
+    "lsp2/test-support",
+    "settings2/test-support",
+    "util/test-support",
+]
+
+[dependencies]
+collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
+gpui2 = { path = "../gpui2" }
+language2 = { path = "../language2" }
+settings2 = { path = "../settings2" }
+theme = { path = "../theme" }
+lsp2 = { path = "../lsp2" }
+node_runtime = { path = "../node_runtime"}
+util = { path = "../util" }
+async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
+async-tar = "0.4.2"
+anyhow.workspace = true
+log.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smol.workspace = true
+futures.workspace = true
+parking_lot.workspace = true
+
+[dev-dependencies]
+clock = { path = "../clock" }
+collections = { path = "../collections", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+language2 = { path = "../language2", features = ["test-support"] }
+lsp2 = { path = "../lsp2", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
+settings2 = { path = "../settings2", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }

crates/copilot2/src/copilot2.rs 🔗

@@ -0,0 +1,1234 @@
+pub mod request;
+mod sign_in;
+
+use anyhow::{anyhow, Context as _, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use collections::{HashMap, HashSet};
+use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
+use gpui2::{
+    AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Model, ModelContext,
+    Task, WeakModel,
+};
+use language2::{
+    language_settings::{all_language_settings, language_settings},
+    point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language,
+    LanguageServerName, PointUtf16, ToPointUtf16,
+};
+use lsp2::{LanguageServer, LanguageServerBinary, LanguageServerId};
+use node_runtime::NodeRuntime;
+use parking_lot::Mutex;
+use request::StatusNotification;
+use settings2::SettingsStore;
+use smol::{fs, io::BufReader, stream::StreamExt};
+use std::{
+    ffi::OsString,
+    mem,
+    ops::Range,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{
+    fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
+};
+
+// todo!()
+// const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
+// actions!(copilot_auth, [SignIn, SignOut]);
+
+// todo!()
+// const COPILOT_NAMESPACE: &'static str = "copilot";
+// actions!(
+//     copilot,
+//     [Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
+// );
+
+pub fn init(
+    new_server_id: LanguageServerId,
+    http: Arc<dyn HttpClient>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    cx: &mut AppContext,
+) {
+    let copilot = cx.build_model({
+        let node_runtime = node_runtime.clone();
+        move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
+    });
+    cx.set_global(copilot.clone());
+
+    // TODO
+    // cx.observe(&copilot, |handle, cx| {
+    //     let status = handle.read(cx).status();
+    //     cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
+    //         match status {
+    //             Status::Disabled => {
+    //                 filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
+    //                 filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
+    //             }
+    //             Status::Authorized => {
+    //                 filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
+    //                 filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
+    //             }
+    //             _ => {
+    //                 filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
+    //                 filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
+    //             }
+    //         }
+    //     });
+    // })
+    // .detach();
+
+    // sign_in::init(cx);
+    // cx.add_global_action(|_: &SignIn, cx| {
+    //     if let Some(copilot) = Copilot::global(cx) {
+    //         copilot
+    //             .update(cx, |copilot, cx| copilot.sign_in(cx))
+    //             .detach_and_log_err(cx);
+    //     }
+    // });
+    // cx.add_global_action(|_: &SignOut, cx| {
+    //     if let Some(copilot) = Copilot::global(cx) {
+    //         copilot
+    //             .update(cx, |copilot, cx| copilot.sign_out(cx))
+    //             .detach_and_log_err(cx);
+    //     }
+    // });
+
+    // cx.add_global_action(|_: &Reinstall, cx| {
+    //     if let Some(copilot) = Copilot::global(cx) {
+    //         copilot
+    //             .update(cx, |copilot, cx| copilot.reinstall(cx))
+    //             .detach();
+    //     }
+    // });
+}
+
+enum CopilotServer {
+    Disabled,
+    Starting { task: Shared<Task<()>> },
+    Error(Arc<str>),
+    Running(RunningCopilotServer),
+}
+
+impl CopilotServer {
+    fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
+        let server = self.as_running()?;
+        if matches!(server.sign_in_status, SignInStatus::Authorized { .. }) {
+            Ok(server)
+        } else {
+            Err(anyhow!("must sign in before using copilot"))
+        }
+    }
+
+    fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {
+        match self {
+            CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")),
+            CopilotServer::Disabled => Err(anyhow!("copilot is disabled")),
+            CopilotServer::Error(error) => Err(anyhow!(
+                "copilot was not started because of an error: {}",
+                error
+            )),
+            CopilotServer::Running(server) => Ok(server),
+        }
+    }
+}
+
+struct RunningCopilotServer {
+    name: LanguageServerName,
+    lsp: Arc<LanguageServer>,
+    sign_in_status: SignInStatus,
+    registered_buffers: HashMap<EntityId, RegisteredBuffer>,
+}
+
+#[derive(Clone, Debug)]
+enum SignInStatus {
+    Authorized,
+    Unauthorized,
+    SigningIn {
+        prompt: Option<request::PromptUserDeviceFlow>,
+        task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
+    },
+    SignedOut,
+}
+
+#[derive(Debug, Clone)]
+pub enum Status {
+    Starting {
+        task: Shared<Task<()>>,
+    },
+    Error(Arc<str>),
+    Disabled,
+    SignedOut,
+    SigningIn {
+        prompt: Option<request::PromptUserDeviceFlow>,
+    },
+    Unauthorized,
+    Authorized,
+}
+
+impl Status {
+    pub fn is_authorized(&self) -> bool {
+        matches!(self, Status::Authorized)
+    }
+}
+
+struct RegisteredBuffer {
+    uri: lsp2::Url,
+    language_id: String,
+    snapshot: BufferSnapshot,
+    snapshot_version: i32,
+    _subscriptions: [gpui2::Subscription; 2],
+    pending_buffer_change: Task<Option<()>>,
+}
+
+impl RegisteredBuffer {
+    fn report_changes(
+        &mut self,
+        buffer: &Model<Buffer>,
+        cx: &mut ModelContext<Copilot>,
+    ) -> oneshot::Receiver<(i32, BufferSnapshot)> {
+        let (done_tx, done_rx) = oneshot::channel();
+
+        if buffer.read(cx).version() == self.snapshot.version {
+            let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
+        } else {
+            let buffer = buffer.downgrade();
+            let id = buffer.entity_id();
+            let prev_pending_change =
+                mem::replace(&mut self.pending_buffer_change, Task::ready(None));
+            self.pending_buffer_change = cx.spawn(move |copilot, mut cx| async move {
+                prev_pending_change.await;
+
+                let old_version = copilot
+                    .update(&mut cx, |copilot, _| {
+                        let server = copilot.server.as_authenticated().log_err()?;
+                        let buffer = server.registered_buffers.get_mut(&id)?;
+                        Some(buffer.snapshot.version.clone())
+                    })
+                    .ok()??;
+                let new_snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot()).ok()?;
+
+                let content_changes = cx
+                    .executor()
+                    .spawn({
+                        let new_snapshot = new_snapshot.clone();
+                        async move {
+                            new_snapshot
+                                .edits_since::<(PointUtf16, usize)>(&old_version)
+                                .map(|edit| {
+                                    let edit_start = edit.new.start.0;
+                                    let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
+                                    let new_text = new_snapshot
+                                        .text_for_range(edit.new.start.1..edit.new.end.1)
+                                        .collect();
+                                    lsp2::TextDocumentContentChangeEvent {
+                                        range: Some(lsp2::Range::new(
+                                            point_to_lsp(edit_start),
+                                            point_to_lsp(edit_end),
+                                        )),
+                                        range_length: None,
+                                        text: new_text,
+                                    }
+                                })
+                                .collect::<Vec<_>>()
+                        }
+                    })
+                    .await;
+
+                copilot
+                    .update(&mut cx, |copilot, _| {
+                        let server = copilot.server.as_authenticated().log_err()?;
+                        let buffer = server.registered_buffers.get_mut(&id)?;
+                        if !content_changes.is_empty() {
+                            buffer.snapshot_version += 1;
+                            buffer.snapshot = new_snapshot;
+                            server
+                                .lsp
+                                .notify::<lsp2::notification::DidChangeTextDocument>(
+                                    lsp2::DidChangeTextDocumentParams {
+                                        text_document: lsp2::VersionedTextDocumentIdentifier::new(
+                                            buffer.uri.clone(),
+                                            buffer.snapshot_version,
+                                        ),
+                                        content_changes,
+                                    },
+                                )
+                                .log_err();
+                        }
+                        let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
+                        Some(())
+                    })
+                    .ok()?;
+
+                Some(())
+            });
+        }
+
+        done_rx
+    }
+}
+
+#[derive(Debug)]
+pub struct Completion {
+    pub uuid: String,
+    pub range: Range<Anchor>,
+    pub text: String,
+}
+
+pub struct Copilot {
+    http: Arc<dyn HttpClient>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    server: CopilotServer,
+    buffers: HashSet<WeakModel<Buffer>>,
+    server_id: LanguageServerId,
+    _subscription: gpui2::Subscription,
+}
+
+pub enum Event {
+    CopilotLanguageServerStarted,
+}
+
+impl EventEmitter for Copilot {
+    type Event = Event;
+}
+
+impl Copilot {
+    pub fn global(cx: &AppContext) -> Option<Model<Self>> {
+        if cx.has_global::<Model<Self>>() {
+            Some(cx.global::<Model<Self>>().clone())
+        } else {
+            None
+        }
+    }
+
+    fn start(
+        new_server_id: LanguageServerId,
+        http: Arc<dyn HttpClient>,
+        node_runtime: Arc<dyn NodeRuntime>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        let mut this = Self {
+            server_id: new_server_id,
+            http,
+            node_runtime,
+            server: CopilotServer::Disabled,
+            buffers: Default::default(),
+            _subscription: cx.on_app_quit(Self::shutdown_language_server),
+        };
+        this.enable_or_disable_copilot(cx);
+        cx.observe_global::<SettingsStore>(move |this, cx| this.enable_or_disable_copilot(cx))
+            .detach();
+        this
+    }
+
+    fn shutdown_language_server(
+        &mut self,
+        _cx: &mut ModelContext<Self>,
+    ) -> impl Future<Output = ()> {
+        let shutdown = match mem::replace(&mut self.server, CopilotServer::Disabled) {
+            CopilotServer::Running(server) => Some(Box::pin(async move { server.lsp.shutdown() })),
+            _ => None,
+        };
+
+        async move {
+            if let Some(shutdown) = shutdown {
+                shutdown.await;
+            }
+        }
+    }
+
+    fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Self>) {
+        let server_id = self.server_id;
+        let http = self.http.clone();
+        let node_runtime = self.node_runtime.clone();
+        if all_language_settings(None, cx).copilot_enabled(None, None) {
+            if matches!(self.server, CopilotServer::Disabled) {
+                let start_task = cx
+                    .spawn(move |this, cx| {
+                        Self::start_language_server(server_id, http, node_runtime, this, cx)
+                    })
+                    .shared();
+                self.server = CopilotServer::Starting { task: start_task };
+                cx.notify();
+            }
+        } else {
+            self.server = CopilotServer::Disabled;
+            cx.notify();
+        }
+    }
+
+    // #[cfg(any(test, feature = "test-support"))]
+    // pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) {
+    //     use node_runtime::FakeNodeRuntime;
+
+    //     let (server, fake_server) =
+    //         LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
+    //     let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
+    //     let node_runtime = FakeNodeRuntime::new();
+    //     let this = cx.add_model(|_| Self {
+    //         server_id: LanguageServerId(0),
+    //         http: http.clone(),
+    //         node_runtime,
+    //         server: CopilotServer::Running(RunningCopilotServer {
+    //             name: LanguageServerName(Arc::from("copilot")),
+    //             lsp: Arc::new(server),
+    //             sign_in_status: SignInStatus::Authorized,
+    //             registered_buffers: Default::default(),
+    //         }),
+    //         buffers: Default::default(),
+    //     });
+    //     (this, fake_server)
+    // }
+
+    fn start_language_server(
+        new_server_id: LanguageServerId,
+        http: Arc<dyn HttpClient>,
+        node_runtime: Arc<dyn NodeRuntime>,
+        this: WeakModel<Self>,
+        mut cx: AsyncAppContext,
+    ) -> impl Future<Output = ()> {
+        async move {
+            let start_language_server = async {
+                let server_path = get_copilot_lsp(http).await?;
+                let node_path = node_runtime.binary_path().await?;
+                let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
+                let binary = LanguageServerBinary {
+                    path: node_path,
+                    arguments,
+                };
+
+                let server = LanguageServer::new(
+                    Arc::new(Mutex::new(None)),
+                    new_server_id,
+                    binary,
+                    Path::new("/"),
+                    None,
+                    cx.clone(),
+                )?;
+
+                server
+                    .on_notification::<StatusNotification, _>(
+                        |_, _| { /* Silence the notification */ },
+                    )
+                    .detach();
+
+                let server = server.initialize(Default::default()).await?;
+
+                let status = server
+                    .request::<request::CheckStatus>(request::CheckStatusParams {
+                        local_checks_only: false,
+                    })
+                    .await?;
+
+                server
+                    .request::<request::SetEditorInfo>(request::SetEditorInfoParams {
+                        editor_info: request::EditorInfo {
+                            name: "zed".into(),
+                            version: env!("CARGO_PKG_VERSION").into(),
+                        },
+                        editor_plugin_info: request::EditorPluginInfo {
+                            name: "zed-copilot".into(),
+                            version: "0.0.1".into(),
+                        },
+                    })
+                    .await?;
+
+                anyhow::Ok((server, status))
+            };
+
+            let server = start_language_server.await;
+            this.update(&mut cx, |this, cx| {
+                cx.notify();
+                match server {
+                    Ok((server, status)) => {
+                        this.server = CopilotServer::Running(RunningCopilotServer {
+                            name: LanguageServerName(Arc::from("copilot")),
+                            lsp: server,
+                            sign_in_status: SignInStatus::SignedOut,
+                            registered_buffers: Default::default(),
+                        });
+                        cx.emit(Event::CopilotLanguageServerStarted);
+                        this.update_sign_in_status(status, cx);
+                    }
+                    Err(error) => {
+                        this.server = CopilotServer::Error(error.to_string().into());
+                        cx.notify()
+                    }
+                }
+            })
+            .ok();
+        }
+    }
+
+    pub fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        if let CopilotServer::Running(server) = &mut self.server {
+            let task = match &server.sign_in_status {
+                SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(),
+                SignInStatus::SigningIn { task, .. } => {
+                    cx.notify();
+                    task.clone()
+                }
+                SignInStatus::SignedOut | SignInStatus::Unauthorized { .. } => {
+                    let lsp = server.lsp.clone();
+                    let task = cx
+                        .spawn(|this, mut cx| async move {
+                            let sign_in = async {
+                                let sign_in = lsp
+                                    .request::<request::SignInInitiate>(
+                                        request::SignInInitiateParams {},
+                                    )
+                                    .await?;
+                                match sign_in {
+                                    request::SignInInitiateResult::AlreadySignedIn { user } => {
+                                        Ok(request::SignInStatus::Ok { user })
+                                    }
+                                    request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
+                                        this.update(&mut cx, |this, cx| {
+                                            if let CopilotServer::Running(RunningCopilotServer {
+                                                sign_in_status: status,
+                                                ..
+                                            }) = &mut this.server
+                                            {
+                                                if let SignInStatus::SigningIn {
+                                                    prompt: prompt_flow,
+                                                    ..
+                                                } = status
+                                                {
+                                                    *prompt_flow = Some(flow.clone());
+                                                    cx.notify();
+                                                }
+                                            }
+                                        })?;
+                                        let response = lsp
+                                            .request::<request::SignInConfirm>(
+                                                request::SignInConfirmParams {
+                                                    user_code: flow.user_code,
+                                                },
+                                            )
+                                            .await?;
+                                        Ok(response)
+                                    }
+                                }
+                            };
+
+                            let sign_in = sign_in.await;
+                            this.update(&mut cx, |this, cx| match sign_in {
+                                Ok(status) => {
+                                    this.update_sign_in_status(status, cx);
+                                    Ok(())
+                                }
+                                Err(error) => {
+                                    this.update_sign_in_status(
+                                        request::SignInStatus::NotSignedIn,
+                                        cx,
+                                    );
+                                    Err(Arc::new(error))
+                                }
+                            })?
+                        })
+                        .shared();
+                    server.sign_in_status = SignInStatus::SigningIn {
+                        prompt: None,
+                        task: task.clone(),
+                    };
+                    cx.notify();
+                    task
+                }
+            };
+
+            cx.executor()
+                .spawn(task.map_err(|err| anyhow!("{:?}", err)))
+        } else {
+            // If we're downloading, wait until download is finished
+            // If we're in a stuck state, display to the user
+            Task::ready(Err(anyhow!("copilot hasn't started yet")))
+        }
+    }
+
+    #[allow(dead_code)] // todo!()
+    fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
+        if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
+            let server = server.clone();
+            cx.executor().spawn(async move {
+                server
+                    .request::<request::SignOut>(request::SignOutParams {})
+                    .await?;
+                anyhow::Ok(())
+            })
+        } else {
+            Task::ready(Err(anyhow!("copilot hasn't started yet")))
+        }
+    }
+
+    pub fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
+        let start_task = cx
+            .spawn({
+                let http = self.http.clone();
+                let node_runtime = self.node_runtime.clone();
+                let server_id = self.server_id;
+                move |this, cx| async move {
+                    clear_copilot_dir().await;
+                    Self::start_language_server(server_id, http, node_runtime, this, cx).await
+                }
+            })
+            .shared();
+
+        self.server = CopilotServer::Starting {
+            task: start_task.clone(),
+        };
+
+        cx.notify();
+
+        cx.executor().spawn(start_task)
+    }
+
+    pub fn language_server(&self) -> Option<(&LanguageServerName, &Arc<LanguageServer>)> {
+        if let CopilotServer::Running(server) = &self.server {
+            Some((&server.name, &server.lsp))
+        } else {
+            None
+        }
+    }
+
+    pub fn register_buffer(&mut self, buffer: &Model<Buffer>, cx: &mut ModelContext<Self>) {
+        let weak_buffer = buffer.downgrade();
+        self.buffers.insert(weak_buffer.clone());
+
+        if let CopilotServer::Running(RunningCopilotServer {
+            lsp: server,
+            sign_in_status: status,
+            registered_buffers,
+            ..
+        }) = &mut self.server
+        {
+            if !matches!(status, SignInStatus::Authorized { .. }) {
+                return;
+            }
+
+            registered_buffers
+                .entry(buffer.entity_id())
+                .or_insert_with(|| {
+                    let uri: lsp2::Url = uri_for_buffer(buffer, cx);
+                    let language_id = id_for_language(buffer.read(cx).language());
+                    let snapshot = buffer.read(cx).snapshot();
+                    server
+                        .notify::<lsp2::notification::DidOpenTextDocument>(
+                            lsp2::DidOpenTextDocumentParams {
+                                text_document: lsp2::TextDocumentItem {
+                                    uri: uri.clone(),
+                                    language_id: language_id.clone(),
+                                    version: 0,
+                                    text: snapshot.text(),
+                                },
+                            },
+                        )
+                        .log_err();
+
+                    RegisteredBuffer {
+                        uri,
+                        language_id,
+                        snapshot,
+                        snapshot_version: 0,
+                        pending_buffer_change: Task::ready(Some(())),
+                        _subscriptions: [
+                            cx.subscribe(buffer, |this, buffer, event, cx| {
+                                this.handle_buffer_event(buffer, event, cx).log_err();
+                            }),
+                            cx.observe_release(buffer, move |this, _buffer, _cx| {
+                                this.buffers.remove(&weak_buffer);
+                                this.unregister_buffer(&weak_buffer);
+                            }),
+                        ],
+                    }
+                });
+        }
+    }
+
+    fn handle_buffer_event(
+        &mut self,
+        buffer: Model<Buffer>,
+        event: &language2::Event,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        if let Ok(server) = self.server.as_running() {
+            if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id())
+            {
+                match event {
+                    language2::Event::Edited => {
+                        let _ = registered_buffer.report_changes(&buffer, cx);
+                    }
+                    language2::Event::Saved => {
+                        server
+                            .lsp
+                            .notify::<lsp2::notification::DidSaveTextDocument>(
+                                lsp2::DidSaveTextDocumentParams {
+                                    text_document: lsp2::TextDocumentIdentifier::new(
+                                        registered_buffer.uri.clone(),
+                                    ),
+                                    text: None,
+                                },
+                            )?;
+                    }
+                    language2::Event::FileHandleChanged | language2::Event::LanguageChanged => {
+                        let new_language_id = id_for_language(buffer.read(cx).language());
+                        let new_uri = uri_for_buffer(&buffer, cx);
+                        if new_uri != registered_buffer.uri
+                            || new_language_id != registered_buffer.language_id
+                        {
+                            let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
+                            registered_buffer.language_id = new_language_id;
+                            server
+                                .lsp
+                                .notify::<lsp2::notification::DidCloseTextDocument>(
+                                    lsp2::DidCloseTextDocumentParams {
+                                        text_document: lsp2::TextDocumentIdentifier::new(old_uri),
+                                    },
+                                )?;
+                            server
+                                .lsp
+                                .notify::<lsp2::notification::DidOpenTextDocument>(
+                                    lsp2::DidOpenTextDocumentParams {
+                                        text_document: lsp2::TextDocumentItem::new(
+                                            registered_buffer.uri.clone(),
+                                            registered_buffer.language_id.clone(),
+                                            registered_buffer.snapshot_version,
+                                            registered_buffer.snapshot.text(),
+                                        ),
+                                    },
+                                )?;
+                        }
+                    }
+                    _ => {}
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    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
+                    .lsp
+                    .notify::<lsp2::notification::DidCloseTextDocument>(
+                        lsp2::DidCloseTextDocumentParams {
+                            text_document: lsp2::TextDocumentIdentifier::new(buffer.uri),
+                        },
+                    )
+                    .log_err();
+            }
+        }
+    }
+
+    pub fn completions<T>(
+        &mut self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>>
+    where
+        T: ToPointUtf16,
+    {
+        self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
+    }
+
+    pub fn completions_cycling<T>(
+        &mut self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>>
+    where
+        T: ToPointUtf16,
+    {
+        self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
+    }
+
+    pub fn accept_completion(
+        &mut self,
+        completion: &Completion,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let request =
+            server
+                .lsp
+                .request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
+                    uuid: completion.uuid.clone(),
+                });
+        cx.executor().spawn(async move {
+            request.await?;
+            Ok(())
+        })
+    }
+
+    pub fn discard_completions(
+        &mut self,
+        completions: &[Completion],
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let request =
+            server
+                .lsp
+                .request::<request::NotifyRejected>(request::NotifyRejectedParams {
+                    uuids: completions
+                        .iter()
+                        .map(|completion| completion.uuid.clone())
+                        .collect(),
+                });
+        cx.executor().spawn(async move {
+            request.await?;
+            Ok(())
+        })
+    }
+
+    fn request_completions<R, T>(
+        &mut self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>>
+    where
+        R: 'static
+            + lsp2::request::Request<
+                Params = request::GetCompletionsParams,
+                Result = request::GetCompletionsResult,
+            >,
+        T: ToPointUtf16,
+    {
+        self.register_buffer(buffer, cx);
+
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let lsp = server.lsp.clone();
+        let registered_buffer = server
+            .registered_buffers
+            .get_mut(&buffer.entity_id())
+            .unwrap();
+        let snapshot = registered_buffer.report_changes(buffer, cx);
+        let buffer = buffer.read(cx);
+        let uri = registered_buffer.uri.clone();
+        let position = position.to_point_utf16(buffer);
+        let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx);
+        let tab_size = settings.tab_size;
+        let hard_tabs = settings.hard_tabs;
+        let relative_path = buffer
+            .file()
+            .map(|file| file.path().to_path_buf())
+            .unwrap_or_default();
+
+        cx.executor().spawn(async move {
+            let (version, snapshot) = snapshot.await?;
+            let result = lsp
+                .request::<R>(request::GetCompletionsParams {
+                    doc: request::GetCompletionsDocument {
+                        uri,
+                        tab_size: tab_size.into(),
+                        indent_size: 1,
+                        insert_spaces: !hard_tabs,
+                        relative_path: relative_path.to_string_lossy().into(),
+                        position: point_to_lsp(position),
+                        version: version.try_into().unwrap(),
+                    },
+                })
+                .await?;
+            let completions = result
+                .completions
+                .into_iter()
+                .map(|completion| {
+                    let start = snapshot
+                        .clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
+                    let end =
+                        snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
+                    Completion {
+                        uuid: completion.uuid,
+                        range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
+                        text: completion.text,
+                    }
+                })
+                .collect();
+            anyhow::Ok(completions)
+        })
+    }
+
+    pub fn status(&self) -> Status {
+        match &self.server {
+            CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
+            CopilotServer::Disabled => Status::Disabled,
+            CopilotServer::Error(error) => Status::Error(error.clone()),
+            CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
+                match sign_in_status {
+                    SignInStatus::Authorized { .. } => Status::Authorized,
+                    SignInStatus::Unauthorized { .. } => Status::Unauthorized,
+                    SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
+                        prompt: prompt.clone(),
+                    },
+                    SignInStatus::SignedOut => Status::SignedOut,
+                }
+            }
+        }
+    }
+
+    fn update_sign_in_status(
+        &mut self,
+        lsp_status: request::SignInStatus,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.buffers.retain(|buffer| buffer.is_upgradable());
+
+        if let Ok(server) = self.server.as_running() {
+            match lsp_status {
+                request::SignInStatus::Ok { .. }
+                | request::SignInStatus::MaybeOk { .. }
+                | request::SignInStatus::AlreadySignedIn { .. } => {
+                    server.sign_in_status = SignInStatus::Authorized;
+                    for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
+                        if let Some(buffer) = buffer.upgrade() {
+                            self.register_buffer(&buffer, cx);
+                        }
+                    }
+                }
+                request::SignInStatus::NotAuthorized { .. } => {
+                    server.sign_in_status = SignInStatus::Unauthorized;
+                    for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
+                        self.unregister_buffer(&buffer);
+                    }
+                }
+                request::SignInStatus::NotSignedIn => {
+                    server.sign_in_status = SignInStatus::SignedOut;
+                    for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
+                        self.unregister_buffer(&buffer);
+                    }
+                }
+            }
+
+            cx.notify();
+        }
+    }
+}
+
+fn id_for_language(language: Option<&Arc<Language>>) -> String {
+    let language_name = language.map(|language| language.name());
+    match language_name.as_deref() {
+        Some("Plain Text") => "plaintext".to_string(),
+        Some(language_name) => language_name.to_lowercase(),
+        None => "plaintext".to_string(),
+    }
+}
+
+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 {
+        format!("buffer://{}", buffer.entity_id()).parse().unwrap()
+    }
+}
+
+async fn clear_copilot_dir() {
+    remove_matching(&paths::COPILOT_DIR, |_| true).await
+}
+
+async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
+    const SERVER_PATH: &'static str = "dist/agent.js";
+
+    ///Check for the latest copilot language server and download it if we haven't already
+    async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
+        let release = latest_github_release("zed-industries/copilot", false, http.clone()).await?;
+
+        let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name));
+
+        fs::create_dir_all(version_dir).await?;
+        let server_path = version_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            // Copilot LSP looks for this dist dir specifcially, so lets add it in.
+            let dist_dir = version_dir.join("dist");
+            fs::create_dir_all(dist_dir.as_path()).await?;
+
+            let url = &release
+                .assets
+                .get(0)
+                .context("Github release for copilot contained no assets")?
+                .browser_download_url;
+
+            let mut response = http
+                .get(&url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(dist_dir).await?;
+
+            remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await;
+        }
+
+        Ok(server_path)
+    }
+
+    match fetch_latest(http).await {
+        ok @ Result::Ok(..) => ok,
+        e @ Err(..) => {
+            e.log_err();
+            // Fetch a cached binary, if it exists
+            (|| async move {
+                let mut last_version_dir = None;
+                let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).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(server_path)
+                } else {
+                    Err(anyhow!(
+                        "missing executable in directory {:?}",
+                        last_version_dir
+                    ))
+                }
+            })()
+            .await
+        }
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use gpui::{executor::Deterministic, TestAppContext};
+
+//     #[gpui::test(iterations = 10)]
+//     async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+//         deterministic.forbid_parking();
+//         let (copilot, mut lsp) = Copilot::fake(cx);
+
+//         let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello"));
+//         let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
+//         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_1_uri.clone(),
+//                     "plaintext".into(),
+//                     0,
+//                     "Hello".into()
+//                 ),
+//             }
+//         );
+
+//         let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye"));
+//         let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
+//         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_2_uri.clone(),
+//                     "plaintext".into(),
+//                     0,
+//                     "Goodbye".into()
+//                 ),
+//             }
+//         );
+
+//         buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
+//                 .await,
+//             lsp::DidChangeTextDocumentParams {
+//                 text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
+//                 content_changes: vec![lsp::TextDocumentContentChangeEvent {
+//                     range: Some(lsp::Range::new(
+//                         lsp::Position::new(0, 5),
+//                         lsp::Position::new(0, 5)
+//                     )),
+//                     range_length: None,
+//                     text: " world".into(),
+//                 }],
+//             }
+//         );
+
+//         // Ensure updates to the file are reflected in the LSP.
+//         buffer_1
+//             .update(cx, |buffer, cx| {
+//                 buffer.file_updated(
+//                     Arc::new(File {
+//                         abs_path: "/root/child/buffer-1".into(),
+//                         path: Path::new("child/buffer-1").into(),
+//                     }),
+//                     cx,
+//                 )
+//             })
+//             .await;
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+//                 .await,
+//             lsp::DidCloseTextDocumentParams {
+//                 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
+//             }
+//         );
+//         let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_1_uri.clone(),
+//                     "plaintext".into(),
+//                     1,
+//                     "Hello world".into()
+//                 ),
+//             }
+//         );
+
+//         // Ensure all previously-registered buffers are closed when signing out.
+//         lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
+//             Ok(request::SignOutResult {})
+//         });
+//         copilot
+//             .update(cx, |copilot, cx| copilot.sign_out(cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+//                 .await,
+//             lsp::DidCloseTextDocumentParams {
+//                 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
+//             }
+//         );
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+//                 .await,
+//             lsp::DidCloseTextDocumentParams {
+//                 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
+//             }
+//         );
+
+//         // Ensure all previously-registered buffers are re-opened when signing in.
+//         lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
+//             Ok(request::SignInInitiateResult::AlreadySignedIn {
+//                 user: "user-1".into(),
+//             })
+//         });
+//         copilot
+//             .update(cx, |copilot, cx| copilot.sign_in(cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_2_uri.clone(),
+//                     "plaintext".into(),
+//                     0,
+//                     "Goodbye".into()
+//                 ),
+//             }
+//         );
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_1_uri.clone(),
+//                     "plaintext".into(),
+//                     0,
+//                     "Hello world".into()
+//                 ),
+//             }
+//         );
+
+//         // Dropping a buffer causes it to be closed on the LSP side as well.
+//         cx.update(|_| drop(buffer_2));
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+//                 .await,
+//             lsp::DidCloseTextDocumentParams {
+//                 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
+//             }
+//         );
+//     }
+
+//     struct File {
+//         abs_path: PathBuf,
+//         path: Arc<Path>,
+//     }
+
+//     impl language2::File for File {
+//         fn as_local(&self) -> Option<&dyn language2::LocalFile> {
+//             Some(self)
+//         }
+
+//         fn mtime(&self) -> std::time::SystemTime {
+//             unimplemented!()
+//         }
+
+//         fn path(&self) -> &Arc<Path> {
+//             &self.path
+//         }
+
+//         fn full_path(&self, _: &AppContext) -> PathBuf {
+//             unimplemented!()
+//         }
+
+//         fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
+//             unimplemented!()
+//         }
+
+//         fn is_deleted(&self) -> bool {
+//             unimplemented!()
+//         }
+
+//         fn as_any(&self) -> &dyn std::any::Any {
+//             unimplemented!()
+//         }
+
+//         fn to_proto(&self) -> rpc::proto::File {
+//             unimplemented!()
+//         }
+
+//         fn worktree_id(&self) -> usize {
+//             0
+//         }
+//     }
+
+//     impl language::LocalFile for File {
+//         fn abs_path(&self, _: &AppContext) -> PathBuf {
+//             self.abs_path.clone()
+//         }
+
+//         fn load(&self, _: &AppContext) -> Task<Result<String>> {
+//             unimplemented!()
+//         }
+
+//         fn buffer_reloaded(
+//             &self,
+//             _: u64,
+//             _: &clock::Global,
+//             _: language::RopeFingerprint,
+//             _: language::LineEnding,
+//             _: std::time::SystemTime,
+//             _: &mut AppContext,
+//         ) {
+//             unimplemented!()
+//         }
+//     }
+// }

crates/copilot2/src/request.rs 🔗

@@ -0,0 +1,225 @@
+use serde::{Deserialize, Serialize};
+
+pub enum CheckStatus {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CheckStatusParams {
+    pub local_checks_only: bool,
+}
+
+impl lsp2::request::Request for CheckStatus {
+    type Params = CheckStatusParams;
+    type Result = SignInStatus;
+    const METHOD: &'static str = "checkStatus";
+}
+
+pub enum SignInInitiate {}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct SignInInitiateParams {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(tag = "status")]
+pub enum SignInInitiateResult {
+    AlreadySignedIn { user: String },
+    PromptUserDeviceFlow(PromptUserDeviceFlow),
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PromptUserDeviceFlow {
+    pub user_code: String,
+    pub verification_uri: String,
+}
+
+impl lsp2::request::Request for SignInInitiate {
+    type Params = SignInInitiateParams;
+    type Result = SignInInitiateResult;
+    const METHOD: &'static str = "signInInitiate";
+}
+
+pub enum SignInConfirm {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SignInConfirmParams {
+    pub user_code: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(tag = "status")]
+pub enum SignInStatus {
+    #[serde(rename = "OK")]
+    Ok {
+        user: String,
+    },
+    MaybeOk {
+        user: String,
+    },
+    AlreadySignedIn {
+        user: String,
+    },
+    NotAuthorized {
+        user: String,
+    },
+    NotSignedIn,
+}
+
+impl lsp2::request::Request for SignInConfirm {
+    type Params = SignInConfirmParams;
+    type Result = SignInStatus;
+    const METHOD: &'static str = "signInConfirm";
+}
+
+pub enum SignOut {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SignOutParams {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SignOutResult {}
+
+impl lsp2::request::Request for SignOut {
+    type Params = SignOutParams;
+    type Result = SignOutResult;
+    const METHOD: &'static str = "signOut";
+}
+
+pub enum GetCompletions {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetCompletionsParams {
+    pub doc: GetCompletionsDocument,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetCompletionsDocument {
+    pub tab_size: u32,
+    pub indent_size: u32,
+    pub insert_spaces: bool,
+    pub uri: lsp2::Url,
+    pub relative_path: String,
+    pub position: lsp2::Position,
+    pub version: usize,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetCompletionsResult {
+    pub completions: Vec<Completion>,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Completion {
+    pub text: String,
+    pub position: lsp2::Position,
+    pub uuid: String,
+    pub range: lsp2::Range,
+    pub display_text: String,
+}
+
+impl lsp2::request::Request for GetCompletions {
+    type Params = GetCompletionsParams;
+    type Result = GetCompletionsResult;
+    const METHOD: &'static str = "getCompletions";
+}
+
+pub enum GetCompletionsCycling {}
+
+impl lsp2::request::Request for GetCompletionsCycling {
+    type Params = GetCompletionsParams;
+    type Result = GetCompletionsResult;
+    const METHOD: &'static str = "getCompletionsCycling";
+}
+
+pub enum LogMessage {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LogMessageParams {
+    pub level: u8,
+    pub message: String,
+    pub metadata_str: String,
+    pub extra: Vec<String>,
+}
+
+impl lsp2::notification::Notification for LogMessage {
+    type Params = LogMessageParams;
+    const METHOD: &'static str = "LogMessage";
+}
+
+pub enum StatusNotification {}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct StatusNotificationParams {
+    pub message: String,
+    pub status: String, // One of Normal/InProgress
+}
+
+impl lsp2::notification::Notification for StatusNotification {
+    type Params = StatusNotificationParams;
+    const METHOD: &'static str = "statusNotification";
+}
+
+pub enum SetEditorInfo {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SetEditorInfoParams {
+    pub editor_info: EditorInfo,
+    pub editor_plugin_info: EditorPluginInfo,
+}
+
+impl lsp2::request::Request for SetEditorInfo {
+    type Params = SetEditorInfoParams;
+    type Result = String;
+    const METHOD: &'static str = "setEditorInfo";
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EditorInfo {
+    pub name: String,
+    pub version: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EditorPluginInfo {
+    pub name: String,
+    pub version: String,
+}
+
+pub enum NotifyAccepted {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NotifyAcceptedParams {
+    pub uuid: String,
+}
+
+impl lsp2::request::Request for NotifyAccepted {
+    type Params = NotifyAcceptedParams;
+    type Result = String;
+    const METHOD: &'static str = "notifyAccepted";
+}
+
+pub enum NotifyRejected {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NotifyRejectedParams {
+    pub uuids: Vec<String>,
+}
+
+impl lsp2::request::Request for NotifyRejected {
+    type Params = NotifyRejectedParams;
+    type Result = String;
+    const METHOD: &'static str = "notifyRejected";
+}

crates/copilot2/src/sign_in.rs 🔗

@@ -0,0 +1,376 @@
+// TODO add logging in
+// use crate::{request::PromptUserDeviceFlow, Copilot, Status};
+// use gpui::{
+//     elements::*,
+//     geometry::rect::RectF,
+//     platform::{WindowBounds, WindowKind, WindowOptions},
+//     AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
+//     WindowHandle,
+// };
+// use theme::ui::modal;
+
+// #[derive(PartialEq, Eq, Debug, Clone)]
+// struct CopyUserCode;
+
+// #[derive(PartialEq, Eq, Debug, Clone)]
+// struct OpenGithub;
+
+// const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
+
+// pub fn init(cx: &mut AppContext) {
+//     if let Some(copilot) = Copilot::global(cx) {
+//         let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
+//         cx.observe(&copilot, move |copilot, cx| {
+//             let status = copilot.read(cx).status();
+
+//             match &status {
+//                 crate::Status::SigningIn { prompt } => {
+//                     if let Some(window) = verification_window.as_mut() {
+//                         let updated = window
+//                             .root(cx)
+//                             .map(|root| {
+//                                 root.update(cx, |verification, cx| {
+//                                     verification.set_status(status.clone(), cx);
+//                                     cx.activate_window();
+//                                 })
+//                             })
+//                             .is_some();
+//                         if !updated {
+//                             verification_window = Some(create_copilot_auth_window(cx, &status));
+//                         }
+//                     } else if let Some(_prompt) = prompt {
+//                         verification_window = Some(create_copilot_auth_window(cx, &status));
+//                     }
+//                 }
+//                 Status::Authorized | Status::Unauthorized => {
+//                     if let Some(window) = verification_window.as_ref() {
+//                         if let Some(verification) = window.root(cx) {
+//                             verification.update(cx, |verification, cx| {
+//                                 verification.set_status(status, cx);
+//                                 cx.platform().activate(true);
+//                                 cx.activate_window();
+//                             });
+//                         }
+//                     }
+//                 }
+//                 _ => {
+//                     if let Some(code_verification) = verification_window.take() {
+//                         code_verification.update(cx, |cx| cx.remove_window());
+//                     }
+//                 }
+//             }
+//         })
+//         .detach();
+//     }
+// }
+
+// fn create_copilot_auth_window(
+//     cx: &mut AppContext,
+//     status: &Status,
+// ) -> WindowHandle<CopilotCodeVerification> {
+//     let window_size = theme::current(cx).copilot.modal.dimensions();
+//     let window_options = WindowOptions {
+//         bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
+//         titlebar: None,
+//         center: true,
+//         focus: true,
+//         show: true,
+//         kind: WindowKind::Normal,
+//         is_movable: true,
+//         screen: None,
+//     };
+//     cx.add_window(window_options, |_cx| {
+//         CopilotCodeVerification::new(status.clone())
+//     })
+// }
+
+// pub struct CopilotCodeVerification {
+//     status: Status,
+//     connect_clicked: bool,
+// }
+
+// impl CopilotCodeVerification {
+//     pub fn new(status: Status) -> Self {
+//         Self {
+//             status,
+//             connect_clicked: false,
+//         }
+//     }
+
+//     pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
+//         self.status = status;
+//         cx.notify();
+//     }
+
+//     fn render_device_code(
+//         data: &PromptUserDeviceFlow,
+//         style: &theme::Copilot,
+//         cx: &mut ViewContext<Self>,
+//     ) -> impl IntoAnyElement<Self> {
+//         let copied = cx
+//             .read_from_clipboard()
+//             .map(|item| item.text() == &data.user_code)
+//             .unwrap_or(false);
+
+//         let device_code_style = &style.auth.prompting.device_code;
+
+//         MouseEventHandler::new::<Self, _>(0, cx, |state, _cx| {
+//             Flex::row()
+//                 .with_child(
+//                     Label::new(data.user_code.clone(), device_code_style.text.clone())
+//                         .aligned()
+//                         .contained()
+//                         .with_style(device_code_style.left_container)
+//                         .constrained()
+//                         .with_width(device_code_style.left),
+//                 )
+//                 .with_child(
+//                     Label::new(
+//                         if copied { "Copied!" } else { "Copy" },
+//                         device_code_style.cta.style_for(state).text.clone(),
+//                     )
+//                     .aligned()
+//                     .contained()
+//                     .with_style(*device_code_style.right_container.style_for(state))
+//                     .constrained()
+//                     .with_width(device_code_style.right),
+//                 )
+//                 .contained()
+//                 .with_style(device_code_style.cta.style_for(state).container)
+//         })
+//         .on_click(gpui::platform::MouseButton::Left, {
+//             let user_code = data.user_code.clone();
+//             move |_, _, cx| {
+//                 cx.platform()
+//                     .write_to_clipboard(ClipboardItem::new(user_code.clone()));
+//                 cx.notify();
+//             }
+//         })
+//         .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
+//     }
+
+//     fn render_prompting_modal(
+//         connect_clicked: bool,
+//         data: &PromptUserDeviceFlow,
+//         style: &theme::Copilot,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum ConnectButton {}
+
+//         Flex::column()
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "Enable Copilot by connecting",
+//                             style.auth.prompting.subheading.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "your existing license.",
+//                             style.auth.prompting.subheading.text.clone(),
+//                         )
+//                         .aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(style.auth.prompting.subheading.container),
+//             )
+//             .with_child(Self::render_device_code(data, &style, cx))
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "Paste this code into GitHub after",
+//                             style.auth.prompting.hint.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "clicking the button below.",
+//                             style.auth.prompting.hint.text.clone(),
+//                         )
+//                         .aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(style.auth.prompting.hint.container.clone()),
+//             )
+//             .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
+//                 if connect_clicked {
+//                     "Waiting for connection..."
+//                 } else {
+//                     "Connect to GitHub"
+//                 },
+//                 style.auth.content_width,
+//                 &style.auth.cta_button,
+//                 cx,
+//                 {
+//                     let verification_uri = data.verification_uri.clone();
+//                     move |_, verification, cx| {
+//                         cx.platform().open_url(&verification_uri);
+//                         verification.connect_clicked = true;
+//                     }
+//                 },
+//             ))
+//             .align_children_center()
+//             .into_any()
+//     }
+
+//     fn render_enabled_modal(
+//         style: &theme::Copilot,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum DoneButton {}
+
+//         let enabled_style = &style.auth.authorized;
+//         Flex::column()
+//             .with_child(
+//                 Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
+//                     .contained()
+//                     .with_style(enabled_style.subheading.container)
+//                     .aligned(),
+//             )
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "You can update your settings or",
+//                             enabled_style.hint.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "sign out from the Copilot menu in",
+//                             enabled_style.hint.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(enabled_style.hint.container),
+//             )
+//             .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
+//                 "Done",
+//                 style.auth.content_width,
+//                 &style.auth.cta_button,
+//                 cx,
+//                 |_, _, cx| cx.remove_window(),
+//             ))
+//             .align_children_center()
+//             .into_any()
+//     }
+
+//     fn render_unauthorized_modal(
+//         style: &theme::Copilot,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         let unauthorized_style = &style.auth.not_authorized;
+
+//         Flex::column()
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "Enable Copilot by connecting",
+//                             unauthorized_style.subheading.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "your existing license.",
+//                             unauthorized_style.subheading.text.clone(),
+//                         )
+//                         .aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(unauthorized_style.subheading.container),
+//             )
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "You must have an active copilot",
+//                             unauthorized_style.warning.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "license to use it in Zed.",
+//                             unauthorized_style.warning.text.clone(),
+//                         )
+//                         .aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(unauthorized_style.warning.container),
+//             )
+//             .with_child(theme::ui::cta_button::<Self, _, _, _>(
+//                 "Subscribe on GitHub",
+//                 style.auth.content_width,
+//                 &style.auth.cta_button,
+//                 cx,
+//                 |_, _, cx| {
+//                     cx.remove_window();
+//                     cx.platform().open_url(COPILOT_SIGN_UP_URL)
+//                 },
+//             ))
+//             .align_children_center()
+//             .into_any()
+//     }
+// }
+
+// impl Entity for CopilotCodeVerification {
+//     type Event = ();
+// }
+
+// impl View for CopilotCodeVerification {
+//     fn ui_name() -> &'static str {
+//         "CopilotCodeVerification"
+//     }
+
+//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         cx.notify()
+//     }
+
+//     fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         cx.notify()
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         enum ConnectModal {}
+
+//         let style = theme::current(cx).clone();
+
+//         modal::<ConnectModal, _, _, _, _>(
+//             "Connect Copilot to Zed",
+//             &style.copilot.modal,
+//             cx,
+//             |cx| {
+//                 Flex::column()
+//                     .with_children([
+//                         theme::ui::icon(&style.copilot.auth.header).into_any(),
+//                         match &self.status {
+//                             Status::SigningIn {
+//                                 prompt: Some(prompt),
+//                             } => Self::render_prompting_modal(
+//                                 self.connect_clicked,
+//                                 &prompt,
+//                                 &style.copilot,
+//                                 cx,
+//                             ),
+//                             Status::Unauthorized => {
+//                                 self.connect_clicked = false;
+//                                 Self::render_unauthorized_modal(&style.copilot, cx)
+//                             }
+//                             Status::Authorized => {
+//                                 self.connect_clicked = false;
+//                                 Self::render_enabled_modal(&style.copilot, cx)
+//                             }
+//                             _ => Empty::new().into_any(),
+//                         },
+//                     ])
+//                     .align_children_center()
+//             },
+//         )
+//         .into_any()
+//     }
+// }

crates/db2/Cargo.toml 🔗

@@ -0,0 +1,33 @@
+[package]
+name = "db2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/db2.rs"
+doctest = false
+
+[features]
+test-support = []
+
+[dependencies]
+collections = { path = "../collections" }
+gpui2 = { path = "../gpui2" }
+sqlez = { path = "../sqlez" }
+sqlez_macros = { path = "../sqlez_macros" }
+util = { path = "../util" }
+anyhow.workspace = true
+indoc.workspace = true
+async-trait.workspace = true
+lazy_static.workspace = true
+log.workspace = true
+parking_lot.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smol.workspace = true
+
+[dev-dependencies]
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+env_logger.workspace = true
+tempdir.workspace = true

crates/db2/README.md 🔗

@@ -0,0 +1,5 @@
+# Building Queries
+
+First, craft your test data. The examples folder shows a template for building a test-db, and can be ran with `cargo run --example [your-example]`.
+
+To actually use and test your queries, import the generated DB file into https://sqliteonline.com/

crates/db2/src/db2.rs 🔗

@@ -0,0 +1,327 @@
+pub mod kvp;
+pub mod query;
+
+// Re-export
+pub use anyhow;
+use anyhow::Context;
+use gpui2::AppContext;
+pub use indoc::indoc;
+pub use lazy_static;
+pub use smol;
+pub use sqlez;
+pub use sqlez_macros;
+pub use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
+pub use util::paths::DB_DIR;
+
+use sqlez::domain::Migrator;
+use sqlez::thread_safe_connection::ThreadSafeConnection;
+use sqlez_macros::sql;
+use std::future::Future;
+use std::path::{Path, PathBuf};
+use std::sync::atomic::{AtomicBool, Ordering};
+use util::channel::ReleaseChannel;
+use util::{async_maybe, ResultExt};
+
+const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
+    PRAGMA foreign_keys=TRUE;
+);
+
+const DB_INITIALIZE_QUERY: &'static str = sql!(
+    PRAGMA journal_mode=WAL;
+    PRAGMA busy_timeout=1;
+    PRAGMA case_sensitive_like=TRUE;
+    PRAGMA synchronous=NORMAL;
+);
+
+const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
+
+const DB_FILE_NAME: &'static str = "db.sqlite";
+
+lazy_static::lazy_static! {
+    pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
+    pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
+}
+
+/// Open or create a database at the given directory path.
+/// This will retry a couple times if there are failures. If opening fails once, the db directory
+/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
+/// In either case, static variables are set so that the user can be notified.
+pub async fn open_db<M: Migrator + 'static>(
+    db_dir: &Path,
+    release_channel: &ReleaseChannel,
+) -> ThreadSafeConnection<M> {
+    if *ZED_STATELESS {
+        return open_fallback_db().await;
+    }
+
+    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_maybe!({
+        smol::fs::create_dir_all(&main_db_dir)
+            .await
+            .context("Could not create db directory")
+            .log_err()?;
+        let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
+        open_main_db(&db_path).await
+    })
+    .await;
+
+    if let Some(connection) = connection {
+        return connection;
+    }
+
+    // Set another static ref so that we can escalate the notification
+    ALL_FILE_DB_FAILED.store(true, Ordering::Release);
+
+    // If still failed, create an in memory db with a known name
+    open_fallback_db().await
+}
+
+async fn open_main_db<M: Migrator>(db_path: &PathBuf) -> Option<ThreadSafeConnection<M>> {
+    log::info!("Opening main db");
+    ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
+        .with_db_initialization_query(DB_INITIALIZE_QUERY)
+        .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
+        .build()
+        .await
+        .log_err()
+}
+
+async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection<M> {
+    log::info!("Opening fallback db");
+    ThreadSafeConnection::<M>::builder(FALLBACK_DB_NAME, false)
+        .with_db_initialization_query(DB_INITIALIZE_QUERY)
+        .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
+        .build()
+        .await
+        .expect(
+            "Fallback in memory database failed. Likely initialization queries or migrations have fundamental errors",
+        )
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection<M> {
+    use sqlez::thread_safe_connection::locking_queue;
+
+    ThreadSafeConnection::<M>::builder(db_name, false)
+        .with_db_initialization_query(DB_INITIALIZE_QUERY)
+        .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
+        // Serialize queued writes via a mutex and run them synchronously
+        .with_write_queue_constructor(locking_queue())
+        .build()
+        .await
+        .unwrap()
+}
+
+/// Implements a basic DB wrapper for a given domain
+#[macro_export]
+macro_rules! define_connection {
+    (pub static ref $id:ident: $t:ident<()> = $migrations:expr;) => {
+        pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>);
+
+        impl ::std::ops::Deref for $t {
+            type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>;
+
+            fn deref(&self) -> &Self::Target {
+                &self.0
+            }
+        }
+
+        impl $crate::sqlez::domain::Domain for $t {
+            fn name() -> &'static str {
+                stringify!($t)
+            }
+
+            fn migrations() -> &'static [&'static str] {
+                $migrations
+            }
+        }
+
+        #[cfg(any(test, feature = "test-support"))]
+        $crate::lazy_static::lazy_static! {
+            pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
+        }
+
+        #[cfg(not(any(test, feature = "test-support")))]
+        $crate::lazy_static::lazy_static! {
+            pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
+        }
+    };
+    (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => {
+        pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>);
+
+        impl ::std::ops::Deref for $t {
+            type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<($($d),+, $t)>;
+
+            fn deref(&self) -> &Self::Target {
+                &self.0
+            }
+        }
+
+        impl $crate::sqlez::domain::Domain for $t {
+            fn name() -> &'static str {
+                stringify!($t)
+            }
+
+            fn migrations() -> &'static [&'static str] {
+                $migrations
+            }
+        }
+
+        #[cfg(any(test, feature = "test-support"))]
+        $crate::lazy_static::lazy_static! {
+            pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
+        }
+
+        #[cfg(not(any(test, feature = "test-support")))]
+        $crate::lazy_static::lazy_static! {
+            pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
+        }
+    };
+}
+
+pub fn write_and_log<F>(cx: &mut AppContext, db_write: impl FnOnce() -> F + Send + 'static)
+where
+    F: Future<Output = anyhow::Result<()>> + Send,
+{
+    cx.executor()
+        .spawn(async move { db_write().await.log_err() })
+        .detach()
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use std::thread;
+
+//     use sqlez::domain::Domain;
+//     use sqlez_macros::sql;
+//     use tempdir::TempDir;
+
+//     use crate::open_db;
+
+//     // Test bad migration panics
+//     #[gpui::test]
+//     #[should_panic]
+//     async fn test_bad_migration_panics() {
+//         enum BadDB {}
+
+//         impl Domain for BadDB {
+//             fn name() -> &'static str {
+//                 "db_tests"
+//             }
+
+//             fn migrations() -> &'static [&'static str] {
+//                 &[
+//                     sql!(CREATE TABLE test(value);),
+//                     // failure because test already exists
+//                     sql!(CREATE TABLE test(value);),
+//                 ]
+//             }
+//         }
+
+//         let tempdir = TempDir::new("DbTests").unwrap();
+//         let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+//     }
+
+//     /// Test that DB exists but corrupted (causing recreate)
+//     #[gpui::test]
+//     async fn test_db_corruption() {
+//         enum CorruptedDB {}
+
+//         impl Domain for CorruptedDB {
+//             fn name() -> &'static str {
+//                 "db_tests"
+//             }
+
+//             fn migrations() -> &'static [&'static str] {
+//                 &[sql!(CREATE TABLE test(value);)]
+//             }
+//         }
+
+//         enum GoodDB {}
+
+//         impl Domain for GoodDB {
+//             fn name() -> &'static str {
+//                 "db_tests" //Notice same name
+//             }
+
+//             fn migrations() -> &'static [&'static str] {
+//                 &[sql!(CREATE TABLE test2(value);)] //But different migration
+//             }
+//         }
+
+//         let tempdir = TempDir::new("DbTests").unwrap();
+//         {
+//             let corrupt_db =
+//                 open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+//             assert!(corrupt_db.persistent());
+//         }
+
+//         let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+//         assert!(
+//             good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
+//                 .unwrap()
+//                 .is_none()
+//         );
+//     }
+
+//     /// Test that DB exists but corrupted (causing recreate)
+//     #[gpui::test(iterations = 30)]
+//     async fn test_simultaneous_db_corruption() {
+//         enum CorruptedDB {}
+
+//         impl Domain for CorruptedDB {
+//             fn name() -> &'static str {
+//                 "db_tests"
+//             }
+
+//             fn migrations() -> &'static [&'static str] {
+//                 &[sql!(CREATE TABLE test(value);)]
+//             }
+//         }
+
+//         enum GoodDB {}
+
+//         impl Domain for GoodDB {
+//             fn name() -> &'static str {
+//                 "db_tests" //Notice same name
+//             }
+
+//             fn migrations() -> &'static [&'static str] {
+//                 &[sql!(CREATE TABLE test2(value);)] //But different migration
+//             }
+//         }
+
+//         let tempdir = TempDir::new("DbTests").unwrap();
+//         {
+//             // Setup the bad database
+//             let corrupt_db =
+//                 open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+//             assert!(corrupt_db.persistent());
+//         }
+
+//         // Try to connect to it a bunch of times at once
+//         let mut guards = vec![];
+//         for _ in 0..10 {
+//             let tmp_path = tempdir.path().to_path_buf();
+//             let guard = thread::spawn(move || {
+//                 let good_db = smol::block_on(open_db::<GoodDB>(
+//                     tmp_path.as_path(),
+//                     &util::channel::ReleaseChannel::Dev,
+//                 ));
+//                 assert!(
+//                     good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
+//                         .unwrap()
+//                         .is_none()
+//                 );
+//             });
+
+//             guards.push(guard);
+//         }
+
+//         for guard in guards.into_iter() {
+//             assert!(guard.join().is_ok());
+//         }
+//     }
+// }

crates/db2/src/kvp.rs 🔗

@@ -0,0 +1,62 @@
+use sqlez_macros::sql;
+
+use crate::{define_connection, query};
+
+define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
+    &[sql!(
+        CREATE TABLE IF NOT EXISTS kv_store(
+            key TEXT PRIMARY KEY,
+            value TEXT NOT NULL
+        ) STRICT;
+    )];
+);
+
+impl KeyValueStore {
+    query! {
+        pub fn read_kvp(key: &str) -> Result<Option<String>> {
+            SELECT value FROM kv_store WHERE key = (?)
+        }
+    }
+
+    query! {
+        pub async fn write_kvp(key: String, value: String) -> Result<()> {
+            INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
+        }
+    }
+
+    query! {
+        pub async fn delete_kvp(key: String) -> Result<()> {
+            DELETE FROM kv_store WHERE key = (?)
+        }
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use crate::kvp::KeyValueStore;
+
+//     #[gpui::test]
+//     async fn test_kvp() {
+//         let db = KeyValueStore(crate::open_test_db("test_kvp").await);
+
+//         assert_eq!(db.read_kvp("key-1").unwrap(), None);
+
+//         db.write_kvp("key-1".to_string(), "one".to_string())
+//             .await
+//             .unwrap();
+//         assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
+
+//         db.write_kvp("key-1".to_string(), "one-2".to_string())
+//             .await
+//             .unwrap();
+//         assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
+
+//         db.write_kvp("key-2".to_string(), "two".to_string())
+//             .await
+//             .unwrap();
+//         assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
+
+//         db.delete_kvp("key-1".to_string()).await.unwrap();
+//         assert_eq!(db.read_kvp("key-1").unwrap(), None);
+//     }
+// }

crates/db2/src/query.rs 🔗

@@ -0,0 +1,314 @@
+#[macro_export]
+macro_rules! query {
+    ($vis:vis fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
+        $vis fn $id(&self) -> $crate::anyhow::Result<()> {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.exec(sql_stmt)?().context(::std::format!(
+                "Error in {}, exec failed to execute or parse for: {}",
+                ::std::stringify!($id),
+                sql_stmt,
+            ))
+        }
+    };
+    ($vis:vis async fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
+        $vis async fn $id(&self) -> $crate::anyhow::Result<()> {
+            use $crate::anyhow::Context;
+
+            self.write(|connection| {
+                let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+                connection.exec(sql_stmt)?().context(::std::format!(
+                    "Error in {}, exec failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+            }).await
+        }
+    };
+    ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
+        $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
+                .context(::std::format!(
+                    "Error in {}, exec_bound failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+        }
+    };
+    ($vis:vis async fn $id:ident($arg:ident: $arg_type:ty) -> Result<()> { $($sql:tt)+ }) => {
+        $vis async fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<()> {
+            use $crate::anyhow::Context;
+
+            self.write(move |connection| {
+                let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+                connection.exec_bound::<$arg_type>(sql_stmt)?($arg)
+                    .context(::std::format!(
+                        "Error in {}, exec_bound failed to execute or parse for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))
+            }).await
+        }
+    };
+    ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
+        $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
+            use $crate::anyhow::Context;
+
+            self.write(move |connection| {
+                let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+                connection.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
+                    .context(::std::format!(
+                        "Error in {}, exec_bound failed to execute or parse for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))
+            }).await
+        }
+    };
+    ($vis:vis fn $id:ident() ->  Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+        $vis fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.select::<$return_type>(sql_stmt)?()
+                .context(::std::format!(
+                    "Error in {}, select_row failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+        }
+    };
+    ($vis:vis async fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+        pub async fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
+            use $crate::anyhow::Context;
+
+            self.write(|connection| {
+                let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+                connection.select::<$return_type>(sql_stmt)?()
+                    .context(::std::format!(
+                        "Error in {}, select_row failed to execute or parse for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))
+            }).await
+        }
+    };
+    ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+        $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+                .context(::std::format!(
+                    "Error in {}, exec_bound failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+        }
+    };
+    ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+        $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
+            use $crate::anyhow::Context;
+
+            self.write(|connection| {
+                let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+                connection.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+                    .context(::std::format!(
+                        "Error in {}, exec_bound failed to execute or parse for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))
+            }).await
+        }
+    };
+    ($vis:vis fn $id:ident() ->  Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+        $vis fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.select_row::<$return_type>(sql_stmt)?()
+                .context(::std::format!(
+                    "Error in {}, select_row failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+        }
+    };
+    ($vis:vis async fn $id:ident() ->  Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+        $vis async fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
+            use $crate::anyhow::Context;
+
+            self.write(|connection| {
+                let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+                connection.select_row::<$return_type>(sql_stmt)?()
+                    .context(::std::format!(
+                        "Error in {}, select_row failed to execute or parse for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))
+            }).await
+        }
+    };
+    ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) ->  Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+        $vis fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<Option<$return_type>>  {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
+                .context(::std::format!(
+                    "Error in {}, select_row_bound failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+
+        }
+    };
+    ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) ->  Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+        $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>>  {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+                .context(::std::format!(
+                    "Error in {}, select_row_bound failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+
+        }
+    };
+    ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) ->  Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+        $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>>  {
+            use $crate::anyhow::Context;
+
+
+            self.write(move |connection| {
+                let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+                connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+                    .context(::std::format!(
+                        "Error in {}, select_row_bound failed to execute or parse for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))
+            }).await
+        }
+    };
+    ($vis:vis fn $id:ident() ->  Result<$return_type:ty> { $($sql:tt)+ }) => {
+        $vis fn $id(&self) ->  $crate::anyhow::Result<$return_type>  {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.select_row::<$return_type>(indoc! { $sql })?()
+                .context(::std::format!(
+                    "Error in {}, select_row_bound failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))?
+                .context(::std::format!(
+                    "Error in {}, select_row_bound expected single row result but found none for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+        }
+    };
+    ($vis:vis async fn $id:ident() ->  Result<$return_type:ty> { $($sql:tt)+ }) => {
+        $vis async fn $id(&self) ->  $crate::anyhow::Result<$return_type>  {
+            use $crate::anyhow::Context;
+
+            self.write(|connection| {
+                let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+                connection.select_row::<$return_type>(sql_stmt)?()
+                    .context(::std::format!(
+                        "Error in {}, select_row_bound failed to execute or parse for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))?
+                    .context(::std::format!(
+                        "Error in {}, select_row_bound expected single row result but found none for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))
+            }).await
+        }
+    };
+    ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) ->  Result<$return_type:ty> { $($sql:tt)+ }) => {
+        pub fn $id(&self, $arg: $arg_type) ->  $crate::anyhow::Result<$return_type>  {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
+                .context(::std::format!(
+                    "Error in {}, select_row_bound failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))?
+                .context(::std::format!(
+                    "Error in {}, select_row_bound expected single row result but found none for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+        }
+    };
+    ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) ->  Result<$return_type:ty> { $($sql:tt)+ }) => {
+        $vis fn $id(&self, $($arg: $arg_type),+) ->  $crate::anyhow::Result<$return_type>  {
+            use $crate::anyhow::Context;
+
+            let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+            self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+                .context(::std::format!(
+                    "Error in {}, select_row_bound failed to execute or parse for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))?
+                .context(::std::format!(
+                    "Error in {}, select_row_bound expected single row result but found none for: {}",
+                    ::std::stringify!($id),
+                    sql_stmt
+                ))
+        }
+    };
+    ($vis:vis fn async $id:ident($($arg:ident: $arg_type:ty),+) ->  Result<$return_type:ty> { $($sql:tt)+ }) => {
+        $vis async fn $id(&self, $($arg: $arg_type),+) ->  $crate::anyhow::Result<$return_type>  {
+            use $crate::anyhow::Context;
+
+
+            self.write(|connection| {
+                let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+                connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+                    .context(::std::format!(
+                        "Error in {}, select_row_bound failed to execute or parse for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))?
+                    .context(::std::format!(
+                        "Error in {}, select_row_bound expected single row result but found none for: {}",
+                        ::std::stringify!($id),
+                        sql_stmt
+                    ))
+            }).await
+        }
+    };
+}

crates/feature_flags2/Cargo.toml 🔗

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

crates/feature_flags2/src/feature_flags2.rs 🔗

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

crates/fs2/Cargo.toml 🔗

@@ -0,0 +1,40 @@
+[package]
+name = "fs2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/fs2.rs"
+
+[dependencies]
+collections = { path = "../collections" }
+rope = { path = "../rope" }
+text = { path = "../text" }
+util = { path = "../util" }
+sum_tree = { path = "../sum_tree" }
+
+anyhow.workspace = true
+async-trait.workspace = true
+futures.workspace = true
+tempfile = "3"
+fsevent = { path = "../fsevent" }
+lazy_static.workspace = true
+parking_lot.workspace = true
+smol.workspace = true
+regex.workspace = true
+git2.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+log.workspace = true
+libc = "0.2"
+time.workspace = true
+
+gpui2 = { path = "../gpui2", optional = true}
+
+[dev-dependencies]
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+
+[features]
+test-support = ["gpui2/test-support"]

crates/fs2/src/fs2.rs 🔗

@@ -0,0 +1,1278 @@
+pub mod repository;
+
+use anyhow::{anyhow, Result};
+use fsevent::EventStream;
+use futures::{future::BoxFuture, Stream, StreamExt};
+use git2::Repository as LibGitRepository;
+use parking_lot::Mutex;
+use repository::GitRepository;
+use rope::Rope;
+use smol::io::{AsyncReadExt, AsyncWriteExt};
+use std::io::Write;
+use std::sync::Arc;
+use std::{
+    io,
+    os::unix::fs::MetadataExt,
+    path::{Component, Path, PathBuf},
+    pin::Pin,
+    time::{Duration, SystemTime},
+};
+use tempfile::NamedTempFile;
+use text::LineEnding;
+use util::ResultExt;
+
+#[cfg(any(test, feature = "test-support"))]
+use collections::{btree_map, BTreeMap};
+#[cfg(any(test, feature = "test-support"))]
+use repository::{FakeGitRepositoryState, GitFileStatus};
+#[cfg(any(test, feature = "test-support"))]
+use std::ffi::OsStr;
+
+#[async_trait::async_trait]
+pub trait Fs: Send + Sync {
+    async fn create_dir(&self, path: &Path) -> Result<()>;
+    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
+    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
+    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
+    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
+    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
+    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
+    async fn load(&self, path: &Path) -> Result<String>;
+    async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
+    async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
+    async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
+    async fn is_file(&self, path: &Path) -> bool;
+    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
+    async fn read_link(&self, path: &Path) -> Result<PathBuf>;
+    async fn read_dir(
+        &self,
+        path: &Path,
+    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
+    async fn watch(
+        &self,
+        path: &Path,
+        latency: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
+    fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>;
+    fn is_fake(&self) -> bool;
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_fake(&self) -> &FakeFs;
+}
+
+#[derive(Copy, Clone, Default)]
+pub struct CreateOptions {
+    pub overwrite: bool,
+    pub ignore_if_exists: bool,
+}
+
+#[derive(Copy, Clone, Default)]
+pub struct CopyOptions {
+    pub overwrite: bool,
+    pub ignore_if_exists: bool,
+}
+
+#[derive(Copy, Clone, Default)]
+pub struct RenameOptions {
+    pub overwrite: bool,
+    pub ignore_if_exists: bool,
+}
+
+#[derive(Copy, Clone, Default)]
+pub struct RemoveOptions {
+    pub recursive: bool,
+    pub ignore_if_not_exists: bool,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct Metadata {
+    pub inode: u64,
+    pub mtime: SystemTime,
+    pub is_symlink: bool,
+    pub is_dir: bool,
+}
+
+pub struct RealFs;
+
+#[async_trait::async_trait]
+impl Fs for RealFs {
+    async fn create_dir(&self, path: &Path) -> Result<()> {
+        Ok(smol::fs::create_dir_all(path).await?)
+    }
+
+    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
+        let mut open_options = smol::fs::OpenOptions::new();
+        open_options.write(true).create(true);
+        if options.overwrite {
+            open_options.truncate(true);
+        } else if !options.ignore_if_exists {
+            open_options.create_new(true);
+        }
+        open_options.open(path).await?;
+        Ok(())
+    }
+
+    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
+        if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
+            if options.ignore_if_exists {
+                return Ok(());
+            } else {
+                return Err(anyhow!("{target:?} already exists"));
+            }
+        }
+
+        smol::fs::copy(source, target).await?;
+        Ok(())
+    }
+
+    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
+        if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
+            if options.ignore_if_exists {
+                return Ok(());
+            } else {
+                return Err(anyhow!("{target:?} already exists"));
+            }
+        }
+
+        smol::fs::rename(source, target).await?;
+        Ok(())
+    }
+
+    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        let result = if options.recursive {
+            smol::fs::remove_dir_all(path).await
+        } else {
+            smol::fs::remove_dir(path).await
+        };
+        match result {
+            Ok(()) => Ok(()),
+            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
+                Ok(())
+            }
+            Err(err) => Err(err)?,
+        }
+    }
+
+    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        match smol::fs::remove_file(path).await {
+            Ok(()) => Ok(()),
+            Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
+                Ok(())
+            }
+            Err(err) => Err(err)?,
+        }
+    }
+
+    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
+        Ok(Box::new(std::fs::File::open(path)?))
+    }
+
+    async fn load(&self, path: &Path) -> Result<String> {
+        let mut file = smol::fs::File::open(path).await?;
+        let mut text = String::new();
+        file.read_to_string(&mut text).await?;
+        Ok(text)
+    }
+
+    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
+        smol::unblock(move || {
+            let mut tmp_file = NamedTempFile::new()?;
+            tmp_file.write_all(data.as_bytes())?;
+            tmp_file.persist(path)?;
+            Ok::<(), anyhow::Error>(())
+        })
+        .await?;
+
+        Ok(())
+    }
+
+    async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
+        let buffer_size = text.summary().len.min(10 * 1024);
+        if let Some(path) = path.parent() {
+            self.create_dir(path).await?;
+        }
+        let file = smol::fs::File::create(path).await?;
+        let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
+        for chunk in chunks(text, line_ending) {
+            writer.write_all(chunk.as_bytes()).await?;
+        }
+        writer.flush().await?;
+        Ok(())
+    }
+
+    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
+        Ok(smol::fs::canonicalize(path).await?)
+    }
+
+    async fn is_file(&self, path: &Path) -> bool {
+        smol::fs::metadata(path)
+            .await
+            .map_or(false, |metadata| metadata.is_file())
+    }
+
+    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
+        let symlink_metadata = match smol::fs::symlink_metadata(path).await {
+            Ok(metadata) => metadata,
+            Err(err) => {
+                return match (err.kind(), err.raw_os_error()) {
+                    (io::ErrorKind::NotFound, _) => Ok(None),
+                    (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
+                    _ => Err(anyhow::Error::new(err)),
+                }
+            }
+        };
+
+        let is_symlink = symlink_metadata.file_type().is_symlink();
+        let metadata = if is_symlink {
+            smol::fs::metadata(path).await?
+        } else {
+            symlink_metadata
+        };
+        Ok(Some(Metadata {
+            inode: metadata.ino(),
+            mtime: metadata.modified().unwrap(),
+            is_symlink,
+            is_dir: metadata.file_type().is_dir(),
+        }))
+    }
+
+    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
+        let path = smol::fs::read_link(path).await?;
+        Ok(path)
+    }
+
+    async fn read_dir(
+        &self,
+        path: &Path,
+    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
+        let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
+            Ok(entry) => Ok(entry.path()),
+            Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
+        });
+        Ok(Box::pin(result))
+    }
+
+    async fn watch(
+        &self,
+        path: &Path,
+        latency: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
+        let (tx, rx) = smol::channel::unbounded();
+        let (stream, handle) = EventStream::new(&[path], latency);
+        std::thread::spawn(move || {
+            stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
+        });
+        Box::pin(rx.chain(futures::stream::once(async move {
+            drop(handle);
+            vec![]
+        })))
+    }
+
+    fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
+        LibGitRepository::open(&dotgit_path)
+            .log_err()
+            .and_then::<Arc<Mutex<dyn GitRepository>>, _>(|libgit_repository| {
+                Some(Arc::new(Mutex::new(libgit_repository)))
+            })
+    }
+
+    fn is_fake(&self) -> bool {
+        false
+    }
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_fake(&self) -> &FakeFs {
+        panic!("called `RealFs::as_fake`")
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct FakeFs {
+    // Use an unfair lock to ensure tests are deterministic.
+    state: Mutex<FakeFsState>,
+    executor: gpui2::Executor,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+struct FakeFsState {
+    root: Arc<Mutex<FakeFsEntry>>,
+    next_inode: u64,
+    next_mtime: SystemTime,
+    event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
+    events_paused: bool,
+    buffered_events: Vec<fsevent::Event>,
+    metadata_call_count: usize,
+    read_dir_call_count: usize,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+#[derive(Debug)]
+enum FakeFsEntry {
+    File {
+        inode: u64,
+        mtime: SystemTime,
+        content: String,
+    },
+    Dir {
+        inode: u64,
+        mtime: SystemTime,
+        entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
+        git_repo_state: Option<Arc<Mutex<repository::FakeGitRepositoryState>>>,
+    },
+    Symlink {
+        target: PathBuf,
+    },
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeFsState {
+    fn read_path<'a>(&'a self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
+        Ok(self
+            .try_read_path(target, true)
+            .ok_or_else(|| anyhow!("path does not exist: {}", target.display()))?
+            .0)
+    }
+
+    fn try_read_path<'a>(
+        &'a self,
+        target: &Path,
+        follow_symlink: bool,
+    ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
+        let mut path = target.to_path_buf();
+        let mut canonical_path = PathBuf::new();
+        let mut entry_stack = Vec::new();
+        'outer: loop {
+            let mut path_components = path.components().peekable();
+            while let Some(component) = path_components.next() {
+                match component {
+                    Component::Prefix(_) => panic!("prefix paths aren't supported"),
+                    Component::RootDir => {
+                        entry_stack.clear();
+                        entry_stack.push(self.root.clone());
+                        canonical_path.clear();
+                        canonical_path.push("/");
+                    }
+                    Component::CurDir => {}
+                    Component::ParentDir => {
+                        entry_stack.pop()?;
+                        canonical_path.pop();
+                    }
+                    Component::Normal(name) => {
+                        let current_entry = entry_stack.last().cloned()?;
+                        let current_entry = current_entry.lock();
+                        if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
+                            let entry = entries.get(name.to_str().unwrap()).cloned()?;
+                            if path_components.peek().is_some() || follow_symlink {
+                                let entry = entry.lock();
+                                if let FakeFsEntry::Symlink { target, .. } = &*entry {
+                                    let mut target = target.clone();
+                                    target.extend(path_components);
+                                    path = target;
+                                    continue 'outer;
+                                }
+                            }
+                            entry_stack.push(entry.clone());
+                            canonical_path.push(name);
+                        } else {
+                            return None;
+                        }
+                    }
+                }
+            }
+            break;
+        }
+        Some((entry_stack.pop()?, canonical_path))
+    }
+
+    fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
+    where
+        Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
+    {
+        let path = normalize_path(path);
+        let filename = path
+            .file_name()
+            .ok_or_else(|| anyhow!("cannot overwrite the root"))?;
+        let parent_path = path.parent().unwrap();
+
+        let parent = self.read_path(parent_path)?;
+        let mut parent = parent.lock();
+        let new_entry = parent
+            .dir_entries(parent_path)?
+            .entry(filename.to_str().unwrap().into());
+        callback(new_entry)
+    }
+
+    fn emit_event<I, T>(&mut self, paths: I)
+    where
+        I: IntoIterator<Item = T>,
+        T: Into<PathBuf>,
+    {
+        self.buffered_events
+            .extend(paths.into_iter().map(|path| fsevent::Event {
+                event_id: 0,
+                flags: fsevent::StreamFlags::empty(),
+                path: path.into(),
+            }));
+
+        if !self.events_paused {
+            self.flush_events(self.buffered_events.len());
+        }
+    }
+
+    fn flush_events(&mut self, mut count: usize) {
+        count = count.min(self.buffered_events.len());
+        let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
+        self.event_txs.retain(|tx| {
+            let _ = tx.try_send(events.clone());
+            !tx.is_closed()
+        });
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+lazy_static::lazy_static! {
+    pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeFs {
+    pub fn new(executor: gpui2::Executor) -> Arc<Self> {
+        Arc::new(Self {
+            executor,
+            state: Mutex::new(FakeFsState {
+                root: Arc::new(Mutex::new(FakeFsEntry::Dir {
+                    inode: 0,
+                    mtime: SystemTime::UNIX_EPOCH,
+                    entries: Default::default(),
+                    git_repo_state: None,
+                })),
+                next_mtime: SystemTime::UNIX_EPOCH,
+                next_inode: 1,
+                event_txs: Default::default(),
+                buffered_events: Vec::new(),
+                events_paused: false,
+                read_dir_call_count: 0,
+                metadata_call_count: 0,
+            }),
+        })
+    }
+
+    pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
+        self.write_file_internal(path, content).unwrap()
+    }
+
+    pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
+        let mut state = self.state.lock();
+        let path = path.as_ref();
+        let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
+        state
+            .write_path(path.as_ref(), move |e| match e {
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(file);
+                    Ok(())
+                }
+                btree_map::Entry::Occupied(mut e) => {
+                    *e.get_mut() = file;
+                    Ok(())
+                }
+            })
+            .unwrap();
+        state.emit_event(&[path]);
+    }
+
+    pub fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
+        let mut state = self.state.lock();
+        let path = path.as_ref();
+        let inode = state.next_inode;
+        let mtime = state.next_mtime;
+        state.next_inode += 1;
+        state.next_mtime += Duration::from_nanos(1);
+        let file = Arc::new(Mutex::new(FakeFsEntry::File {
+            inode,
+            mtime,
+            content,
+        }));
+        state.write_path(path, move |entry| {
+            match entry {
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(file);
+                }
+                btree_map::Entry::Occupied(mut e) => {
+                    *e.get_mut() = file;
+                }
+            }
+            Ok(())
+        })?;
+        state.emit_event(&[path]);
+        Ok(())
+    }
+
+    pub fn pause_events(&self) {
+        self.state.lock().events_paused = true;
+    }
+
+    pub fn buffered_event_count(&self) -> usize {
+        self.state.lock().buffered_events.len()
+    }
+
+    pub fn flush_events(&self, count: usize) {
+        self.state.lock().flush_events(count);
+    }
+
+    #[must_use]
+    pub fn insert_tree<'a>(
+        &'a self,
+        path: impl 'a + AsRef<Path> + Send,
+        tree: serde_json::Value,
+    ) -> futures::future::BoxFuture<'a, ()> {
+        use futures::FutureExt as _;
+        use serde_json::Value::*;
+
+        async move {
+            let path = path.as_ref();
+
+            match tree {
+                Object(map) => {
+                    self.create_dir(path).await.unwrap();
+                    for (name, contents) in map {
+                        let mut path = PathBuf::from(path);
+                        path.push(name);
+                        self.insert_tree(&path, contents).await;
+                    }
+                }
+                Null => {
+                    self.create_dir(path).await.unwrap();
+                }
+                String(contents) => {
+                    self.insert_file(&path, contents).await;
+                }
+                _ => {
+                    panic!("JSON object must contain only objects, strings, or null");
+                }
+            }
+        }
+        .boxed()
+    }
+
+    pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
+    where
+        F: FnOnce(&mut FakeGitRepositoryState),
+    {
+        let mut state = self.state.lock();
+        let entry = state.read_path(dot_git).unwrap();
+        let mut entry = entry.lock();
+
+        if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
+            let repo_state = git_repo_state.get_or_insert_with(Default::default);
+            let mut repo_state = repo_state.lock();
+
+            f(&mut repo_state);
+
+            if emit_git_event {
+                state.emit_event([dot_git]);
+            }
+        } else {
+            panic!("not a directory");
+        }
+    }
+
+    pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
+        self.with_git_state(dot_git, true, |state| {
+            state.branch_name = branch.map(Into::into)
+        })
+    }
+
+    pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
+        self.with_git_state(dot_git, true, |state| {
+            state.index_contents.clear();
+            state.index_contents.extend(
+                head_state
+                    .iter()
+                    .map(|(path, content)| (path.to_path_buf(), content.clone())),
+            );
+        });
+    }
+
+    pub fn set_status_for_repo_via_working_copy_change(
+        &self,
+        dot_git: &Path,
+        statuses: &[(&Path, GitFileStatus)],
+    ) {
+        self.with_git_state(dot_git, false, |state| {
+            state.worktree_statuses.clear();
+            state.worktree_statuses.extend(
+                statuses
+                    .iter()
+                    .map(|(path, content)| ((**path).into(), content.clone())),
+            );
+        });
+        self.state.lock().emit_event(
+            statuses
+                .iter()
+                .map(|(path, _)| dot_git.parent().unwrap().join(path)),
+        );
+    }
+
+    pub fn set_status_for_repo_via_git_operation(
+        &self,
+        dot_git: &Path,
+        statuses: &[(&Path, GitFileStatus)],
+    ) {
+        self.with_git_state(dot_git, true, |state| {
+            state.worktree_statuses.clear();
+            state.worktree_statuses.extend(
+                statuses
+                    .iter()
+                    .map(|(path, content)| ((**path).into(), content.clone())),
+            );
+        });
+    }
+
+    pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
+        let mut result = Vec::new();
+        let mut queue = collections::VecDeque::new();
+        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
+        while let Some((path, entry)) = queue.pop_front() {
+            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
+                for (name, entry) in entries {
+                    queue.push_back((path.join(name), entry.clone()));
+                }
+            }
+            if include_dot_git
+                || !path
+                    .components()
+                    .any(|component| component.as_os_str() == *FS_DOT_GIT)
+            {
+                result.push(path);
+            }
+        }
+        result
+    }
+
+    pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
+        let mut result = Vec::new();
+        let mut queue = collections::VecDeque::new();
+        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
+        while let Some((path, entry)) = queue.pop_front() {
+            if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
+                for (name, entry) in entries {
+                    queue.push_back((path.join(name), entry.clone()));
+                }
+                if include_dot_git
+                    || !path
+                        .components()
+                        .any(|component| component.as_os_str() == *FS_DOT_GIT)
+                {
+                    result.push(path);
+                }
+            }
+        }
+        result
+    }
+
+    pub fn files(&self) -> Vec<PathBuf> {
+        let mut result = Vec::new();
+        let mut queue = collections::VecDeque::new();
+        queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
+        while let Some((path, entry)) = queue.pop_front() {
+            let e = entry.lock();
+            match &*e {
+                FakeFsEntry::File { .. } => result.push(path),
+                FakeFsEntry::Dir { entries, .. } => {
+                    for (name, entry) in entries {
+                        queue.push_back((path.join(name), entry.clone()));
+                    }
+                }
+                FakeFsEntry::Symlink { .. } => {}
+            }
+        }
+        result
+    }
+
+    /// How many `read_dir` calls have been issued.
+    pub fn read_dir_call_count(&self) -> usize {
+        self.state.lock().read_dir_call_count
+    }
+
+    /// How many `metadata` calls have been issued.
+    pub fn metadata_call_count(&self) -> usize {
+        self.state.lock().metadata_call_count
+    }
+
+    fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
+        self.executor.simulate_random_delay()
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeFsEntry {
+    fn is_file(&self) -> bool {
+        matches!(self, Self::File { .. })
+    }
+
+    fn is_symlink(&self) -> bool {
+        matches!(self, Self::Symlink { .. })
+    }
+
+    fn file_content(&self, path: &Path) -> Result<&String> {
+        if let Self::File { content, .. } = self {
+            Ok(content)
+        } else {
+            Err(anyhow!("not a file: {}", path.display()))
+        }
+    }
+
+    fn set_file_content(&mut self, path: &Path, new_content: String) -> Result<()> {
+        if let Self::File { content, mtime, .. } = self {
+            *mtime = SystemTime::now();
+            *content = new_content;
+            Ok(())
+        } else {
+            Err(anyhow!("not a file: {}", path.display()))
+        }
+    }
+
+    fn dir_entries(
+        &mut self,
+        path: &Path,
+    ) -> Result<&mut BTreeMap<String, Arc<Mutex<FakeFsEntry>>>> {
+        if let Self::Dir { entries, .. } = self {
+            Ok(entries)
+        } else {
+            Err(anyhow!("not a directory: {}", path.display()))
+        }
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+#[async_trait::async_trait]
+impl Fs for FakeFs {
+    async fn create_dir(&self, path: &Path) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let mut created_dirs = Vec::new();
+        let mut cur_path = PathBuf::new();
+        for component in path.components() {
+            let mut state = self.state.lock();
+            cur_path.push(component);
+            if cur_path == Path::new("/") {
+                continue;
+            }
+
+            let inode = state.next_inode;
+            let mtime = state.next_mtime;
+            state.next_mtime += Duration::from_nanos(1);
+            state.next_inode += 1;
+            state.write_path(&cur_path, |entry| {
+                entry.or_insert_with(|| {
+                    created_dirs.push(cur_path.clone());
+                    Arc::new(Mutex::new(FakeFsEntry::Dir {
+                        inode,
+                        mtime,
+                        entries: Default::default(),
+                        git_repo_state: None,
+                    }))
+                });
+                Ok(())
+            })?
+        }
+
+        self.state.lock().emit_event(&created_dirs);
+        Ok(())
+    }
+
+    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+        let mut state = self.state.lock();
+        let inode = state.next_inode;
+        let mtime = state.next_mtime;
+        state.next_mtime += Duration::from_nanos(1);
+        state.next_inode += 1;
+        let file = Arc::new(Mutex::new(FakeFsEntry::File {
+            inode,
+            mtime,
+            content: String::new(),
+        }));
+        state.write_path(path, |entry| {
+            match entry {
+                btree_map::Entry::Occupied(mut e) => {
+                    if options.overwrite {
+                        *e.get_mut() = file;
+                    } else if !options.ignore_if_exists {
+                        return Err(anyhow!("path already exists: {}", path.display()));
+                    }
+                }
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(file);
+                }
+            }
+            Ok(())
+        })?;
+        state.emit_event(&[path]);
+        Ok(())
+    }
+
+    async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let old_path = normalize_path(old_path);
+        let new_path = normalize_path(new_path);
+
+        let mut state = self.state.lock();
+        let moved_entry = state.write_path(&old_path, |e| {
+            if let btree_map::Entry::Occupied(e) = e {
+                Ok(e.get().clone())
+            } else {
+                Err(anyhow!("path does not exist: {}", &old_path.display()))
+            }
+        })?;
+
+        state.write_path(&new_path, |e| {
+            match e {
+                btree_map::Entry::Occupied(mut e) => {
+                    if options.overwrite {
+                        *e.get_mut() = moved_entry;
+                    } else if !options.ignore_if_exists {
+                        return Err(anyhow!("path already exists: {}", new_path.display()));
+                    }
+                }
+                btree_map::Entry::Vacant(e) => {
+                    e.insert(moved_entry);
+                }
+            }
+            Ok(())
+        })?;
+
+        state
+            .write_path(&old_path, |e| {
+                if let btree_map::Entry::Occupied(e) = e {
+                    Ok(e.remove())
+                } else {
+                    unreachable!()
+                }
+            })
+            .unwrap();
+
+        state.emit_event(&[old_path, new_path]);
+        Ok(())
+    }
+
+    async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let source = normalize_path(source);
+        let target = normalize_path(target);
+        let mut state = self.state.lock();
+        let mtime = state.next_mtime;
+        let inode = util::post_inc(&mut state.next_inode);
+        state.next_mtime += Duration::from_nanos(1);
+        let source_entry = state.read_path(&source)?;
+        let content = source_entry.lock().file_content(&source)?.clone();
+        let entry = state.write_path(&target, |e| match e {
+            btree_map::Entry::Occupied(e) => {
+                if options.overwrite {
+                    Ok(Some(e.get().clone()))
+                } else if !options.ignore_if_exists {
+                    return Err(anyhow!("{target:?} already exists"));
+                } else {
+                    Ok(None)
+                }
+            }
+            btree_map::Entry::Vacant(e) => Ok(Some(
+                e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
+                    inode,
+                    mtime,
+                    content: String::new(),
+                })))
+                .clone(),
+            )),
+        })?;
+        if let Some(entry) = entry {
+            entry.lock().set_file_content(&target, content)?;
+        }
+        state.emit_event(&[target]);
+        Ok(())
+    }
+
+    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let path = normalize_path(path);
+        let parent_path = path
+            .parent()
+            .ok_or_else(|| anyhow!("cannot remove the root"))?;
+        let base_name = path.file_name().unwrap();
+
+        let mut state = self.state.lock();
+        let parent_entry = state.read_path(parent_path)?;
+        let mut parent_entry = parent_entry.lock();
+        let entry = parent_entry
+            .dir_entries(parent_path)?
+            .entry(base_name.to_str().unwrap().into());
+
+        match entry {
+            btree_map::Entry::Vacant(_) => {
+                if !options.ignore_if_not_exists {
+                    return Err(anyhow!("{path:?} does not exist"));
+                }
+            }
+            btree_map::Entry::Occupied(e) => {
+                {
+                    let mut entry = e.get().lock();
+                    let children = entry.dir_entries(&path)?;
+                    if !options.recursive && !children.is_empty() {
+                        return Err(anyhow!("{path:?} is not empty"));
+                    }
+                }
+                e.remove();
+            }
+        }
+        state.emit_event(&[path]);
+        Ok(())
+    }
+
+    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
+        self.simulate_random_delay().await;
+
+        let path = normalize_path(path);
+        let parent_path = path
+            .parent()
+            .ok_or_else(|| anyhow!("cannot remove the root"))?;
+        let base_name = path.file_name().unwrap();
+        let mut state = self.state.lock();
+        let parent_entry = state.read_path(parent_path)?;
+        let mut parent_entry = parent_entry.lock();
+        let entry = parent_entry
+            .dir_entries(parent_path)?
+            .entry(base_name.to_str().unwrap().into());
+        match entry {
+            btree_map::Entry::Vacant(_) => {
+                if !options.ignore_if_not_exists {
+                    return Err(anyhow!("{path:?} does not exist"));
+                }
+            }
+            btree_map::Entry::Occupied(e) => {
+                e.get().lock().file_content(&path)?;
+                e.remove();
+            }
+        }
+        state.emit_event(&[path]);
+        Ok(())
+    }
+
+    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
+        let text = self.load(path).await?;
+        Ok(Box::new(io::Cursor::new(text)))
+    }
+
+    async fn load(&self, path: &Path) -> Result<String> {
+        let path = normalize_path(path);
+        self.simulate_random_delay().await;
+        let state = self.state.lock();
+        let entry = state.read_path(&path)?;
+        let entry = entry.lock();
+        entry.file_content(&path).cloned()
+    }
+
+    async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path.as_path());
+        self.write_file_internal(path, data.to_string())?;
+
+        Ok(())
+    }
+
+    async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path);
+        let content = chunks(text, line_ending).collect();
+        if let Some(path) = path.parent() {
+            self.create_dir(path).await?;
+        }
+        self.write_file_internal(path, content)?;
+        Ok(())
+    }
+
+    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
+        let path = normalize_path(path);
+        self.simulate_random_delay().await;
+        let state = self.state.lock();
+        if let Some((_, canonical_path)) = state.try_read_path(&path, true) {
+            Ok(canonical_path)
+        } else {
+            Err(anyhow!("path does not exist: {}", path.display()))
+        }
+    }
+
+    async fn is_file(&self, path: &Path) -> bool {
+        let path = normalize_path(path);
+        self.simulate_random_delay().await;
+        let state = self.state.lock();
+        if let Some((entry, _)) = state.try_read_path(&path, true) {
+            entry.lock().is_file()
+        } else {
+            false
+        }
+    }
+
+    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path);
+        let mut state = self.state.lock();
+        state.metadata_call_count += 1;
+        if let Some((mut entry, _)) = state.try_read_path(&path, false) {
+            let is_symlink = entry.lock().is_symlink();
+            if is_symlink {
+                if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) {
+                    entry = e;
+                } else {
+                    return Ok(None);
+                }
+            }
+
+            let entry = entry.lock();
+            Ok(Some(match &*entry {
+                FakeFsEntry::File { inode, mtime, .. } => Metadata {
+                    inode: *inode,
+                    mtime: *mtime,
+                    is_dir: false,
+                    is_symlink,
+                },
+                FakeFsEntry::Dir { inode, mtime, .. } => Metadata {
+                    inode: *inode,
+                    mtime: *mtime,
+                    is_dir: true,
+                    is_symlink,
+                },
+                FakeFsEntry::Symlink { .. } => unreachable!(),
+            }))
+        } else {
+            Ok(None)
+        }
+    }
+
+    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path);
+        let state = self.state.lock();
+        if let Some((entry, _)) = state.try_read_path(&path, false) {
+            let entry = entry.lock();
+            if let FakeFsEntry::Symlink { target } = &*entry {
+                Ok(target.clone())
+            } else {
+                Err(anyhow!("not a symlink: {}", path.display()))
+            }
+        } else {
+            Err(anyhow!("path does not exist: {}", path.display()))
+        }
+    }
+
+    async fn read_dir(
+        &self,
+        path: &Path,
+    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path);
+        let mut state = self.state.lock();
+        state.read_dir_call_count += 1;
+        let entry = state.read_path(&path)?;
+        let mut entry = entry.lock();
+        let children = entry.dir_entries(&path)?;
+        let paths = children
+            .keys()
+            .map(|file_name| Ok(path.join(file_name)))
+            .collect::<Vec<_>>();
+        Ok(Box::pin(futures::stream::iter(paths)))
+    }
+
+    async fn watch(
+        &self,
+        path: &Path,
+        _: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
+        self.simulate_random_delay().await;
+        let (tx, rx) = smol::channel::unbounded();
+        self.state.lock().event_txs.push(tx);
+        let path = path.to_path_buf();
+        let executor = self.executor.clone();
+        Box::pin(futures::StreamExt::filter(rx, move |events| {
+            let result = events.iter().any(|event| event.path.starts_with(&path));
+            let executor = executor.clone();
+            async move {
+                executor.simulate_random_delay().await;
+                result
+            }
+        }))
+    }
+
+    fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
+        let state = self.state.lock();
+        let entry = state.read_path(abs_dot_git).unwrap();
+        let mut entry = entry.lock();
+        if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
+            let state = git_repo_state
+                .get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
+                .clone();
+            Some(repository::FakeGitRepository::open(state))
+        } else {
+            None
+        }
+    }
+
+    fn is_fake(&self) -> bool {
+        true
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_fake(&self) -> &FakeFs {
+        self
+    }
+}
+
+fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
+    rope.chunks().flat_map(move |chunk| {
+        let mut newline = false;
+        chunk.split('\n').flat_map(move |line| {
+            let ending = if newline {
+                Some(line_ending.as_str())
+            } else {
+                None
+            };
+            newline = true;
+            ending.into_iter().chain([line])
+        })
+    })
+}
+
+pub fn normalize_path(path: &Path) -> PathBuf {
+    let mut components = path.components().peekable();
+    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
+        components.next();
+        PathBuf::from(c.as_os_str())
+    } else {
+        PathBuf::new()
+    };
+
+    for component in components {
+        match component {
+            Component::Prefix(..) => unreachable!(),
+            Component::RootDir => {
+                ret.push(component.as_os_str());
+            }
+            Component::CurDir => {}
+            Component::ParentDir => {
+                ret.pop();
+            }
+            Component::Normal(c) => {
+                ret.push(c);
+            }
+        }
+    }
+    ret
+}
+
+pub fn copy_recursive<'a>(
+    fs: &'a dyn Fs,
+    source: &'a Path,
+    target: &'a Path,
+    options: CopyOptions,
+) -> BoxFuture<'a, Result<()>> {
+    use futures::future::FutureExt;
+
+    async move {
+        let metadata = fs
+            .metadata(source)
+            .await?
+            .ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
+        if metadata.is_dir {
+            if !options.overwrite && fs.metadata(target).await.is_ok() {
+                if options.ignore_if_exists {
+                    return Ok(());
+                } else {
+                    return Err(anyhow!("{target:?} already exists"));
+                }
+            }
+
+            let _ = fs
+                .remove_dir(
+                    target,
+                    RemoveOptions {
+                        recursive: true,
+                        ignore_if_not_exists: true,
+                    },
+                )
+                .await;
+            fs.create_dir(target).await?;
+            let mut children = fs.read_dir(source).await?;
+            while let Some(child_path) = children.next().await {
+                if let Ok(child_path) = child_path {
+                    if let Some(file_name) = child_path.file_name() {
+                        let child_target_path = target.join(file_name);
+                        copy_recursive(fs, &child_path, &child_target_path, options).await?;
+                    }
+                }
+            }
+
+            Ok(())
+        } else {
+            fs.copy_file(source, target, options).await
+        }
+    }
+    .boxed()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui2::Executor;
+    use serde_json::json;
+
+    #[gpui2::test]
+    async fn test_fake_fs(executor: Executor) {
+        let fs = FakeFs::new(executor.clone());
+        fs.insert_tree(
+            "/root",
+            json!({
+                "dir1": {
+                    "a": "A",
+                    "b": "B"
+                },
+                "dir2": {
+                    "c": "C",
+                    "dir3": {
+                        "d": "D"
+                    }
+                }
+            }),
+        )
+        .await;
+
+        assert_eq!(
+            fs.files(),
+            vec![
+                PathBuf::from("/root/dir1/a"),
+                PathBuf::from("/root/dir1/b"),
+                PathBuf::from("/root/dir2/c"),
+                PathBuf::from("/root/dir2/dir3/d"),
+            ]
+        );
+
+        fs.insert_symlink("/root/dir2/link-to-dir3", "./dir3".into())
+            .await;
+
+        assert_eq!(
+            fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
+                .await
+                .unwrap(),
+            PathBuf::from("/root/dir2/dir3"),
+        );
+        assert_eq!(
+            fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
+                .await
+                .unwrap(),
+            PathBuf::from("/root/dir2/dir3/d"),
+        );
+        assert_eq!(
+            fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
+            "D",
+        );
+    }
+}

crates/fs2/src/repository.rs 🔗

@@ -0,0 +1,417 @@
+use anyhow::Result;
+use collections::HashMap;
+use git2::{BranchType, StatusShow};
+use parking_lot::Mutex;
+use serde_derive::{Deserialize, Serialize};
+use std::{
+    cmp::Ordering,
+    ffi::OsStr,
+    os::unix::prelude::OsStrExt,
+    path::{Component, Path, PathBuf},
+    sync::Arc,
+    time::SystemTime,
+};
+use sum_tree::{MapSeekTarget, TreeMap};
+use util::ResultExt;
+
+pub use git2::Repository as LibGitRepository;
+
+#[derive(Clone, Debug, Hash, PartialEq)]
+pub struct Branch {
+    pub name: Box<str>,
+    /// Timestamp of most recent commit, normalized to Unix Epoch format.
+    pub unix_timestamp: Option<i64>,
+}
+
+#[async_trait::async_trait]
+pub trait GitRepository: Send {
+    fn reload_index(&self);
+    fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
+    fn branch_name(&self) -> Option<String>;
+
+    /// Get the statuses of all of the files in the index that start with the given
+    /// path and have changes with resepect to the HEAD commit. This is fast because
+    /// the index stores hashes of trees, so that unchanged directories can be skipped.
+    fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus>;
+
+    /// Get the status of a given file in the working directory with respect to
+    /// the index. In the common case, when there are no changes, this only requires
+    /// an index lookup. The index stores the mtime of each file when it was added,
+    /// so there's no work to do if the mtime matches.
+    fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
+
+    /// Get the status of a given file in the working directory with respect to
+    /// the HEAD commit. In the common case, when there are no changes, this only
+    /// requires an index lookup and blob comparison between the index and the HEAD
+    /// commit. The index stores the mtime of each file when it was added, so there's
+    /// no need to consider the working directory file if the mtime matches.
+    fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
+
+    fn branches(&self) -> Result<Vec<Branch>>;
+    fn change_branch(&self, _: &str) -> Result<()>;
+    fn create_branch(&self, _: &str) -> Result<()>;
+}
+
+impl std::fmt::Debug for dyn GitRepository {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("dyn GitRepository<...>").finish()
+    }
+}
+
+impl GitRepository for LibGitRepository {
+    fn reload_index(&self) {
+        if let Ok(mut index) = self.index() {
+            _ = index.read(false);
+        }
+    }
+
+    fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
+        fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
+            const STAGE_NORMAL: i32 = 0;
+            let index = repo.index()?;
+
+            // This check is required because index.get_path() unwraps internally :(
+            check_path_to_repo_path_errors(relative_file_path)?;
+
+            let oid = match index.get_path(&relative_file_path, STAGE_NORMAL) {
+                Some(entry) => entry.id,
+                None => return Ok(None),
+            };
+
+            let content = repo.find_blob(oid)?.content().to_owned();
+            Ok(Some(String::from_utf8(content)?))
+        }
+
+        match logic(&self, relative_file_path) {
+            Ok(value) => return value,
+            Err(err) => log::error!("Error loading head text: {:?}", err),
+        }
+        None
+    }
+
+    fn branch_name(&self) -> Option<String> {
+        let head = self.head().log_err()?;
+        let branch = String::from_utf8_lossy(head.shorthand_bytes());
+        Some(branch.to_string())
+    }
+
+    fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
+        let mut map = TreeMap::default();
+
+        let mut options = git2::StatusOptions::new();
+        options.pathspec(path_prefix);
+        options.show(StatusShow::Index);
+
+        if let Some(statuses) = self.statuses(Some(&mut options)).log_err() {
+            for status in statuses.iter() {
+                let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
+                let status = status.status();
+                if !status.contains(git2::Status::IGNORED) {
+                    if let Some(status) = read_status(status) {
+                        map.insert(path, status)
+                    }
+                }
+            }
+        }
+        map
+    }
+
+    fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
+        // If the file has not changed since it was added to the index, then
+        // there can't be any changes.
+        if matches_index(self, path, mtime) {
+            return None;
+        }
+
+        let mut options = git2::StatusOptions::new();
+        options.pathspec(&path.0);
+        options.disable_pathspec_match(true);
+        options.include_untracked(true);
+        options.recurse_untracked_dirs(true);
+        options.include_unmodified(true);
+        options.show(StatusShow::Workdir);
+
+        let statuses = self.statuses(Some(&mut options)).log_err()?;
+        let status = statuses.get(0).and_then(|s| read_status(s.status()));
+        status
+    }
+
+    fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
+        let mut options = git2::StatusOptions::new();
+        options.pathspec(&path.0);
+        options.disable_pathspec_match(true);
+        options.include_untracked(true);
+        options.recurse_untracked_dirs(true);
+        options.include_unmodified(true);
+
+        // If the file has not changed since it was added to the index, then
+        // there's no need to examine the working directory file: just compare
+        // the blob in the index to the one in the HEAD commit.
+        if matches_index(self, path, mtime) {
+            options.show(StatusShow::Index);
+        }
+
+        let statuses = self.statuses(Some(&mut options)).log_err()?;
+        let status = statuses.get(0).and_then(|s| read_status(s.status()));
+        status
+    }
+
+    fn branches(&self) -> Result<Vec<Branch>> {
+        let local_branches = self.branches(Some(BranchType::Local))?;
+        let valid_branches = local_branches
+            .filter_map(|branch| {
+                branch.ok().and_then(|(branch, _)| {
+                    let name = branch.name().ok().flatten().map(Box::from)?;
+                    let timestamp = branch.get().peel_to_commit().ok()?.time();
+                    let unix_timestamp = timestamp.seconds();
+                    let timezone_offset = timestamp.offset_minutes();
+                    let utc_offset =
+                        time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
+                    let unix_timestamp =
+                        time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
+                    Some(Branch {
+                        name,
+                        unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
+                    })
+                })
+            })
+            .collect();
+        Ok(valid_branches)
+    }
+    fn change_branch(&self, name: &str) -> Result<()> {
+        let revision = self.find_branch(name, BranchType::Local)?;
+        let revision = revision.get();
+        let as_tree = revision.peel_to_tree()?;
+        self.checkout_tree(as_tree.as_object(), None)?;
+        self.set_head(
+            revision
+                .name()
+                .ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
+        )?;
+        Ok(())
+    }
+    fn create_branch(&self, name: &str) -> Result<()> {
+        let current_commit = self.head()?.peel_to_commit()?;
+        self.branch(name, &current_commit, false)?;
+
+        Ok(())
+    }
+}
+
+fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool {
+    if let Some(index) = repo.index().log_err() {
+        if let Some(entry) = index.get_path(&path, 0) {
+            if let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() {
+                if entry.mtime.seconds() == mtime.as_secs() as i32
+                    && entry.mtime.nanoseconds() == mtime.subsec_nanos()
+                {
+                    return true;
+                }
+            }
+        }
+    }
+    false
+}
+
+fn read_status(status: git2::Status) -> Option<GitFileStatus> {
+    if status.contains(git2::Status::CONFLICTED) {
+        Some(GitFileStatus::Conflict)
+    } else if status.intersects(
+        git2::Status::WT_MODIFIED
+            | git2::Status::WT_RENAMED
+            | git2::Status::INDEX_MODIFIED
+            | git2::Status::INDEX_RENAMED,
+    ) {
+        Some(GitFileStatus::Modified)
+    } else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) {
+        Some(GitFileStatus::Added)
+    } else {
+        None
+    }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct FakeGitRepository {
+    state: Arc<Mutex<FakeGitRepositoryState>>,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct FakeGitRepositoryState {
+    pub index_contents: HashMap<PathBuf, String>,
+    pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
+    pub branch_name: Option<String>,
+}
+
+impl FakeGitRepository {
+    pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
+        Arc::new(Mutex::new(FakeGitRepository { state }))
+    }
+}
+
+#[async_trait::async_trait]
+impl GitRepository for FakeGitRepository {
+    fn reload_index(&self) {}
+
+    fn load_index_text(&self, path: &Path) -> Option<String> {
+        let state = self.state.lock();
+        state.index_contents.get(path).cloned()
+    }
+
+    fn branch_name(&self) -> Option<String> {
+        let state = self.state.lock();
+        state.branch_name.clone()
+    }
+
+    fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
+        let mut map = TreeMap::default();
+        let state = self.state.lock();
+        for (repo_path, status) in state.worktree_statuses.iter() {
+            if repo_path.0.starts_with(path_prefix) {
+                map.insert(repo_path.to_owned(), status.to_owned());
+            }
+        }
+        map
+    }
+
+    fn unstaged_status(&self, _path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
+        None
+    }
+
+    fn status(&self, path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
+        let state = self.state.lock();
+        state.worktree_statuses.get(path).cloned()
+    }
+
+    fn branches(&self) -> Result<Vec<Branch>> {
+        Ok(vec![])
+    }
+
+    fn change_branch(&self, name: &str) -> Result<()> {
+        let mut state = self.state.lock();
+        state.branch_name = Some(name.to_owned());
+        Ok(())
+    }
+
+    fn create_branch(&self, name: &str) -> Result<()> {
+        let mut state = self.state.lock();
+        state.branch_name = Some(name.to_owned());
+        Ok(())
+    }
+}
+
+fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
+    match relative_file_path.components().next() {
+        None => anyhow::bail!("repo path should not be empty"),
+        Some(Component::Prefix(_)) => anyhow::bail!(
+            "repo path `{}` should be relative, not a windows prefix",
+            relative_file_path.to_string_lossy()
+        ),
+        Some(Component::RootDir) => {
+            anyhow::bail!(
+                "repo path `{}` should be relative",
+                relative_file_path.to_string_lossy()
+            )
+        }
+        Some(Component::CurDir) => {
+            anyhow::bail!(
+                "repo path `{}` should not start with `.`",
+                relative_file_path.to_string_lossy()
+            )
+        }
+        Some(Component::ParentDir) => {
+            anyhow::bail!(
+                "repo path `{}` should not start with `..`",
+                relative_file_path.to_string_lossy()
+            )
+        }
+        _ => Ok(()),
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum GitFileStatus {
+    Added,
+    Modified,
+    Conflict,
+}
+
+impl GitFileStatus {
+    pub fn merge(
+        this: Option<GitFileStatus>,
+        other: Option<GitFileStatus>,
+        prefer_other: bool,
+    ) -> Option<GitFileStatus> {
+        if prefer_other {
+            return other;
+        } else {
+            match (this, other) {
+                (Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => {
+                    Some(GitFileStatus::Conflict)
+                }
+                (Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => {
+                    Some(GitFileStatus::Modified)
+                }
+                (Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => {
+                    Some(GitFileStatus::Added)
+                }
+                _ => None,
+            }
+        }
+    }
+}
+
+#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
+pub struct RepoPath(pub PathBuf);
+
+impl RepoPath {
+    pub fn new(path: PathBuf) -> Self {
+        debug_assert!(path.is_relative(), "Repo paths must be relative");
+
+        RepoPath(path)
+    }
+}
+
+impl From<&Path> for RepoPath {
+    fn from(value: &Path) -> Self {
+        RepoPath::new(value.to_path_buf())
+    }
+}
+
+impl From<PathBuf> for RepoPath {
+    fn from(value: PathBuf) -> Self {
+        RepoPath::new(value)
+    }
+}
+
+impl Default for RepoPath {
+    fn default() -> Self {
+        RepoPath(PathBuf::new())
+    }
+}
+
+impl AsRef<Path> for RepoPath {
+    fn as_ref(&self) -> &Path {
+        self.0.as_ref()
+    }
+}
+
+impl std::ops::Deref for RepoPath {
+    type Target = PathBuf;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+#[derive(Debug)]
+pub struct RepoPathDescendants<'a>(pub &'a Path);
+
+impl<'a> MapSeekTarget<RepoPath> for RepoPathDescendants<'a> {
+    fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
+        if key.starts_with(&self.0) {
+            Ordering::Greater
+        } else {
+            self.0.cmp(key)
+        }
+    }
+}

crates/fuzzy2/Cargo.toml 🔗

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

crates/fuzzy2/src/char_bag.rs 🔗

@@ -0,0 +1,63 @@
+use std::iter::FromIterator;
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub struct CharBag(u64);
+
+impl CharBag {
+    pub fn is_superset(self, other: CharBag) -> bool {
+        self.0 & other.0 == other.0
+    }
+
+    fn insert(&mut self, c: char) {
+        let c = c.to_ascii_lowercase();
+        if ('a'..='z').contains(&c) {
+            let mut count = self.0;
+            let idx = c as u8 - b'a';
+            count >>= idx * 2;
+            count = ((count << 1) | 1) & 3;
+            count <<= idx * 2;
+            self.0 |= count;
+        } else if ('0'..='9').contains(&c) {
+            let idx = c as u8 - b'0';
+            self.0 |= 1 << (idx + 52);
+        } else if c == '-' {
+            self.0 |= 1 << 62;
+        }
+    }
+}
+
+impl Extend<char> for CharBag {
+    fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
+        for c in iter {
+            self.insert(c);
+        }
+    }
+}
+
+impl FromIterator<char> for CharBag {
+    fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
+        let mut result = Self::default();
+        result.extend(iter);
+        result
+    }
+}
+
+impl From<&str> for CharBag {
+    fn from(s: &str) -> Self {
+        let mut bag = Self(0);
+        for c in s.chars() {
+            bag.insert(c);
+        }
+        bag
+    }
+}
+
+impl From<&[char]> for CharBag {
+    fn from(chars: &[char]) -> Self {
+        let mut bag = Self(0);
+        for c in chars {
+            bag.insert(*c);
+        }
+        bag
+    }
+}

crates/fuzzy2/src/fuzzy2.rs 🔗

@@ -0,0 +1,10 @@
+mod char_bag;
+mod matcher;
+mod paths;
+mod strings;
+
+pub use char_bag::CharBag;
+pub use paths::{
+    match_fixed_path_set, match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet,
+};
+pub use strings::{match_strings, StringMatch, StringMatchCandidate};

crates/fuzzy2/src/matcher.rs 🔗

@@ -0,0 +1,464 @@
+use std::{
+    borrow::Cow,
+    sync::atomic::{self, AtomicBool},
+};
+
+use crate::CharBag;
+
+const BASE_DISTANCE_PENALTY: f64 = 0.6;
+const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
+const MIN_DISTANCE_PENALTY: f64 = 0.2;
+
+pub struct Matcher<'a> {
+    query: &'a [char],
+    lowercase_query: &'a [char],
+    query_char_bag: CharBag,
+    smart_case: bool,
+    max_results: usize,
+    min_score: f64,
+    match_positions: Vec<usize>,
+    last_positions: Vec<usize>,
+    score_matrix: Vec<Option<f64>>,
+    best_position_matrix: Vec<usize>,
+}
+
+pub trait Match: Ord {
+    fn score(&self) -> f64;
+    fn set_positions(&mut self, positions: Vec<usize>);
+}
+
+pub trait MatchCandidate {
+    fn has_chars(&self, bag: CharBag) -> bool;
+    fn to_string(&self) -> Cow<'_, str>;
+}
+
+impl<'a> Matcher<'a> {
+    pub fn new(
+        query: &'a [char],
+        lowercase_query: &'a [char],
+        query_char_bag: CharBag,
+        smart_case: bool,
+        max_results: usize,
+    ) -> Self {
+        Self {
+            query,
+            lowercase_query,
+            query_char_bag,
+            min_score: 0.0,
+            last_positions: vec![0; query.len()],
+            match_positions: vec![0; query.len()],
+            score_matrix: Vec::new(),
+            best_position_matrix: Vec::new(),
+            smart_case,
+            max_results,
+        }
+    }
+
+    pub fn match_candidates<C: MatchCandidate, R, F>(
+        &mut self,
+        prefix: &[char],
+        lowercase_prefix: &[char],
+        candidates: impl Iterator<Item = C>,
+        results: &mut Vec<R>,
+        cancel_flag: &AtomicBool,
+        build_match: F,
+    ) where
+        R: Match,
+        F: Fn(&C, f64) -> R,
+    {
+        let mut candidate_chars = Vec::new();
+        let mut lowercase_candidate_chars = Vec::new();
+
+        for candidate in candidates {
+            if !candidate.has_chars(self.query_char_bag) {
+                continue;
+            }
+
+            if cancel_flag.load(atomic::Ordering::Relaxed) {
+                break;
+            }
+
+            candidate_chars.clear();
+            lowercase_candidate_chars.clear();
+            for c in candidate.to_string().chars() {
+                candidate_chars.push(c);
+                lowercase_candidate_chars.push(c.to_ascii_lowercase());
+            }
+
+            if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
+                continue;
+            }
+
+            let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
+            self.score_matrix.clear();
+            self.score_matrix.resize(matrix_len, None);
+            self.best_position_matrix.clear();
+            self.best_position_matrix.resize(matrix_len, 0);
+
+            let score = self.score_match(
+                &candidate_chars,
+                &lowercase_candidate_chars,
+                prefix,
+                lowercase_prefix,
+            );
+
+            if score > 0.0 {
+                let mut mat = build_match(&candidate, score);
+                if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
+                    if results.len() < self.max_results {
+                        mat.set_positions(self.match_positions.clone());
+                        results.insert(i, mat);
+                    } else if i < results.len() {
+                        results.pop();
+                        mat.set_positions(self.match_positions.clone());
+                        results.insert(i, mat);
+                    }
+                    if results.len() == self.max_results {
+                        self.min_score = results.last().unwrap().score();
+                    }
+                }
+            }
+        }
+    }
+
+    fn find_last_positions(
+        &mut self,
+        lowercase_prefix: &[char],
+        lowercase_candidate: &[char],
+    ) -> bool {
+        let mut lowercase_prefix = lowercase_prefix.iter();
+        let mut lowercase_candidate = lowercase_candidate.iter();
+        for (i, char) in self.lowercase_query.iter().enumerate().rev() {
+            if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
+                self.last_positions[i] = j + lowercase_prefix.len();
+            } else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
+                self.last_positions[i] = j;
+            } else {
+                return false;
+            }
+        }
+        true
+    }
+
+    fn score_match(
+        &mut self,
+        path: &[char],
+        path_cased: &[char],
+        prefix: &[char],
+        lowercase_prefix: &[char],
+    ) -> f64 {
+        let score = self.recursive_score_match(
+            path,
+            path_cased,
+            prefix,
+            lowercase_prefix,
+            0,
+            0,
+            self.query.len() as f64,
+        ) * self.query.len() as f64;
+
+        if score <= 0.0 {
+            return 0.0;
+        }
+
+        let path_len = prefix.len() + path.len();
+        let mut cur_start = 0;
+        let mut byte_ix = 0;
+        let mut char_ix = 0;
+        for i in 0..self.query.len() {
+            let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
+            while char_ix < match_char_ix {
+                let ch = prefix
+                    .get(char_ix)
+                    .or_else(|| path.get(char_ix - prefix.len()))
+                    .unwrap();
+                byte_ix += ch.len_utf8();
+                char_ix += 1;
+            }
+            cur_start = match_char_ix + 1;
+            self.match_positions[i] = byte_ix;
+        }
+
+        score
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    fn recursive_score_match(
+        &mut self,
+        path: &[char],
+        path_cased: &[char],
+        prefix: &[char],
+        lowercase_prefix: &[char],
+        query_idx: usize,
+        path_idx: usize,
+        cur_score: f64,
+    ) -> f64 {
+        if query_idx == self.query.len() {
+            return 1.0;
+        }
+
+        let path_len = prefix.len() + path.len();
+
+        if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
+            return memoized;
+        }
+
+        let mut score = 0.0;
+        let mut best_position = 0;
+
+        let query_char = self.lowercase_query[query_idx];
+        let limit = self.last_positions[query_idx];
+
+        let mut last_slash = 0;
+        for j in path_idx..=limit {
+            let path_char = if j < prefix.len() {
+                lowercase_prefix[j]
+            } else {
+                path_cased[j - prefix.len()]
+            };
+            let is_path_sep = path_char == '/' || path_char == '\\';
+
+            if query_idx == 0 && is_path_sep {
+                last_slash = j;
+            }
+
+            if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
+                let curr = if j < prefix.len() {
+                    prefix[j]
+                } else {
+                    path[j - prefix.len()]
+                };
+
+                let mut char_score = 1.0;
+                if j > path_idx {
+                    let last = if j - 1 < prefix.len() {
+                        prefix[j - 1]
+                    } else {
+                        path[j - 1 - prefix.len()]
+                    };
+
+                    if last == '/' {
+                        char_score = 0.9;
+                    } else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
+                        || (last.is_lowercase() && curr.is_uppercase())
+                    {
+                        char_score = 0.8;
+                    } else if last == '.' {
+                        char_score = 0.7;
+                    } else if query_idx == 0 {
+                        char_score = BASE_DISTANCE_PENALTY;
+                    } else {
+                        char_score = MIN_DISTANCE_PENALTY.max(
+                            BASE_DISTANCE_PENALTY
+                                - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
+                        );
+                    }
+                }
+
+                // Apply a severe penalty if the case doesn't match.
+                // This will make the exact matches have higher score than the case-insensitive and the
+                // path insensitive matches.
+                if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
+                    char_score *= 0.001;
+                }
+
+                let mut multiplier = char_score;
+
+                // Scale the score based on how deep within the path we found the match.
+                if query_idx == 0 {
+                    multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
+                }
+
+                let mut next_score = 1.0;
+                if self.min_score > 0.0 {
+                    next_score = cur_score * multiplier;
+                    // Scores only decrease. If we can't pass the previous best, bail
+                    if next_score < self.min_score {
+                        // Ensure that score is non-zero so we use it in the memo table.
+                        if score == 0.0 {
+                            score = 1e-18;
+                        }
+                        continue;
+                    }
+                }
+
+                let new_score = self.recursive_score_match(
+                    path,
+                    path_cased,
+                    prefix,
+                    lowercase_prefix,
+                    query_idx + 1,
+                    j + 1,
+                    next_score,
+                ) * multiplier;
+
+                if new_score > score {
+                    score = new_score;
+                    best_position = j;
+                    // Optimization: can't score better than 1.
+                    if new_score == 1.0 {
+                        break;
+                    }
+                }
+            }
+        }
+
+        if best_position != 0 {
+            self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
+        }
+
+        self.score_matrix[query_idx * path_len + path_idx] = Some(score);
+        score
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{PathMatch, PathMatchCandidate};
+
+    use super::*;
+    use std::{
+        path::{Path, PathBuf},
+        sync::Arc,
+    };
+
+    #[test]
+    fn test_get_last_positions() {
+        let mut query: &[char] = &['d', 'c'];
+        let mut matcher = Matcher::new(query, query, query.into(), false, 10);
+        let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
+        assert!(!result);
+
+        query = &['c', 'd'];
+        let mut matcher = Matcher::new(query, query, query.into(), false, 10);
+        let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
+        assert!(result);
+        assert_eq!(matcher.last_positions, vec![2, 4]);
+
+        query = &['z', '/', 'z', 'f'];
+        let mut matcher = Matcher::new(query, query, query.into(), false, 10);
+        let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
+        assert!(result);
+        assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
+    }
+
+    #[test]
+    fn test_match_path_entries() {
+        let paths = vec![
+            "",
+            "a",
+            "ab",
+            "abC",
+            "abcd",
+            "alphabravocharlie",
+            "AlphaBravoCharlie",
+            "thisisatestdir",
+            "/////ThisIsATestDir",
+            "/this/is/a/test/dir",
+            "/test/tiatd",
+        ];
+
+        assert_eq!(
+            match_single_path_query("abc", false, &paths),
+            vec![
+                ("abC", vec![0, 1, 2]),
+                ("abcd", vec![0, 1, 2]),
+                ("AlphaBravoCharlie", vec![0, 5, 10]),
+                ("alphabravocharlie", vec![4, 5, 10]),
+            ]
+        );
+        assert_eq!(
+            match_single_path_query("t/i/a/t/d", false, &paths),
+            vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
+        );
+
+        assert_eq!(
+            match_single_path_query("tiatd", false, &paths),
+            vec![
+                ("/test/tiatd", vec![6, 7, 8, 9, 10]),
+                ("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
+                ("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
+                ("thisisatestdir", vec![0, 2, 6, 7, 11]),
+            ]
+        );
+    }
+
+    #[test]
+    fn test_match_multibyte_path_entries() {
+        let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"];
+        assert_eq!("1️⃣".len(), 7);
+        assert_eq!(
+            match_single_path_query("bcd", false, &paths),
+            vec![
+                ("αβγδ/bcde", vec![9, 10, 11]),
+                ("aαbβ/cγdδ", vec![3, 7, 10]),
+            ]
+        );
+        assert_eq!(
+            match_single_path_query("cde", false, &paths),
+            vec![
+                ("αβγδ/bcde", vec![10, 11, 12]),
+                ("c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", vec![0, 23, 46]),
+            ]
+        );
+    }
+
+    fn match_single_path_query<'a>(
+        query: &str,
+        smart_case: bool,
+        paths: &[&'a str],
+    ) -> Vec<(&'a str, Vec<usize>)> {
+        let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+        let query = query.chars().collect::<Vec<_>>();
+        let query_chars = CharBag::from(&lowercase_query[..]);
+
+        let path_arcs: Vec<Arc<Path>> = paths
+            .iter()
+            .map(|path| Arc::from(PathBuf::from(path)))
+            .collect::<Vec<_>>();
+        let mut path_entries = Vec::new();
+        for (i, path) in paths.iter().enumerate() {
+            let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
+            let char_bag = CharBag::from(lowercase_path.as_slice());
+            path_entries.push(PathMatchCandidate {
+                char_bag,
+                path: &path_arcs[i],
+            });
+        }
+
+        let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
+
+        let cancel_flag = AtomicBool::new(false);
+        let mut results = Vec::new();
+
+        matcher.match_candidates(
+            &[],
+            &[],
+            path_entries.into_iter(),
+            &mut results,
+            &cancel_flag,
+            |candidate, score| PathMatch {
+                score,
+                worktree_id: 0,
+                positions: Vec::new(),
+                path: Arc::from(candidate.path),
+                path_prefix: "".into(),
+                distance_to_relative_ancestor: usize::MAX,
+            },
+        );
+
+        results
+            .into_iter()
+            .map(|result| {
+                (
+                    paths
+                        .iter()
+                        .copied()
+                        .find(|p| result.path.as_ref() == Path::new(p))
+                        .unwrap(),
+                    result.positions,
+                )
+            })
+            .collect()
+    }
+}

crates/fuzzy2/src/paths.rs 🔗

@@ -0,0 +1,257 @@
+use gpui2::Executor;
+use std::{
+    borrow::Cow,
+    cmp::{self, Ordering},
+    path::Path,
+    sync::{atomic::AtomicBool, Arc},
+};
+
+use crate::{
+    matcher::{Match, MatchCandidate, Matcher},
+    CharBag,
+};
+
+#[derive(Clone, Debug)]
+pub struct PathMatchCandidate<'a> {
+    pub path: &'a Path,
+    pub char_bag: CharBag,
+}
+
+#[derive(Clone, Debug)]
+pub struct PathMatch {
+    pub score: f64,
+    pub positions: Vec<usize>,
+    pub worktree_id: usize,
+    pub path: Arc<Path>,
+    pub path_prefix: Arc<str>,
+    /// Number of steps removed from a shared parent with the relative path
+    /// Used to order closer paths first in the search list
+    pub distance_to_relative_ancestor: usize,
+}
+
+pub trait PathMatchCandidateSet<'a>: Send + Sync {
+    type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
+    fn id(&self) -> usize;
+    fn len(&self) -> usize;
+    fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+    fn prefix(&self) -> Arc<str>;
+    fn candidates(&'a self, start: usize) -> Self::Candidates;
+}
+
+impl Match for PathMatch {
+    fn score(&self) -> f64 {
+        self.score
+    }
+
+    fn set_positions(&mut self, positions: Vec<usize>) {
+        self.positions = positions;
+    }
+}
+
+impl<'a> MatchCandidate for PathMatchCandidate<'a> {
+    fn has_chars(&self, bag: CharBag) -> bool {
+        self.char_bag.is_superset(bag)
+    }
+
+    fn to_string(&self) -> Cow<'a, str> {
+        self.path.to_string_lossy()
+    }
+}
+
+impl PartialEq for PathMatch {
+    fn eq(&self, other: &Self) -> bool {
+        self.cmp(other).is_eq()
+    }
+}
+
+impl Eq for PathMatch {}
+
+impl PartialOrd for PathMatch {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for PathMatch {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.score
+            .partial_cmp(&other.score)
+            .unwrap_or(Ordering::Equal)
+            .then_with(|| self.worktree_id.cmp(&other.worktree_id))
+            .then_with(|| {
+                other
+                    .distance_to_relative_ancestor
+                    .cmp(&self.distance_to_relative_ancestor)
+            })
+            .then_with(|| self.path.cmp(&other.path))
+    }
+}
+
+pub fn match_fixed_path_set(
+    candidates: Vec<PathMatchCandidate>,
+    worktree_id: usize,
+    query: &str,
+    smart_case: bool,
+    max_results: usize,
+) -> Vec<PathMatch> {
+    let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+    let query = query.chars().collect::<Vec<_>>();
+    let query_char_bag = CharBag::from(&lowercase_query[..]);
+
+    let mut matcher = Matcher::new(
+        &query,
+        &lowercase_query,
+        query_char_bag,
+        smart_case,
+        max_results,
+    );
+
+    let mut results = Vec::new();
+    matcher.match_candidates(
+        &[],
+        &[],
+        candidates.into_iter(),
+        &mut results,
+        &AtomicBool::new(false),
+        |candidate, score| PathMatch {
+            score,
+            worktree_id,
+            positions: Vec::new(),
+            path: Arc::from(candidate.path),
+            path_prefix: Arc::from(""),
+            distance_to_relative_ancestor: usize::MAX,
+        },
+    );
+    results
+}
+
+pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
+    candidate_sets: &'a [Set],
+    query: &str,
+    relative_to: Option<Arc<Path>>,
+    smart_case: bool,
+    max_results: usize,
+    cancel_flag: &AtomicBool,
+    executor: Executor,
+) -> Vec<PathMatch> {
+    let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
+    if path_count == 0 {
+        return Vec::new();
+    }
+
+    let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+    let query = query.chars().collect::<Vec<_>>();
+
+    let lowercase_query = &lowercase_query;
+    let query = &query;
+    let query_char_bag = CharBag::from(&lowercase_query[..]);
+
+    let num_cpus = executor.num_cpus().min(path_count);
+    let segment_size = (path_count + num_cpus - 1) / num_cpus;
+    let mut segment_results = (0..num_cpus)
+        .map(|_| Vec::with_capacity(max_results))
+        .collect::<Vec<_>>();
+
+    executor
+        .scoped(|scope| {
+            for (segment_idx, results) in segment_results.iter_mut().enumerate() {
+                let relative_to = relative_to.clone();
+                scope.spawn(async move {
+                    let segment_start = segment_idx * segment_size;
+                    let segment_end = segment_start + segment_size;
+                    let mut matcher = Matcher::new(
+                        query,
+                        lowercase_query,
+                        query_char_bag,
+                        smart_case,
+                        max_results,
+                    );
+
+                    let mut tree_start = 0;
+                    for candidate_set in candidate_sets {
+                        let tree_end = tree_start + candidate_set.len();
+
+                        if tree_start < segment_end && segment_start < tree_end {
+                            let start = cmp::max(tree_start, segment_start) - tree_start;
+                            let end = cmp::min(tree_end, segment_end) - tree_start;
+                            let candidates = candidate_set.candidates(start).take(end - start);
+
+                            let worktree_id = candidate_set.id();
+                            let prefix = candidate_set.prefix().chars().collect::<Vec<_>>();
+                            let lowercase_prefix = prefix
+                                .iter()
+                                .map(|c| c.to_ascii_lowercase())
+                                .collect::<Vec<_>>();
+                            matcher.match_candidates(
+                                &prefix,
+                                &lowercase_prefix,
+                                candidates,
+                                results,
+                                cancel_flag,
+                                |candidate, score| PathMatch {
+                                    score,
+                                    worktree_id,
+                                    positions: Vec::new(),
+                                    path: Arc::from(candidate.path),
+                                    path_prefix: candidate_set.prefix(),
+                                    distance_to_relative_ancestor: relative_to.as_ref().map_or(
+                                        usize::MAX,
+                                        |relative_to| {
+                                            distance_between_paths(
+                                                candidate.path.as_ref(),
+                                                relative_to.as_ref(),
+                                            )
+                                        },
+                                    ),
+                                },
+                            );
+                        }
+                        if tree_end >= segment_end {
+                            break;
+                        }
+                        tree_start = tree_end;
+                    }
+                })
+            }
+        })
+        .await;
+
+    let mut results = Vec::new();
+    for segment_result in segment_results {
+        if results.is_empty() {
+            results = segment_result;
+        } else {
+            util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
+        }
+    }
+    results
+}
+
+/// Compute the distance from a given path to some other path
+/// If there is no shared path, returns usize::MAX
+fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
+    let mut path_components = path.components();
+    let mut relative_components = relative_to.components();
+
+    while path_components
+        .next()
+        .zip(relative_components.next())
+        .map(|(path_component, relative_component)| path_component == relative_component)
+        .unwrap_or_default()
+    {}
+    path_components.count() + relative_components.count() + 1
+}
+
+#[cfg(test)]
+mod tests {
+    use std::path::Path;
+
+    use super::distance_between_paths;
+
+    #[test]
+    fn test_distance_between_paths_empty() {
+        distance_between_paths(Path::new(""), Path::new(""));
+    }
+}

crates/fuzzy2/src/strings.rs 🔗

@@ -0,0 +1,159 @@
+use crate::{
+    matcher::{Match, MatchCandidate, Matcher},
+    CharBag,
+};
+use gpui2::Executor;
+use std::{
+    borrow::Cow,
+    cmp::{self, Ordering},
+    sync::atomic::AtomicBool,
+};
+
+#[derive(Clone, Debug)]
+pub struct StringMatchCandidate {
+    pub id: usize,
+    pub string: String,
+    pub char_bag: CharBag,
+}
+
+impl Match for StringMatch {
+    fn score(&self) -> f64 {
+        self.score
+    }
+
+    fn set_positions(&mut self, positions: Vec<usize>) {
+        self.positions = positions;
+    }
+}
+
+impl StringMatchCandidate {
+    pub fn new(id: usize, string: String) -> Self {
+        Self {
+            id,
+            char_bag: CharBag::from(string.as_str()),
+            string,
+        }
+    }
+}
+
+impl<'a> MatchCandidate for &'a StringMatchCandidate {
+    fn has_chars(&self, bag: CharBag) -> bool {
+        self.char_bag.is_superset(bag)
+    }
+
+    fn to_string(&self) -> Cow<'a, str> {
+        self.string.as_str().into()
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct StringMatch {
+    pub candidate_id: usize,
+    pub score: f64,
+    pub positions: Vec<usize>,
+    pub string: String,
+}
+
+impl PartialEq for StringMatch {
+    fn eq(&self, other: &Self) -> bool {
+        self.cmp(other).is_eq()
+    }
+}
+
+impl Eq for StringMatch {}
+
+impl PartialOrd for StringMatch {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for StringMatch {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.score
+            .partial_cmp(&other.score)
+            .unwrap_or(Ordering::Equal)
+            .then_with(|| self.candidate_id.cmp(&other.candidate_id))
+    }
+}
+
+pub async fn match_strings(
+    candidates: &[StringMatchCandidate],
+    query: &str,
+    smart_case: bool,
+    max_results: usize,
+    cancel_flag: &AtomicBool,
+    executor: Executor,
+) -> Vec<StringMatch> {
+    if candidates.is_empty() || max_results == 0 {
+        return Default::default();
+    }
+
+    if query.is_empty() {
+        return candidates
+            .iter()
+            .map(|candidate| StringMatch {
+                candidate_id: candidate.id,
+                score: 0.,
+                positions: Default::default(),
+                string: candidate.string.clone(),
+            })
+            .collect();
+    }
+
+    let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+    let query = query.chars().collect::<Vec<_>>();
+
+    let lowercase_query = &lowercase_query;
+    let query = &query;
+    let query_char_bag = CharBag::from(&lowercase_query[..]);
+
+    let num_cpus = executor.num_cpus().min(candidates.len());
+    let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
+    let mut segment_results = (0..num_cpus)
+        .map(|_| Vec::with_capacity(max_results.min(candidates.len())))
+        .collect::<Vec<_>>();
+
+    executor
+        .scoped(|scope| {
+            for (segment_idx, results) in segment_results.iter_mut().enumerate() {
+                let cancel_flag = &cancel_flag;
+                scope.spawn(async move {
+                    let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
+                    let segment_end = cmp::min(segment_start + segment_size, candidates.len());
+                    let mut matcher = Matcher::new(
+                        query,
+                        lowercase_query,
+                        query_char_bag,
+                        smart_case,
+                        max_results,
+                    );
+
+                    matcher.match_candidates(
+                        &[],
+                        &[],
+                        candidates[segment_start..segment_end].iter(),
+                        results,
+                        cancel_flag,
+                        |candidate, score| StringMatch {
+                            candidate_id: candidate.id,
+                            score,
+                            positions: Vec::new(),
+                            string: candidate.string.to_string(),
+                        },
+                    );
+                });
+            }
+        })
+        .await;
+
+    let mut results = Vec::new();
+    for segment_result in segment_results {
+        if results.is_empty() {
+            results = segment_result;
+        } else {
+            util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
+        }
+    }
+    results
+}

crates/gpui/src/app.rs 🔗

@@ -3607,7 +3607,7 @@ impl<V> BorrowWindowContext for EventContext<'_, '_, '_, V> {
     }
 }
 
-pub(crate) enum Reference<'a, T> {
+pub enum Reference<'a, T> {
     Immutable(&'a T),
     Mutable(&'a mut T),
 }

crates/gpui/src/fonts.rs 🔗

@@ -154,6 +154,11 @@ impl Refineable for TextStyleRefinement {
             self.underline = refinement.underline;
         }
     }
+
+    fn refined(mut self, refinement: Self::Refinement) -> Self {
+        self.refine(&refinement);
+        self
+    }
 }
 
 #[derive(JsonSchema)]

crates/gpui/src/image_cache.rs 🔗

@@ -84,7 +84,6 @@ impl ImageCache {
                         let format = image::guess_format(&body)?;
                         let image =
                             image::load_from_memory_with_format(&body, format)?.into_bgra8();
-
                         Ok(ImageData::new(image))
                     }
                 }

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

@@ -109,6 +109,7 @@ impl AtlasAllocator {
             };
             descriptor.set_width(size.x() as u64);
             descriptor.set_height(size.y() as u64);
+
             self.device.new_texture(&descriptor)
         } else {
             self.device.new_texture(&self.texture_descriptor)

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

@@ -632,6 +632,7 @@ impl Renderer {
             ) {
                 // Snap sprite to pixel grid.
                 let origin = (glyph.origin * scale_factor).floor() + sprite.offset.to_f32();
+
                 sprites_by_atlas
                     .entry(sprite.atlas_id)
                     .or_insert_with(Vec::new)

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

@@ -82,6 +82,7 @@ const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
 
 #[ctor]
 unsafe fn build_classes() {
+    ::util::gpui1_loaded();
     WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
     PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
     VIEW_CLASS = {

crates/gpui/src/text_layout.rs 🔗

@@ -22,8 +22,8 @@ use std::{
 };
 
 pub struct TextLayoutCache {
-    prev_frame: Mutex<HashMap<CacheKeyValue, Arc<LineLayout>>>,
-    curr_frame: RwLock<HashMap<CacheKeyValue, Arc<LineLayout>>>,
+    prev_frame: Mutex<HashMap<OwnedCacheKey, Arc<LineLayout>>>,
+    curr_frame: RwLock<HashMap<OwnedCacheKey, Arc<LineLayout>>>,
     fonts: Arc<dyn platform::FontSystem>,
 }
 
@@ -56,7 +56,7 @@ impl TextLayoutCache {
         font_size: f32,
         runs: &'a [(usize, RunStyle)],
     ) -> Line {
-        let key = &CacheKeyRef {
+        let key = &BorrowedCacheKey {
             text,
             font_size: OrderedFloat(font_size),
             runs,
@@ -72,7 +72,7 @@ impl TextLayoutCache {
             Line::new(layout, runs)
         } else {
             let layout = Arc::new(self.fonts.layout_line(text, font_size, runs));
-            let key = CacheKeyValue {
+            let key = OwnedCacheKey {
                 text: text.into(),
                 font_size: OrderedFloat(font_size),
                 runs: SmallVec::from(runs),
@@ -84,7 +84,7 @@ impl TextLayoutCache {
 }
 
 trait CacheKey {
-    fn key(&self) -> CacheKeyRef;
+    fn key(&self) -> BorrowedCacheKey;
 }
 
 impl<'a> PartialEq for (dyn CacheKey + 'a) {
@@ -102,15 +102,15 @@ impl<'a> Hash for (dyn CacheKey + 'a) {
 }
 
 #[derive(Eq)]
-struct CacheKeyValue {
+struct OwnedCacheKey {
     text: String,
     font_size: OrderedFloat<f32>,
     runs: SmallVec<[(usize, RunStyle); 1]>,
 }
 
-impl CacheKey for CacheKeyValue {
-    fn key(&self) -> CacheKeyRef {
-        CacheKeyRef {
+impl CacheKey for OwnedCacheKey {
+    fn key(&self) -> BorrowedCacheKey {
+        BorrowedCacheKey {
             text: self.text.as_str(),
             font_size: self.font_size,
             runs: self.runs.as_slice(),
@@ -118,38 +118,38 @@ impl CacheKey for CacheKeyValue {
     }
 }
 
-impl PartialEq for CacheKeyValue {
+impl PartialEq for OwnedCacheKey {
     fn eq(&self, other: &Self) -> bool {
         self.key().eq(&other.key())
     }
 }
 
-impl Hash for CacheKeyValue {
+impl Hash for OwnedCacheKey {
     fn hash<H: Hasher>(&self, state: &mut H) {
         self.key().hash(state);
     }
 }
 
-impl<'a> Borrow<dyn CacheKey + 'a> for CacheKeyValue {
+impl<'a> Borrow<dyn CacheKey + 'a> for OwnedCacheKey {
     fn borrow(&self) -> &(dyn CacheKey + 'a) {
         self as &dyn CacheKey
     }
 }
 
 #[derive(Copy, Clone)]
-struct CacheKeyRef<'a> {
+struct BorrowedCacheKey<'a> {
     text: &'a str,
     font_size: OrderedFloat<f32>,
     runs: &'a [(usize, RunStyle)],
 }
 
-impl<'a> CacheKey for CacheKeyRef<'a> {
-    fn key(&self) -> CacheKeyRef {
+impl<'a> CacheKey for BorrowedCacheKey<'a> {
+    fn key(&self) -> BorrowedCacheKey {
         *self
     }
 }
 
-impl<'a> PartialEq for CacheKeyRef<'a> {
+impl<'a> PartialEq for BorrowedCacheKey<'a> {
     fn eq(&self, other: &Self) -> bool {
         self.text == other.text
             && self.font_size == other.font_size
@@ -162,7 +162,7 @@ impl<'a> PartialEq for CacheKeyRef<'a> {
     }
 }
 
-impl<'a> Hash for CacheKeyRef<'a> {
+impl<'a> Hash for BorrowedCacheKey<'a> {
     fn hash<H: Hasher>(&self, state: &mut H) {
         self.text.hash(state);
         self.font_size.hash(state);

crates/gpui2/Cargo.toml 🔗

@@ -2,31 +2,86 @@
 name = "gpui2"
 version = "0.1.0"
 edition = "2021"
+authors = ["Nathan Sobo <nathan@zed.dev>"]
+description = "The next version of Zed's GPU-accelerated UI framework"
 publish = false
 
+[features]
+test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"]
+
 [lib]
-name = "gpui2"
 path = "src/gpui2.rs"
-
-[features]
-test-support = ["gpui/test-support"]
+doctest = false
 
 [dependencies]
-anyhow.workspace = true
+collections = { path = "../collections" }
+gpui_macros = { path = "../gpui_macros" }
+gpui2_macros = { path = "../gpui2_macros" }
+util = { path = "../util" }
+sum_tree = { path = "../sum_tree" }
+sqlez = { path = "../sqlez" }
+async-task = "4.0.3"
+backtrace = { version = "0.3", optional = true }
+ctor.workspace = true
 derive_more.workspace = true
-gpui = { path = "../gpui" }
-log.workspace = true
+dhat = { version = "0.3", optional = true }
+env_logger = { version = "0.9", optional = true }
+etagere = "0.2"
 futures.workspace = true
-gpui2_macros = { path = "../gpui2_macros" }
+image = "0.23"
+itertools = "0.10"
+lazy_static.workspace = true
+log.workspace = true
+num_cpus = "1.13"
+ordered-float.workspace = true
+parking = "2.0.0"
 parking_lot.workspace = true
+pathfinder_geometry = "0.5"
+postage.workspace = true
+rand.workspace = true
 refineable.workspace = true
-rust-embed.workspace = true
+resvg = "0.14"
+seahash = "4.1"
 serde.workspace = true
-settings = { path = "../settings" }
-simplelog = "0.9"
+serde_derive.workspace = true
+serde_json.workspace = true
 smallvec.workspace = true
-theme = { path = "../theme" }
-util = { path = "../util" }
+smol.workspace = true
+taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" }
+thiserror.workspace = true
+time.workspace = true
+tiny-skia = "0.5"
+usvg = { version = "0.14", features = [] }
+uuid = { version = "1.1.2", features = ["v4"] }
+waker-fn = "1.1.0"
+slotmap = "1.0.6"
+schemars.workspace = true
+plane-split = "0.18.0"
+bitflags = "2.4.0"
 
 [dev-dependencies]
-gpui = { path = "../gpui", features = ["test-support"] }
+backtrace = "0.3"
+collections = { path = "../collections", features = ["test-support"] }
+dhat = "0.3"
+env_logger.workspace = true
+png = "0.16"
+simplelog = "0.9"
+util = { path = "../util", features = ["test-support"] }
+
+[build-dependencies]
+bindgen = "0.65.1"
+cbindgen = "0.26.0"
+
+[target.'cfg(target_os = "macos")'.dependencies]
+media = { path = "../media" }
+anyhow.workspace = true
+block = "0.1"
+cocoa = "0.24"
+core-foundation = { version = "0.9.3", features = ["with-uuid"] }
+core-graphics = "0.22.3"
+core-text = "19.2"
+font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" }
+foreign-types = "0.3"
+log.workspace = true
+metal = "0.21.0"
+objc = "0.2"

crates/gpui2/build.rs 🔗

@@ -0,0 +1,134 @@
+use std::{
+    env,
+    path::{Path, PathBuf},
+    process::{self, Command},
+};
+
+use cbindgen::Config;
+
+fn main() {
+    generate_dispatch_bindings();
+    let header_path = generate_shader_bindings();
+    compile_metal_shaders(&header_path);
+}
+
+fn generate_dispatch_bindings() {
+    println!("cargo:rustc-link-lib=framework=System");
+    println!("cargo:rerun-if-changed=src/platform/mac/dispatch.h");
+
+    let bindings = bindgen::Builder::default()
+        .header("src/platform/mac/dispatch.h")
+        .allowlist_var("_dispatch_main_q")
+        .allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT")
+        .allowlist_function("dispatch_get_global_queue")
+        .allowlist_function("dispatch_async_f")
+        .allowlist_function("dispatch_after_f")
+        .allowlist_function("dispatch_time")
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+        .layout_tests(false)
+        .generate()
+        .expect("unable to generate bindings");
+
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(out_path.join("dispatch_sys.rs"))
+        .expect("couldn't write dispatch bindings");
+}
+
+fn generate_shader_bindings() -> PathBuf {
+    let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
+    let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+    let mut config = Config::default();
+    config.include_guard = Some("SCENE_H".into());
+    config.language = cbindgen::Language::C;
+    config.export.include.extend([
+        "Bounds".into(),
+        "Corners".into(),
+        "Edges".into(),
+        "Size".into(),
+        "Pixels".into(),
+        "PointF".into(),
+        "Hsla".into(),
+        "ContentMask".into(),
+        "Uniforms".into(),
+        "AtlasTile".into(),
+        "PathRasterizationInputIndex".into(),
+        "PathVertex_ScaledPixels".into(),
+        "ShadowInputIndex".into(),
+        "Shadow".into(),
+        "QuadInputIndex".into(),
+        "Underline".into(),
+        "UnderlineInputIndex".into(),
+        "Quad".into(),
+        "SpriteInputIndex".into(),
+        "MonochromeSprite".into(),
+        "PolychromeSprite".into(),
+        "PathSprite".into(),
+    ]);
+    config.no_includes = true;
+    config.enumeration.prefix_with_name = true;
+    cbindgen::Builder::new()
+        .with_src(crate_dir.join("src/scene.rs"))
+        .with_src(crate_dir.join("src/geometry.rs"))
+        .with_src(crate_dir.join("src/color.rs"))
+        .with_src(crate_dir.join("src/window.rs"))
+        .with_src(crate_dir.join("src/platform.rs"))
+        .with_src(crate_dir.join("src/platform/mac/metal_renderer.rs"))
+        .with_config(config)
+        .generate()
+        .expect("Unable to generate bindings")
+        .write_to_file(&output_path);
+
+    output_path
+}
+
+fn compile_metal_shaders(header_path: &Path) {
+    let shader_path = "./src/platform/mac/shaders.metal";
+    let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
+    let metallib_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
+
+    println!("cargo:rerun-if-changed={}", header_path.display());
+    println!("cargo:rerun-if-changed={}", shader_path);
+
+    let output = Command::new("xcrun")
+        .args([
+            "-sdk",
+            "macosx",
+            "metal",
+            "-gline-tables-only",
+            "-mmacosx-version-min=10.15.7",
+            "-MO",
+            "-c",
+            shader_path,
+            "-include",
+            &header_path.to_str().unwrap(),
+            "-o",
+        ])
+        .arg(&air_output_path)
+        .output()
+        .unwrap();
+
+    if !output.status.success() {
+        eprintln!(
+            "metal shader compilation failed:\n{}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+        process::exit(1);
+    }
+
+    let output = Command::new("xcrun")
+        .args(["-sdk", "macosx", "metallib"])
+        .arg(air_output_path)
+        .arg("-o")
+        .arg(metallib_output_path)
+        .output()
+        .unwrap();
+
+    if !output.status.success() {
+        eprintln!(
+            "metallib compilation failed:\n{}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+        process::exit(1);
+    }
+}

crates/gpui2/src/action.rs 🔗

@@ -0,0 +1,432 @@
+use crate::SharedString;
+use anyhow::{anyhow, Context, Result};
+use collections::{HashMap, HashSet};
+use serde::Deserialize;
+use std::any::{type_name, Any};
+
+pub trait Action: Any + Send {
+    fn qualified_name() -> SharedString
+    where
+        Self: Sized;
+    fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+    where
+        Self: Sized;
+
+    fn partial_eq(&self, action: &dyn Action) -> bool;
+    fn boxed_clone(&self) -> Box<dyn Action>;
+    fn as_any(&self) -> &dyn Any;
+}
+
+impl<A> Action for A
+where
+    A: for<'a> Deserialize<'a> + PartialEq + Any + Send + Clone + Default,
+{
+    fn qualified_name() -> SharedString {
+        type_name::<A>().into()
+    }
+
+    fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+    where
+        Self: Sized,
+    {
+        let action = if let Some(params) = params {
+            serde_json::from_value(params).context("failed to deserialize action")?
+        } else {
+            Self::default()
+        };
+        Ok(Box::new(action))
+    }
+
+    fn partial_eq(&self, action: &dyn Action) -> bool {
+        action
+            .as_any()
+            .downcast_ref::<Self>()
+            .map_or(false, |a| self == a)
+    }
+
+    fn boxed_clone(&self) -> Box<dyn Action> {
+        Box::new(self.clone())
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct DispatchContext {
+    set: HashSet<SharedString>,
+    map: HashMap<SharedString, SharedString>,
+}
+
+impl<'a> TryFrom<&'a str> for DispatchContext {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &'a str) -> Result<Self> {
+        Self::parse(value)
+    }
+}
+
+impl DispatchContext {
+    pub fn parse(source: &str) -> Result<Self> {
+        let mut context = Self::default();
+        let source = skip_whitespace(source);
+        Self::parse_expr(&source, &mut context)?;
+        Ok(context)
+    }
+
+    fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
+        if source.is_empty() {
+            return Ok(());
+        }
+
+        let key = source
+            .chars()
+            .take_while(|c| is_identifier_char(*c))
+            .collect::<String>();
+        source = skip_whitespace(&source[key.len()..]);
+        if let Some(suffix) = source.strip_prefix('=') {
+            source = skip_whitespace(suffix);
+            let value = source
+                .chars()
+                .take_while(|c| is_identifier_char(*c))
+                .collect::<String>();
+            source = skip_whitespace(&source[value.len()..]);
+            context.set(key, value);
+        } else {
+            context.insert(key);
+        }
+
+        Self::parse_expr(source, context)
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.set.is_empty() && self.map.is_empty()
+    }
+
+    pub fn clear(&mut self) {
+        self.set.clear();
+        self.map.clear();
+    }
+
+    pub fn extend(&mut self, other: &Self) {
+        for v in &other.set {
+            self.set.insert(v.clone());
+        }
+        for (k, v) in &other.map {
+            self.map.insert(k.clone(), v.clone());
+        }
+    }
+
+    pub fn insert<I: Into<SharedString>>(&mut self, identifier: I) {
+        self.set.insert(identifier.into());
+    }
+
+    pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
+        self.map.insert(key.into(), value.into());
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub enum DispatchContextPredicate {
+    Identifier(SharedString),
+    Equal(SharedString, SharedString),
+    NotEqual(SharedString, SharedString),
+    Child(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
+    Not(Box<DispatchContextPredicate>),
+    And(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
+    Or(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
+}
+
+impl DispatchContextPredicate {
+    pub fn parse(source: &str) -> Result<Self> {
+        let source = skip_whitespace(source);
+        let (predicate, rest) = Self::parse_expr(source, 0)?;
+        if let Some(next) = rest.chars().next() {
+            Err(anyhow!("unexpected character {next:?}"))
+        } else {
+            Ok(predicate)
+        }
+    }
+
+    pub fn eval(&self, contexts: &[&DispatchContext]) -> bool {
+        let Some(context) = contexts.last() else {
+            return false;
+        };
+        match self {
+            Self::Identifier(name) => context.set.contains(name),
+            Self::Equal(left, right) => context
+                .map
+                .get(left)
+                .map(|value| value == right)
+                .unwrap_or(false),
+            Self::NotEqual(left, right) => context
+                .map
+                .get(left)
+                .map(|value| value != right)
+                .unwrap_or(true),
+            Self::Not(pred) => !pred.eval(contexts),
+            Self::Child(parent, child) => {
+                parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
+            }
+            Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
+            Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
+        }
+    }
+
+    fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
+        type Op = fn(
+            DispatchContextPredicate,
+            DispatchContextPredicate,
+        ) -> Result<DispatchContextPredicate>;
+
+        let (mut predicate, rest) = Self::parse_primary(source)?;
+        source = rest;
+
+        'parse: loop {
+            for (operator, precedence, constructor) in [
+                (">", PRECEDENCE_CHILD, Self::new_child as Op),
+                ("&&", PRECEDENCE_AND, Self::new_and as Op),
+                ("||", PRECEDENCE_OR, Self::new_or as Op),
+                ("==", PRECEDENCE_EQ, Self::new_eq as Op),
+                ("!=", PRECEDENCE_EQ, Self::new_neq as Op),
+            ] {
+                if source.starts_with(operator) && precedence >= min_precedence {
+                    source = skip_whitespace(&source[operator.len()..]);
+                    let (right, rest) = Self::parse_expr(source, precedence + 1)?;
+                    predicate = constructor(predicate, right)?;
+                    source = rest;
+                    continue 'parse;
+                }
+            }
+            break;
+        }
+
+        Ok((predicate, source))
+    }
+
+    fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
+        let next = source
+            .chars()
+            .next()
+            .ok_or_else(|| anyhow!("unexpected eof"))?;
+        match next {
+            '(' => {
+                source = skip_whitespace(&source[1..]);
+                let (predicate, rest) = Self::parse_expr(source, 0)?;
+                if rest.starts_with(')') {
+                    source = skip_whitespace(&rest[1..]);
+                    Ok((predicate, source))
+                } else {
+                    Err(anyhow!("expected a ')'"))
+                }
+            }
+            '!' => {
+                let source = skip_whitespace(&source[1..]);
+                let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
+                Ok((DispatchContextPredicate::Not(Box::new(predicate)), source))
+            }
+            _ if is_identifier_char(next) => {
+                let len = source
+                    .find(|c: char| !is_identifier_char(c))
+                    .unwrap_or(source.len());
+                let (identifier, rest) = source.split_at(len);
+                source = skip_whitespace(rest);
+                Ok((
+                    DispatchContextPredicate::Identifier(identifier.to_string().into()),
+                    source,
+                ))
+            }
+            _ => Err(anyhow!("unexpected character {next:?}")),
+        }
+    }
+
+    fn new_or(self, other: Self) -> Result<Self> {
+        Ok(Self::Or(Box::new(self), Box::new(other)))
+    }
+
+    fn new_and(self, other: Self) -> Result<Self> {
+        Ok(Self::And(Box::new(self), Box::new(other)))
+    }
+
+    fn new_child(self, other: Self) -> Result<Self> {
+        Ok(Self::Child(Box::new(self), Box::new(other)))
+    }
+
+    fn new_eq(self, other: Self) -> Result<Self> {
+        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
+            Ok(Self::Equal(left, right))
+        } else {
+            Err(anyhow!("operands must be identifiers"))
+        }
+    }
+
+    fn new_neq(self, other: Self) -> Result<Self> {
+        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
+            Ok(Self::NotEqual(left, right))
+        } else {
+            Err(anyhow!("operands must be identifiers"))
+        }
+    }
+}
+
+const PRECEDENCE_CHILD: u32 = 1;
+const PRECEDENCE_OR: u32 = 2;
+const PRECEDENCE_AND: u32 = 3;
+const PRECEDENCE_EQ: u32 = 4;
+const PRECEDENCE_NOT: u32 = 5;
+
+fn is_identifier_char(c: char) -> bool {
+    c.is_alphanumeric() || c == '_' || c == '-'
+}
+
+fn skip_whitespace(source: &str) -> &str {
+    let len = source
+        .find(|c: char| !c.is_whitespace())
+        .unwrap_or(source.len());
+    &source[len..]
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use DispatchContextPredicate::*;
+
+    #[test]
+    fn test_parse_context() {
+        let mut expected = DispatchContext::default();
+        expected.set("foo", "bar");
+        expected.insert("baz");
+        assert_eq!(DispatchContext::parse("baz foo=bar").unwrap(), expected);
+        assert_eq!(DispatchContext::parse("foo = bar baz").unwrap(), expected);
+        assert_eq!(
+            DispatchContext::parse("  baz foo   =   bar baz").unwrap(),
+            expected
+        );
+        assert_eq!(DispatchContext::parse(" foo = bar baz").unwrap(), expected);
+    }
+
+    #[test]
+    fn test_parse_identifiers() {
+        // Identifiers
+        assert_eq!(
+            DispatchContextPredicate::parse("abc12").unwrap(),
+            Identifier("abc12".into())
+        );
+        assert_eq!(
+            DispatchContextPredicate::parse("_1a").unwrap(),
+            Identifier("_1a".into())
+        );
+    }
+
+    #[test]
+    fn test_parse_negations() {
+        assert_eq!(
+            DispatchContextPredicate::parse("!abc").unwrap(),
+            Not(Box::new(Identifier("abc".into())))
+        );
+        assert_eq!(
+            DispatchContextPredicate::parse(" ! ! abc").unwrap(),
+            Not(Box::new(Not(Box::new(Identifier("abc".into())))))
+        );
+    }
+
+    #[test]
+    fn test_parse_equality_operators() {
+        assert_eq!(
+            DispatchContextPredicate::parse("a == b").unwrap(),
+            Equal("a".into(), "b".into())
+        );
+        assert_eq!(
+            DispatchContextPredicate::parse("c!=d").unwrap(),
+            NotEqual("c".into(), "d".into())
+        );
+        assert_eq!(
+            DispatchContextPredicate::parse("c == !d")
+                .unwrap_err()
+                .to_string(),
+            "operands must be identifiers"
+        );
+    }
+
+    #[test]
+    fn test_parse_boolean_operators() {
+        assert_eq!(
+            DispatchContextPredicate::parse("a || b").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(Identifier("b".into()))
+            )
+        );
+        assert_eq!(
+            DispatchContextPredicate::parse("a || !b && c").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(And(
+                    Box::new(Not(Box::new(Identifier("b".into())))),
+                    Box::new(Identifier("c".into()))
+                ))
+            )
+        );
+        assert_eq!(
+            DispatchContextPredicate::parse("a && b || c&&d").unwrap(),
+            Or(
+                Box::new(And(
+                    Box::new(Identifier("a".into())),
+                    Box::new(Identifier("b".into()))
+                )),
+                Box::new(And(
+                    Box::new(Identifier("c".into())),
+                    Box::new(Identifier("d".into()))
+                ))
+            )
+        );
+        assert_eq!(
+            DispatchContextPredicate::parse("a == b && c || d == e && f").unwrap(),
+            Or(
+                Box::new(And(
+                    Box::new(Equal("a".into(), "b".into())),
+                    Box::new(Identifier("c".into()))
+                )),
+                Box::new(And(
+                    Box::new(Equal("d".into(), "e".into())),
+                    Box::new(Identifier("f".into()))
+                ))
+            )
+        );
+        assert_eq!(
+            DispatchContextPredicate::parse("a && b && c && d").unwrap(),
+            And(
+                Box::new(And(
+                    Box::new(And(
+                        Box::new(Identifier("a".into())),
+                        Box::new(Identifier("b".into()))
+                    )),
+                    Box::new(Identifier("c".into())),
+                )),
+                Box::new(Identifier("d".into()))
+            ),
+        );
+    }
+
+    #[test]
+    fn test_parse_parenthesized_expressions() {
+        assert_eq!(
+            DispatchContextPredicate::parse("a && (b == c || d != e)").unwrap(),
+            And(
+                Box::new(Identifier("a".into())),
+                Box::new(Or(
+                    Box::new(Equal("b".into(), "c".into())),
+                    Box::new(NotEqual("d".into(), "e".into())),
+                )),
+            ),
+        );
+        assert_eq!(
+            DispatchContextPredicate::parse(" ( a || b ) ").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(Identifier("b".into())),
+            )
+        );
+    }
+}

crates/gpui2/src/adapter.rs 🔗

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

crates/gpui2/src/app.rs 🔗

@@ -0,0 +1,919 @@
+mod async_context;
+mod entity_map;
+mod model_context;
+#[cfg(any(test, feature = "test-support"))]
+mod test_context;
+
+pub use async_context::*;
+pub use entity_map::*;
+pub use model_context::*;
+use refineable::Refineable;
+use smallvec::SmallVec;
+#[cfg(any(test, feature = "test-support"))]
+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, 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;
+use slotmap::SlotMap;
+use std::{
+    any::{type_name, Any, TypeId},
+    borrow::Borrow,
+    marker::PhantomData,
+    mem,
+    ops::{Deref, DerefMut},
+    path::PathBuf,
+    sync::{atomic::Ordering::SeqCst, Arc, Weak},
+    time::Duration,
+};
+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(),
+            asset_source,
+            http::client(),
+        ))
+    }
+
+    /// 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>),
+    {
+        let this = self.0.clone();
+        let platform = self.0.lock().platform.clone();
+        platform.borrow_on_main_thread().run(Box::new(move || {
+            let cx = &mut *this.lock();
+            let cx = unsafe { mem::transmute::<&mut AppContext, &mut MainThread<AppContext>>(cx) };
+            on_finish_launching(cx);
+        }));
+    }
+
+    /// 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),
+    {
+        let this = Arc::downgrade(&self.0);
+        self.0
+            .lock()
+            .platform
+            .borrow_on_main_thread()
+            .on_open_urls(Box::new(move |urls| {
+                if let Some(app) = this.upgrade() {
+                    callback(urls, &mut app.lock());
+                }
+            }));
+        self
+    }
+
+    pub fn on_reopen<F>(&self, mut callback: F) -> &Self
+    where
+        F: 'static + FnMut(&mut AppContext),
+    {
+        let this = Arc::downgrade(&self.0);
+        self.0
+            .lock()
+            .platform
+            .borrow_on_main_thread()
+            .on_reopen(Box::new(move || {
+                if let Some(app) = this.upgrade() {
+                    callback(&mut app.lock());
+                }
+            }));
+        self
+    }
+
+    pub fn metadata(&self) -> AppMetadata {
+        self.0.lock().app_metadata.clone()
+    }
+
+    pub fn executor(&self) -> Executor {
+        self.0.lock().executor.clone()
+    }
+
+    pub fn text_system(&self) -> Arc<TextSystem> {
+        self.0.lock().text_system.clone()
+    }
+}
+
+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 + '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>>,
+    pub(crate) platform: MainThreadOnly<dyn Platform>,
+    app_metadata: AppMetadata,
+    text_system: Arc<TextSystem>,
+    flushing_effects: bool,
+    pending_updates: usize,
+    pub(crate) active_drag: Option<AnyDrag>,
+    pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
+    pub(crate) executor: Executor,
+    pub(crate) svg_renderer: SvgRenderer,
+    asset_source: Arc<dyn AssetSource>,
+    pub(crate) image_cache: ImageCache,
+    pub(crate) text_style_stack: Vec<TextStyleRefinement>,
+    pub(crate) globals_by_type: HashMap<TypeId, AnyBox>,
+    pub(crate) entities: EntityMap,
+    pub(crate) windows: SlotMap<WindowId, Option<Window>>,
+    pub(crate) keymap: Arc<Mutex<Keymap>>,
+    pub(crate) global_action_listeners:
+        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>,
+    pub(crate) pending_global_notifications: HashSet<TypeId>,
+    pub(crate) observers: SubscriberSet<EntityId, Handler>,
+    pub(crate) event_listeners: SubscriberSet<EntityId, Listener>,
+    pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
+    pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
+    pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
+    pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
+    pub(crate) propagate_event: bool,
+}
+
+impl AppContext {
+    pub(crate) fn new(
+        platform: Arc<dyn Platform>,
+        asset_source: Arc<dyn AssetSource>,
+        http_client: Arc<dyn HttpClient>,
+    ) -> Arc<Mutex<Self>> {
+        let executor = platform.executor();
+        assert!(
+            executor.is_main_thread(),
+            "must construct App on main thread"
+        );
+
+        let text_system = Arc::new(TextSystem::new(platform.text_system()));
+        let entities = EntityMap::new();
+
+        let app_metadata = AppMetadata {
+            os_name: platform.os_name(),
+            os_version: platform.os_version().ok(),
+            app_version: platform.app_version().ok(),
+        };
+
+        Arc::new_cyclic(|this| {
+            Mutex::new(AppContext {
+                this: this.clone(),
+                text_system,
+                platform: MainThreadOnly::new(platform, executor.clone()),
+                app_metadata,
+                flushing_effects: false,
+                pending_updates: 0,
+                next_frame_callbacks: Default::default(),
+                executor,
+                svg_renderer: SvgRenderer::new(asset_source.clone()),
+                asset_source,
+                image_cache: ImageCache::new(http_client),
+                text_style_stack: Vec::new(),
+                globals_by_type: HashMap::default(),
+                entities,
+                windows: SlotMap::with_key(),
+                keymap: Arc::new(Mutex::new(Keymap::default())),
+                global_action_listeners: HashMap::default(),
+                action_builders: HashMap::default(),
+                pending_effects: VecDeque::new(),
+                pending_notifications: HashSet::default(),
+                pending_global_notifications: HashSet::default(),
+                observers: SubscriberSet::new(),
+                event_listeners: SubscriberSet::new(),
+                release_listeners: SubscriberSet::new(),
+                global_observers: SubscriberSet::new(),
+                quit_observers: SubscriberSet::new(),
+                layout_id_buffer: Default::default(),
+                propagate_event: true,
+                active_drag: None,
+            })
+        })
+    }
+
+    /// 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();
+
+        self.quit_observers.clone().retain(&(), |observer| {
+            futures.push(observer(self));
+            true
+        });
+
+        self.windows.clear();
+        self.flush_effects();
+
+        let futures = futures::future::join_all(futures);
+        if self
+            .executor
+            .block_with_timeout(Duration::from_millis(100), futures)
+            .is_err()
+        {
+            log::error!("timed out waiting on app_will_quit");
+        }
+
+        self.globals_by_type.clear();
+    }
+
+    pub fn app_metadata(&self) -> AppMetadata {
+        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);
+    }
+
+    pub(crate) fn update<R>(&mut self, update: impl FnOnce(&mut Self) -> R) -> R {
+        self.pending_updates += 1;
+        let result = update(self);
+        if !self.flushing_effects && self.pending_updates == 1 {
+            self.flushing_effects = true;
+            self.flush_effects();
+            self.flushing_effects = false;
+        }
+        self.pending_updates -= 1;
+        result
+    }
+
+    pub(crate) fn read_window<R>(
+        &mut self,
+        id: WindowId,
+        read: impl FnOnce(&WindowContext) -> R,
+    ) -> Result<R> {
+        let window = self
+            .windows
+            .get(id)
+            .ok_or_else(|| anyhow!("window not found"))?
+            .as_ref()
+            .unwrap();
+        Ok(read(&WindowContext::immutable(self, &window)))
+    }
+
+    pub(crate) fn update_window<R>(
+        &mut self,
+        id: WindowId,
+        update: impl FnOnce(&mut WindowContext) -> R,
+    ) -> Result<R> {
+        self.update(|cx| {
+            let mut window = cx
+                .windows
+                .get_mut(id)
+                .ok_or_else(|| anyhow!("window not found"))?
+                .take()
+                .unwrap();
+
+            let result = update(&mut WindowContext::mutable(cx, &mut window));
+
+            cx.windows
+                .get_mut(id)
+                .ok_or_else(|| anyhow!("window not found"))?
+                .replace(window);
+
+            Ok(result)
+        })
+    }
+
+    pub(crate) fn push_effect(&mut self, effect: Effect) {
+        match &effect {
+            Effect::Notify { emitter } => {
+                if !self.pending_notifications.insert(*emitter) {
+                    return;
+                }
+            }
+            Effect::NotifyGlobalObservers { global_type } => {
+                if !self.pending_global_notifications.insert(*global_type) {
+                    return;
+                }
+            }
+            _ => {}
+        };
+
+        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();
+            self.release_dropped_focus_handles();
+            if let Some(effect) = self.pending_effects.pop_front() {
+                match effect {
+                    Effect::Notify { emitter } => {
+                        self.apply_notify_effect(emitter);
+                    }
+                    Effect::Emit { emitter, event } => self.apply_emit_effect(emitter, event),
+                    Effect::FocusChanged { window_id, focused } => {
+                        self.apply_focus_changed_effect(window_id, focused);
+                    }
+                    Effect::Refresh => {
+                        self.apply_refresh_effect();
+                    }
+                    Effect::NotifyGlobalObservers { global_type } => {
+                        self.apply_notify_global_observers_effect(global_type);
+                    }
+                    Effect::Defer { callback } => {
+                        self.apply_defer_effect(callback);
+                    }
+                }
+            } else {
+                break;
+            }
+        }
+
+        let dirty_window_ids = self
+            .windows
+            .iter()
+            .filter_map(|(window_id, window)| {
+                let window = window.as_ref().unwrap();
+                if window.dirty {
+                    Some(window_id)
+                } else {
+                    None
+                }
+            })
+            .collect::<SmallVec<[_; 8]>>();
+
+        for dirty_window_id in dirty_window_ids {
+            self.update_window(dirty_window_id, |cx| cx.draw()).unwrap();
+        }
+    }
+
+    /// 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();
+            if dropped.is_empty() {
+                break;
+            }
+
+            for (entity_id, mut entity) in dropped {
+                self.observers.remove(&entity_id);
+                self.event_listeners.remove(&entity_id);
+                for mut release_callback in self.release_listeners.remove(&entity_id) {
+                    release_callback(&mut entity, self);
+                }
+            }
+        }
+    }
+
+    /// 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 {
+            self.update_window(window_id, |cx| {
+                let mut blur_window = false;
+                let focus = cx.window.focus;
+                cx.window.focus_handles.write().retain(|handle_id, count| {
+                    if count.load(SeqCst) == 0 {
+                        if focus == Some(handle_id) {
+                            blur_window = true;
+                        }
+                        false
+                    } else {
+                        true
+                    }
+                });
+
+                if blur_window {
+                    cx.blur();
+                }
+            })
+            .unwrap();
+        }
+    }
+
+    fn apply_notify_effect(&mut self, emitter: EntityId) {
+        self.pending_notifications.remove(&emitter);
+        self.observers
+            .clone()
+            .retain(&emitter, |handler| handler(self));
+    }
+
+    fn apply_emit_effect(&mut self, emitter: EntityId, event: Box<dyn Any>) {
+        self.event_listeners
+            .clone()
+            .retain(&emitter, |handler| handler(event.as_ref(), self));
+    }
+
+    fn apply_focus_changed_effect(&mut self, window_id: WindowId, focused: Option<FocusId>) {
+        self.update_window(window_id, |cx| {
+            if cx.window.focus == focused {
+                let mut listeners = mem::take(&mut cx.window.focus_listeners);
+                let focused =
+                    focused.map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap());
+                let blurred = cx
+                    .window
+                    .last_blur
+                    .take()
+                    .unwrap()
+                    .and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles));
+                if focused.is_some() || blurred.is_some() {
+                    let event = FocusEvent { focused, blurred };
+                    for listener in &listeners {
+                        listener(&event, cx);
+                    }
+                }
+
+                listeners.extend(cx.window.focus_listeners.drain(..));
+                cx.window.focus_listeners = listeners;
+            }
+        })
+        .ok();
+    }
+
+    fn apply_refresh_effect(&mut self) {
+        for window in self.windows.values_mut() {
+            if let Some(window) = window.as_mut() {
+                window.dirty = true;
+            }
+        }
+    }
+
+    fn apply_notify_global_observers_effect(&mut self, type_id: TypeId) {
+        self.pending_global_notifications.remove(&type_id);
+        self.global_observers
+            .clone()
+            .retain(&type_id, |observer| observer(self));
+    }
+
+    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()) },
+            executor: self.executor.clone(),
+        }
+    }
+
+    /// 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,
+    ) -> Task<R>
+    where
+        R: Send + 'static,
+    {
+        if self.executor.is_main_thread() {
+            Task::ready(f(unsafe {
+                mem::transmute::<&mut AppContext, &mut MainThread<AppContext>>(self)
+            }))
+        } else {
+            let this = self.this.upgrade().unwrap();
+            self.executor.run_on_main(move || {
+                let cx = &mut *this.lock();
+                cx.update(|cx| f(unsafe { mem::transmute::<&mut Self, &mut MainThread<Self>>(cx) }))
+            })
+        }
+    }
+
+    /// 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,
+    ) -> Task<R>
+    where
+        F: Future<Output = R> + 'static,
+        R: Send + 'static,
+    {
+        let cx = self.to_async();
+        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,
+        R: Send + 'static,
+    {
+        let cx = self.to_async();
+        self.executor.spawn(async move {
+            let future = f(cx);
+            future.await
+        })
+    }
+
+    /// 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 {
+            style.refine(refinement);
+        }
+        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>())
+            .map(|any_state| any_state.downcast_ref::<G>().unwrap())
+            .ok_or_else(|| anyhow!("no state of type {} exists", type_name::<G>()))
+            .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 });
+        self.globals_by_type
+            .get_mut(&global_type)
+            .and_then(|any_state| any_state.downcast_mut::<G>())
+            .ok_or_else(|| anyhow!("no state of type {} exists", type_name::<G>()))
+            .unwrap()
+    }
+
+    /// 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
+            .entry(global_type)
+            .or_insert_with(|| Box::new(G::default()))
+            .downcast_mut::<G>()
+            .unwrap()
+    }
+
+    /// 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);
+        self.end_global_lease(global);
+        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 + 'static,
+    ) -> Subscription {
+        self.global_observers.insert(
+            TypeId::of::<G>(),
+            Box::new(move |cx| {
+                f(cx);
+                true
+            }),
+        )
+    }
+
+    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
+                .remove(&TypeId::of::<G>())
+                .ok_or_else(|| anyhow!("no global registered of type {}", type_name::<G>()))
+                .unwrap(),
+        )
+    }
+
+    /// 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 });
+        self.globals_by_type.insert(global_type, lease.global);
+    }
+
+    pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
+        self.text_style_stack.push(text_style);
+    }
+
+    pub(crate) fn pop_text_style(&mut self) {
+        self.text_style_stack.pop();
+    }
+
+    /// Register key bindings.
+    pub fn bind_keys(&mut self, bindings: impl IntoIterator<Item = KeyBinding>) {
+        self.keymap.lock().add_bindings(bindings);
+        self.pending_effects.push_back(Effect::Refresh);
+    }
+
+    /// 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()
+            .push(Box::new(move |action, phase, cx| {
+                if phase == DispatchPhase::Bubble {
+                    let action = action.as_any().downcast_ref().unwrap();
+                    listener(action, cx)
+                }
+            }));
+    }
+
+    /// 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,
+        params: Option<serde_json::Value>,
+    ) -> Result<Box<dyn Action>> {
+        let build = self
+            .action_builders
+            .get(name)
+            .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
+        (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 ModelContext<'a, T> = ModelContext<'a, T>;
+    type Result<T> = T;
+
+    /// 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_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Model<T> {
+        self.update(|cx| {
+            let slot = cx.entities.reserve();
+            let entity = build_model(&mut ModelContext::mutable(cx, slot.downgrade()));
+            cx.entities.insert(slot, entity)
+        })
+    }
+
+    /// 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,
+        model: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
+    ) -> R {
+        self.update(|cx| {
+            let mut entity = cx.entities.lease(model);
+            let result = update(
+                &mut entity,
+                &mut ModelContext::mutable(cx, model.downgrade()),
+            );
+            cx.entities.end_lease(entity);
+            result
+        })
+    }
+}
+
+impl<C> MainThread<C>
+where
+    C: Borrow<AppContext>,
+{
+    pub(crate) fn platform(&self) -> &dyn Platform {
+        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);
+    }
+
+    pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
+        self.platform().path_for_auxiliary_executable(name)
+    }
+}
+
+impl MainThread<AppContext> {
+    fn update<R>(&mut self, update: impl FnOnce(&mut Self) -> R) -> R {
+        self.0.update(|cx| {
+            update(unsafe {
+                std::mem::transmute::<&mut AppContext, &mut MainThread<AppContext>>(cx)
+            })
+        })
+    }
+
+    pub(crate) fn update_window<R>(
+        &mut self,
+        id: WindowId,
+        update: impl FnOnce(&mut MainThread<WindowContext>) -> R,
+    ) -> Result<R> {
+        self.0.update_window(id, |cx| {
+            update(unsafe {
+                std::mem::transmute::<&mut WindowContext, &mut MainThread<WindowContext>>(cx)
+            })
+        })
+    }
+
+    /// 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,
+    ) -> WindowHandle<V> {
+        self.update(|cx| {
+            let id = cx.windows.insert(None);
+            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());
+            cx.windows.get_mut(id).unwrap().replace(window);
+            handle
+        })
+    }
+
+    /// 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 {
+        self.0.update_global(|global, cx| {
+            let cx = unsafe { mem::transmute::<&mut AppContext, &mut MainThread<AppContext>>(cx) };
+            update(global, cx)
+        })
+    }
+}
+
+/// 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 + 'static>,
+    },
+    FocusChanged {
+        window_id: WindowId,
+        focused: Option<FocusId>,
+    },
+    Refresh,
+    NotifyGlobalObservers {
+        global_type: TypeId,
+    },
+    Defer {
+        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>,
+}
+
+impl<G: 'static> GlobalLease<G> {
+    fn new(global: AnyBox) -> Self {
+        GlobalLease {
+            global,
+            global_type: PhantomData,
+        }
+    }
+}
+
+impl<G: 'static> Deref for GlobalLease<G> {
+    type Target = G;
+
+    fn deref(&self) -> &Self::Target {
+        self.global.downcast_ref().unwrap()
+    }
+}
+
+impl<G: 'static> DerefMut for GlobalLease<G> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.global.downcast_mut().unwrap()
+    }
+}
+
+/// 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 view: AnyView,
+    pub cursor_offset: Point<Pixels>,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::AppContext;
+
+    #[test]
+    fn test_app_context_send_sync() {
+        // This will not compile if `AppContext` does not implement `Send`
+        fn assert_send<T: Send>() {}
+        assert_send::<AppContext>();
+    }
+}

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

@@ -0,0 +1,252 @@
+use crate::{
+    AnyWindowHandle, AppContext, Context, Executor, MainThread, Model, ModelContext, Result, Task,
+    WindowContext,
+};
+use anyhow::anyhow;
+use derive_more::{Deref, DerefMut};
+use parking_lot::Mutex;
+use std::{future::Future, sync::Weak};
+
+#[derive(Clone)]
+pub struct AsyncAppContext {
+    pub(crate) app: Weak<Mutex<AppContext>>,
+    pub(crate) executor: Executor,
+}
+
+impl Context for AsyncAppContext {
+    type ModelContext<'a, T> = ModelContext<'a, T>;
+    type Result<T> = Result<T>;
+
+    fn build_model<T: 'static>(
+        &mut self,
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Self::Result<Model<T>>
+    where
+        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.build_model(build_model))
+    }
+
+    fn update_model<T: 'static, R>(
+        &mut self,
+        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_model(handle, update))
+    }
+}
+
+impl AsyncAppContext {
+    pub fn refresh(&mut self) -> Result<()> {
+        let app = self
+            .app
+            .upgrade()
+            .ok_or_else(|| anyhow!("app was released"))?;
+        let mut lock = app.lock(); // Need this to compile
+        lock.refresh();
+        Ok(())
+    }
+
+    pub fn executor(&self) -> &Executor {
+        &self.executor
+    }
+
+    pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result<R> {
+        let app = self
+            .app
+            .upgrade()
+            .ok_or_else(|| anyhow!("app was released"))?;
+        let mut lock = app.lock();
+        Ok(f(&mut *lock))
+    }
+
+    pub fn read_window<R>(
+        &self,
+        handle: AnyWindowHandle,
+        update: impl FnOnce(&WindowContext) -> R,
+    ) -> Result<R> {
+        let app = self
+            .app
+            .upgrade()
+            .ok_or_else(|| anyhow!("app was released"))?;
+        let mut app_context = app.lock();
+        app_context.read_window(handle.id, update)
+    }
+
+    pub fn update_window<R>(
+        &self,
+        handle: AnyWindowHandle,
+        update: impl FnOnce(&mut WindowContext) -> R,
+    ) -> Result<R> {
+        let app = self
+            .app
+            .upgrade()
+            .ok_or_else(|| anyhow!("app was released"))?;
+        let mut app_context = app.lock();
+        app_context.update_window(handle.id, update)
+    }
+
+    pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static) -> Task<R>
+    where
+        Fut: Future<Output = R> + Send + 'static,
+        R: Send + 'static,
+    {
+        let this = self.clone();
+        self.executor.spawn(async move { f(this).await })
+    }
+
+    pub fn spawn_on_main<Fut, R>(
+        &self,
+        f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static,
+    ) -> Task<R>
+    where
+        Fut: Future<Output = R> + 'static,
+        R: Send + 'static,
+    {
+        let this = self.clone();
+        self.executor.spawn_on_main(|| f(this))
+    }
+
+    pub fn run_on_main<R>(
+        &self,
+        f: impl FnOnce(&mut MainThread<AppContext>) -> R + Send + 'static,
+    ) -> Result<Task<R>>
+    where
+        R: Send + 'static,
+    {
+        let app = self
+            .app
+            .upgrade()
+            .ok_or_else(|| anyhow!("app was released"))?;
+        let mut app_context = app.lock();
+        Ok(app_context.run_on_main(f))
+    }
+
+    pub fn has_global<G: 'static>(&self) -> Result<bool> {
+        let app = self
+            .app
+            .upgrade()
+            .ok_or_else(|| anyhow!("app was released"))?;
+        let lock = app.lock(); // Need this to compile
+        Ok(lock.has_global::<G>())
+    }
+
+    pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result<R> {
+        let app = self
+            .app
+            .upgrade()
+            .ok_or_else(|| anyhow!("app was released"))?;
+        let lock = app.lock(); // Need this to compile
+        Ok(read(lock.global(), &lock))
+    }
+
+    pub fn try_read_global<G: 'static, R>(
+        &self,
+        read: impl FnOnce(&G, &AppContext) -> R,
+    ) -> Option<R> {
+        let app = self.app.upgrade()?;
+        let lock = app.lock(); // Need this to compile
+        Some(read(lock.try_global()?, &lock))
+    }
+
+    pub fn update_global<G: 'static, R>(
+        &mut self,
+        update: impl FnOnce(&mut G, &mut AppContext) -> R,
+    ) -> 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_global(update))
+    }
+}
+
+#[derive(Clone, Deref, DerefMut)]
+pub struct AsyncWindowContext {
+    #[deref]
+    #[deref_mut]
+    app: AsyncAppContext,
+    window: AnyWindowHandle,
+}
+
+impl AsyncWindowContext {
+    pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
+        Self { app, window }
+    }
+
+    pub fn update<R>(&self, update: impl FnOnce(&mut WindowContext) -> R) -> Result<R> {
+        self.app.update_window(self.window, update)
+    }
+
+    pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + Send + 'static) {
+        self.app
+            .update_window(self.window, |cx| cx.on_next_frame(f))
+            .ok();
+    }
+
+    pub fn read_global<G: 'static, R>(
+        &self,
+        read: impl FnOnce(&G, &WindowContext) -> R,
+    ) -> Result<R> {
+        self.app
+            .read_window(self.window, |cx| read(cx.global(), cx))
+    }
+
+    pub fn update_global<G, R>(
+        &mut self,
+        update: impl FnOnce(&mut G, &mut WindowContext) -> R,
+    ) -> Result<R>
+    where
+        G: 'static,
+    {
+        self.app
+            .update_window(self.window, |cx| cx.update_global(update))
+    }
+}
+
+impl Context for AsyncWindowContext {
+    type ModelContext<'a, T> = ModelContext<'a, T>;
+    type Result<T> = Result<T>;
+
+    fn build_model<T>(
+        &mut self,
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Result<Model<T>>
+    where
+        T: 'static + Send,
+    {
+        self.app
+            .update_window(self.window, |cx| cx.build_model(build_model))
+    }
+
+    fn update_model<T: 'static, R>(
+        &mut self,
+        handle: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
+    ) -> Result<R> {
+        self.app
+            .update_window(self.window, |cx| cx.update_model(handle, update))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_async_app_context_send_sync() {
+        fn assert_send_sync<T: Send + Sync>() {}
+        assert_send_sync::<AsyncAppContext>();
+    }
+}

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

@@ -0,0 +1,501 @@
+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, TypeId},
+    fmt::{self, Display},
+    hash::{Hash, Hasher},
+    marker::PhantomData,
+    mem,
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc, Weak,
+    },
+};
+
+slotmap::new_key_type! { pub struct EntityId; }
+
+impl EntityId {
+    pub fn as_u64(self) -> u64 {
+        self.0.as_ffi()
+    }
+}
+
+impl Display for EntityId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self)
+    }
+}
+
+pub(crate) struct EntityMap {
+    entities: SecondaryMap<EntityId, AnyBox>,
+    ref_counts: Arc<RwLock<EntityRefCounts>>,
+}
+
+struct EntityRefCounts {
+    counts: SlotMap<EntityId, AtomicUsize>,
+    dropped_entity_ids: Vec<EntityId>,
+}
+
+impl EntityMap {
+    pub fn new() -> Self {
+        Self {
+            entities: SecondaryMap::new(),
+            ref_counts: Arc::new(RwLock::new(EntityRefCounts {
+                counts: SlotMap::with_key(),
+                dropped_entity_ids: Vec::new(),
+            })),
+        }
+    }
+
+    /// 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(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) -> Model<T>
+    where
+        T: 'static + Send,
+    {
+        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, model: &'a Model<T>) -> Lease<'a, T> {
+        self.assert_valid_context(model);
+        let entity = Some(
+            self.entities
+                .remove(model.entity_id)
+                .expect("Circular entity lease. Is the entity already being updated?"),
+        );
+        Lease {
+            model,
+            entity,
+            entity_type: PhantomData,
+        }
+    }
+
+    /// Return an entity after moving it to the stack.
+    pub fn end_lease<T>(&mut self, mut lease: Lease<T>) {
+        self.entities
+            .insert(lease.model.entity_id, lease.entity.take().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, model: &AnyModel) {
+        debug_assert!(
+            Weak::ptr_eq(&model.entity_map, &Arc::downgrade(&self.ref_counts)),
+            "used a model with the wrong context"
+        );
+    }
+
+    pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> {
+        let mut ref_counts = self.ref_counts.write();
+        let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
+
+        dropped_entity_ids
+            .into_iter()
+            .map(|entity_id| {
+                ref_counts.counts.remove(entity_id);
+                (entity_id, self.entities.remove(entity_id).unwrap())
+            })
+            .collect()
+    }
+}
+
+pub struct Lease<'a, T> {
+    entity: Option<AnyBox>,
+    pub model: &'a Model<T>,
+    entity_type: PhantomData<T>,
+}
+
+impl<'a, T: 'static> core::ops::Deref for Lease<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        self.entity.as_ref().unwrap().downcast_ref().unwrap()
+    }
+}
+
+impl<'a, T: 'static> core::ops::DerefMut for Lease<'a, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.entity.as_mut().unwrap().downcast_mut().unwrap()
+    }
+}
+
+impl<'a, T> Drop for Lease<'a, T> {
+    fn drop(&mut self) {
+        if self.entity.is_some() {
+            // We don't panic here, because other panics can cause us to drop the lease without ending it cleanly.
+            log::error!("Leases must be ended with EntityMap::end_lease")
+        }
+    }
+}
+
+#[derive(Deref, DerefMut)]
+pub struct Slot<T>(Model<T>);
+
+pub struct AnyModel {
+    pub(crate) entity_id: EntityId,
+    pub(crate) entity_type: TypeId,
+    entity_map: Weak<RwLock<EntityRefCounts>>,
+}
+
+impl AnyModel {
+    fn new(id: EntityId, entity_type: TypeId, entity_map: Weak<RwLock<EntityRefCounts>>) -> Self {
+        Self {
+            entity_id: id,
+            entity_type,
+            entity_map,
+        }
+    }
+
+    pub fn entity_id(&self) -> EntityId {
+        self.entity_id
+    }
+
+    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) -> Result<Model<T>, AnyModel> {
+        if TypeId::of::<T>() == self.entity_type {
+            Ok(Model {
+                any_model: self,
+                entity_type: PhantomData,
+            })
+        } else {
+            Err(self)
+        }
+    }
+}
+
+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 model");
+            let prev_count = count.fetch_add(1, SeqCst);
+            assert_ne!(prev_count, 0, "Detected over-release of a model.");
+        }
+
+        Self {
+            entity_id: self.entity_id,
+            entity_type: self.entity_type,
+            entity_map: self.entity_map.clone(),
+        }
+    }
+}
+
+impl Drop for AnyModel {
+    fn drop(&mut self) {
+        if let Some(entity_map) = self.entity_map.upgrade() {
+            let entity_map = entity_map.upgradable_read();
+            let count = entity_map
+                .counts
+                .get(self.entity_id)
+                .expect("Detected over-release of a model.");
+            let prev_count = count.fetch_sub(1, SeqCst);
+            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);
+                entity_map.dropped_entity_ids.push(self.entity_id);
+            }
+        }
+    }
+}
+
+impl<T> From<Model<T>> for AnyModel {
+    fn from(model: Model<T>) -> Self {
+        model.any_model
+    }
+}
+
+impl Hash for AnyModel {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.entity_id.hash(state);
+    }
+}
+
+impl PartialEq for AnyModel {
+    fn eq(&self, other: &Self) -> bool {
+        self.entity_id == other.entity_id
+    }
+}
+
+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 Model<T> {
+    #[deref]
+    #[deref_mut]
+    pub(crate) any_model: AnyModel,
+    pub(crate) entity_type: PhantomData<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,
+        }
+    }
+
+    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_model: AnyModel::new(id, TypeId::of::<T>(), entity_map),
+            entity_type: PhantomData,
+        }
+    }
+
+    /// 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 model with the given function.
+    ///
+    /// The update function receives a context appropriate for its environment.
+    /// When updating in an `AppContext`, it receives a `ModelContext`.
+    /// When updating an a `WindowContext`, it receives a `ViewContext`.
+    pub fn update<C, R>(
+        &self,
+        cx: &mut C,
+        update: impl FnOnce(&mut T, &mut C::ModelContext<'_, T>) -> R,
+    ) -> C::Result<R>
+    where
+        C: Context,
+    {
+        cx.update_model(self, update)
+    }
+}
+
+impl<T> Clone for Model<T> {
+    fn clone(&self) -> Self {
+        Self {
+            any_model: self.any_model.clone(),
+            entity_type: self.entity_type,
+        }
+    }
+}
+
+impl<T> std::fmt::Debug for Model<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "Model {{ entity_id: {:?}, entity_type: {:?} }}",
+            self.any_model.entity_id,
+            type_name::<T>()
+        )
+    }
+}
+
+impl<T> Hash for Model<T> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.any_model.hash(state);
+    }
+}
+
+impl<T> PartialEq for Model<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.any_model == other.any_model
+    }
+}
+
+impl<T> Eq for Model<T> {}
+
+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 AnyWeakModel {
+    pub(crate) entity_id: EntityId,
+    entity_type: TypeId,
+    entity_ref_counts: Weak<RwLock<EntityRefCounts>>,
+}
+
+impl AnyWeakModel {
+    pub fn entity_id(&self) -> EntityId {
+        self.entity_id
+    }
+
+    pub fn is_upgradable(&self) -> bool {
+        let ref_count = self
+            .entity_ref_counts
+            .upgrade()
+            .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst)))
+            .unwrap_or(0);
+        ref_count > 0
+    }
+
+    pub fn upgrade(&self) -> Option<AnyModel> {
+        let entity_map = self.entity_ref_counts.upgrade()?;
+        entity_map
+            .read()
+            .counts
+            .get(self.entity_id)?
+            .fetch_add(1, SeqCst);
+        Some(AnyModel {
+            entity_id: self.entity_id,
+            entity_type: self.entity_type,
+            entity_map: self.entity_ref_counts.clone(),
+        })
+    }
+}
+
+impl<T> From<WeakModel<T>> for AnyWeakModel {
+    fn from(model: WeakModel<T>) -> Self {
+        model.any_model
+    }
+}
+
+impl Hash for AnyWeakModel {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.entity_id.hash(state);
+    }
+}
+
+impl PartialEq for AnyWeakModel {
+    fn eq(&self, other: &Self) -> bool {
+        self.entity_id == other.entity_id
+    }
+}
+
+impl Eq for AnyWeakModel {}
+
+#[derive(Deref, DerefMut)]
+pub struct WeakModel<T> {
+    #[deref]
+    #[deref_mut]
+    any_model: AnyWeakModel,
+    entity_type: PhantomData<T>,
+}
+
+unsafe impl<T> Send for WeakModel<T> {}
+unsafe impl<T> Sync for WeakModel<T> {}
+
+impl<T> Clone for WeakModel<T> {
+    fn clone(&self) -> Self {
+        Self {
+            any_model: self.any_model.clone(),
+            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 model with the given function if
+    /// the referenced entity still exists. Returns an error if the entity has
+    /// been released.
+    ///
+    /// The update function receives a context appropriate for its environment.
+    /// When updating in an `AppContext`, it receives a `ModelContext`.
+    /// When updating an a `WindowContext`, it receives a `ViewContext`.
+    pub fn update<C, R>(
+        &self,
+        cx: &mut C,
+        update: impl FnOnce(&mut T, &mut C::ModelContext<'_, T>) -> R,
+    ) -> Result<R>
+    where
+        C: Context,
+        Result<C::Result<R>>: crate::Flatten<R>,
+    {
+        crate::Flatten::flatten(
+            self.upgrade()
+                .ok_or_else(|| anyhow!("entity release"))
+                .map(|this| cx.update_model(&this, update)),
+        )
+    }
+}
+
+impl<T> Hash for WeakModel<T> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.any_model.hash(state);
+    }
+}
+
+impl<T> PartialEq for WeakModel<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.any_model == other.any_model
+    }
+}
+
+impl<T> Eq for WeakModel<T> {}
+
+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 🔗

@@ -0,0 +1,266 @@
+use crate::{
+    AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, EventEmitter, MainThread,
+    Model, Reference, Subscription, Task, WeakModel,
+};
+use derive_more::{Deref, DerefMut};
+use futures::FutureExt;
+use std::{
+    any::{Any, TypeId},
+    borrow::{Borrow, BorrowMut},
+    future::Future,
+};
+
+#[derive(Deref, DerefMut)]
+pub struct ModelContext<'a, T> {
+    #[deref]
+    #[deref_mut]
+    app: Reference<'a, AppContext>,
+    model_state: WeakModel<T>,
+}
+
+impl<'a, T: 'static> ModelContext<'a, T> {
+    pub(crate) fn mutable(app: &'a mut AppContext, model_state: WeakModel<T>) -> Self {
+        Self {
+            app: Reference::Mutable(app),
+            model_state,
+        }
+    }
+
+    pub fn entity_id(&self) -> EntityId {
+        self.model_state.entity_id
+    }
+
+    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_model(&self) -> WeakModel<T> {
+        self.model_state.clone()
+    }
+
+    pub fn observe<T2, E>(
+        &mut self,
+        entity: &E,
+        mut on_notify: impl FnMut(&mut T, E, &mut ModelContext<'_, T>) + Send + 'static,
+    ) -> Subscription
+    where
+        T: 'static + Send,
+        T2: 'static,
+        E: Entity<T2>,
+    {
+        let this = self.weak_model();
+        let entity_id = entity.entity_id();
+        let handle = entity.downgrade();
+        self.app.observers.insert(
+            entity_id,
+            Box::new(move |cx| {
+                if let Some((this, handle)) = this.upgrade().zip(E::upgrade_from(&handle)) {
+                    this.update(cx, |this, cx| on_notify(this, handle, cx));
+                    true
+                } else {
+                    false
+                }
+            }),
+        )
+    }
+
+    pub fn subscribe<T2, E>(
+        &mut self,
+        entity: &E,
+        mut on_event: impl FnMut(&mut T, E, &T2::Event, &mut ModelContext<'_, T>) + Send + 'static,
+    ) -> Subscription
+    where
+        T: 'static + Send,
+        T2: 'static + EventEmitter,
+        E: Entity<T2>,
+    {
+        let this = self.weak_model();
+        let entity_id = entity.entity_id();
+        let entity = entity.downgrade();
+        self.app.event_listeners.insert(
+            entity_id,
+            Box::new(move |event, cx| {
+                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 {
+                    false
+                }
+            }),
+        )
+    }
+
+    pub fn on_release(
+        &mut self,
+        mut on_release: impl FnMut(&mut T, &mut AppContext) + Send + 'static,
+    ) -> Subscription
+    where
+        T: 'static,
+    {
+        self.app.release_listeners.insert(
+            self.model_state.entity_id,
+            Box::new(move |this, cx| {
+                let this = this.downcast_mut().expect("invalid entity type");
+                on_release(this, cx);
+            }),
+        )
+    }
+
+    pub fn observe_release<T2, E>(
+        &mut self,
+        entity: &E,
+        mut on_release: impl FnMut(&mut T, &mut T2, &mut ModelContext<'_, T>) + Send + 'static,
+    ) -> Subscription
+    where
+        T: Any + Send,
+        T2: 'static,
+        E: Entity<T2>,
+    {
+        let entity_id = entity.entity_id();
+        let this = self.weak_model();
+        self.app.release_listeners.insert(
+            entity_id,
+            Box::new(move |entity, cx| {
+                let entity = entity.downcast_mut().expect("invalid entity type");
+                if let Some(this) = this.upgrade() {
+                    this.update(cx, |this, cx| on_release(this, entity, cx));
+                }
+            }),
+        )
+    }
+
+    pub fn observe_global<G: 'static>(
+        &mut self,
+        mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + Send + 'static,
+    ) -> Subscription
+    where
+        T: 'static + Send,
+    {
+        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()),
+        )
+    }
+
+    pub fn on_app_quit<Fut>(
+        &mut self,
+        mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + Send + 'static,
+    ) -> Subscription
+    where
+        Fut: 'static + Future<Output = ()> + Send,
+        T: 'static + Send,
+    {
+        let handle = self.weak_model();
+        self.app.quit_observers.insert(
+            (),
+            Box::new(move |cx| {
+                let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
+                async move {
+                    if let Some(future) = future {
+                        future.await;
+                    }
+                }
+                .boxed()
+            }),
+        )
+    }
+
+    pub fn notify(&mut self) {
+        if self
+            .app
+            .pending_notifications
+            .insert(self.model_state.entity_id)
+        {
+            self.app.pending_effects.push_back(Effect::Notify {
+                emitter: self.model_state.entity_id,
+            });
+        }
+    }
+
+    pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
+    where
+        G: 'static + Send,
+    {
+        let mut global = self.app.lease_global::<G>();
+        let result = f(&mut global, self);
+        self.app.end_global_lease(global);
+        result
+    }
+
+    pub fn spawn<Fut, R>(
+        &self,
+        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_model();
+        self.app.spawn(|cx| f(this, cx))
+    }
+
+    pub fn spawn_on_main<Fut, R>(
+        &self,
+        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_model();
+        self.app.spawn_on_main(|cx| f(this, cx))
+    }
+}
+
+impl<'a, T> ModelContext<'a, T>
+where
+    T: EventEmitter,
+    T::Event: Send,
+{
+    pub fn emit(&mut self, event: T::Event) {
+        self.app.pending_effects.push_back(Effect::Emit {
+            emitter: self.model_state.entity_id,
+            event: Box::new(event),
+        });
+    }
+}
+
+impl<'a, T> Context for ModelContext<'a, T> {
+    type ModelContext<'b, U> = ModelContext<'b, U>;
+    type Result<U> = U;
+
+    fn build_model<U>(
+        &mut self,
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, U>) -> U,
+    ) -> Model<U>
+    where
+        U: 'static + Send,
+    {
+        self.app.build_model(build_model)
+    }
+
+    fn update_model<U: 'static, R>(
+        &mut self,
+        handle: &Model<U>,
+        update: impl FnOnce(&mut U, &mut Self::ModelContext<'_, U>) -> R,
+    ) -> R {
+        self.app.update_model(handle, update)
+    }
+}
+
+impl<T> Borrow<AppContext> for ModelContext<'_, T> {
+    fn borrow(&self) -> &AppContext {
+        &self.app
+    }
+}
+
+impl<T> BorrowMut<AppContext> for ModelContext<'_, T> {
+    fn borrow_mut(&mut self) -> &mut AppContext {
+        &mut self.app
+    }
+}

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

@@ -0,0 +1,152 @@
+use crate::{
+    AnyWindowHandle, AppContext, AsyncAppContext, Context, Executor, MainThread, Model,
+    ModelContext, Result, Task, TestDispatcher, TestPlatform, WindowContext,
+};
+use parking_lot::Mutex;
+use std::{future::Future, sync::Arc};
+
+#[derive(Clone)]
+pub struct TestAppContext {
+    pub app: Arc<Mutex<AppContext>>,
+    pub executor: Executor,
+}
+
+impl Context for TestAppContext {
+    type ModelContext<'a, T> = ModelContext<'a, T>;
+    type Result<T> = T;
+
+    fn build_model<T: 'static>(
+        &mut self,
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Self::Result<Model<T>>
+    where
+        T: 'static + Send,
+    {
+        let mut lock = self.app.lock();
+        lock.build_model(build_model)
+    }
+
+    fn update_model<T: 'static, R>(
+        &mut self,
+        handle: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
+    ) -> Self::Result<R> {
+        let mut lock = self.app.lock();
+        lock.update_model(handle, update)
+    }
+}
+
+impl TestAppContext {
+    pub fn new(dispatcher: TestDispatcher) -> Self {
+        let executor = Executor::new(Arc::new(dispatcher));
+        let platform = Arc::new(TestPlatform::new(executor.clone()));
+        let asset_source = Arc::new(());
+        let http_client = util::http::FakeHttpClient::with_404_response();
+        Self {
+            app: AppContext::new(platform, asset_source, http_client),
+            executor,
+        }
+    }
+
+    pub fn quit(&self) {
+        self.app.lock().quit();
+    }
+
+    pub fn refresh(&mut self) -> Result<()> {
+        let mut lock = self.app.lock();
+        lock.refresh();
+        Ok(())
+    }
+
+    pub fn executor(&self) -> &Executor {
+        &self.executor
+    }
+
+    pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> R {
+        let mut lock = self.app.lock();
+        f(&mut *lock)
+    }
+
+    pub fn read_window<R>(
+        &self,
+        handle: AnyWindowHandle,
+        read: impl FnOnce(&WindowContext) -> R,
+    ) -> R {
+        let mut app_context = self.app.lock();
+        app_context.read_window(handle.id, read).unwrap()
+    }
+
+    pub fn update_window<R>(
+        &self,
+        handle: AnyWindowHandle,
+        update: impl FnOnce(&mut WindowContext) -> R,
+    ) -> R {
+        let mut app = self.app.lock();
+        app.update_window(handle.id, update).unwrap()
+    }
+
+    pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static) -> Task<R>
+    where
+        Fut: Future<Output = R> + Send + 'static,
+        R: Send + 'static,
+    {
+        let cx = self.to_async();
+        self.executor.spawn(async move { f(cx).await })
+    }
+
+    pub fn spawn_on_main<Fut, R>(
+        &self,
+        f: impl FnOnce(AsyncAppContext) -> Fut + Send + 'static,
+    ) -> Task<R>
+    where
+        Fut: Future<Output = R> + 'static,
+        R: Send + 'static,
+    {
+        let cx = self.to_async();
+        self.executor.spawn_on_main(|| f(cx))
+    }
+
+    pub fn run_on_main<R>(
+        &self,
+        f: impl FnOnce(&mut MainThread<AppContext>) -> R + Send + 'static,
+    ) -> Task<R>
+    where
+        R: Send + 'static,
+    {
+        let mut app_context = self.app.lock();
+        app_context.run_on_main(f)
+    }
+
+    pub fn has_global<G: 'static>(&self) -> bool {
+        let lock = self.app.lock();
+        lock.has_global::<G>()
+    }
+
+    pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R {
+        let lock = self.app.lock();
+        read(lock.global(), &lock)
+    }
+
+    pub fn try_read_global<G: 'static, R>(
+        &self,
+        read: impl FnOnce(&G, &AppContext) -> R,
+    ) -> Option<R> {
+        let lock = self.app.lock();
+        Some(read(lock.try_global()?, &lock))
+    }
+
+    pub fn update_global<G: 'static, R>(
+        &mut self,
+        update: impl FnOnce(&mut G, &mut AppContext) -> R,
+    ) -> R {
+        let mut lock = self.app.lock();
+        lock.update_global(update)
+    }
+
+    pub fn to_async(&self) -> AsyncAppContext {
+        AsyncAppContext {
+            app: Arc::downgrade(&self.app),
+            executor: self.executor.clone(),
+        }
+    }
+}

crates/gpui2/src/assets.rs 🔗

@@ -0,0 +1,64 @@
+use crate::{size, DevicePixels, Result, SharedString, Size};
+use anyhow::anyhow;
+use image::{Bgra, ImageBuffer};
+use std::{
+    borrow::Cow,
+    fmt,
+    hash::Hash,
+    sync::atomic::{AtomicUsize, Ordering::SeqCst},
+};
+
+pub trait AssetSource: 'static + Send + Sync {
+    fn load(&self, path: &str) -> Result<Cow<[u8]>>;
+    fn list(&self, path: &str) -> Result<Vec<SharedString>>;
+}
+
+impl AssetSource for () {
+    fn load(&self, path: &str) -> Result<Cow<[u8]>> {
+        Err(anyhow!(
+            "get called on empty asset provider with \"{}\"",
+            path
+        ))
+    }
+
+    fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
+        Ok(vec![])
+    }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct ImageId(usize);
+
+pub struct ImageData {
+    pub id: ImageId,
+    data: ImageBuffer<Bgra<u8>, Vec<u8>>,
+}
+
+impl ImageData {
+    pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
+        static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
+
+        Self {
+            id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
+            data,
+        }
+    }
+
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.data
+    }
+
+    pub fn size(&self) -> Size<DevicePixels> {
+        let (width, height) = self.data.dimensions();
+        size(width.into(), height.into())
+    }
+}
+
+impl fmt::Debug for ImageData {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("ImageData")
+            .field("id", &self.id)
+            .field("size", &self.data.dimensions())
+            .finish()
+    }
+}

crates/gpui2/src/color.rs 🔗

@@ -1,9 +1,8 @@
 #![allow(dead_code)]
 
 use serde::de::{self, Deserialize, Deserializer, Visitor};
-use smallvec::SmallVec;
 use std::fmt;
-use std::{num::ParseIntError, ops::Range};
+use std::num::ParseIntError;
 
 pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
     let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
@@ -12,7 +11,15 @@ pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
     Rgba { r, g, b, a: 1.0 }.into()
 }
 
-#[derive(Clone, Copy, Default, Debug)]
+pub fn rgba(hex: u32) -> Rgba {
+    let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
+    let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
+    let b = ((hex >> 8) & 0xFF) as f32 / 255.0;
+    let a = (hex & 0xFF) as f32 / 255.0;
+    Rgba { r, g, b, a }
+}
+
+#[derive(Clone, Copy, Default)]
 pub struct Rgba {
     pub r: f32,
     pub g: f32,
@@ -20,6 +27,39 @@ pub struct Rgba {
     pub a: f32,
 }
 
+impl fmt::Debug for Rgba {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "rgba({:#010x})", u32::from(*self))
+    }
+}
+
+impl Rgba {
+    pub fn blend(&self, other: Rgba) -> Self {
+        if other.a >= 1.0 {
+            return other;
+        } else if other.a <= 0.0 {
+            return *self;
+        } else {
+            return Rgba {
+                r: (self.r * (1.0 - other.a)) + (other.r * other.a),
+                g: (self.g * (1.0 - other.a)) + (other.g * other.a),
+                b: (self.b * (1.0 - other.a)) + (other.b * other.a),
+                a: self.a,
+            };
+        }
+    }
+}
+
+impl From<Rgba> for u32 {
+    fn from(rgba: Rgba) -> Self {
+        let r = (rgba.r * 255.0) as u32;
+        let g = (rgba.g * 255.0) as u32;
+        let b = (rgba.b * 255.0) as u32;
+        let a = (rgba.a * 255.0) as u32;
+        (r << 24) | (g << 16) | (b << 8) | a
+    }
+}
+
 struct RgbaVisitor;
 
 impl<'de> Visitor<'de> for RgbaVisitor {
@@ -54,33 +94,6 @@ impl<'de> Deserialize<'de> for Rgba {
     }
 }
 
-pub trait Lerp {
-    fn lerp(&self, level: f32) -> Hsla;
-}
-
-impl Lerp for Range<Hsla> {
-    fn lerp(&self, level: f32) -> Hsla {
-        let level = level.clamp(0., 1.);
-        Hsla {
-            h: self.start.h + (level * (self.end.h - self.start.h)),
-            s: self.start.s + (level * (self.end.s - self.start.s)),
-            l: self.start.l + (level * (self.end.l - self.start.l)),
-            a: self.start.a + (level * (self.end.a - self.start.a)),
-        }
-    }
-}
-
-impl From<gpui::color::Color> for Rgba {
-    fn from(value: gpui::color::Color) -> Self {
-        Self {
-            r: value.0.r as f32 / 255.0,
-            g: value.0.g as f32 / 255.0,
-            b: value.0.b as f32 / 255.0,
-            a: value.0.a as f32 / 255.0,
-        }
-    }
-}
-
 impl From<Hsla> for Rgba {
     fn from(color: Hsla) -> Self {
         let h = color.h;
@@ -128,13 +141,8 @@ impl TryFrom<&'_ str> for Rgba {
     }
 }
 
-impl Into<gpui::color::Color> for Rgba {
-    fn into(self) -> gpui::color::Color {
-        gpui::color::rgba(self.r, self.g, self.b, self.a)
-    }
-}
-
 #[derive(Default, Copy, Clone, Debug, PartialEq)]
+#[repr(C)]
 pub struct Hsla {
     pub h: f32,
     pub s: f32,
@@ -142,6 +150,14 @@ pub struct Hsla {
     pub a: f32,
 }
 
+impl Hsla {
+    pub fn to_rgb(self) -> Rgba {
+        self.into()
+    }
+}
+
+impl Eq for Hsla {}
+
 pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
     Hsla {
         h: h.clamp(0., 1.),
@@ -160,6 +176,73 @@ pub fn black() -> Hsla {
     }
 }
 
+pub fn white() -> Hsla {
+    Hsla {
+        h: 0.,
+        s: 0.,
+        l: 1.,
+        a: 1.,
+    }
+}
+
+pub fn red() -> Hsla {
+    Hsla {
+        h: 0.,
+        s: 1.,
+        l: 0.5,
+        a: 1.,
+    }
+}
+
+impl Hsla {
+    /// Returns true if the HSLA color is fully transparent, false otherwise.
+    pub fn is_transparent(&self) -> bool {
+        self.a == 0.0
+    }
+
+    /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
+    ///
+    /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
+    /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
+    /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
+    ///
+    /// Assumptions:
+    /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
+    /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s  alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing it's own alpha value.
+    /// - RGB color components are contained in the range [0, 1].
+    /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
+    pub fn blend(self, other: Hsla) -> Hsla {
+        let alpha = other.a;
+
+        if alpha >= 1.0 {
+            return other;
+        } else if alpha <= 0.0 {
+            return self;
+        } else {
+            let converted_self = Rgba::from(self);
+            let converted_other = Rgba::from(other);
+            let blended_rgb = converted_self.blend(converted_other);
+            return Hsla::from(blended_rgb);
+        }
+    }
+
+    /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
+    /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
+    pub fn fade_out(&mut self, factor: f32) {
+        self.a *= 1.0 - factor.clamp(0., 1.);
+    }
+}
+
+// impl From<Hsla> for Rgba {
+//     fn from(value: Hsla) -> Self {
+//         let h = value.h;
+//         let s = value.s;
+//         let l = value.l;
+
+//         let c = (1 - |2L - 1|) X s
+//     }
+// }
+
 impl From<Rgba> for Hsla {
     fn from(color: Rgba) -> Self {
         let r = color.r;
@@ -198,62 +281,6 @@ impl From<Rgba> for Hsla {
     }
 }
 
-impl Hsla {
-    /// Scales the saturation and lightness by the given values, clamping at 1.0.
-    pub fn scale_sl(mut self, s: f32, l: f32) -> Self {
-        self.s = (self.s * s).clamp(0., 1.);
-        self.l = (self.l * l).clamp(0., 1.);
-        self
-    }
-
-    /// Increases the saturation of the color by a certain amount, with a max
-    /// value of 1.0.
-    pub fn saturate(mut self, amount: f32) -> Self {
-        self.s += amount;
-        self.s = self.s.clamp(0.0, 1.0);
-        self
-    }
-
-    /// Decreases the saturation of the color by a certain amount, with a min
-    /// value of 0.0.
-    pub fn desaturate(mut self, amount: f32) -> Self {
-        self.s -= amount;
-        self.s = self.s.max(0.0);
-        if self.s < 0.0 {
-            self.s = 0.0;
-        }
-        self
-    }
-
-    /// Brightens the color by increasing the lightness by a certain amount,
-    /// with a max value of 1.0.
-    pub fn brighten(mut self, amount: f32) -> Self {
-        self.l += amount;
-        self.l = self.l.clamp(0.0, 1.0);
-        self
-    }
-
-    /// Darkens the color by decreasing the lightness by a certain amount,
-    /// with a max value of 0.0.
-    pub fn darken(mut self, amount: f32) -> Self {
-        self.l -= amount;
-        self.l = self.l.clamp(0.0, 1.0);
-        self
-    }
-}
-
-impl From<gpui::color::Color> for Hsla {
-    fn from(value: gpui::color::Color) -> Self {
-        Rgba::from(value).into()
-    }
-}
-
-impl Into<gpui::color::Color> for Hsla {
-    fn into(self) -> gpui::color::Color {
-        Rgba::from(self).into()
-    }
-}
-
 impl<'de> Deserialize<'de> for Hsla {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
@@ -266,59 +293,3 @@ impl<'de> Deserialize<'de> for Hsla {
         Ok(Hsla::from(rgba))
     }
 }
-
-pub struct ColorScale {
-    colors: SmallVec<[Hsla; 2]>,
-    positions: SmallVec<[f32; 2]>,
-}
-
-pub fn scale<I, C>(colors: I) -> ColorScale
-where
-    I: IntoIterator<Item = C>,
-    C: Into<Hsla>,
-{
-    let mut scale = ColorScale {
-        colors: colors.into_iter().map(Into::into).collect(),
-        positions: SmallVec::new(),
-    };
-    let num_colors: f32 = scale.colors.len() as f32 - 1.0;
-    scale.positions = (0..scale.colors.len())
-        .map(|i| i as f32 / num_colors)
-        .collect();
-    scale
-}
-
-impl ColorScale {
-    fn at(&self, t: f32) -> Hsla {
-        // Ensure that the input is within [0.0, 1.0]
-        debug_assert!(
-            0.0 <= t && t <= 1.0,
-            "t value {} is out of range. Expected value in range 0.0 to 1.0",
-            t
-        );
-
-        let position = match self
-            .positions
-            .binary_search_by(|a| a.partial_cmp(&t).unwrap())
-        {
-            Ok(index) | Err(index) => index,
-        };
-        let lower_bound = position.saturating_sub(1);
-        let upper_bound = position.min(self.colors.len() - 1);
-        let lower_color = &self.colors[lower_bound];
-        let upper_color = &self.colors[upper_bound];
-
-        match upper_bound.checked_sub(lower_bound) {
-            Some(0) | None => *lower_color,
-            Some(_) => {
-                let interval_t = (t - self.positions[lower_bound])
-                    / (self.positions[upper_bound] - self.positions[lower_bound]);
-                let h = lower_color.h + interval_t * (upper_color.h - lower_color.h);
-                let s = lower_color.s + interval_t * (upper_color.s - lower_color.s);
-                let l = lower_color.l + interval_t * (upper_color.l - lower_color.l);
-                let a = lower_color.a + interval_t * (upper_color.a - lower_color.a);
-                Hsla { h, s, l, a }
-            }
-        }
-    }
-}

crates/gpui2/src/element.rs 🔗

@@ -1,232 +1,284 @@
-pub use crate::ViewContext;
-use anyhow::Result;
-use gpui::geometry::vector::Vector2F;
-pub use gpui::{Layout, LayoutId};
-use smallvec::SmallVec;
+use crate::{BorrowWindow, Bounds, ElementId, LayoutId, Pixels, ViewContext};
+use derive_more::{Deref, DerefMut};
+pub(crate) use smallvec::SmallVec;
+use std::{any::Any, mem};
 
-pub trait Element<V: 'static>: 'static + IntoElement<V> {
-    type PaintState;
+pub trait Element<V: 'static> {
+    type ElementState: 'static + Send;
+
+    fn id(&self) -> Option<ElementId>;
+
+    /// Called to initialize this element for the current frame. If this
+    /// element had state in a previous frame, it will be passed in for the 3rd argument.
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState;
 
     fn layout(
         &mut self,
-        view: &mut V,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
-    ) -> Result<(LayoutId, Self::PaintState)>
-    where
-        Self: Sized;
+    ) -> LayoutId;
 
     fn paint(
         &mut self,
-        view: &mut V,
-        parent_origin: Vector2F,
-        layout: &Layout,
-        state: &mut Self::PaintState,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
-    ) where
-        Self: Sized;
+    );
+}
+
+#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
+pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
+
+pub trait ParentElement<V: 'static> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
 
-    fn into_any(self) -> AnyElement<V>
+    fn child(mut self, child: impl Component<V>) -> Self
     where
-        Self: 'static + Sized,
+        Self: Sized,
     {
-        AnyElement(Box::new(StatefulElement {
-            element: self,
-            phase: ElementPhase::Init,
-        }))
-    }
-
-    /// Applies a given function `then` to the current element if `condition` is true.
-    /// This function is used to conditionally modify the element based on a given condition.
-    /// If `condition` is false, it just returns the current element as it is.
-    ///
-    /// # Parameters
-    /// - `self`: The current element
-    /// - `condition`: The boolean condition based on which `then` is applied to the element.
-    /// - `then`: A function that takes in the current element and returns a possibly modified element.
-    ///
-    /// # Return
-    /// It returns the potentially modified element.
-    fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
+        self.children_mut().push(child.render());
+        self
+    }
+
+    fn children(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
     where
         Self: Sized,
     {
-        if condition {
-            self = then(self);
-        }
+        self.children_mut()
+            .extend(iter.into_iter().map(|item| item.render()));
         self
     }
 }
 
-/// Used to make ElementState<V, E> into a trait object, so we can wrap it in AnyElement<V>.
-trait AnyStatefulElement<V> {
-    fn layout(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> Result<LayoutId>;
-    fn paint(&mut self, view: &mut V, parent_origin: Vector2F, cx: &mut ViewContext<V>);
+trait ElementObject<V> {
+    fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
+    fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
+    fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
 }
 
-/// A wrapper around an element that stores its layout state.
-struct StatefulElement<V: 'static, E: Element<V>> {
+struct RenderedElement<V: 'static, E: Element<V>> {
     element: E,
-    phase: ElementPhase<V, E>,
+    phase: ElementRenderPhase<E::ElementState>,
 }
 
-enum ElementPhase<V: 'static, E: Element<V>> {
-    Init,
-    PostLayout {
-        layout_id: LayoutId,
-        paint_state: E::PaintState,
+#[derive(Default)]
+enum ElementRenderPhase<V> {
+    #[default]
+    Start,
+    Initialized {
+        frame_state: Option<V>,
     },
-    #[allow(dead_code)]
-    PostPaint {
-        layout: Layout,
-        paint_state: E::PaintState,
+    LayoutRequested {
+        layout_id: LayoutId,
+        frame_state: Option<V>,
     },
-    Error(String),
+    Painted,
 }
 
-impl<V: 'static, E: Element<V>> std::fmt::Debug for ElementPhase<V, E> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            ElementPhase::Init => write!(f, "Init"),
-            ElementPhase::PostLayout { layout_id, .. } => {
-                write!(f, "PostLayout with layout id: {:?}", layout_id)
-            }
-            ElementPhase::PostPaint { layout, .. } => {
-                write!(f, "PostPaint with layout: {:?}", layout)
-            }
-            ElementPhase::Error(err) => write!(f, "Error: {}", err),
+/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
+/// It's allocated as a trait object to erase the element type and wrapped in AnyElement<E::State> for
+/// improved usability.
+impl<V, E: Element<V>> RenderedElement<V, E> {
+    fn new(element: E) -> Self {
+        RenderedElement {
+            element,
+            phase: ElementRenderPhase::Start,
         }
     }
 }
 
-impl<V: 'static, E: Element<V>> Default for ElementPhase<V, E> {
-    fn default() -> Self {
-        Self::Init
+impl<V, E> ElementObject<V> for RenderedElement<V, E>
+where
+    E: Element<V>,
+    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() {
+            cx.with_element_state(id, |element_state, cx| {
+                let element_state = self.element.initialize(view_state, element_state, cx);
+                ((), element_state)
+            });
+            None
+        } else {
+            let frame_state = self.element.initialize(view_state, None, cx);
+            Some(frame_state)
+        };
+
+        self.phase = ElementRenderPhase::Initialized { frame_state };
     }
-}
 
-/// We blanket-implement the object-safe ElementStateObject interface to make ElementStates into trait objects
-impl<V, E: Element<V>> AnyStatefulElement<V> for StatefulElement<V, E> {
-    fn layout(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> Result<LayoutId> {
-        let result;
-        self.phase = match self.element.layout(view, cx) {
-            Ok((layout_id, paint_state)) => {
-                result = Ok(layout_id);
-                ElementPhase::PostLayout {
-                    layout_id,
-                    paint_state,
+    fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
+        let layout_id;
+        let mut frame_state;
+        match mem::take(&mut self.phase) {
+            ElementRenderPhase::Initialized {
+                frame_state: initial_frame_state,
+            } => {
+                frame_state = initial_frame_state;
+                if let Some(id) = self.element.id() {
+                    layout_id = cx.with_element_state(id, |element_state, cx| {
+                        let mut element_state = element_state.unwrap();
+                        let layout_id = self.element.layout(state, &mut element_state, cx);
+                        (layout_id, element_state)
+                    });
+                } else {
+                    layout_id = self
+                        .element
+                        .layout(state, frame_state.as_mut().unwrap(), cx);
                 }
             }
-            Err(error) => {
-                let message = error.to_string();
-                result = Err(error);
-                ElementPhase::Error(message)
-            }
+            _ => panic!("must call initialize before layout"),
+        };
+
+        self.phase = ElementRenderPhase::LayoutRequested {
+            layout_id,
+            frame_state,
         };
-        result
+        layout_id
     }
 
-    fn paint(&mut self, view: &mut V, parent_origin: Vector2F, cx: &mut ViewContext<V>) {
-        self.phase = match std::mem::take(&mut self.phase) {
-            ElementPhase::PostLayout {
+    fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
+        self.phase = match mem::take(&mut self.phase) {
+            ElementRenderPhase::LayoutRequested {
                 layout_id,
-                mut paint_state,
-            } => match cx.computed_layout(layout_id) {
-                Ok(layout) => {
-                    self.element
-                        .paint(view, parent_origin, &layout, &mut paint_state, cx);
-                    ElementPhase::PostPaint {
-                        layout,
-                        paint_state,
-                    }
-                }
-                Err(error) => ElementPhase::Error(error.to_string()),
-            },
-            ElementPhase::PostPaint {
-                layout,
-                mut paint_state,
+                mut frame_state,
             } => {
-                self.element
-                    .paint(view, parent_origin, &layout, &mut paint_state, cx);
-                ElementPhase::PostPaint {
-                    layout,
-                    paint_state,
+                let bounds = cx.layout_bounds(layout_id);
+                if let Some(id) = self.element.id() {
+                    cx.with_element_state(id, |element_state, cx| {
+                        let mut element_state = element_state.unwrap();
+                        self.element
+                            .paint(bounds, view_state, &mut element_state, cx);
+                        ((), element_state)
+                    });
+                } else {
+                    self.element
+                        .paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
                 }
+                ElementRenderPhase::Painted
             }
-            phase @ ElementPhase::Error(_) => phase,
 
-            phase @ _ => {
-                panic!("invalid element phase to call paint: {:?}", phase);
-            }
+            _ => panic!("must call layout before paint"),
         };
     }
 }
 
-/// A dynamic element.
-pub struct AnyElement<V>(Box<dyn AnyStatefulElement<V>>);
+pub struct AnyElement<V>(Box<dyn ElementObject<V> + Send>);
+
+unsafe impl<V> Send for AnyElement<V> {}
 
 impl<V> AnyElement<V> {
-    pub fn layout(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> Result<LayoutId> {
-        self.0.layout(view, cx)
+    pub fn new<E>(element: E) -> Self
+    where
+        V: 'static,
+        E: 'static + Element<V> + Send,
+        E::ElementState: Any + Send,
+    {
+        AnyElement(Box::new(RenderedElement::new(element)))
+    }
+
+    pub fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
+        self.0.initialize(view_state, cx);
     }
 
-    pub fn paint(&mut self, view: &mut V, parent_origin: Vector2F, cx: &mut ViewContext<V>) {
-        self.0.paint(view, parent_origin, cx)
+    pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
+        self.0.layout(view_state, cx)
+    }
+
+    pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
+        self.0.paint(view_state, cx)
     }
 }
 
-pub trait ParentElement<V: 'static> {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
+pub trait Component<V> {
+    fn render(self) -> AnyElement<V>;
 
-    fn child(mut self, child: impl IntoElement<V>) -> Self
+    fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
     where
         Self: Sized,
     {
-        self.children_mut().push(child.into_element().into_any());
+        if condition {
+            self = then(self);
+        }
         self
     }
+}
 
-    fn children<I, E>(mut self, children: I) -> Self
-    where
-        I: IntoIterator<Item = E>,
-        E: IntoElement<V>,
-        Self: Sized,
-    {
-        self.children_mut().extend(
-            children
-                .into_iter()
-                .map(|child| child.into_element().into_any()),
-        );
+impl<V> Component<V> for AnyElement<V> {
+    fn render(self) -> AnyElement<V> {
         self
     }
+}
 
-    // HACK: This is a temporary hack to get children working for the purposes
-    // of building UI on top of the current version of gpui2.
-    //
-    // We'll (hopefully) be moving away from this in the future.
-    fn children_any<I>(mut self, children: I) -> Self
-    where
-        I: IntoIterator<Item = AnyElement<V>>,
-        Self: Sized,
-    {
-        self.children_mut().extend(children.into_iter());
-        self
+impl<V, E, F> Element<V> for Option<F>
+where
+    V: 'static,
+    E: 'static + Component<V> + Send,
+    F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static,
+{
+    type ElementState = AnyElement<V>;
+
+    fn id(&self) -> Option<ElementId> {
+        None
     }
 
-    // HACK: This is a temporary hack to get children working for the purposes
-    // of building UI on top of the current version of gpui2.
-    //
-    // We'll (hopefully) be moving away from this in the future.
-    fn child_any(mut self, children: AnyElement<V>) -> Self
-    where
-        Self: Sized,
-    {
-        self.children_mut().push(children);
-        self
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        _rendered_element: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        let render = self.take().unwrap();
+        let mut rendered_element = (render)(view_state, cx).render();
+        rendered_element.initialize(view_state, cx);
+        rendered_element
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        rendered_element: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        rendered_element.layout(view_state, cx)
+    }
+
+    fn paint(
+        &mut self,
+        _bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        rendered_element: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        rendered_element.paint(view_state, cx)
     }
 }
 
-pub trait IntoElement<V: 'static> {
-    type Element: Element<V>;
+impl<V, E, F> Component<V> for Option<F>
+where
+    V: 'static,
+    E: 'static + Component<V> + Send,
+    F: FnOnce(&mut V, &mut ViewContext<'_, '_, V>) -> E + Send + 'static,
+{
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
 
-    fn into_element(self) -> Self::Element;
+impl<V, E, F> Component<V> for F
+where
+    V: '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/elements.rs 🔗

@@ -1,10 +1,9 @@
-pub mod div;
-pub mod hoverable;
+mod div;
 mod img;
-pub mod pressable;
-pub mod svg;
-pub mod text;
+mod svg;
+mod text;
 
-pub use div::div;
-pub use img::img;
-pub use svg::svg;
+pub use div::*;
+pub use img::*;
+pub use svg::*;
+pub use text::*;

crates/gpui2/src/elements/div.rs 🔗

@@ -1,320 +1,354 @@
-use std::{cell::Cell, rc::Rc};
-
 use crate::{
-    element::{AnyElement, Element, IntoElement, Layout, ParentElement},
-    hsla,
-    style::{CornerRadii, Overflow, Style, StyleHelpers, Styleable},
-    InteractionHandlers, Interactive, ViewContext,
-};
-use anyhow::Result;
-use gpui::{
-    geometry::{rect::RectF, vector::Vector2F, Point},
-    platform::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent},
-    scene::{self},
-    LayoutId,
+    point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementFocus, ElementId,
+    ElementInteraction, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable,
+    GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement,
+    Pixels, Point, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction,
+    StatelessInteractive, Style, StyleRefinement, Styled, ViewContext,
 };
-use refineable::{Refineable, RefinementCascade};
+use refineable::Refineable;
 use smallvec::SmallVec;
-use util::ResultExt;
 
-pub struct Div<V: 'static> {
-    styles: RefinementCascade<Style>,
-    handlers: InteractionHandlers<V>,
+pub struct Div<
+    V: 'static,
+    I: ElementInteraction<V> = StatelessInteraction<V>,
+    F: ElementFocus<V> = FocusDisabled,
+> {
+    interaction: I,
+    focus: F,
     children: SmallVec<[AnyElement<V>; 2]>,
-    scroll_state: Option<ScrollState>,
+    group: Option<SharedString>,
+    base_style: StyleRefinement,
 }
 
-pub fn div<V>() -> Div<V> {
+pub fn div<V: 'static>() -> Div<V, StatelessInteraction<V>, FocusDisabled> {
     Div {
-        styles: Default::default(),
-        handlers: Default::default(),
-        children: Default::default(),
-        scroll_state: None,
+        interaction: StatelessInteraction::default(),
+        focus: FocusDisabled,
+        children: SmallVec::new(),
+        group: None,
+        base_style: StyleRefinement::default(),
     }
 }
 
-impl<V: 'static> Element<V> for Div<V> {
-    type PaintState = Vec<LayoutId>;
-
-    fn layout(
-        &mut self,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> Result<(LayoutId, Self::PaintState)>
-    where
-        Self: Sized,
-    {
-        let style = self.computed_style();
-        let pop_text_style = style.text_style(cx).map_or(false, |style| {
-            cx.push_text_style(&style).log_err().is_some()
-        });
-
-        let children = self
-            .children
-            .iter_mut()
-            .map(|child| child.layout(view, cx))
-            .collect::<Result<Vec<LayoutId>>>()?;
-
-        if pop_text_style {
-            cx.pop_text_style();
+impl<V, F> Div<V, StatelessInteraction<V>, F>
+where
+    V: 'static,
+    F: ElementFocus<V>,
+{
+    pub fn id(self, id: impl Into<ElementId>) -> Div<V, StatefulInteraction<V>, F> {
+        Div {
+            interaction: id.into().into(),
+            focus: self.focus,
+            children: self.children,
+            group: self.group,
+            base_style: self.base_style,
         }
+    }
+}
 
-        Ok((cx.add_layout_node(style, children.clone())?, children))
+impl<V, I, F> Div<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    pub fn group(mut self, group: impl Into<SharedString>) -> Self {
+        self.group = Some(group.into());
+        self
     }
 
-    fn paint(
+    pub fn z_index(mut self, z_index: u32) -> Self {
+        self.base_style.z_index = Some(z_index);
+        self
+    }
+
+    pub fn overflow_hidden(mut self) -> Self {
+        self.base_style.overflow.x = Some(Overflow::Hidden);
+        self.base_style.overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    pub fn overflow_hidden_x(mut self) -> Self {
+        self.base_style.overflow.x = Some(Overflow::Hidden);
+        self
+    }
+
+    pub fn overflow_hidden_y(mut self) -> Self {
+        self.base_style.overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    fn with_element_id<R>(
         &mut self,
-        view: &mut V,
-        parent_origin: Vector2F,
-        layout: &Layout,
-        child_layouts: &mut Vec<LayoutId>,
         cx: &mut ViewContext<V>,
-    ) where
-        Self: Sized,
-    {
-        let order = layout.order;
-        let bounds = layout.bounds + parent_origin;
-
-        let style = self.computed_style();
-        let pop_text_style = style.text_style(cx).map_or(false, |style| {
-            cx.push_text_style(&style).log_err().is_some()
-        });
-        style.paint_background(bounds, cx);
-        self.interaction_handlers().paint(order, bounds, cx);
-
-        let scrolled_origin = bounds.origin() - self.scroll_offset(&style.overflow);
-
-        // TODO: Support only one dimension being hidden
-        let mut pop_layer = false;
-        if style.overflow.y != Overflow::Visible || style.overflow.x != Overflow::Visible {
-            cx.scene().push_layer(Some(bounds));
-            pop_layer = true;
-        }
-
-        for child in &mut self.children {
-            child.paint(view, scrolled_origin, cx);
+        f: impl FnOnce(&mut Self, Option<GlobalElementId>, &mut ViewContext<V>) -> R,
+    ) -> R {
+        if let Some(id) = self.id() {
+            cx.with_element_id(id, |global_id, cx| f(self, Some(global_id), cx))
+        } else {
+            f(self, None, cx)
         }
+    }
 
-        if pop_layer {
-            cx.scene().pop_layer();
-        }
+    pub fn compute_style(
+        &self,
+        bounds: Bounds<Pixels>,
+        element_state: &DivState,
+        cx: &mut ViewContext<V>,
+    ) -> Style {
+        let mut computed_style = Style::default();
+        computed_style.refine(&self.base_style);
+        self.focus.refine_style(&mut computed_style, cx);
+        self.interaction
+            .refine_style(&mut computed_style, bounds, &element_state.interactive, cx);
+        computed_style
+    }
+}
 
-        style.paint_foreground(bounds, cx);
-        if pop_text_style {
-            cx.pop_text_style();
+impl<V: 'static> Div<V, StatefulInteraction<V>, FocusDisabled> {
+    pub fn focusable(self) -> Div<V, StatefulInteraction<V>, FocusEnabled<V>> {
+        Div {
+            interaction: self.interaction,
+            focus: FocusEnabled::new(),
+            children: self.children,
+            group: self.group,
+            base_style: self.base_style,
         }
+    }
 
-        self.handle_scroll(order, bounds, style.overflow.clone(), child_layouts, cx);
-
-        if cx.is_inspector_enabled() {
-            self.paint_inspector(parent_origin, layout, cx);
+    pub fn track_focus(
+        self,
+        handle: &FocusHandle,
+    ) -> Div<V, StatefulInteraction<V>, FocusEnabled<V>> {
+        Div {
+            interaction: self.interaction,
+            focus: FocusEnabled::tracked(handle),
+            children: self.children,
+            group: self.group,
+            base_style: self.base_style,
         }
     }
-}
 
-impl<V: 'static> Div<V> {
-    pub fn overflow_hidden(mut self) -> Self {
-        self.declared_style().overflow.x = Some(Overflow::Hidden);
-        self.declared_style().overflow.y = Some(Overflow::Hidden);
+    pub fn overflow_scroll(mut self) -> Self {
+        self.base_style.overflow.x = Some(Overflow::Scroll);
+        self.base_style.overflow.y = Some(Overflow::Scroll);
         self
     }
 
-    pub fn overflow_hidden_x(mut self) -> Self {
-        self.declared_style().overflow.x = Some(Overflow::Hidden);
+    pub fn overflow_x_scroll(mut self) -> Self {
+        self.base_style.overflow.x = Some(Overflow::Scroll);
         self
     }
 
-    pub fn overflow_hidden_y(mut self) -> Self {
-        self.declared_style().overflow.y = Some(Overflow::Hidden);
+    pub fn overflow_y_scroll(mut self) -> Self {
+        self.base_style.overflow.y = Some(Overflow::Scroll);
         self
     }
+}
 
-    pub fn overflow_scroll(mut self, scroll_state: ScrollState) -> Self {
-        self.scroll_state = Some(scroll_state);
-        self.declared_style().overflow.x = Some(Overflow::Scroll);
-        self.declared_style().overflow.y = Some(Overflow::Scroll);
-        self
+impl<V: 'static> Div<V, StatelessInteraction<V>, FocusDisabled> {
+    pub fn track_focus(
+        self,
+        handle: &FocusHandle,
+    ) -> Div<V, StatefulInteraction<V>, FocusEnabled<V>> {
+        Div {
+            interaction: self.interaction.into_stateful(handle),
+            focus: handle.clone().into(),
+            children: self.children,
+            group: self.group,
+            base_style: self.base_style,
+        }
+    }
+}
+
+impl<V, I> Focusable<V> for Div<V, I, FocusEnabled<V>>
+where
+    V: 'static,
+    I: ElementInteraction<V>,
+{
+    fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
+        &mut self.focus.focus_listeners
     }
 
-    pub fn overflow_x_scroll(mut self, scroll_state: ScrollState) -> Self {
-        self.scroll_state = Some(scroll_state);
-        self.declared_style().overflow.x = Some(Overflow::Scroll);
-        self
+    fn set_focus_style(&mut self, style: StyleRefinement) {
+        self.focus.focus_style = style;
     }
 
-    pub fn overflow_y_scroll(mut self, scroll_state: ScrollState) -> Self {
-        self.scroll_state = Some(scroll_state);
-        self.declared_style().overflow.y = Some(Overflow::Scroll);
-        self
+    fn set_focus_in_style(&mut self, style: StyleRefinement) {
+        self.focus.focus_in_style = style;
     }
 
-    fn scroll_offset(&self, overflow: &Point<Overflow>) -> Vector2F {
-        let mut offset = Vector2F::zero();
-        if overflow.y == Overflow::Scroll {
-            offset.set_y(self.scroll_state.as_ref().unwrap().y());
-        }
-        if overflow.x == Overflow::Scroll {
-            offset.set_x(self.scroll_state.as_ref().unwrap().x());
-        }
+    fn set_in_focus_style(&mut self, style: StyleRefinement) {
+        self.focus.in_focus_style = style;
+    }
+}
 
-        offset
+#[derive(Default)]
+pub struct DivState {
+    interactive: InteractiveElementState,
+    focus_handle: Option<FocusHandle>,
+    child_layout_ids: SmallVec<[LayoutId; 4]>,
+}
+
+impl<V, I, F> Element<V> for Div<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    type ElementState = DivState;
+
+    fn id(&self) -> Option<ElementId> {
+        self.interaction
+            .as_stateful()
+            .map(|identified| identified.id.clone())
     }
 
-    fn handle_scroll(
+    fn initialize(
         &mut self,
-        order: u32,
-        bounds: RectF,
-        overflow: Point<Overflow>,
-        child_layout_ids: &[LayoutId],
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
-    ) {
-        if overflow.y == Overflow::Scroll || overflow.x == Overflow::Scroll {
-            let mut scroll_max = Vector2F::zero();
-            for child_layout_id in child_layout_ids {
-                if let Some(child_layout) = cx
-                    .layout_engine()
-                    .unwrap()
-                    .computed_layout(*child_layout_id)
-                    .log_err()
-                {
-                    scroll_max = scroll_max.max(child_layout.bounds.lower_right());
-                }
-            }
-            scroll_max -= bounds.size();
-
-            let scroll_state = self.scroll_state.as_ref().unwrap().clone();
-            cx.on_event(order, move |_, event: &ScrollWheelEvent, cx| {
-                if bounds.contains_point(event.position) {
-                    let scroll_delta = match event.delta {
-                        gpui::platform::ScrollDelta::Pixels(delta) => delta,
-                        gpui::platform::ScrollDelta::Lines(delta) => {
-                            delta * cx.text_style().font_size
-                        }
-                    };
-                    if overflow.x == Overflow::Scroll {
-                        scroll_state.set_x(
-                            (scroll_state.x() - scroll_delta.x())
-                                .max(0.)
-                                .min(scroll_max.x()),
-                        );
-                    }
-                    if overflow.y == Overflow::Scroll {
-                        scroll_state.set_y(
-                            (scroll_state.y() - scroll_delta.y())
-                                .max(0.)
-                                .min(scroll_max.y()),
-                        );
+    ) -> Self::ElementState {
+        let mut element_state = element_state.unwrap_or_default();
+        self.focus
+            .initialize(element_state.focus_handle.take(), cx, |focus_handle, cx| {
+                element_state.focus_handle = focus_handle;
+                self.interaction.initialize(cx, |cx| {
+                    for child in &mut self.children {
+                        child.initialize(view_state, cx);
                     }
-                    cx.repaint();
-                } else {
-                    cx.bubble_event();
-                }
-            })
-        }
+                })
+            });
+        element_state
     }
 
-    fn paint_inspector(&self, parent_origin: Vector2F, layout: &Layout, cx: &mut ViewContext<V>) {
-        let style = self.styles.merged();
-        let bounds = layout.bounds + parent_origin;
-
-        let hovered = bounds.contains_point(cx.mouse_position());
-        if hovered {
-            let rem_size = cx.rem_size();
-            cx.scene().push_quad(scene::Quad {
-                bounds,
-                background: Some(hsla(0., 0., 1., 0.05).into()),
-                border: gpui::Border {
-                    color: hsla(0., 0., 1., 0.2).into(),
-                    top: 1.,
-                    right: 1.,
-                    bottom: 1.,
-                    left: 1.,
-                },
-                corner_radii: CornerRadii::default()
-                    .refined(&style.corner_radii)
-                    .to_gpui(bounds.size(), rem_size),
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        let style = self.compute_style(Bounds::default(), element_state, cx);
+        style.apply_text_style(cx, |cx| {
+            self.with_element_id(cx, |this, _global_id, cx| {
+                let layout_ids = this
+                    .children
+                    .iter_mut()
+                    .map(|child| child.layout(view_state, cx))
+                    .collect::<SmallVec<_>>();
+                element_state.child_layout_ids = layout_ids.clone();
+                cx.request_layout(&style, layout_ids)
             })
-        }
+        })
+    }
 
-        let pressed = Cell::new(hovered && cx.is_mouse_down(MouseButton::Left));
-        cx.on_event(layout.order, move |_, event: &MouseButtonEvent, _| {
-            if bounds.contains_point(event.position) {
-                if event.is_down {
-                    pressed.set(true);
-                } else if pressed.get() {
-                    pressed.set(false);
-                    eprintln!("clicked div {:?} {:#?}", bounds, style);
-                }
-            }
-        });
-
-        let hovered = Cell::new(hovered);
-        cx.on_event(layout.order, move |_, event: &MouseMovedEvent, cx| {
-            cx.bubble_event();
-            let hovered_now = bounds.contains_point(event.position);
-            if hovered.get() != hovered_now {
-                hovered.set(hovered_now);
-                cx.repaint();
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.with_element_id(cx, |this, _global_id, cx| {
+            if let Some(group) = this.group.clone() {
+                GroupBounds::push(group, bounds, cx);
             }
-        });
-    }
-}
 
-impl<V> Styleable for Div<V> {
-    type Style = Style;
+            let style = this.compute_style(bounds, element_state, cx);
+            let z_index = style.z_index.unwrap_or(0);
 
-    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
-        &mut self.styles
-    }
+            let mut child_min = point(Pixels::MAX, Pixels::MAX);
+            let mut child_max = Point::default();
 
-    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
-        self.styles.base()
+            let content_size = if element_state.child_layout_ids.is_empty() {
+                bounds.size
+            } else {
+                for child_layout_id in &element_state.child_layout_ids {
+                    let child_bounds = cx.layout_bounds(*child_layout_id);
+                    child_min = child_min.min(&child_bounds.origin);
+                    child_max = child_max.max(&child_bounds.lower_right());
+                }
+                (child_max - child_min).into()
+            };
+
+            cx.stack(z_index, |cx| {
+                cx.stack(0, |cx| {
+                    style.paint(bounds, cx);
+                    this.focus.paint(bounds, cx);
+                    this.interaction.paint(
+                        bounds,
+                        content_size,
+                        style.overflow,
+                        &mut element_state.interactive,
+                        cx,
+                    );
+                });
+                cx.stack(1, |cx| {
+                    style.apply_text_style(cx, |cx| {
+                        style.apply_overflow(bounds, cx, |cx| {
+                            let scroll_offset = element_state.interactive.scroll_offset();
+                            cx.with_element_offset(scroll_offset, |cx| {
+                                for child in &mut this.children {
+                                    child.paint(view_state, cx);
+                                }
+                            });
+                        })
+                    })
+                });
+            });
+
+            if let Some(group) = this.group.as_ref() {
+                GroupBounds::pop(group, cx);
+            }
+        })
     }
 }
 
-impl<V> StyleHelpers for Div<V> {}
-
-impl<V> Interactive<V> for Div<V> {
-    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
-        &mut self.handlers
+impl<V, I, F> Component<V> for Div<V, I, F>
+where
+    // V: Any + Send + Sync,
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
     }
 }
 
-impl<V: 'static> ParentElement<V> for Div<V> {
+impl<V, I, F> ParentElement<V> for Div<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }
 }
 
-impl<V: 'static> IntoElement<V> for Div<V> {
-    type Element = Self;
-
-    fn into_element(self) -> Self::Element {
-        self
+impl<V, I, F> Styled for Div<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    fn style(&mut self) -> &mut StyleRefinement {
+        &mut self.base_style
     }
 }
 
-#[derive(Default, Clone)]
-pub struct ScrollState(Rc<Cell<Vector2F>>);
-
-impl ScrollState {
-    pub fn x(&self) -> f32 {
-        self.0.get().x()
-    }
-
-    pub fn set_x(&self, value: f32) {
-        let mut current_value = self.0.get();
-        current_value.set_x(value);
-        self.0.set(current_value);
-    }
-
-    pub fn y(&self) -> f32 {
-        self.0.get().y()
+impl<V, I, F> StatelessInteractive<V> for Div<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
+        self.interaction.as_stateless_mut()
     }
+}
 
-    pub fn set_y(&self, value: f32) {
-        let mut current_value = self.0.get();
-        current_value.set_y(value);
-        self.0.set(current_value);
+impl<V, F> StatefulInteractive<V> for Div<V, StatefulInteraction<V>, F>
+where
+    F: ElementFocus<V>,
+{
+    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<V> {
+        &mut self.interaction
     }
 }

crates/gpui2/src/elements/hoverable.rs 🔗

@@ -1,105 +0,0 @@
-use crate::{
-    element::{AnyElement, Element, IntoElement, Layout, ParentElement},
-    interactive::{InteractionHandlers, Interactive},
-    style::{Style, StyleHelpers, Styleable},
-    ViewContext,
-};
-use anyhow::Result;
-use gpui::{geometry::vector::Vector2F, platform::MouseMovedEvent, LayoutId};
-use refineable::{CascadeSlot, Refineable, RefinementCascade};
-use smallvec::SmallVec;
-use std::{cell::Cell, rc::Rc};
-
-pub struct Hoverable<E: Styleable> {
-    hovered: Rc<Cell<bool>>,
-    cascade_slot: CascadeSlot,
-    hovered_style: <E::Style as Refineable>::Refinement,
-    child: E,
-}
-
-pub fn hoverable<E: Styleable>(mut child: E) -> Hoverable<E> {
-    Hoverable {
-        hovered: Rc::new(Cell::new(false)),
-        cascade_slot: child.style_cascade().reserve(),
-        hovered_style: Default::default(),
-        child,
-    }
-}
-
-impl<E: Styleable> Styleable for Hoverable<E> {
-    type Style = E::Style;
-
-    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
-        self.child.style_cascade()
-    }
-
-    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
-        &mut self.hovered_style
-    }
-}
-
-impl<V: 'static, E: Element<V> + Styleable> Element<V> for Hoverable<E> {
-    type PaintState = E::PaintState;
-
-    fn layout(
-        &mut self,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> Result<(LayoutId, Self::PaintState)>
-    where
-        Self: Sized,
-    {
-        Ok(self.child.layout(view, cx)?)
-    }
-
-    fn paint(
-        &mut self,
-        view: &mut V,
-        parent_origin: Vector2F,
-        layout: &Layout,
-        paint_state: &mut Self::PaintState,
-        cx: &mut ViewContext<V>,
-    ) where
-        Self: Sized,
-    {
-        let bounds = layout.bounds + parent_origin;
-        self.hovered.set(bounds.contains_point(cx.mouse_position()));
-
-        let slot = self.cascade_slot;
-        let style = self.hovered.get().then_some(self.hovered_style.clone());
-        self.style_cascade().set(slot, style);
-
-        let hovered = self.hovered.clone();
-        cx.on_event(layout.order, move |_view, _: &MouseMovedEvent, cx| {
-            cx.bubble_event();
-            if bounds.contains_point(cx.mouse_position()) != hovered.get() {
-                cx.repaint();
-            }
-        });
-
-        self.child
-            .paint(view, parent_origin, layout, paint_state, cx);
-    }
-}
-
-impl<E: Styleable<Style = Style>> StyleHelpers for Hoverable<E> {}
-
-impl<V: 'static, E: Interactive<V> + Styleable> Interactive<V> for Hoverable<E> {
-    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
-        self.child.interaction_handlers()
-    }
-}
-
-impl<V: 'static, E: ParentElement<V> + Styleable> ParentElement<V> for Hoverable<E> {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
-        self.child.children_mut()
-    }
-}
-
-impl<V: 'static, E: Element<V> + Styleable> IntoElement<V> for Hoverable<E> {
-    type Element = Self;
-
-    fn into_element(self) -> Self::Element {
-        self
-    }
-}

crates/gpui2/src/elements/img.rs 🔗

@@ -1,110 +1,184 @@
-use crate as gpui2;
 use crate::{
-    style::{Style, StyleHelpers, Styleable},
-    Element,
+    div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus,
+    ElementId, ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable,
+    LayoutId, Pixels, SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction,
+    StatelessInteractive, StyleRefinement, Styled, ViewContext,
 };
 use futures::FutureExt;
-use gpui::geometry::vector::Vector2F;
-use gpui::scene;
-use gpui2_macros::IntoElement;
-use refineable::RefinementCascade;
-use util::arc_cow::ArcCow;
 use util::ResultExt;
 
-#[derive(IntoElement)]
-pub struct Img {
-    style: RefinementCascade<Style>,
-    uri: Option<ArcCow<'static, str>>,
+pub struct Img<
+    V: 'static,
+    I: ElementInteraction<V> = StatelessInteraction<V>,
+    F: ElementFocus<V> = FocusDisabled,
+> {
+    base: Div<V, I, F>,
+    uri: Option<SharedString>,
+    grayscale: bool,
 }
 
-pub fn img() -> Img {
+pub fn img<V: 'static>() -> Img<V, StatelessInteraction<V>, FocusDisabled> {
     Img {
-        style: RefinementCascade::default(),
+        base: div(),
         uri: None,
+        grayscale: false,
     }
 }
 
-impl Img {
-    pub fn uri(mut self, uri: impl Into<ArcCow<'static, str>>) -> Self {
+impl<V, I, F> Img<V, I, F>
+where
+    V: 'static,
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
         self.uri = Some(uri.into());
         self
     }
+
+    pub fn grayscale(mut self, grayscale: bool) -> Self {
+        self.grayscale = grayscale;
+        self
+    }
+}
+
+impl<V, F> Img<V, StatelessInteraction<V>, F>
+where
+    F: ElementFocus<V>,
+{
+    pub fn id(self, id: impl Into<ElementId>) -> Img<V, StatefulInteraction<V>, F> {
+        Img {
+            base: self.base.id(id),
+            uri: self.uri,
+            grayscale: self.grayscale,
+        }
+    }
 }
 
-impl<V: 'static> Element<V> for Img {
-    type PaintState = ();
+impl<V, I, F> Component<V> for Img<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
+impl<V, I, F> Element<V> for Img<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    type ElementState = DivState;
+
+    fn id(&self) -> Option<crate::ElementId> {
+        self.base.id()
+    }
+
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        self.base.initialize(view_state, element_state, cx)
+    }
 
     fn layout(
         &mut self,
-        _: &mut V,
-        cx: &mut crate::ViewContext<V>,
-    ) -> anyhow::Result<(gpui::LayoutId, Self::PaintState)>
-    where
-        Self: Sized,
-    {
-        let style = self.computed_style();
-        let layout_id = cx.add_layout_node(style, [])?;
-        Ok((layout_id, ()))
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        self.base.layout(view_state, element_state, cx)
     }
 
     fn paint(
         &mut self,
-        _: &mut V,
-        parent_origin: Vector2F,
-        layout: &gpui::Layout,
-        _: &mut Self::PaintState,
-        cx: &mut crate::ViewContext<V>,
-    ) where
-        Self: Sized,
-    {
-        let style = self.computed_style();
-        let bounds = layout.bounds + parent_origin;
-
-        style.paint_background(bounds, cx);
-
-        if let Some(uri) = &self.uri {
-            let image_future = cx.image_cache.get(uri.clone());
+        bounds: Bounds<Pixels>,
+        view: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        cx.stack(0, |cx| {
+            self.base.paint(bounds, view, element_state, cx);
+        });
+
+        let style = self.base.compute_style(bounds, element_state, cx);
+        let corner_radii = style.corner_radii;
+
+        if let Some(uri) = self.uri.clone() {
+            let image_future = cx.image_cache.get(uri);
             if let Some(data) = image_future
                 .clone()
                 .now_or_never()
                 .and_then(ResultExt::log_err)
             {
-                let rem_size = cx.rem_size();
-                cx.scene().push_image(scene::Image {
-                    bounds,
-                    border: gpui::Border {
-                        color: style.border_color.unwrap_or_default().into(),
-                        top: style.border_widths.top.to_pixels(rem_size),
-                        right: style.border_widths.right.to_pixels(rem_size),
-                        bottom: style.border_widths.bottom.to_pixels(rem_size),
-                        left: style.border_widths.left.to_pixels(rem_size),
-                    },
-                    corner_radii: style.corner_radii.to_gpui(bounds.size(), rem_size),
-                    grayscale: false,
-                    data,
-                })
+                let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
+                cx.stack(1, |cx| {
+                    cx.paint_image(bounds, corner_radii, data, self.grayscale)
+                        .log_err()
+                });
             } else {
-                cx.spawn(|this, mut cx| async move {
+                cx.spawn(|_, mut cx| async move {
                     if image_future.await.log_err().is_some() {
-                        this.update(&mut cx, |_, cx| cx.notify()).ok();
+                        cx.on_next_frame(|cx| cx.notify());
                     }
                 })
-                .detach();
+                .detach()
             }
         }
     }
 }
 
-impl Styleable for Img {
-    type Style = Style;
+impl<V, I, F> Styled for Img<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    fn style(&mut self) -> &mut StyleRefinement {
+        self.base.style()
+    }
+}
 
-    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
-        &mut self.style
+impl<V, I, F> StatelessInteractive<V> for Img<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
+        self.base.stateless_interaction()
     }
+}
 
-    fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
-        self.style.base()
+impl<V, F> StatefulInteractive<V> for Img<V, StatefulInteraction<V>, F>
+where
+    F: ElementFocus<V>,
+{
+    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<V> {
+        self.base.stateful_interaction()
     }
 }
 
-impl StyleHelpers for Img {}
+impl<V, I> Focusable<V> for Img<V, I, FocusEnabled<V>>
+where
+    V: 'static,
+    I: ElementInteraction<V>,
+{
+    fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
+        self.base.focus_listeners()
+    }
+
+    fn set_focus_style(&mut self, style: StyleRefinement) {
+        self.base.set_focus_style(style)
+    }
+
+    fn set_focus_in_style(&mut self, style: StyleRefinement) {
+        self.base.set_focus_in_style(style)
+    }
+
+    fn set_in_focus_style(&mut self, style: StyleRefinement) {
+        self.base.set_in_focus_style(style)
+    }
+}

crates/gpui2/src/elements/pressable.rs 🔗

@@ -1,113 +0,0 @@
-use crate::{
-    element::{AnyElement, Element, IntoElement, Layout, ParentElement},
-    interactive::{InteractionHandlers, Interactive},
-    style::{Style, StyleHelpers, Styleable},
-    ViewContext,
-};
-use anyhow::Result;
-use gpui::{geometry::vector::Vector2F, platform::MouseButtonEvent, LayoutId};
-use refineable::{CascadeSlot, Refineable, RefinementCascade};
-use smallvec::SmallVec;
-use std::{cell::Cell, rc::Rc};
-
-pub struct Pressable<E: Styleable> {
-    pressed: Rc<Cell<bool>>,
-    pressed_style: <E::Style as Refineable>::Refinement,
-    cascade_slot: CascadeSlot,
-    child: E,
-}
-
-pub fn pressable<E: Styleable>(mut child: E) -> Pressable<E> {
-    Pressable {
-        pressed: Rc::new(Cell::new(false)),
-        pressed_style: Default::default(),
-        cascade_slot: child.style_cascade().reserve(),
-        child,
-    }
-}
-
-impl<E: Styleable> Styleable for Pressable<E> {
-    type Style = E::Style;
-
-    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
-        &mut self.pressed_style
-    }
-
-    fn style_cascade(&mut self) -> &mut RefinementCascade<E::Style> {
-        self.child.style_cascade()
-    }
-}
-
-impl<V: 'static, E: Element<V> + Styleable> Element<V> for Pressable<E> {
-    type PaintState = E::PaintState;
-
-    fn layout(
-        &mut self,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> Result<(LayoutId, Self::PaintState)>
-    where
-        Self: Sized,
-    {
-        self.child.layout(view, cx)
-    }
-
-    fn paint(
-        &mut self,
-        view: &mut V,
-        parent_origin: Vector2F,
-        layout: &Layout,
-        paint_state: &mut Self::PaintState,
-        cx: &mut ViewContext<V>,
-    ) where
-        Self: Sized,
-    {
-        let slot = self.cascade_slot;
-        let style = self.pressed.get().then_some(self.pressed_style.clone());
-        self.style_cascade().set(slot, style);
-
-        let pressed = self.pressed.clone();
-        let bounds = layout.bounds + parent_origin;
-        cx.on_event(layout.order, move |_view, event: &MouseButtonEvent, cx| {
-            if event.is_down {
-                if bounds.contains_point(event.position) {
-                    pressed.set(true);
-                    cx.repaint();
-                } else {
-                    cx.bubble_event();
-                }
-            } else {
-                if pressed.get() {
-                    pressed.set(false);
-                    cx.repaint();
-                }
-                cx.bubble_event();
-            }
-        });
-
-        self.child
-            .paint(view, parent_origin, layout, paint_state, cx);
-    }
-}
-
-impl<E: Styleable<Style = Style>> StyleHelpers for Pressable<E> {}
-
-impl<V: 'static, E: Interactive<V> + Styleable> Interactive<V> for Pressable<E> {
-    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
-        self.child.interaction_handlers()
-    }
-}
-
-impl<V: 'static, E: ParentElement<V> + Styleable> ParentElement<V> for Pressable<E> {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
-        self.child.children_mut()
-    }
-}
-
-impl<V: 'static, E: Element<V> + Styleable> IntoElement<V> for Pressable<E> {
-    type Element = Self;
-
-    fn into_element(self) -> Self::Element {
-        self
-    }
-}

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

@@ -1,84 +1,157 @@
 use crate::{
-    self as gpui2, scene,
-    style::{Style, StyleHelpers, Styleable},
-    Element, IntoElement, Layout, LayoutId, Rgba,
+    div, AnyElement, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId,
+    ElementInteraction, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels,
+    SharedString, StatefulInteraction, StatefulInteractive, StatelessInteraction,
+    StatelessInteractive, StyleRefinement, Styled, ViewContext,
 };
-use gpui::geometry::vector::Vector2F;
-use refineable::RefinementCascade;
-use std::borrow::Cow;
 use util::ResultExt;
 
-#[derive(IntoElement)]
-pub struct Svg {
-    path: Option<Cow<'static, str>>,
-    style: RefinementCascade<Style>,
+pub struct Svg<
+    V: 'static,
+    I: ElementInteraction<V> = StatelessInteraction<V>,
+    F: ElementFocus<V> = FocusDisabled,
+> {
+    base: Div<V, I, F>,
+    path: Option<SharedString>,
 }
 
-pub fn svg() -> Svg {
+pub fn svg<V: 'static>() -> Svg<V, StatelessInteraction<V>, FocusDisabled> {
     Svg {
+        base: div(),
         path: None,
-        style: RefinementCascade::<Style>::default(),
     }
 }
 
-impl Svg {
-    pub fn path(mut self, path: impl Into<Cow<'static, str>>) -> Self {
+impl<V, I, F> Svg<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    pub fn path(mut self, path: impl Into<SharedString>) -> Self {
         self.path = Some(path.into());
         self
     }
 }
 
-impl<V: 'static> Element<V> for Svg {
-    type PaintState = ();
+impl<V, F> Svg<V, StatelessInteraction<V>, F>
+where
+    F: ElementFocus<V>,
+{
+    pub fn id(self, id: impl Into<ElementId>) -> Svg<V, StatefulInteraction<V>, F> {
+        Svg {
+            base: self.base.id(id),
+            path: self.path,
+        }
+    }
+}
+
+impl<V, I, F> Component<V> for Svg<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
+impl<V, I, F> Element<V> for Svg<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    type ElementState = DivState;
+
+    fn id(&self) -> Option<crate::ElementId> {
+        self.base.id()
+    }
+
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        self.base.initialize(view_state, element_state, cx)
+    }
 
     fn layout(
         &mut self,
-        _: &mut V,
-        cx: &mut crate::ViewContext<V>,
-    ) -> anyhow::Result<(LayoutId, Self::PaintState)>
-    where
-        Self: Sized,
-    {
-        let style = self.computed_style();
-        Ok((cx.add_layout_node(style, [])?, ()))
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        self.base.layout(view_state, element_state, cx)
     }
 
     fn paint(
         &mut self,
-        _: &mut V,
-        parent_origin: Vector2F,
-        layout: &Layout,
-        _: &mut Self::PaintState,
-        cx: &mut crate::ViewContext<V>,
+        bounds: Bounds<Pixels>,
+        view: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
     ) where
         Self: Sized,
     {
-        let fill_color = self.computed_style().fill.and_then(|fill| fill.color());
-        if let Some((path, fill_color)) = self.path.as_ref().zip(fill_color) {
-            if let Some(svg_tree) = cx.asset_cache.svg(path).log_err() {
-                let icon = scene::Icon {
-                    bounds: layout.bounds + parent_origin,
-                    svg: svg_tree,
-                    path: path.clone(),
-                    color: Rgba::from(fill_color).into(),
-                };
-
-                cx.scene().push_icon(icon);
-            }
+        self.base.paint(bounds, view, element_state, cx);
+        let color = self
+            .base
+            .compute_style(bounds, element_state, cx)
+            .text
+            .color;
+        if let Some((path, color)) = self.path.as_ref().zip(color) {
+            cx.paint_svg(bounds, path.clone(), color).log_err();
         }
     }
 }
 
-impl Styleable for Svg {
-    type Style = Style;
+impl<V, I, F> Styled for Svg<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    fn style(&mut self) -> &mut StyleRefinement {
+        self.base.style()
+    }
+}
 
-    fn style_cascade(&mut self) -> &mut refineable::RefinementCascade<Self::Style> {
-        &mut self.style
+impl<V, I, F> StatelessInteractive<V> for Svg<V, I, F>
+where
+    I: ElementInteraction<V>,
+    F: ElementFocus<V>,
+{
+    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V> {
+        self.base.stateless_interaction()
     }
+}
 
-    fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
-        self.style.base()
+impl<V, F> StatefulInteractive<V> for Svg<V, StatefulInteraction<V>, F>
+where
+    V: 'static,
+    F: ElementFocus<V>,
+{
+    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<V> {
+        self.base.stateful_interaction()
     }
 }
 
-impl StyleHelpers for Svg {}
+impl<V: 'static, I> Focusable<V> for Svg<V, I, FocusEnabled<V>>
+where
+    I: ElementInteraction<V>,
+{
+    fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
+        self.base.focus_listeners()
+    }
+
+    fn set_focus_style(&mut self, style: StyleRefinement) {
+        self.base.set_focus_style(style)
+    }
+
+    fn set_focus_in_style(&mut self, style: StyleRefinement) {
+        self.base.set_focus_in_style(style)
+    }
+
+    fn set_in_focus_style(&mut self, style: StyleRefinement) {
+        self.base.set_in_focus_style(style)
+    }
+}

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

@@ -1,105 +1,145 @@
 use crate::{
-    element::{Element, IntoElement, Layout},
-    ViewContext,
-};
-use anyhow::Result;
-use gpui::{
-    geometry::{vector::Vector2F, Size},
-    text_layout::Line,
-    LayoutId,
+    AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
+    Size, ViewContext,
 };
 use parking_lot::Mutex;
-use std::sync::Arc;
-use util::arc_cow::ArcCow;
+use smallvec::SmallVec;
+use std::{marker::PhantomData, sync::Arc};
+use util::ResultExt;
+
+impl<V: 'static> Component<V> for SharedString {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self,
+            state_type: PhantomData,
+        }
+        .render()
+    }
+}
 
-impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
-    type Element = Text;
+impl<V: 'static> Component<V> for &'static str {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            state_type: PhantomData,
+        }
+        .render()
+    }
+}
 
-    fn into_element(self) -> Self::Element {
-        Text { text: self.into() }
+// TODO: Figure out how to pass `String` to `child` without this.
+// This impl doesn't exist in the `gpui2` crate.
+impl<V: 'static> Component<V> for String {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            state_type: PhantomData,
+        }
+        .render()
     }
 }
 
-pub struct Text {
-    text: ArcCow<'static, str>,
+pub struct Text<V> {
+    text: SharedString,
+    state_type: PhantomData<V>,
+}
+
+unsafe impl<V> Send for Text<V> {}
+unsafe impl<V> Sync for Text<V> {}
+
+impl<V: 'static> Component<V> for Text<V> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
 }
 
-impl<V: 'static> Element<V> for Text {
-    type PaintState = Arc<Mutex<Option<TextLayout>>>;
+impl<V: 'static> Element<V> for Text<V> {
+    type ElementState = Arc<Mutex<Option<TextElementState>>>;
+
+    fn id(&self) -> Option<crate::ElementId> {
+        None
+    }
+
+    fn initialize(
+        &mut self,
+        _view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        _cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        element_state.unwrap_or_default()
+    }
 
     fn layout(
         &mut self,
         _view: &mut V,
+        element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
-    ) -> Result<(LayoutId, Self::PaintState)> {
-        let layout_cache = cx.text_layout_cache().clone();
+    ) -> LayoutId {
+        let text_system = cx.text_system().clone();
         let text_style = cx.text_style();
-        let line_height = cx.font_cache().line_height(text_style.font_size);
+        let font_size = text_style.font_size * cx.rem_size();
+        let line_height = text_style
+            .line_height
+            .to_pixels(font_size.into(), cx.rem_size());
         let text = self.text.clone();
-        let paint_state = Arc::new(Mutex::new(None));
 
-        let layout_id = cx.add_measured_layout_node(Default::default(), {
-            let paint_state = paint_state.clone();
-            move |_params| {
-                let line_layout = layout_cache.layout_str(
-                    text.as_ref(),
-                    text_style.font_size,
-                    &[(text.len(), text_style.to_run())],
-                );
+        let rem_size = cx.rem_size();
+        let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
+            let element_state = element_state.clone();
+            move |known_dimensions, _| {
+                let Some(lines) = text_system
+                    .layout_text(
+                        &text,
+                        font_size,
+                        &[text_style.to_run(text.len())],
+                        known_dimensions.width, // Wrap if we know the width.
+                    )
+                    .log_err()
+                else {
+                    return Size::default();
+                };
 
+                let line_count = lines
+                    .iter()
+                    .map(|line| line.wrap_count() + 1)
+                    .sum::<usize>();
                 let size = Size {
-                    width: line_layout.width(),
-                    height: line_height,
+                    width: lines.iter().map(|line| line.layout.width).max().unwrap(),
+                    height: line_height * line_count,
                 };
 
-                paint_state.lock().replace(TextLayout {
-                    line_layout: Arc::new(line_layout),
-                    line_height,
-                });
+                element_state
+                    .lock()
+                    .replace(TextElementState { lines, line_height });
 
                 size
             }
         });
 
-        Ok((layout_id?, paint_state))
+        layout_id
     }
 
-    fn paint<'a>(
+    fn paint(
         &mut self,
-        _view: &mut V,
-        parent_origin: Vector2F,
-        layout: &Layout,
-        paint_state: &mut Self::PaintState,
+        bounds: Bounds<Pixels>,
+        _: &mut V,
+        element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) {
-        let bounds = layout.bounds + parent_origin;
-
-        let line_layout;
-        let line_height;
-        {
-            let paint_state = paint_state.lock();
-            let paint_state = paint_state
-                .as_ref()
-                .expect("measurement has not been performed");
-            line_layout = paint_state.line_layout.clone();
-            line_height = paint_state.line_height;
+        let element_state = element_state.lock();
+        let element_state = element_state
+            .as_ref()
+            .expect("measurement has not been performed");
+        let line_height = element_state.line_height;
+        let mut line_origin = bounds.origin;
+        for line in &element_state.lines {
+            line.paint(line_origin, line_height, cx).log_err();
+            line_origin.y += line.size(line_height).height;
         }
-
-        // TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder.
-        let visible_bounds = bounds;
-        line_layout.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
-    }
-}
-
-impl<V: 'static> IntoElement<V> for Text {
-    type Element = Self;
-
-    fn into_element(self) -> Self::Element {
-        self
     }
 }
 
-pub struct TextLayout {
-    line_layout: Arc<Line>,
-    line_height: f32,
+pub struct TextElementState {
+    lines: SmallVec<[Line; 1]>,
+    line_height: Pixels,
 }

crates/gpui2/src/executor.rs 🔗

@@ -0,0 +1,290 @@
+use crate::{AppContext, PlatformDispatcher};
+use futures::{channel::mpsc, pin_mut, FutureExt};
+use smol::prelude::*;
+use std::{
+    fmt::Debug,
+    marker::PhantomData,
+    mem,
+    pin::Pin,
+    sync::Arc,
+    task::{Context, Poll},
+    time::Duration,
+};
+use util::TryFutureExt;
+use waker_fn::waker_fn;
+
+#[derive(Clone)]
+pub struct Executor {
+    dispatcher: Arc<dyn PlatformDispatcher>,
+}
+
+#[must_use]
+pub enum Task<T> {
+    Ready(Option<T>),
+    Spawned(async_task::Task<T>),
+}
+
+impl<T> Task<T> {
+    pub fn ready(val: T) -> Self {
+        Task::Ready(Some(val))
+    }
+
+    pub fn detach(self) {
+        match self {
+            Task::Ready(_) => {}
+            Task::Spawned(task) => task.detach(),
+        }
+    }
+}
+
+impl<E, T> Task<Result<T, E>>
+where
+    T: 'static + Send,
+    E: 'static + Send + Debug,
+{
+    pub fn detach_and_log_err(self, cx: &mut AppContext) {
+        cx.executor().spawn(self.log_err()).detach();
+    }
+}
+
+impl<T> Future for Task<T> {
+    type Output = T;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
+        match unsafe { self.get_unchecked_mut() } {
+            Task::Ready(val) => Poll::Ready(val.take().unwrap()),
+            Task::Spawned(task) => task.poll(cx),
+        }
+    }
+}
+
+impl Executor {
+    pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
+        Self { dispatcher }
+    }
+
+    /// Enqueues the given closure to be run on any thread. The closure returns
+    /// a future which will be run to completion on any available thread.
+    pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
+    where
+        R: Send + 'static,
+    {
+        let dispatcher = self.dispatcher.clone();
+        let (runnable, task) =
+            async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable));
+        runnable.schedule();
+        Task::Spawned(task)
+    }
+
+    /// Enqueues the given closure to run on the application's event loop.
+    /// Returns the result asynchronously.
+    pub fn run_on_main<F, R>(&self, func: F) -> Task<R>
+    where
+        F: FnOnce() -> R + Send + 'static,
+        R: Send + 'static,
+    {
+        if self.dispatcher.is_main_thread() {
+            Task::ready(func())
+        } else {
+            self.spawn_on_main(move || async move { func() })
+        }
+    }
+
+    /// Enqueues the given closure to be run on the application's event loop. The
+    /// closure returns a future which will be run to completion on the main thread.
+    pub fn spawn_on_main<F, R>(&self, func: impl FnOnce() -> F + Send + 'static) -> Task<R>
+    where
+        F: Future<Output = R> + 'static,
+        R: Send + 'static,
+    {
+        let (runnable, task) = async_task::spawn(
+            {
+                let this = self.clone();
+                async move {
+                    let task = this.spawn_on_main_local(func());
+                    task.await
+                }
+            },
+            {
+                let dispatcher = self.dispatcher.clone();
+                move |runnable| dispatcher.dispatch_on_main_thread(runnable)
+            },
+        );
+        runnable.schedule();
+        Task::Spawned(task)
+    }
+
+    /// Enqueues the given closure to be run on the application's event loop. Must
+    /// be called on the main thread.
+    pub fn spawn_on_main_local<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
+    where
+        R: 'static,
+    {
+        assert!(
+            self.dispatcher.is_main_thread(),
+            "must be called on main thread"
+        );
+
+        let dispatcher = self.dispatcher.clone();
+        let (runnable, task) = async_task::spawn_local(future, move |runnable| {
+            dispatcher.dispatch_on_main_thread(runnable)
+        });
+        runnable.schedule();
+        Task::Spawned(task)
+    }
+
+    pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
+        pin_mut!(future);
+        let (parker, unparker) = parking::pair();
+        let waker = waker_fn(move || {
+            unparker.unpark();
+        });
+        let mut cx = std::task::Context::from_waker(&waker);
+
+        loop {
+            match future.as_mut().poll(&mut cx) {
+                Poll::Ready(result) => return result,
+                Poll::Pending => {
+                    if !self.dispatcher.poll() {
+                        #[cfg(any(test, feature = "test-support"))]
+                        if let Some(_) = self.dispatcher.as_test() {
+                            panic!("blocked with nothing left to run")
+                        }
+                        parker.park();
+                    }
+                }
+            }
+        }
+    }
+
+    pub fn block_with_timeout<R>(
+        &self,
+        duration: Duration,
+        future: impl Future<Output = R>,
+    ) -> Result<R, impl Future<Output = R>> {
+        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) {
+            Ok(value) => Ok(value),
+            Err(_) => Err(future),
+        }
+    }
+
+    pub async fn scoped<'scope, F>(&self, scheduler: F)
+    where
+        F: FnOnce(&mut Scope<'scope>),
+    {
+        let mut scope = Scope::new(self.clone());
+        (scheduler)(&mut scope);
+        let spawned = mem::take(&mut scope.futures)
+            .into_iter()
+            .map(|f| self.spawn(f))
+            .collect::<Vec<_>>();
+        for task in spawned {
+            task.await;
+        }
+    }
+
+    pub fn timer(&self, duration: Duration) -> Task<()> {
+        let (runnable, task) = async_task::spawn(async move {}, {
+            let dispatcher = self.dispatcher.clone();
+            move |runnable| dispatcher.dispatch_after(duration, runnable)
+        });
+        runnable.schedule();
+        Task::Spawned(task)
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn start_waiting(&self) {
+        todo!("start_waiting")
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn finish_waiting(&self) {
+        todo!("finish_waiting")
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
+        self.spawn(self.dispatcher.as_test().unwrap().simulate_random_delay())
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn advance_clock(&self, duration: Duration) {
+        self.dispatcher.as_test().unwrap().advance_clock(duration)
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn run_until_parked(&self) {
+        self.dispatcher.as_test().unwrap().run_until_parked()
+    }
+
+    pub fn num_cpus(&self) -> usize {
+        num_cpus::get()
+    }
+
+    pub fn is_main_thread(&self) -> bool {
+        self.dispatcher.is_main_thread()
+    }
+}
+
+pub struct Scope<'a> {
+    executor: Executor,
+    futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
+    tx: Option<mpsc::Sender<()>>,
+    rx: mpsc::Receiver<()>,
+    lifetime: PhantomData<&'a ()>,
+}
+
+impl<'a> Scope<'a> {
+    fn new(executor: Executor) -> Self {
+        let (tx, rx) = mpsc::channel(1);
+        Self {
+            executor,
+            tx: Some(tx),
+            rx,
+            futures: Default::default(),
+            lifetime: PhantomData,
+        }
+    }
+
+    pub fn spawn<F>(&mut self, f: F)
+    where
+        F: Future<Output = ()> + Send + 'a,
+    {
+        let tx = self.tx.clone().unwrap();
+
+        // Safety: The 'a lifetime is guaranteed to outlive any of these futures because
+        // dropping this `Scope` blocks until all of the futures have resolved.
+        let f = unsafe {
+            mem::transmute::<
+                Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
+                Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
+            >(Box::pin(async move {
+                f.await;
+                drop(tx);
+            }))
+        };
+        self.futures.push(f);
+    }
+}
+
+impl<'a> Drop for Scope<'a> {
+    fn drop(&mut self) {
+        self.tx.take().unwrap();
+
+        // Wait until the channel is closed, which means that all of the spawned
+        // futures have resolved.
+        self.executor.block(self.rx.next());
+    }
+}

crates/gpui2/src/focusable.rs 🔗

@@ -0,0 +1,252 @@
+use crate::{
+    Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, MouseDownEvent, Pixels, Style,
+    StyleRefinement, ViewContext, WindowContext,
+};
+use refineable::Refineable;
+use smallvec::SmallVec;
+
+pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
+
+pub type FocusListener<V> =
+    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>;
+    fn set_focus_style(&mut self, style: StyleRefinement);
+    fn set_focus_in_style(&mut self, style: StyleRefinement);
+    fn set_in_focus_style(&mut self, style: StyleRefinement);
+
+    fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.set_focus_style(f(StyleRefinement::default()));
+        self
+    }
+
+    fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.set_focus_in_style(f(StyleRefinement::default()));
+        self
+    }
+
+    fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.set_in_focus_style(f(StyleRefinement::default()));
+        self
+    }
+
+    fn on_focus(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.focus_listeners()
+            .push(Box::new(move |view, focus_handle, event, cx| {
+                if event.focused.as_ref() == Some(focus_handle) {
+                    listener(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_blur(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.focus_listeners()
+            .push(Box::new(move |view, focus_handle, event, cx| {
+                if event.blurred.as_ref() == Some(focus_handle) {
+                    listener(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_focus_in(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.focus_listeners()
+            .push(Box::new(move |view, focus_handle, event, cx| {
+                let descendant_blurred = event
+                    .blurred
+                    .as_ref()
+                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
+                let descendant_focused = event
+                    .focused
+                    .as_ref()
+                    .map_or(false, |focused| focus_handle.contains(focused, cx));
+
+                if !descendant_blurred && descendant_focused {
+                    listener(view, event, cx)
+                }
+            }));
+        self
+    }
+
+    fn on_focus_out(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.focus_listeners()
+            .push(Box::new(move |view, focus_handle, event, cx| {
+                let descendant_blurred = event
+                    .blurred
+                    .as_ref()
+                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
+                let descendant_focused = event
+                    .focused
+                    .as_ref()
+                    .map_or(false, |focused| focus_handle.contains(focused, cx));
+                if descendant_blurred && !descendant_focused {
+                    listener(view, event, cx)
+                }
+            }));
+        self
+    }
+}
+
+pub trait ElementFocus<V: 'static>: 'static + Send {
+    fn as_focusable(&self) -> Option<&FocusEnabled<V>>;
+    fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>>;
+
+    fn initialize<R>(
+        &mut self,
+        focus_handle: Option<FocusHandle>,
+        cx: &mut ViewContext<V>,
+        f: impl FnOnce(Option<FocusHandle>, &mut ViewContext<V>) -> R,
+    ) -> R {
+        if let Some(focusable) = self.as_focusable_mut() {
+            let focus_handle = focusable
+                .focus_handle
+                .get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle()))
+                .clone();
+            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)
+                });
+            }
+            cx.with_focus(focus_handle.clone(), |cx| f(Some(focus_handle), cx))
+        } else {
+            f(None, cx)
+        }
+    }
+
+    fn refine_style(&self, style: &mut Style, cx: &WindowContext) {
+        if let Some(focusable) = self.as_focusable() {
+            let focus_handle = focusable
+                .focus_handle
+                .as_ref()
+                .expect("must call initialize before refine_style");
+            if focus_handle.contains_focused(cx) {
+                style.refine(&focusable.focus_in_style);
+            }
+
+            if focus_handle.within_focused(cx) {
+                style.refine(&focusable.in_focus_style);
+            }
+
+            if focus_handle.is_focused(cx) {
+                style.refine(&focusable.focus_style);
+            }
+        }
+    }
+
+    fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
+        if let Some(focusable) = self.as_focusable() {
+            let focus_handle = focusable
+                .focus_handle
+                .clone()
+                .expect("must call initialize before paint");
+            cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    if !cx.default_prevented() {
+                        cx.focus(&focus_handle);
+                        cx.prevent_default();
+                    }
+                }
+            })
+        }
+    }
+}
+
+pub struct FocusEnabled<V> {
+    pub focus_handle: Option<FocusHandle>,
+    pub focus_listeners: FocusListeners<V>,
+    pub focus_style: StyleRefinement,
+    pub focus_in_style: StyleRefinement,
+    pub in_focus_style: StyleRefinement,
+}
+
+impl<V> FocusEnabled<V> {
+    pub fn new() -> Self {
+        Self {
+            focus_handle: None,
+            focus_listeners: FocusListeners::default(),
+            focus_style: StyleRefinement::default(),
+            focus_in_style: StyleRefinement::default(),
+            in_focus_style: StyleRefinement::default(),
+        }
+    }
+
+    pub fn tracked(handle: &FocusHandle) -> Self {
+        Self {
+            focus_handle: Some(handle.clone()),
+            focus_listeners: FocusListeners::default(),
+            focus_style: StyleRefinement::default(),
+            focus_in_style: StyleRefinement::default(),
+            in_focus_style: StyleRefinement::default(),
+        }
+    }
+}
+
+impl<V: 'static> ElementFocus<V> for FocusEnabled<V> {
+    fn as_focusable(&self) -> Option<&FocusEnabled<V>> {
+        Some(self)
+    }
+
+    fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>> {
+        Some(self)
+    }
+}
+
+impl<V> From<FocusHandle> for FocusEnabled<V> {
+    fn from(value: FocusHandle) -> Self {
+        Self {
+            focus_handle: Some(value),
+            focus_listeners: FocusListeners::default(),
+            focus_style: StyleRefinement::default(),
+            focus_in_style: StyleRefinement::default(),
+            in_focus_style: StyleRefinement::default(),
+        }
+    }
+}
+
+pub struct FocusDisabled;
+
+impl<V: 'static> ElementFocus<V> for FocusDisabled {
+    fn as_focusable(&self) -> Option<&FocusEnabled<V>> {
+        None
+    }
+
+    fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>> {
+        None
+    }
+}

crates/gpui2/src/geometry.rs 🔗

@@ -0,0 +1,1244 @@
+use core::fmt::Debug;
+use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign};
+use refineable::Refineable;
+use serde_derive::{Deserialize, Serialize};
+use std::{
+    cmp::{self, PartialOrd},
+    fmt,
+    ops::{Add, Div, Mul, MulAssign, Sub},
+};
+
+#[derive(Refineable, Default, Add, AddAssign, Sub, SubAssign, Copy, Debug, PartialEq, Eq, Hash)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Point<T: Default + Clone + Debug> {
+    pub x: T,
+    pub y: T,
+}
+
+pub fn point<T: Clone + Debug + Default>(x: T, y: T) -> Point<T> {
+    Point { x, y }
+}
+
+impl<T: Clone + Debug + Default> Point<T> {
+    pub fn new(x: T, y: T) -> Self {
+        Self { x, y }
+    }
+
+    pub fn map<U: Clone + Default + Debug>(&self, f: impl Fn(T) -> U) -> Point<U> {
+        Point {
+            x: f(self.x.clone()),
+            y: f(self.y.clone()),
+        }
+    }
+}
+
+impl Point<Pixels> {
+    pub fn scale(&self, factor: f32) -> Point<ScaledPixels> {
+        Point {
+            x: self.x.scale(factor),
+            y: self.y.scale(factor),
+        }
+    }
+
+    pub fn magnitude(&self) -> f64 {
+        ((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt()
+    }
+}
+
+impl<T, Rhs> Mul<Rhs> for Point<T>
+where
+    T: Mul<Rhs, Output = T> + Clone + Default + Debug,
+    Rhs: Clone + Debug,
+{
+    type Output = Point<T>;
+
+    fn mul(self, rhs: Rhs) -> Self::Output {
+        Point {
+            x: self.x * rhs.clone(),
+            y: self.y * rhs,
+        }
+    }
+}
+
+impl<T, S> MulAssign<S> for Point<T>
+where
+    T: Clone + Mul<S, Output = T> + Default + Debug,
+    S: Clone,
+{
+    fn mul_assign(&mut self, rhs: S) {
+        self.x = self.x.clone() * rhs.clone();
+        self.y = self.y.clone() * rhs;
+    }
+}
+
+impl<T, S> Div<S> for Point<T>
+where
+    T: Div<S, Output = T> + Clone + Default + Debug,
+    S: Clone,
+{
+    type Output = Self;
+
+    fn div(self, rhs: S) -> Self::Output {
+        Self {
+            x: self.x / rhs.clone(),
+            y: self.y / rhs,
+        }
+    }
+}
+
+impl<T> Point<T>
+where
+    T: PartialOrd + Clone + Default + Debug,
+{
+    pub fn max(&self, other: &Self) -> Self {
+        Point {
+            x: if self.x >= other.x {
+                self.x.clone()
+            } else {
+                other.x.clone()
+            },
+            y: if self.y >= other.y {
+                self.y.clone()
+            } else {
+                other.y.clone()
+            },
+        }
+    }
+
+    pub fn min(&self, other: &Self) -> Self {
+        Point {
+            x: if self.x <= other.x {
+                self.x.clone()
+            } else {
+                other.x.clone()
+            },
+            y: if self.y <= other.y {
+                self.y.clone()
+            } else {
+                other.y.clone()
+            },
+        }
+    }
+}
+
+impl<T: Clone + Default + Debug> Clone for Point<T> {
+    fn clone(&self) -> Self {
+        Self {
+            x: self.x.clone(),
+            y: self.y.clone(),
+        }
+    }
+}
+
+#[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Size<T: Clone + Default + Debug> {
+    pub width: T,
+    pub height: T,
+}
+
+pub fn size<T>(width: T, height: T) -> Size<T>
+where
+    T: Clone + Default + Debug,
+{
+    Size { width, height }
+}
+
+impl<T> Size<T>
+where
+    T: Clone + Default + Debug,
+{
+    pub fn map<U>(&self, f: impl Fn(T) -> U) -> Size<U>
+    where
+        U: Clone + Default + Debug,
+    {
+        Size {
+            width: f(self.width.clone()),
+            height: f(self.height.clone()),
+        }
+    }
+}
+
+impl Size<Pixels> {
+    pub fn scale(&self, factor: f32) -> Size<ScaledPixels> {
+        Size {
+            width: self.width.scale(factor),
+            height: self.height.scale(factor),
+        }
+    }
+}
+
+impl<T> Size<T>
+where
+    T: PartialOrd + Clone + Default + Debug,
+{
+    pub fn max(&self, other: &Self) -> Self {
+        Size {
+            width: if self.width >= other.width {
+                self.width.clone()
+            } else {
+                other.width.clone()
+            },
+            height: if self.height >= other.height {
+                self.height.clone()
+            } else {
+                other.height.clone()
+            },
+        }
+    }
+}
+
+impl<T> Sub for Size<T>
+where
+    T: Sub<Output = T> + Clone + Default + Debug,
+{
+    type Output = Size<T>;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Size {
+            width: self.width - rhs.width,
+            height: self.height - rhs.height,
+        }
+    }
+}
+
+impl<T, Rhs> Mul<Rhs> for Size<T>
+where
+    T: Mul<Rhs, Output = Rhs> + Clone + Default + Debug,
+    Rhs: Clone + Default + Debug,
+{
+    type Output = Size<Rhs>;
+
+    fn mul(self, rhs: Rhs) -> Self::Output {
+        Size {
+            width: self.width * rhs.clone(),
+            height: self.height * rhs,
+        }
+    }
+}
+
+impl<T, S> MulAssign<S> for Size<T>
+where
+    T: Mul<S, Output = T> + Clone + Default + Debug,
+    S: Clone,
+{
+    fn mul_assign(&mut self, rhs: S) {
+        self.width = self.width.clone() * rhs.clone();
+        self.height = self.height.clone() * rhs;
+    }
+}
+
+impl<T> Eq for Size<T> where T: Eq + Default + Debug + Clone {}
+
+impl<T> Debug for Size<T>
+where
+    T: Clone + Default + Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height)
+    }
+}
+
+impl<T: Clone + Default + Debug> From<Point<T>> for Size<T> {
+    fn from(point: Point<T>) -> Self {
+        Self {
+            width: point.x,
+            height: point.y,
+        }
+    }
+}
+
+impl From<Size<Pixels>> for Size<GlobalPixels> {
+    fn from(size: Size<Pixels>) -> Self {
+        Size {
+            width: GlobalPixels(size.width.0),
+            height: GlobalPixels(size.height.0),
+        }
+    }
+}
+
+impl Size<Length> {
+    pub fn full() -> Self {
+        Self {
+            width: relative(1.).into(),
+            height: relative(1.).into(),
+        }
+    }
+}
+
+impl Size<DefiniteLength> {
+    pub fn zero() -> Self {
+        Self {
+            width: px(0.).into(),
+            height: px(0.).into(),
+        }
+    }
+}
+
+impl Size<Length> {
+    pub fn auto() -> Self {
+        Self {
+            width: Length::Auto,
+            height: Length::Auto,
+        }
+    }
+}
+
+#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Bounds<T: Clone + Default + Debug> {
+    pub origin: Point<T>,
+    pub size: Size<T>,
+}
+
+impl<T> Bounds<T>
+where
+    T: Clone + Debug + Sub<Output = T> + Default,
+{
+    pub fn from_corners(upper_left: Point<T>, lower_right: Point<T>) -> Self {
+        let origin = Point {
+            x: upper_left.x.clone(),
+            y: upper_left.y.clone(),
+        };
+        let size = Size {
+            width: lower_right.x - upper_left.x,
+            height: lower_right.y - upper_left.y,
+        };
+        Bounds { origin, size }
+    }
+}
+
+impl<T> Bounds<T>
+where
+    T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default,
+{
+    pub fn intersects(&self, other: &Bounds<T>) -> bool {
+        let my_lower_right = self.lower_right();
+        let their_lower_right = other.lower_right();
+
+        self.origin.x < their_lower_right.x
+            && my_lower_right.x > other.origin.x
+            && self.origin.y < their_lower_right.y
+            && my_lower_right.y > other.origin.y
+    }
+
+    pub fn dilate(&mut self, amount: T) {
+        self.origin.x = self.origin.x.clone() - amount.clone();
+        self.origin.y = self.origin.y.clone() - amount.clone();
+        let double_amount = amount.clone() + amount;
+        self.size.width = self.size.width.clone() + double_amount.clone();
+        self.size.height = self.size.height.clone() + double_amount;
+    }
+}
+
+impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
+    pub fn intersect(&self, other: &Self) -> Self {
+        let upper_left = self.origin.max(&other.origin);
+        let lower_right = self.lower_right().min(&other.lower_right());
+        Self::from_corners(upper_left, lower_right)
+    }
+
+    pub fn union(&self, other: &Self) -> Self {
+        let top_left = self.origin.min(&other.origin);
+        let bottom_right = self.lower_right().max(&other.lower_right());
+        Bounds::from_corners(top_left, bottom_right)
+    }
+}
+
+impl<T, Rhs> Mul<Rhs> for Bounds<T>
+where
+    T: Mul<Rhs, Output = Rhs> + Clone + Default + Debug,
+    Point<T>: Mul<Rhs, Output = Point<Rhs>>,
+    Rhs: Clone + Default + Debug,
+{
+    type Output = Bounds<Rhs>;
+
+    fn mul(self, rhs: Rhs) -> Self::Output {
+        Bounds {
+            origin: self.origin * rhs.clone(),
+            size: self.size * rhs,
+        }
+    }
+}
+
+impl<T, S> MulAssign<S> for Bounds<T>
+where
+    T: Mul<S, Output = T> + Clone + Default + Debug,
+    S: Clone,
+{
+    fn mul_assign(&mut self, rhs: S) {
+        self.origin *= rhs.clone();
+        self.size *= rhs;
+    }
+}
+
+impl<T, S> Div<S> for Bounds<T>
+where
+    Size<T>: Div<S, Output = Size<T>>,
+    T: Div<S, Output = T> + Default + Clone + Debug,
+    S: Clone,
+{
+    type Output = Self;
+
+    fn div(self, rhs: S) -> Self {
+        Self {
+            origin: self.origin / rhs.clone(),
+            size: self.size / rhs,
+        }
+    }
+}
+
+impl<T> Bounds<T>
+where
+    T: Add<T, Output = T> + Clone + Default + Debug,
+{
+    pub fn upper_right(&self) -> Point<T> {
+        Point {
+            x: self.origin.x.clone() + self.size.width.clone(),
+            y: self.origin.y.clone(),
+        }
+    }
+
+    pub fn lower_right(&self) -> Point<T> {
+        Point {
+            x: self.origin.x.clone() + self.size.width.clone(),
+            y: self.origin.y.clone() + self.size.height.clone(),
+        }
+    }
+
+    pub fn lower_left(&self) -> Point<T> {
+        Point {
+            x: self.origin.x.clone(),
+            y: self.origin.y.clone() + self.size.height.clone(),
+        }
+    }
+}
+
+impl<T> Bounds<T>
+where
+    T: Add<T, Output = T> + PartialOrd + Clone + Default + Debug,
+{
+    pub fn contains_point(&self, point: &Point<T>) -> bool {
+        point.x >= self.origin.x
+            && point.x <= self.origin.x.clone() + self.size.width.clone()
+            && point.y >= self.origin.y
+            && point.y <= self.origin.y.clone() + self.size.height.clone()
+    }
+
+    pub fn map<U>(&self, f: impl Fn(T) -> U) -> Bounds<U>
+    where
+        U: Clone + Default + Debug,
+    {
+        Bounds {
+            origin: self.origin.map(&f),
+            size: self.size.map(f),
+        }
+    }
+}
+
+impl Bounds<Pixels> {
+    pub fn scale(&self, factor: f32) -> Bounds<ScaledPixels> {
+        Bounds {
+            origin: self.origin.scale(factor),
+            size: self.size.scale(factor),
+        }
+    }
+}
+
+impl<T: Clone + Debug + Copy + Default> Copy for Bounds<T> {}
+
+#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Edges<T: Clone + Default + Debug> {
+    pub top: T,
+    pub right: T,
+    pub bottom: T,
+    pub left: T,
+}
+
+impl<T> Mul for Edges<T>
+where
+    T: Mul<Output = T> + Clone + Default + Debug,
+{
+    type Output = Self;
+
+    fn mul(self, rhs: Self) -> Self::Output {
+        Self {
+            top: self.top.clone() * rhs.top,
+            right: self.right.clone() * rhs.right,
+            bottom: self.bottom.clone() * rhs.bottom,
+            left: self.left.clone() * rhs.left,
+        }
+    }
+}
+
+impl<T, S> MulAssign<S> for Edges<T>
+where
+    T: Mul<S, Output = T> + Clone + Default + Debug,
+    S: Clone,
+{
+    fn mul_assign(&mut self, rhs: S) {
+        self.top = self.top.clone() * rhs.clone();
+        self.right = self.right.clone() * rhs.clone();
+        self.bottom = self.bottom.clone() * rhs.clone();
+        self.left = self.left.clone() * rhs;
+    }
+}
+
+impl<T: Clone + Default + Debug + Copy> Copy for Edges<T> {}
+
+impl<T: Clone + Default + Debug> Edges<T> {
+    pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Edges<U>
+    where
+        U: Clone + Default + Debug,
+    {
+        Edges {
+            top: f(&self.top),
+            right: f(&self.right),
+            bottom: f(&self.bottom),
+            left: f(&self.left),
+        }
+    }
+
+    pub fn any<F: Fn(&T) -> bool>(&self, predicate: F) -> bool {
+        predicate(&self.top)
+            || predicate(&self.right)
+            || predicate(&self.bottom)
+            || predicate(&self.left)
+    }
+}
+
+impl Edges<Length> {
+    pub fn auto() -> Self {
+        Self {
+            top: Length::Auto,
+            right: Length::Auto,
+            bottom: Length::Auto,
+            left: Length::Auto,
+        }
+    }
+
+    pub fn zero() -> Self {
+        Self {
+            top: px(0.).into(),
+            right: px(0.).into(),
+            bottom: px(0.).into(),
+            left: px(0.).into(),
+        }
+    }
+}
+
+impl Edges<DefiniteLength> {
+    pub fn zero() -> Self {
+        Self {
+            top: px(0.).into(),
+            right: px(0.).into(),
+            bottom: px(0.).into(),
+            left: px(0.).into(),
+        }
+    }
+}
+
+impl Edges<AbsoluteLength> {
+    pub fn zero() -> Self {
+        Self {
+            top: px(0.).into(),
+            right: px(0.).into(),
+            bottom: px(0.).into(),
+            left: px(0.).into(),
+        }
+    }
+
+    pub fn to_pixels(&self, rem_size: Pixels) -> Edges<Pixels> {
+        Edges {
+            top: self.top.to_pixels(rem_size),
+            right: self.right.to_pixels(rem_size),
+            bottom: self.bottom.to_pixels(rem_size),
+            left: self.left.to_pixels(rem_size),
+        }
+    }
+}
+
+impl Edges<Pixels> {
+    pub fn scale(&self, factor: f32) -> Edges<ScaledPixels> {
+        Edges {
+            top: self.top.scale(factor),
+            right: self.right.scale(factor),
+            bottom: self.bottom.scale(factor),
+            left: self.left.scale(factor),
+        }
+    }
+}
+
+#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Corners<T: Clone + Default + Debug> {
+    pub top_left: T,
+    pub top_right: T,
+    pub bottom_right: T,
+    pub bottom_left: T,
+}
+
+impl Corners<AbsoluteLength> {
+    pub fn to_pixels(&self, size: Size<Pixels>, rem_size: Pixels) -> Corners<Pixels> {
+        let max = size.width.max(size.height) / 2.;
+        Corners {
+            top_left: self.top_left.to_pixels(rem_size).min(max),
+            top_right: self.top_right.to_pixels(rem_size).min(max),
+            bottom_right: self.bottom_right.to_pixels(rem_size).min(max),
+            bottom_left: self.bottom_left.to_pixels(rem_size).min(max),
+        }
+    }
+}
+
+impl Corners<Pixels> {
+    pub fn scale(&self, factor: f32) -> Corners<ScaledPixels> {
+        Corners {
+            top_left: self.top_left.scale(factor),
+            top_right: self.top_right.scale(factor),
+            bottom_right: self.bottom_right.scale(factor),
+            bottom_left: self.bottom_left.scale(factor),
+        }
+    }
+}
+
+impl<T: Clone + Default + Debug> Corners<T> {
+    pub fn map<U>(&self, f: impl Fn(&T) -> U) -> Corners<U>
+    where
+        U: Clone + Default + Debug,
+    {
+        Corners {
+            top_left: f(&self.top_left),
+            top_right: f(&self.top_right),
+            bottom_right: f(&self.bottom_right),
+            bottom_left: f(&self.bottom_left),
+        }
+    }
+}
+
+impl<T> Mul for Corners<T>
+where
+    T: Mul<Output = T> + Clone + Default + Debug,
+{
+    type Output = Self;
+
+    fn mul(self, rhs: Self) -> Self::Output {
+        Self {
+            top_left: self.top_left.clone() * rhs.top_left,
+            top_right: self.top_right.clone() * rhs.top_right,
+            bottom_right: self.bottom_right.clone() * rhs.bottom_right,
+            bottom_left: self.bottom_left.clone() * rhs.bottom_left,
+        }
+    }
+}
+
+impl<T, S> MulAssign<S> for Corners<T>
+where
+    T: Mul<S, Output = T> + Clone + Default + Debug,
+    S: Clone,
+{
+    fn mul_assign(&mut self, rhs: S) {
+        self.top_left = self.top_left.clone() * rhs.clone();
+        self.top_right = self.top_right.clone() * rhs.clone();
+        self.bottom_right = self.bottom_right.clone() * rhs.clone();
+        self.bottom_left = self.bottom_left.clone() * rhs;
+    }
+}
+
+impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
+
+#[derive(
+    Clone,
+    Copy,
+    Default,
+    Add,
+    AddAssign,
+    Sub,
+    SubAssign,
+    Neg,
+    Div,
+    DivAssign,
+    PartialEq,
+    PartialOrd,
+    Serialize,
+    Deserialize,
+)]
+#[repr(transparent)]
+pub struct Pixels(pub(crate) f32);
+
+impl std::ops::Div for Pixels {
+    type Output = Self;
+
+    fn div(self, rhs: Self) -> Self::Output {
+        Self(self.0 / rhs.0)
+    }
+}
+
+impl std::ops::DivAssign for Pixels {
+    fn div_assign(&mut self, rhs: Self) {
+        self.0 /= rhs.0;
+    }
+}
+
+impl std::ops::RemAssign for Pixels {
+    fn rem_assign(&mut self, rhs: Self) {
+        self.0 %= rhs.0;
+    }
+}
+
+impl std::ops::Rem for Pixels {
+    type Output = Self;
+
+    fn rem(self, rhs: Self) -> Self {
+        Self(self.0 % rhs.0)
+    }
+}
+
+impl Mul<f32> for Pixels {
+    type Output = Pixels;
+
+    fn mul(self, other: f32) -> Pixels {
+        Pixels(self.0 * other)
+    }
+}
+
+impl Mul<usize> for Pixels {
+    type Output = Pixels;
+
+    fn mul(self, other: usize) -> Pixels {
+        Pixels(self.0 * other as f32)
+    }
+}
+
+impl Mul<Pixels> for f32 {
+    type Output = Pixels;
+
+    fn mul(self, rhs: Pixels) -> Self::Output {
+        Pixels(self * rhs.0)
+    }
+}
+
+impl MulAssign<f32> for Pixels {
+    fn mul_assign(&mut self, other: f32) {
+        self.0 *= other;
+    }
+}
+
+impl Pixels {
+    pub const MAX: Pixels = Pixels(f32::MAX);
+
+    pub fn as_usize(&self) -> usize {
+        self.0 as usize
+    }
+
+    pub fn as_isize(&self) -> isize {
+        self.0 as isize
+    }
+
+    pub fn floor(&self) -> Self {
+        Self(self.0.floor())
+    }
+
+    pub fn round(&self) -> Self {
+        Self(self.0.round())
+    }
+
+    pub fn scale(&self, factor: f32) -> ScaledPixels {
+        ScaledPixels(self.0 * factor)
+    }
+
+    pub fn pow(&self, exponent: f32) -> Self {
+        Self(self.0.powf(exponent))
+    }
+}
+
+impl Mul<Pixels> for Pixels {
+    type Output = Pixels;
+
+    fn mul(self, rhs: Pixels) -> Self::Output {
+        Pixels(self.0 * rhs.0)
+    }
+}
+
+impl Eq for Pixels {}
+
+impl Ord for Pixels {
+    fn cmp(&self, other: &Self) -> cmp::Ordering {
+        self.0.partial_cmp(&other.0).unwrap()
+    }
+}
+
+impl std::hash::Hash for Pixels {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.0.to_bits().hash(state);
+    }
+}
+
+impl From<f64> for Pixels {
+    fn from(pixels: f64) -> Self {
+        Pixels(pixels as f32)
+    }
+}
+
+impl From<f32> for Pixels {
+    fn from(pixels: f32) -> Self {
+        Pixels(pixels)
+    }
+}
+
+impl Debug for Pixels {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} px", self.0)
+    }
+}
+
+impl From<Pixels> for f32 {
+    fn from(pixels: Pixels) -> Self {
+        pixels.0
+    }
+}
+
+impl From<&Pixels> for f32 {
+    fn from(pixels: &Pixels) -> Self {
+        pixels.0
+    }
+}
+
+impl From<Pixels> for f64 {
+    fn from(pixels: Pixels) -> Self {
+        pixels.0 as f64
+    }
+}
+
+#[derive(
+    Add, AddAssign, Clone, Copy, Default, Div, Eq, Hash, Ord, PartialEq, PartialOrd, Sub, SubAssign,
+)]
+#[repr(transparent)]
+pub struct DevicePixels(pub(crate) i32);
+
+impl DevicePixels {
+    pub fn to_bytes(&self, bytes_per_pixel: u8) -> u32 {
+        self.0 as u32 * bytes_per_pixel as u32
+    }
+}
+
+impl fmt::Debug for DevicePixels {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} px (device)", self.0)
+    }
+}
+
+impl From<DevicePixels> for i32 {
+    fn from(device_pixels: DevicePixels) -> Self {
+        device_pixels.0
+    }
+}
+
+impl From<i32> for DevicePixels {
+    fn from(device_pixels: i32) -> Self {
+        DevicePixels(device_pixels)
+    }
+}
+
+impl From<u32> for DevicePixels {
+    fn from(device_pixels: u32) -> Self {
+        DevicePixels(device_pixels as i32)
+    }
+}
+
+impl From<DevicePixels> for u32 {
+    fn from(device_pixels: DevicePixels) -> Self {
+        device_pixels.0 as u32
+    }
+}
+
+impl From<DevicePixels> for u64 {
+    fn from(device_pixels: DevicePixels) -> Self {
+        device_pixels.0 as u64
+    }
+}
+
+impl From<u64> for DevicePixels {
+    fn from(device_pixels: u64) -> Self {
+        DevicePixels(device_pixels as i32)
+    }
+}
+
+#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)]
+#[repr(transparent)]
+pub struct ScaledPixels(pub(crate) f32);
+
+impl ScaledPixels {
+    pub fn floor(&self) -> Self {
+        Self(self.0.floor())
+    }
+
+    pub fn ceil(&self) -> Self {
+        Self(self.0.ceil())
+    }
+}
+
+impl Eq for ScaledPixels {}
+
+impl Debug for ScaledPixels {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} px (scaled)", self.0)
+    }
+}
+
+impl From<ScaledPixels> for DevicePixels {
+    fn from(scaled: ScaledPixels) -> Self {
+        DevicePixels(scaled.0.ceil() as i32)
+    }
+}
+
+impl From<DevicePixels> for ScaledPixels {
+    fn from(device: DevicePixels) -> Self {
+        ScaledPixels(device.0 as f32)
+    }
+}
+
+impl From<ScaledPixels> for f64 {
+    fn from(scaled_pixels: ScaledPixels) -> Self {
+        scaled_pixels.0 as f64
+    }
+}
+
+#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)]
+#[repr(transparent)]
+pub struct GlobalPixels(pub(crate) f32);
+
+impl Debug for GlobalPixels {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} px (global coordinate space)", self.0)
+    }
+}
+
+impl From<GlobalPixels> for f64 {
+    fn from(global_pixels: GlobalPixels) -> Self {
+        global_pixels.0 as f64
+    }
+}
+
+impl From<f64> for GlobalPixels {
+    fn from(global_pixels: f64) -> Self {
+        GlobalPixels(global_pixels as f32)
+    }
+}
+
+#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)]
+pub struct Rems(f32);
+
+impl Mul<Pixels> for Rems {
+    type Output = Pixels;
+
+    fn mul(self, other: Pixels) -> Pixels {
+        Pixels(self.0 * other.0)
+    }
+}
+
+impl Debug for Rems {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} rem", self.0)
+    }
+}
+
+#[derive(Clone, Copy, Debug, Neg)]
+pub enum AbsoluteLength {
+    Pixels(Pixels),
+    Rems(Rems),
+}
+
+impl AbsoluteLength {
+    pub fn is_zero(&self) -> bool {
+        match self {
+            AbsoluteLength::Pixels(px) => px.0 == 0.,
+            AbsoluteLength::Rems(rems) => rems.0 == 0.,
+        }
+    }
+}
+
+impl From<Pixels> for AbsoluteLength {
+    fn from(pixels: Pixels) -> Self {
+        AbsoluteLength::Pixels(pixels)
+    }
+}
+
+impl From<Rems> for AbsoluteLength {
+    fn from(rems: Rems) -> Self {
+        AbsoluteLength::Rems(rems)
+    }
+}
+
+impl AbsoluteLength {
+    pub fn to_pixels(&self, rem_size: Pixels) -> Pixels {
+        match self {
+            AbsoluteLength::Pixels(pixels) => *pixels,
+            AbsoluteLength::Rems(rems) => *rems * rem_size,
+        }
+    }
+}
+
+impl Default for AbsoluteLength {
+    fn default() -> Self {
+        px(0.).into()
+    }
+}
+
+/// A non-auto length that can be defined in pixels, rems, or percent of parent.
+#[derive(Clone, Copy, Neg)]
+pub enum DefiniteLength {
+    Absolute(AbsoluteLength),
+    /// A fraction of the parent's size between 0 and 1.
+    Fraction(f32),
+}
+
+impl DefiniteLength {
+    pub fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels {
+        match self {
+            DefiniteLength::Absolute(size) => size.to_pixels(rem_size),
+            DefiniteLength::Fraction(fraction) => match base_size {
+                AbsoluteLength::Pixels(px) => px * *fraction,
+                AbsoluteLength::Rems(rems) => rems * rem_size * *fraction,
+            },
+        }
+    }
+}
+
+impl Debug for DefiniteLength {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            DefiniteLength::Absolute(length) => Debug::fmt(length, f),
+            DefiniteLength::Fraction(fract) => write!(f, "{}%", (fract * 100.0) as i32),
+        }
+    }
+}
+
+impl From<Pixels> for DefiniteLength {
+    fn from(pixels: Pixels) -> Self {
+        Self::Absolute(pixels.into())
+    }
+}
+
+impl From<Rems> for DefiniteLength {
+    fn from(rems: Rems) -> Self {
+        Self::Absolute(rems.into())
+    }
+}
+
+impl From<AbsoluteLength> for DefiniteLength {
+    fn from(length: AbsoluteLength) -> Self {
+        Self::Absolute(length)
+    }
+}
+
+impl Default for DefiniteLength {
+    fn default() -> Self {
+        Self::Absolute(AbsoluteLength::default())
+    }
+}
+
+/// A length that can be defined in pixels, rems, percent of parent, or auto.
+#[derive(Clone, Copy)]
+pub enum Length {
+    Definite(DefiniteLength),
+    Auto,
+}
+
+impl Debug for Length {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Length::Definite(definite_length) => write!(f, "{:?}", definite_length),
+            Length::Auto => write!(f, "auto"),
+        }
+    }
+}
+
+pub fn relative(fraction: f32) -> DefiniteLength {
+    DefiniteLength::Fraction(fraction).into()
+}
+
+/// Returns the Golden Ratio, i.e. `~(1.0 + sqrt(5.0)) / 2.0`.
+pub fn phi() -> DefiniteLength {
+    relative(1.61803398875)
+}
+
+pub fn rems(rems: f32) -> Rems {
+    Rems(rems)
+}
+
+pub const fn px(pixels: f32) -> Pixels {
+    Pixels(pixels)
+}
+
+pub fn auto() -> Length {
+    Length::Auto
+}
+
+impl From<Pixels> for Length {
+    fn from(pixels: Pixels) -> Self {
+        Self::Definite(pixels.into())
+    }
+}
+
+impl From<Rems> for Length {
+    fn from(rems: Rems) -> Self {
+        Self::Definite(rems.into())
+    }
+}
+
+impl From<DefiniteLength> for Length {
+    fn from(length: DefiniteLength) -> Self {
+        Self::Definite(length)
+    }
+}
+
+impl From<AbsoluteLength> for Length {
+    fn from(length: AbsoluteLength) -> Self {
+        Self::Definite(length.into())
+    }
+}
+
+impl Default for Length {
+    fn default() -> Self {
+        Self::Definite(DefiniteLength::default())
+    }
+}
+
+impl From<()> for Length {
+    fn from(_: ()) -> Self {
+        Self::Definite(DefiniteLength::default())
+    }
+}
+
+pub trait IsZero {
+    fn is_zero(&self) -> bool;
+}
+
+impl IsZero for DevicePixels {
+    fn is_zero(&self) -> bool {
+        self.0 == 0
+    }
+}
+
+impl IsZero for ScaledPixels {
+    fn is_zero(&self) -> bool {
+        self.0 == 0.
+    }
+}
+
+impl IsZero for Pixels {
+    fn is_zero(&self) -> bool {
+        self.0 == 0.
+    }
+}
+
+impl IsZero for Rems {
+    fn is_zero(&self) -> bool {
+        self.0 == 0.
+    }
+}
+
+impl IsZero for AbsoluteLength {
+    fn is_zero(&self) -> bool {
+        match self {
+            AbsoluteLength::Pixels(pixels) => pixels.is_zero(),
+            AbsoluteLength::Rems(rems) => rems.is_zero(),
+        }
+    }
+}
+
+impl IsZero for DefiniteLength {
+    fn is_zero(&self) -> bool {
+        match self {
+            DefiniteLength::Absolute(length) => length.is_zero(),
+            DefiniteLength::Fraction(fraction) => *fraction == 0.,
+        }
+    }
+}
+
+impl IsZero for Length {
+    fn is_zero(&self) -> bool {
+        match self {
+            Length::Definite(length) => length.is_zero(),
+            Length::Auto => false,
+        }
+    }
+}
+
+impl<T: IsZero + Debug + Clone + Default> IsZero for Point<T> {
+    fn is_zero(&self) -> bool {
+        self.x.is_zero() && self.y.is_zero()
+    }
+}
+
+impl<T> IsZero for Size<T>
+where
+    T: IsZero + Default + Debug + Clone,
+{
+    fn is_zero(&self) -> bool {
+        self.width.is_zero() || self.height.is_zero()
+    }
+}
+
+impl<T: IsZero + Debug + Clone + Default> IsZero for Bounds<T> {
+    fn is_zero(&self) -> bool {
+        self.size.is_zero()
+    }
+}
+
+impl<T> IsZero for Corners<T>
+where
+    T: IsZero + Clone + Default + Debug,
+{
+    fn is_zero(&self) -> bool {
+        self.top_left.is_zero()
+            && self.top_right.is_zero()
+            && self.bottom_right.is_zero()
+            && self.bottom_left.is_zero()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_bounds_intersects() {
+        let bounds1 = Bounds {
+            origin: Point { x: 0.0, y: 0.0 },
+            size: Size {
+                width: 5.0,
+                height: 5.0,
+            },
+        };
+        let bounds2 = Bounds {
+            origin: Point { x: 4.0, y: 4.0 },
+            size: Size {
+                width: 5.0,
+                height: 5.0,
+            },
+        };
+        let bounds3 = Bounds {
+            origin: Point { x: 10.0, y: 10.0 },
+            size: Size {
+                width: 5.0,
+                height: 5.0,
+            },
+        };
+
+        // Test Case 1: Intersecting bounds
+        assert_eq!(bounds1.intersects(&bounds2), true);
+
+        // Test Case 2: Non-Intersecting bounds
+        assert_eq!(bounds1.intersects(&bounds3), false);
+
+        // Test Case 3: Bounds intersecting with themselves
+        assert_eq!(bounds1.intersects(&bounds1), true);
+    }
+}

crates/gpui2/src/gpui2.rs 🔗

@@ -1,22 +1,364 @@
-pub mod adapter;
-pub mod color;
-pub mod element;
-pub mod elements;
-pub mod interactive;
-pub mod style;
-pub mod view;
-pub mod view_context;
+mod action;
+mod app;
+mod assets;
+mod color;
+mod element;
+mod elements;
+mod executor;
+mod focusable;
+mod geometry;
+mod image_cache;
+mod interactive;
+mod keymap;
+mod platform;
+mod scene;
+mod style;
+mod styled;
+mod subscription;
+mod svg_renderer;
+mod taffy;
+#[cfg(any(test, feature = "test-support"))]
+mod test;
+mod text_system;
+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::*;
+pub use assets::*;
 pub use color::*;
-pub use element::{AnyElement, Element, IntoElement, Layout, ParentElement};
-pub use geometry::{
-    rect::RectF,
-    vector::{vec2f, Vector2F},
-};
-pub use gpui::*;
-pub use gpui2_macros::{Element, *};
+pub use element::*;
+pub use elements::*;
+pub use executor::*;
+pub use focusable::*;
+pub use geometry::*;
+pub use gpui2_macros::*;
+pub use image_cache::*;
 pub use interactive::*;
-pub use platform::{Platform, WindowBounds, WindowOptions};
+pub use keymap::*;
+pub use platform::*;
+use private::Sealed;
+pub use refineable::*;
+pub use scene::*;
+pub use serde;
+pub use serde_json;
+pub use smallvec;
+pub use smol::Timer;
+pub use style::*;
+pub use styled::*;
+pub use subscription::*;
+pub use svg_renderer::*;
+pub use taffy::{AvailableSpace, LayoutId};
+#[cfg(any(test, feature = "test-support"))]
+pub use test::*;
+pub use text_system::*;
 pub use util::arc_cow::ArcCow;
 pub use view::*;
-pub use view_context::ViewContext;
+pub use window::*;
+
+use derive_more::{Deref, DerefMut};
+use std::{
+    any::{Any, TypeId},
+    borrow::{Borrow, BorrowMut},
+    mem,
+    ops::{Deref, DerefMut},
+    sync::Arc,
+};
+use taffy::TaffyLayoutEngine;
+
+type AnyBox = Box<dyn Any + Send>;
+
+pub trait Context {
+    type ModelContext<'a, T>;
+    type Result<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_view_state: impl FnOnce(&mut Self::ViewContext<'_, '_, V>) -> V,
+    ) -> Self::Result<View<V>>
+    where
+        V: 'static + Send;
+
+    fn update_view<V: 'static, R>(
+        &mut self,
+        view: &View<V>,
+        update: impl FnOnce(&mut V, &mut Self::ViewContext<'_, '_, V>) -> R,
+    ) -> Self::Result<R>;
+}
+
+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),
+    Type(TypeId),
+}
+
+#[repr(transparent)]
+pub struct MainThread<T>(T);
+
+impl<T> Deref for MainThread<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl<T> DerefMut for MainThread<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl<C: Context> Context for MainThread<C> {
+    type ModelContext<'a, T> = MainThread<C::ModelContext<'a, T>>;
+    type Result<T> = C::Result<T>;
+
+    fn build_model<T>(
+        &mut self,
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Self::Result<Model<T>>
+    where
+        T: 'static + Send,
+    {
+        self.0.build_model(|cx| {
+            let cx = unsafe {
+                mem::transmute::<
+                    &mut C::ModelContext<'_, T>,
+                    &mut MainThread<C::ModelContext<'_, T>>,
+                >(cx)
+            };
+            build_model(cx)
+        })
+    }
+
+    fn update_model<T: 'static, R>(
+        &mut self,
+        handle: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
+    ) -> Self::Result<R> {
+        self.0.update_model(handle, |entity, cx| {
+            let cx = unsafe {
+                mem::transmute::<
+                    &mut C::ModelContext<'_, T>,
+                    &mut MainThread<C::ModelContext<'_, T>>,
+                >(cx)
+            };
+            update(entity, cx)
+        })
+    }
+}
+
+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 + 'static>(&mut self, global: T);
+}
+
+impl<C> BorrowAppContext for C
+where
+    C: BorrowMut<AppContext>,
+{
+    fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R
+    where
+        F: FnOnce(&mut Self) -> R,
+    {
+        self.borrow_mut().push_text_style(style);
+        let result = f(self);
+        self.borrow_mut().pop_text_style();
+        result
+    }
+
+    fn set_global<G: 'static + Send>(&mut self, global: G) {
+        self.borrow_mut().set_global(global)
+    }
+}
+
+pub trait EventEmitter: 'static {
+    type Event: Any;
+}
+
+pub trait Flatten<T> {
+    fn flatten(self) -> Result<T>;
+}
+
+impl<T> Flatten<T> for Result<Result<T>> {
+    fn flatten(self) -> Result<T> {
+        self?
+    }
+}
+
+impl<T> Flatten<T> for Result<T> {
+    fn flatten(self) -> Result<T> {
+        self
+    }
+}
+
+#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
+pub struct SharedString(ArcCow<'static, str>);
+
+impl Default for SharedString {
+    fn default() -> Self {
+        Self(ArcCow::Owned("".into()))
+    }
+}
+
+impl AsRef<str> for SharedString {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
+impl Borrow<str> for SharedString {
+    fn borrow(&self) -> &str {
+        self.as_ref()
+    }
+}
+
+impl std::fmt::Debug for SharedString {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl std::fmt::Display for SharedString {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0.as_ref())
+    }
+}
+
+impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
+    fn from(value: T) -> Self {
+        Self(value.into())
+    }
+}
+
+pub enum Reference<'a, T> {
+    Immutable(&'a T),
+    Mutable(&'a mut T),
+}
+
+impl<'a, T> Deref for Reference<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        match self {
+            Reference::Immutable(target) => target,
+            Reference::Mutable(target) => target,
+        }
+    }
+}
+
+impl<'a, T> DerefMut for Reference<'a, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        match self {
+            Reference::Immutable(_) => {
+                panic!("cannot mutably deref an immutable reference. this is a bug in GPUI.");
+            }
+            Reference::Mutable(target) => target,
+        }
+    }
+}
+
+pub(crate) struct MainThreadOnly<T: ?Sized> {
+    executor: Executor,
+    value: Arc<T>,
+}
+
+impl<T: ?Sized> Clone for MainThreadOnly<T> {
+    fn clone(&self) -> Self {
+        Self {
+            executor: self.executor.clone(),
+            value: self.value.clone(),
+        }
+    }
+}
+
+/// Allows a value to be accessed only on the main thread, allowing a non-`Send` type
+/// to become `Send`.
+impl<T: 'static + ?Sized> MainThreadOnly<T> {
+    pub(crate) fn new(value: Arc<T>, executor: Executor) -> Self {
+        Self { executor, value }
+    }
+
+    pub(crate) fn borrow_on_main_thread(&self) -> &T {
+        assert!(self.executor.is_main_thread());
+        &self.value
+    }
+}
+
+unsafe impl<T: ?Sized> Send for MainThreadOnly<T> {}

crates/gpui2/src/image_cache.rs 🔗

@@ -0,0 +1,99 @@
+use crate::{ImageData, ImageId, SharedString};
+use collections::HashMap;
+use futures::{
+    future::{BoxFuture, Shared},
+    AsyncReadExt, FutureExt,
+};
+use image::ImageError;
+use parking_lot::Mutex;
+use std::sync::Arc;
+use thiserror::Error;
+use util::http::{self, HttpClient};
+
+#[derive(PartialEq, Eq, Hash, Clone)]
+pub struct RenderImageParams {
+    pub(crate) image_id: ImageId,
+}
+
+#[derive(Debug, Error, Clone)]
+pub enum Error {
+    #[error("http error: {0}")]
+    Client(#[from] http::Error),
+    #[error("IO error: {0}")]
+    Io(Arc<std::io::Error>),
+    #[error("unexpected http status: {status}, body: {body}")]
+    BadStatus {
+        status: http::StatusCode,
+        body: String,
+    },
+    #[error("image error: {0}")]
+    Image(Arc<ImageError>),
+}
+
+impl From<std::io::Error> for Error {
+    fn from(error: std::io::Error) -> Self {
+        Error::Io(Arc::new(error))
+    }
+}
+
+impl From<ImageError> for Error {
+    fn from(error: ImageError) -> Self {
+        Error::Image(Arc::new(error))
+    }
+}
+
+pub struct ImageCache {
+    client: Arc<dyn HttpClient>,
+    images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>,
+}
+
+type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
+
+impl ImageCache {
+    pub fn new(client: Arc<dyn HttpClient>) -> Self {
+        ImageCache {
+            client,
+            images: Default::default(),
+        }
+    }
+
+    pub fn get(
+        &self,
+        uri: impl Into<SharedString>,
+    ) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
+        let uri = uri.into();
+        let mut images = self.images.lock();
+
+        match images.get(&uri) {
+            Some(future) => future.clone(),
+            None => {
+                let client = self.client.clone();
+                let future = {
+                    let uri = uri.clone();
+                    async move {
+                        let mut response = client.get(uri.as_ref(), ().into(), true).await?;
+                        let mut body = Vec::new();
+                        response.body_mut().read_to_end(&mut body).await?;
+
+                        if !response.status().is_success() {
+                            return Err(Error::BadStatus {
+                                status: response.status(),
+                                body: String::from_utf8_lossy(&body).into_owned(),
+                            });
+                        }
+
+                        let format = image::guess_format(&body)?;
+                        let image =
+                            image::load_from_memory_with_format(&body, format)?.into_bgra8();
+                        Ok(Arc::new(ImageData::new(image)))
+                    }
+                }
+                .boxed()
+                .shared();
+
+                images.insert(uri, future.clone());
+                future
+            }
+        }
+    }
+}

crates/gpui2/src/interactive.rs 🔗

@@ -1,28 +1,67 @@
-use gpui::{
-    geometry::rect::RectF,
-    platform::{MouseButton, MouseButtonEvent},
-    EventContext,
+use crate::{
+    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};
+use parking_lot::Mutex;
+use refineable::Refineable;
 use smallvec::SmallVec;
-use std::{cell::Cell, rc::Rc};
+use std::{
+    any::{Any, TypeId},
+    fmt::Debug,
+    marker::PhantomData,
+    mem,
+    ops::Deref,
+    path::PathBuf,
+    sync::Arc,
+};
 
-use crate::ViewContext;
+const DRAG_THRESHOLD: f64 = 2.;
 
-pub trait Interactive<V: 'static> {
-    fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V>;
+pub trait StatelessInteractive<V: 'static>: Element<V> {
+    fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V>;
+
+    fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction().hover_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn group_hover(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction().group_hover_style = Some(GroupStyle {
+            group: group_name.into(),
+            style: f(StyleRefinement::default()),
+        });
+        self
+    }
 
     fn on_mouse_down(
         mut self,
         button: MouseButton,
-        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
-        self.interaction_handlers()
-            .mouse_down
-            .push(Rc::new(move |view, event, cx| {
-                if event.button == button {
+        self.stateless_interaction()
+            .mouse_down_listeners
+            .push(Box::new(move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.contains_point(&event.position)
+                {
                     handler(view, event, cx)
                 }
             }));
@@ -32,15 +71,18 @@ pub trait Interactive<V: 'static> {
     fn on_mouse_up(
         mut self,
         button: MouseButton,
-        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
-        self.interaction_handlers()
-            .mouse_up
-            .push(Rc::new(move |view, event, cx| {
-                if event.button == button {
+        self.stateless_interaction()
+            .mouse_up_listeners
+            .push(Box::new(move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.contains_point(&event.position)
+                {
                     handler(view, event, cx)
                 }
             }));
@@ -50,15 +92,18 @@ pub trait Interactive<V: 'static> {
     fn on_mouse_down_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
-        self.interaction_handlers()
-            .mouse_down_out
-            .push(Rc::new(move |view, event, cx| {
-                if event.button == button {
+        self.stateless_interaction()
+            .mouse_down_listeners
+            .push(Box::new(move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture
+                    && event.button == button
+                    && !bounds.contains_point(&event.position)
+                {
                     handler(view, event, cx)
                 }
             }));
@@ -68,98 +113,1005 @@ pub trait Interactive<V: 'static> {
     fn on_mouse_up_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + Send + 'static,
     ) -> Self
     where
         Self: Sized,
     {
-        self.interaction_handlers()
-            .mouse_up_out
-            .push(Rc::new(move |view, event, cx| {
-                if event.button == button {
-                    handler(view, event, cx)
+        self.stateless_interaction()
+            .mouse_up_listeners
+            .push(Box::new(move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture
+                    && event.button == button
+                    && !bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx);
                 }
             }));
         self
     }
 
+    fn on_mouse_move(
+        mut self,
+        handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + Send + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction()
+            .mouse_move_listeners
+            .push(Box::new(move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx);
+                }
+            }));
+        self
+    }
+
+    fn on_scroll_wheel(
+        mut self,
+        handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + Send + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction()
+            .scroll_wheel_listeners
+            .push(Box::new(move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx);
+                }
+            }));
+        self
+    }
+
+    fn context<C>(mut self, context: C) -> Self
+    where
+        Self: Sized,
+        C: TryInto<DispatchContext>,
+        C::Error: Debug,
+    {
+        self.stateless_interaction().dispatch_context =
+            context.try_into().expect("invalid dispatch context");
+        self
+    }
+
+    fn on_action<A: 'static>(
+        mut self,
+        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>(),
+            Box::new(move |view, event, _, phase, cx| {
+                let event = event.downcast_ref().unwrap();
+                listener(view, event, phase, cx);
+                None
+            }),
+        ));
+        self
+    }
+
+    fn on_key_down(
+        mut self,
+        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>(),
+            Box::new(move |view, event, _, phase, cx| {
+                let event = event.downcast_ref().unwrap();
+                listener(view, event, phase, cx);
+                None
+            }),
+        ));
+        self
+    }
+
+    fn on_key_up(
+        mut self,
+        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>(),
+            Box::new(move |view, event, _, phase, cx| {
+                let event = event.downcast_ref().unwrap();
+                listener(view, event, phase, cx);
+                None
+            }),
+        ));
+        self
+    }
+
+    fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction()
+            .drag_over_styles
+            .push((TypeId::of::<S>(), f(StyleRefinement::default())));
+        self
+    }
+
+    fn group_drag_over<S: 'static>(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction().group_drag_over_styles.push((
+            TypeId::of::<S>(),
+            GroupStyle {
+                group: group_name.into(),
+                style: f(StyleRefinement::default()),
+            },
+        ));
+        self
+    }
+
+    fn on_drop<W: 'static + Send>(
+        mut self,
+        listener: impl Fn(&mut V, View<W>, &mut ViewContext<V>) + Send + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateless_interaction().drop_listeners.push((
+            TypeId::of::<W>(),
+            Box::new(move |view, dragged_view, cx| {
+                listener(view, dragged_view.downcast().unwrap(), cx);
+            }),
+        ));
+        self
+    }
+}
+
+pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
+    fn stateful_interaction(&mut self) -> &mut StatefulInteraction<V>;
+
+    fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateful_interaction().active_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn group_active(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateful_interaction().group_active_style = Some(GroupStyle {
+            group: group_name.into(),
+            style: f(StyleRefinement::default()),
+        });
+        self
+    }
+
     fn on_click(
-        self,
-        button: MouseButton,
-        handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
+        mut self,
+        listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + Send + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.stateful_interaction()
+            .click_listeners
+            .push(Box::new(move |view, event, cx| listener(view, event, cx)));
+        self
+    }
+
+    fn on_drag<W>(
+        mut self,
+        listener: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + Send + 'static,
     ) -> Self
     where
         Self: Sized,
+        W: 'static + Send + Render,
     {
-        let pressed = Rc::new(Cell::new(false));
-        self.on_mouse_down(button, {
-            let pressed = pressed.clone();
-            move |_, _, _| {
-                pressed.set(true);
+        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(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 {
+    fn as_stateless(&self) -> &StatelessInteraction<V>;
+    fn as_stateless_mut(&mut self) -> &mut StatelessInteraction<V>;
+    fn as_stateful(&self) -> Option<&StatefulInteraction<V>>;
+    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteraction<V>>;
+
+    fn initialize<R>(
+        &mut self,
+        cx: &mut ViewContext<V>,
+        f: impl FnOnce(&mut ViewContext<V>) -> R,
+    ) -> R {
+        if let Some(stateful) = self.as_stateful_mut() {
+            cx.with_element_id(stateful.id.clone(), |global_id, cx| {
+                stateful.key_listeners.push((
+                    TypeId::of::<KeyDownEvent>(),
+                    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) =
+                                cx.match_keystroke(&global_id, &key_down.keystroke, context)
+                            {
+                                return Some(action);
+                            }
+                        }
+
+                        None
+                    }),
+                ));
+                let result = stateful.stateless.initialize(cx, f);
+                stateful.key_listeners.pop();
+                result
+            })
+        } else {
+            let stateless = self.as_stateless_mut();
+            cx.with_key_dispatch_context(stateless.dispatch_context.clone(), |cx| {
+                cx.with_key_listeners(mem::take(&mut stateless.key_listeners), f)
+            })
+        }
+    }
+
+    fn refine_style(
+        &self,
+        style: &mut Style,
+        bounds: Bounds<Pixels>,
+        element_state: &InteractiveElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        let mouse_position = cx.mouse_position();
+        let stateless = self.as_stateless();
+        if let Some(group_hover) = stateless.group_hover_style.as_ref() {
+            if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
+                if group_bounds.contains_point(&mouse_position) {
+                    style.refine(&group_hover.style);
+                }
             }
-        })
-        .on_mouse_up_out(button, {
-            let pressed = pressed.clone();
-            move |_, _, _| {
-                pressed.set(false);
+        }
+        if bounds.contains_point(&mouse_position) {
+            style.refine(&stateless.hover_style);
+        }
+
+        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.view.entity_type()
+                        && group_bounds.contains_point(&mouse_position)
+                    {
+                        style.refine(&group_drag_style.style);
+                    }
+                }
             }
-        })
-        .on_mouse_up(button, move |view, event, cx| {
-            if pressed.get() {
-                pressed.set(false);
-                handler(view, event, cx);
+
+            for (state_type, drag_over_style) in &self.as_stateless().drag_over_styles {
+                if *state_type == drag.view.entity_type() && bounds.contains_point(&mouse_position)
+                {
+                    style.refine(drag_over_style);
+                }
             }
-        })
-    }
-}
 
-pub struct InteractionHandlers<V: 'static> {
-    mouse_down: SmallVec<[Rc<dyn Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>)>; 2]>,
-    mouse_down_out: SmallVec<[Rc<dyn Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>)>; 2]>,
-    mouse_up: SmallVec<[Rc<dyn Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>)>; 2]>,
-    mouse_up_out: SmallVec<[Rc<dyn Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>)>; 2]>,
-}
+            cx.active_drag = Some(drag);
+        }
 
-impl<V: 'static> InteractionHandlers<V> {
-    pub fn paint(&self, order: u32, bounds: RectF, cx: &mut ViewContext<V>) {
-        for handler in self.mouse_down.iter().cloned() {
-            cx.on_event(order, move |view, event: &MouseButtonEvent, cx| {
-                if event.is_down && bounds.contains_point(event.position) {
-                    handler(view, event, cx);
+        if let Some(stateful) = self.as_stateful() {
+            let active_state = element_state.active_state.lock();
+            if active_state.group {
+                if let Some(group_style) = stateful.group_active_style.as_ref() {
+                    style.refine(&group_style.style);
                 }
+            }
+            if active_state.element {
+                style.refine(&stateful.active_style);
+            }
+        }
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        content_size: Size<Pixels>,
+        overflow: Point<Overflow>,
+        element_state: &mut InteractiveElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        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 handler in self.mouse_up.iter().cloned() {
-            cx.on_event(order, move |view, event: &MouseButtonEvent, cx| {
-                if !event.is_down && bounds.contains_point(event.position) {
-                    handler(view, event, cx);
-                }
+
+        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 handler in self.mouse_down_out.iter().cloned() {
-            cx.on_event(order, move |view, event: &MouseButtonEvent, cx| {
-                if event.is_down && !bounds.contains_point(event.position) {
-                    handler(view, event, cx);
-                }
+
+        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 handler in self.mouse_up_out.iter().cloned() {
-            cx.on_event(order, move |view, event: &MouseButtonEvent, cx| {
-                if !event.is_down && !bounds.contains_point(event.position) {
-                    handler(view, event, cx);
-                }
+
+        for listener in stateless.scroll_wheel_listeners.drain(..) {
+            cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| {
+                listener(state, event, &bounds, phase, cx);
             })
         }
+
+        let hover_group_bounds = stateless
+            .group_hover_style
+            .as_ref()
+            .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
+
+        if let Some(group_bounds) = hover_group_bounds {
+            let hovered = group_bounds.contains_point(&cx.mouse_position());
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    if group_bounds.contains_point(&event.position) != hovered {
+                        cx.notify();
+                    }
+                }
+            });
+        }
+
+        if stateless.hover_style.is_some()
+            || (cx.active_drag.is_some() && !stateless.drag_over_styles.is_empty())
+        {
+            let hovered = bounds.contains_point(&cx.mouse_position());
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    if bounds.contains_point(&event.position) != hovered {
+                        cx.notify();
+                    }
+                }
+            });
+        }
+
+        if cx.active_drag.is_some() {
+            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.view.entity_type())
+                    {
+                        for (drop_state_type, listener) in &drop_listeners {
+                            if *drop_state_type == drag_state_type {
+                                let drag = cx
+                                    .active_drag
+                                    .take()
+                                    .expect("checked for type drag state type above");
+                                listener(view, drag.view.clone(), cx);
+                                cx.notify();
+                                cx.stop_propagation();
+                            }
+                        }
+                    }
+                }
+            });
+        }
+
+        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();
+                let mouse_down = pending_mouse_down.lock().clone();
+                if let Some(mouse_down) = mouse_down {
+                    if let Some(drag_listener) = drag_listener {
+                        let active_state = element_state.active_state.clone();
+
+                        cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+                            if cx.active_drag.is_some() {
+                                if phase == DispatchPhase::Capture {
+                                    cx.notify();
+                                }
+                            } else if phase == DispatchPhase::Bubble
+                                && bounds.contains_point(&event.position)
+                                && (event.position - mouse_down.position).magnitude()
+                                    > DRAG_THRESHOLD
+                            {
+                                *active_state.lock() = ActiveState::default();
+                                let cursor_offset = event.position - bounds.origin;
+                                let drag = drag_listener(view_state, cursor_offset, cx);
+                                cx.active_drag = Some(drag);
+                                cx.notify();
+                                cx.stop_propagation();
+                            }
+                        });
+                    }
+
+                    cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
+                        if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
+                        {
+                            let mouse_click = ClickEvent {
+                                down: mouse_down.clone(),
+                                up: event.clone(),
+                            };
+                            for listener in &click_listeners {
+                                listener(view_state, &mouse_click, cx);
+                            }
+                        }
+                        *pending_mouse_down.lock() = None;
+                    });
+                } else {
+                    cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| {
+                        if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
+                        {
+                            *pending_mouse_down.lock() = Some(event.clone());
+                        }
+                    });
+                }
+            }
+
+            let active_state = element_state.active_state.clone();
+            if active_state.lock().is_none() {
+                let active_group_bounds = stateful
+                    .group_active_style
+                    .as_ref()
+                    .and_then(|group_active| GroupBounds::get(&group_active.group, cx));
+                cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| {
+                    if phase == DispatchPhase::Bubble {
+                        let group = active_group_bounds
+                            .map_or(false, |bounds| bounds.contains_point(&down.position));
+                        let element = bounds.contains_point(&down.position);
+                        if group || element {
+                            *active_state.lock() = ActiveState { group, element };
+                            cx.notify();
+                        }
+                    }
+                });
+            } else {
+                cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
+                    if phase == DispatchPhase::Capture {
+                        *active_state.lock() = ActiveState::default();
+                        cx.notify();
+                    }
+                });
+            }
+
+            if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
+                let scroll_offset = element_state
+                    .scroll_offset
+                    .get_or_insert_with(Arc::default)
+                    .clone();
+                let line_height = cx.line_height();
+                let scroll_max = (content_size - bounds.size).max(&Size::default());
+
+                cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
+                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                        let mut scroll_offset = scroll_offset.lock();
+                        let old_scroll_offset = *scroll_offset;
+                        let delta = event.delta.pixel_delta(line_height);
+
+                        if overflow.x == Overflow::Scroll {
+                            scroll_offset.x =
+                                (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.));
+                        }
+
+                        if overflow.y == Overflow::Scroll {
+                            scroll_offset.y =
+                                (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.));
+                        }
+
+                        if *scroll_offset != old_scroll_offset {
+                            cx.notify();
+                            cx.stop_propagation();
+                        }
+                    }
+                });
+            }
+        }
+    }
+}
+
+#[derive(Deref, DerefMut)]
+pub struct StatefulInteraction<V> {
+    pub id: ElementId,
+    #[deref]
+    #[deref_mut]
+    stateless: StatelessInteraction<V>,
+    click_listeners: SmallVec<[ClickListener<V>; 2]>,
+    active_style: StyleRefinement,
+    group_active_style: Option<GroupStyle>,
+    drag_listener: Option<DragListener<V>>,
+}
+
+impl<V: 'static> ElementInteraction<V> for StatefulInteraction<V> {
+    fn as_stateful(&self) -> Option<&StatefulInteraction<V>> {
+        Some(self)
+    }
+
+    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteraction<V>> {
+        Some(self)
+    }
+
+    fn as_stateless(&self) -> &StatelessInteraction<V> {
+        &self.stateless
+    }
+
+    fn as_stateless_mut(&mut self) -> &mut StatelessInteraction<V> {
+        &mut self.stateless
+    }
+}
+
+impl<V> From<ElementId> for StatefulInteraction<V> {
+    fn from(id: ElementId) -> Self {
+        Self {
+            id,
+            stateless: StatelessInteraction::default(),
+            click_listeners: SmallVec::new(),
+            drag_listener: None,
+            active_style: StyleRefinement::default(),
+            group_active_style: None,
+        }
+    }
+}
+
+type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static + Send;
+
+pub struct StatelessInteraction<V> {
+    pub dispatch_context: DispatchContext,
+    pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
+    pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
+    pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
+    pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
+    pub key_listeners: SmallVec<[(TypeId, KeyListener<V>); 32]>,
+    pub hover_style: StyleRefinement,
+    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, Box<DropListener<V>>); 2]>,
+}
+
+impl<V> StatelessInteraction<V> {
+    pub fn into_stateful(self, id: impl Into<ElementId>) -> StatefulInteraction<V> {
+        StatefulInteraction {
+            id: id.into(),
+            stateless: self,
+            click_listeners: SmallVec::new(),
+            drag_listener: None,
+            active_style: StyleRefinement::default(),
+            group_active_style: None,
+        }
+    }
+}
+
+pub struct GroupStyle {
+    pub group: SharedString,
+    pub style: StyleRefinement,
+}
+
+#[derive(Default)]
+pub struct GroupBounds(HashMap<SharedString, SmallVec<[Bounds<Pixels>; 1]>>);
+
+impl GroupBounds {
+    pub fn get(name: &SharedString, cx: &mut AppContext) -> Option<Bounds<Pixels>> {
+        cx.default_global::<Self>()
+            .0
+            .get(name)
+            .and_then(|bounds_stack| bounds_stack.last())
+            .cloned()
+    }
+
+    pub fn push(name: SharedString, bounds: Bounds<Pixels>, cx: &mut AppContext) {
+        cx.default_global::<Self>()
+            .0
+            .entry(name)
+            .or_default()
+            .push(bounds);
+    }
+
+    pub fn pop(name: &SharedString, cx: &mut AppContext) {
+        cx.default_global::<Self>().0.get_mut(name).unwrap().pop();
     }
 }
 
-impl<V> Default for InteractionHandlers<V> {
+#[derive(Copy, Clone, Default, Eq, PartialEq)]
+struct ActiveState {
+    pub group: bool,
+    pub element: bool,
+}
+
+impl ActiveState {
+    pub fn is_none(&self) -> bool {
+        !self.group && !self.element
+    }
+}
+
+#[derive(Default)]
+pub struct InteractiveElementState {
+    active_state: Arc<Mutex<ActiveState>>,
+    pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
+    scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
+}
+
+impl InteractiveElementState {
+    pub fn scroll_offset(&self) -> Option<Point<Pixels>> {
+        self.scroll_offset
+            .as_ref()
+            .map(|offset| offset.lock().clone())
+    }
+}
+
+impl<V> Default for StatelessInteraction<V> {
     fn default() -> Self {
         Self {
-            mouse_down: Default::default(),
-            mouse_up: Default::default(),
-            mouse_down_out: Default::default(),
-            mouse_up_out: Default::default(),
+            dispatch_context: DispatchContext::default(),
+            mouse_down_listeners: SmallVec::new(),
+            mouse_up_listeners: SmallVec::new(),
+            mouse_move_listeners: SmallVec::new(),
+            scroll_wheel_listeners: SmallVec::new(),
+            key_listeners: SmallVec::new(),
+            hover_style: StyleRefinement::default(),
+            group_hover_style: None,
+            drag_over_styles: SmallVec::new(),
+            group_drag_over_styles: SmallVec::new(),
+            drop_listeners: SmallVec::new(),
+        }
+    }
+}
+
+impl<V: 'static> ElementInteraction<V> for StatelessInteraction<V> {
+    fn as_stateful(&self) -> Option<&StatefulInteraction<V>> {
+        None
+    }
+
+    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteraction<V>> {
+        None
+    }
+
+    fn as_stateless(&self) -> &StatelessInteraction<V> {
+        self
+    }
+
+    fn as_stateless_mut(&mut self) -> &mut StatelessInteraction<V> {
+        self
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct KeyDownEvent {
+    pub keystroke: Keystroke,
+    pub is_held: bool,
+}
+
+#[derive(Clone, Debug)]
+pub struct KeyUpEvent {
+    pub keystroke: Keystroke,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct ModifiersChangedEvent {
+    pub modifiers: Modifiers,
+}
+
+impl Deref for ModifiersChangedEvent {
+    type Target = Modifiers;
+
+    fn deref(&self) -> &Self::Target {
+        &self.modifiers
+    }
+}
+
+/// The phase of a touch motion event.
+/// Based on the winit enum of the same name.
+#[derive(Clone, Copy, Debug)]
+pub enum TouchPhase {
+    Started,
+    Moved,
+    Ended,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseDownEvent {
+    pub button: MouseButton,
+    pub position: Point<Pixels>,
+    pub modifiers: Modifiers,
+    pub click_count: usize,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseUpEvent {
+    pub button: MouseButton,
+    pub position: Point<Pixels>,
+    pub modifiers: Modifiers,
+    pub click_count: usize,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct ClickEvent {
+    pub down: MouseDownEvent,
+    pub up: MouseUpEvent,
+}
+
+pub struct Drag<S, R, V, E>
+where
+    R: Fn(&mut V, &mut ViewContext<V>) -> E,
+    V: 'static,
+    E: Component<()>,
+{
+    pub state: S,
+    pub render_drag_handle: R,
+    view_type: PhantomData<V>,
+}
+
+impl<S, R, V, E> Drag<S, R, V, E>
+where
+    R: Fn(&mut V, &mut ViewContext<V>) -> E,
+    V: 'static,
+    E: Component<()>,
+{
+    pub fn new(state: S, render_drag_handle: R) -> Self {
+        Drag {
+            state,
+            render_drag_handle,
+            view_type: PhantomData,
+        }
+    }
+}
+
+// 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,
+    Right,
+    Middle,
+    Navigate(NavigationDirection),
+}
+
+impl MouseButton {
+    pub fn all() -> Vec<Self> {
+        vec![
+            MouseButton::Left,
+            MouseButton::Right,
+            MouseButton::Middle,
+            MouseButton::Navigate(NavigationDirection::Back),
+            MouseButton::Navigate(NavigationDirection::Forward),
+        ]
+    }
+}
+
+impl Default for MouseButton {
+    fn default() -> Self {
+        Self::Left
+    }
+}
+
+#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
+pub enum NavigationDirection {
+    Back,
+    Forward,
+}
+
+impl Default for NavigationDirection {
+    fn default() -> Self {
+        Self::Back
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseMoveEvent {
+    pub position: Point<Pixels>,
+    pub pressed_button: Option<MouseButton>,
+    pub modifiers: Modifiers,
+}
+
+#[derive(Clone, Debug)]
+pub struct ScrollWheelEvent {
+    pub position: Point<Pixels>,
+    pub delta: ScrollDelta,
+    pub modifiers: Modifiers,
+    pub touch_phase: TouchPhase,
+}
+
+impl Deref for ScrollWheelEvent {
+    type Target = Modifiers;
+
+    fn deref(&self) -> &Self::Target {
+        &self.modifiers
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum ScrollDelta {
+    Pixels(Point<Pixels>),
+    Lines(Point<f32>),
+}
+
+impl Default for ScrollDelta {
+    fn default() -> Self {
+        Self::Lines(Default::default())
+    }
+}
+
+impl ScrollDelta {
+    pub fn precise(&self) -> bool {
+        match self {
+            ScrollDelta::Pixels(_) => true,
+            ScrollDelta::Lines(_) => false,
         }
     }
+
+    pub fn pixel_delta(&self, line_height: Pixels) -> Point<Pixels> {
+        match self {
+            ScrollDelta::Pixels(delta) => *delta,
+            ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseExitEvent {
+    pub position: Point<Pixels>,
+    pub pressed_button: Option<MouseButton>,
+    pub modifiers: Modifiers,
+}
+
+impl Deref for MouseExitEvent {
+    type Target = Modifiers;
+
+    fn deref(&self) -> &Self::Target {
+        &self.modifiers
+    }
+}
+
+#[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 {
+        position: Point<Pixels>,
+        files: ExternalPaths,
+    },
+    Pending {
+        position: Point<Pixels>,
+    },
+    Submit {
+        position: Point<Pixels>,
+    },
+    Exited,
 }
+
+#[derive(Clone, Debug)]
+pub enum InputEvent {
+    KeyDown(KeyDownEvent),
+    KeyUp(KeyUpEvent),
+    ModifiersChanged(ModifiersChangedEvent),
+    MouseDown(MouseDownEvent),
+    MouseUp(MouseUpEvent),
+    MouseMove(MouseMoveEvent),
+    MouseExited(MouseExitEvent),
+    ScrollWheel(ScrollWheelEvent),
+    FileDrop(FileDropEvent),
+}
+
+impl InputEvent {
+    pub fn position(&self) -> Option<Point<Pixels>> {
+        match self {
+            InputEvent::KeyDown { .. } => None,
+            InputEvent::KeyUp { .. } => None,
+            InputEvent::ModifiersChanged { .. } => None,
+            InputEvent::MouseDown(event) => Some(event.position),
+            InputEvent::MouseUp(event) => Some(event.position),
+            InputEvent::MouseMove(event) => Some(event.position),
+            InputEvent::MouseExited(event) => Some(event.position),
+            InputEvent::ScrollWheel(event) => Some(event.position),
+            InputEvent::FileDrop(FileDropEvent::Exited) => None,
+            InputEvent::FileDrop(
+                FileDropEvent::Entered { position, .. }
+                | FileDropEvent::Pending { position, .. }
+                | FileDropEvent::Submit { position, .. },
+            ) => Some(*position),
+        }
+    }
+
+    pub fn mouse_event<'a>(&'a self) -> Option<&'a dyn Any> {
+        match self {
+            InputEvent::KeyDown { .. } => None,
+            InputEvent::KeyUp { .. } => None,
+            InputEvent::ModifiersChanged { .. } => None,
+            InputEvent::MouseDown(event) => Some(event),
+            InputEvent::MouseUp(event) => Some(event),
+            InputEvent::MouseMove(event) => Some(event),
+            InputEvent::MouseExited(event) => Some(event),
+            InputEvent::ScrollWheel(event) => Some(event),
+            InputEvent::FileDrop(event) => Some(event),
+        }
+    }
+
+    pub fn keyboard_event<'a>(&'a self) -> Option<&'a dyn Any> {
+        match self {
+            InputEvent::KeyDown(event) => Some(event),
+            InputEvent::KeyUp(event) => Some(event),
+            InputEvent::ModifiersChanged(event) => Some(event),
+            InputEvent::MouseDown(_) => None,
+            InputEvent::MouseUp(_) => None,
+            InputEvent::MouseMove(_) => None,
+            InputEvent::MouseExited(_) => None,
+            InputEvent::ScrollWheel(_) => None,
+            InputEvent::FileDrop(_) => None,
+        }
+    }
+}
+
+pub struct FocusEvent {
+    pub blurred: Option<FocusHandle>,
+    pub focused: Option<FocusHandle>,
+}
+
+pub type MouseDownListener<V> = Box<
+    dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
+        + Send
+        + 'static,
+>;
+pub type MouseUpListener<V> = Box<
+    dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
+        + Send
+        + 'static,
+>;
+
+pub type MouseMoveListener<V> = Box<
+    dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
+        + Send
+        + 'static,
+>;
+
+pub type ScrollWheelListener<V> = Box<
+    dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
+        + Send
+        + 'static,
+>;
+
+pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + Send + 'static>;
+
+pub(crate) type DragListener<V> =
+    Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + Send + 'static>;
+
+pub type KeyListener<V> = Box<
+    dyn Fn(
+            &mut V,
+            &dyn Any,
+            &[&DispatchContext],
+            DispatchPhase,
+            &mut ViewContext<V>,
+        ) -> Option<Box<dyn Action>>
+        + Send
+        + 'static,
+>;

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

@@ -0,0 +1,80 @@
+use crate::{Action, DispatchContext, DispatchContextPredicate, KeyMatch, Keystroke};
+use anyhow::Result;
+use smallvec::SmallVec;
+
+pub struct KeyBinding {
+    action: Box<dyn Action>,
+    pub(super) keystrokes: SmallVec<[Keystroke; 2]>,
+    pub(super) context_predicate: Option<DispatchContextPredicate>,
+}
+
+impl KeyBinding {
+    pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
+        Self::load(keystrokes, Box::new(action), context_predicate).unwrap()
+    }
+
+    pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
+        let context = if let Some(context) = context {
+            Some(DispatchContextPredicate::parse(context)?)
+        } else {
+            None
+        };
+
+        let keystrokes = keystrokes
+            .split_whitespace()
+            .map(Keystroke::parse)
+            .collect::<Result<_>>()?;
+
+        Ok(Self {
+            keystrokes,
+            action,
+            context_predicate: context,
+        })
+    }
+
+    pub fn matches_context(&self, contexts: &[&DispatchContext]) -> bool {
+        self.context_predicate
+            .as_ref()
+            .map(|predicate| predicate.eval(contexts))
+            .unwrap_or(true)
+    }
+
+    pub fn match_keystrokes(
+        &self,
+        pending_keystrokes: &[Keystroke],
+        contexts: &[&DispatchContext],
+    ) -> KeyMatch {
+        if self.keystrokes.as_ref().starts_with(&pending_keystrokes)
+            && self.matches_context(contexts)
+        {
+            // If the binding is completed, push it onto the matches list
+            if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
+                KeyMatch::Some(self.action.boxed_clone())
+            } else {
+                KeyMatch::Pending
+            }
+        } else {
+            KeyMatch::None
+        }
+    }
+
+    pub fn keystrokes_for_action(
+        &self,
+        action: &dyn Action,
+        contexts: &[&DispatchContext],
+    ) -> Option<SmallVec<[Keystroke; 2]>> {
+        if self.action.partial_eq(action) && self.matches_context(contexts) {
+            Some(self.keystrokes.clone())
+        } else {
+            None
+        }
+    }
+
+    pub fn keystrokes(&self) -> &[Keystroke] {
+        self.keystrokes.as_slice()
+    }
+
+    pub fn action(&self) -> &dyn Action {
+        self.action.as_ref()
+    }
+}

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

@@ -0,0 +1,398 @@
+use crate::{DispatchContextPredicate, KeyBinding, Keystroke};
+use collections::HashSet;
+use smallvec::SmallVec;
+use std::{any::TypeId, collections::HashMap};
+
+#[derive(Copy, Clone, Eq, PartialEq, Default)]
+pub struct KeymapVersion(usize);
+
+#[derive(Default)]
+pub struct Keymap {
+    bindings: Vec<KeyBinding>,
+    binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
+    disabled_keystrokes:
+        HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<DispatchContextPredicate>>>,
+    version: KeymapVersion,
+}
+
+impl Keymap {
+    pub fn new(bindings: Vec<KeyBinding>) -> Self {
+        let mut this = Self::default();
+        this.add_bindings(bindings);
+        this
+    }
+
+    pub fn version(&self) -> KeymapVersion {
+        self.version
+    }
+
+    pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator<Item = &'_ KeyBinding> {
+        self.binding_indices_by_action_id
+            .get(&action_id)
+            .map(SmallVec::as_slice)
+            .unwrap_or(&[])
+            .iter()
+            .map(|ix| &self.bindings[*ix])
+            .filter(|binding| !self.binding_disabled(binding))
+    }
+
+    pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
+        // todo!("no action")
+        // let no_action_id = (NoAction {}).id();
+        let mut new_bindings = Vec::new();
+        let has_new_disabled_keystrokes = false;
+        for binding in bindings {
+            // if binding.action().id() == no_action_id {
+            //     has_new_disabled_keystrokes |= self
+            //         .disabled_keystrokes
+            //         .entry(binding.keystrokes)
+            //         .or_default()
+            //         .insert(binding.context_predicate);
+            // } else {
+            new_bindings.push(binding);
+            // }
+        }
+
+        if has_new_disabled_keystrokes {
+            self.binding_indices_by_action_id.retain(|_, indices| {
+                indices.retain(|ix| {
+                    let binding = &self.bindings[*ix];
+                    match self.disabled_keystrokes.get(&binding.keystrokes) {
+                        Some(disabled_predicates) => {
+                            !disabled_predicates.contains(&binding.context_predicate)
+                        }
+                        None => true,
+                    }
+                });
+                !indices.is_empty()
+            });
+        }
+
+        for new_binding in new_bindings {
+            if !self.binding_disabled(&new_binding) {
+                self.binding_indices_by_action_id
+                    .entry(new_binding.action().as_any().type_id())
+                    .or_default()
+                    .push(self.bindings.len());
+                self.bindings.push(new_binding);
+            }
+        }
+
+        self.version.0 += 1;
+    }
+
+    pub fn clear(&mut self) {
+        self.bindings.clear();
+        self.binding_indices_by_action_id.clear();
+        self.disabled_keystrokes.clear();
+        self.version.0 += 1;
+    }
+
+    pub fn bindings(&self) -> Vec<&KeyBinding> {
+        self.bindings
+            .iter()
+            .filter(|binding| !self.binding_disabled(binding))
+            .collect()
+    }
+
+    fn binding_disabled(&self, binding: &KeyBinding) -> bool {
+        match self.disabled_keystrokes.get(&binding.keystrokes) {
+            Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate),
+            None => false,
+        }
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use crate::actions;
+
+//     use super::*;
+
+//     actions!(
+//         keymap_test,
+//         [Present1, Present2, Present3, Duplicate, Missing]
+//     );
+
+//     #[test]
+//     fn regular_keymap() {
+//         let present_1 = Binding::new("ctrl-q", Present1 {}, None);
+//         let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+//         let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
+//         let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
+//         let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+//         let missing = Binding::new("ctrl-r", Missing {}, None);
+//         let all_bindings = [
+//             &present_1,
+//             &present_2,
+//             &present_3,
+//             &keystroke_duplicate_to_1,
+//             &full_duplicate_to_2,
+//             &missing,
+//         ];
+
+//         let mut keymap = Keymap::default();
+//         assert_absent(&keymap, &all_bindings);
+//         assert!(keymap.bindings().is_empty());
+
+//         keymap.add_bindings([present_1.clone(), present_2.clone(), present_3.clone()]);
+//         assert_absent(&keymap, &[&keystroke_duplicate_to_1, &missing]);
+//         assert_present(
+//             &keymap,
+//             &[(&present_1, "q"), (&present_2, "w"), (&present_3, "e")],
+//         );
+
+//         keymap.add_bindings([
+//             keystroke_duplicate_to_1.clone(),
+//             full_duplicate_to_2.clone(),
+//         ]);
+//         assert_absent(&keymap, &[&missing]);
+//         assert!(
+//             !keymap.binding_disabled(&keystroke_duplicate_to_1),
+//             "Duplicate binding 1 was added and should not be disabled"
+//         );
+//         assert!(
+//             !keymap.binding_disabled(&full_duplicate_to_2),
+//             "Duplicate binding 2 was added and should not be disabled"
+//         );
+
+//         assert_eq!(
+//             keymap
+//                 .bindings_for_action(keystroke_duplicate_to_1.action().id())
+//                 .map(|binding| &binding.keystrokes)
+//                 .flatten()
+//                 .collect::<Vec<_>>(),
+//             vec![&Keystroke {
+//                 ctrl: true,
+//                 alt: false,
+//                 shift: false,
+//                 cmd: false,
+//                 function: false,
+//                 key: "q".to_string(),
+//                 ime_key: None,
+//             }],
+//             "{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap"
+//         );
+//         assert_eq!(
+//             keymap
+//                 .bindings_for_action(full_duplicate_to_2.action().id())
+//                 .map(|binding| &binding.keystrokes)
+//                 .flatten()
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 &Keystroke {
+//                     ctrl: true,
+//                     alt: false,
+//                     shift: false,
+//                     cmd: false,
+//                     function: false,
+//                     key: "w".to_string(),
+//                     ime_key: None,
+//                 },
+//                 &Keystroke {
+//                     ctrl: true,
+//                     alt: false,
+//                     shift: false,
+//                     cmd: false,
+//                     function: false,
+//                     key: "w".to_string(),
+//                     ime_key: None,
+//                 }
+//             ],
+//             "{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap"
+//         );
+
+//         let updated_bindings = keymap.bindings();
+//         let expected_updated_bindings = vec![
+//             &present_1,
+//             &present_2,
+//             &present_3,
+//             &keystroke_duplicate_to_1,
+//             &full_duplicate_to_2,
+//         ];
+//         assert_eq!(
+//             updated_bindings.len(),
+//             expected_updated_bindings.len(),
+//             "Unexpected updated keymap bindings {updated_bindings:?}"
+//         );
+//         for (i, expected) in expected_updated_bindings.iter().enumerate() {
+//             let keymap_binding = &updated_bindings[i];
+//             assert_eq!(
+//                 keymap_binding.context_predicate, expected.context_predicate,
+//                 "Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
+//             );
+//             assert_eq!(
+//                 keymap_binding.keystrokes, expected.keystrokes,
+//                 "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
+//             );
+//         }
+
+//         keymap.clear();
+//         assert_absent(&keymap, &all_bindings);
+//         assert!(keymap.bindings().is_empty());
+//     }
+
+//     #[test]
+//     fn keymap_with_ignored() {
+//         let present_1 = Binding::new("ctrl-q", Present1 {}, None);
+//         let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+//         let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor"));
+//         let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None);
+//         let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane"));
+//         let ignored_1 = Binding::new("ctrl-q", NoAction {}, None);
+//         let ignored_2 = Binding::new("ctrl-w", NoAction {}, Some("pane"));
+//         let ignored_3_with_other_context =
+//             Binding::new("ctrl-e", NoAction {}, Some("other_context"));
+
+//         let mut keymap = Keymap::default();
+
+//         keymap.add_bindings([
+//             ignored_1.clone(),
+//             ignored_2.clone(),
+//             ignored_3_with_other_context.clone(),
+//         ]);
+//         assert_absent(&keymap, &[&present_3]);
+//         assert_disabled(
+//             &keymap,
+//             &[
+//                 &present_1,
+//                 &present_2,
+//                 &ignored_1,
+//                 &ignored_2,
+//                 &ignored_3_with_other_context,
+//             ],
+//         );
+//         assert!(keymap.bindings().is_empty());
+//         keymap.clear();
+
+//         keymap.add_bindings([
+//             present_1.clone(),
+//             present_2.clone(),
+//             present_3.clone(),
+//             ignored_1.clone(),
+//             ignored_2.clone(),
+//             ignored_3_with_other_context.clone(),
+//         ]);
+//         assert_present(&keymap, &[(&present_3, "e")]);
+//         assert_disabled(
+//             &keymap,
+//             &[
+//                 &present_1,
+//                 &present_2,
+//                 &ignored_1,
+//                 &ignored_2,
+//                 &ignored_3_with_other_context,
+//             ],
+//         );
+//         keymap.clear();
+
+//         keymap.add_bindings([
+//             present_1.clone(),
+//             present_2.clone(),
+//             present_3.clone(),
+//             ignored_1.clone(),
+//         ]);
+//         assert_present(&keymap, &[(&present_2, "w"), (&present_3, "e")]);
+//         assert_disabled(&keymap, &[&present_1, &ignored_1]);
+//         assert_absent(&keymap, &[&ignored_2, &ignored_3_with_other_context]);
+//         keymap.clear();
+
+//         keymap.add_bindings([
+//             present_1.clone(),
+//             present_2.clone(),
+//             present_3.clone(),
+//             keystroke_duplicate_to_1.clone(),
+//             full_duplicate_to_2.clone(),
+//             ignored_1.clone(),
+//             ignored_2.clone(),
+//             ignored_3_with_other_context.clone(),
+//         ]);
+//         assert_present(&keymap, &[(&present_3, "e")]);
+//         assert_disabled(
+//             &keymap,
+//             &[
+//                 &present_1,
+//                 &present_2,
+//                 &keystroke_duplicate_to_1,
+//                 &full_duplicate_to_2,
+//                 &ignored_1,
+//                 &ignored_2,
+//                 &ignored_3_with_other_context,
+//             ],
+//         );
+//         keymap.clear();
+//     }
+
+//     #[track_caller]
+//     fn assert_present(keymap: &Keymap, expected_bindings: &[(&Binding, &str)]) {
+//         let keymap_bindings = keymap.bindings();
+//         assert_eq!(
+//             expected_bindings.len(),
+//             keymap_bindings.len(),
+//             "Unexpected keymap bindings {keymap_bindings:?}"
+//         );
+//         for (i, (expected, expected_key)) in expected_bindings.iter().enumerate() {
+//             assert!(
+//                 !keymap.binding_disabled(expected),
+//                 "{expected:?} should not be disabled as it was added into keymap for element {i}"
+//             );
+//             assert_eq!(
+//                 keymap
+//                     .bindings_for_action(expected.action().id())
+//                     .map(|binding| &binding.keystrokes)
+//                     .flatten()
+//                     .collect::<Vec<_>>(),
+//                 vec![&Keystroke {
+//                     ctrl: true,
+//                     alt: false,
+//                     shift: false,
+//                     cmd: false,
+//                     function: false,
+//                     key: expected_key.to_string(),
+//                     ime_key: None,
+//                 }],
+//                 "{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}"
+//             );
+
+//             let keymap_binding = &keymap_bindings[i];
+//             assert_eq!(
+//                 keymap_binding.context_predicate, expected.context_predicate,
+//                 "Unexpected context predicate for keymap {i} element: {keymap_binding:?}"
+//             );
+//             assert_eq!(
+//                 keymap_binding.keystrokes, expected.keystrokes,
+//                 "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}"
+//             );
+//         }
+//     }
+
+//     #[track_caller]
+//     fn assert_absent(keymap: &Keymap, bindings: &[&Binding]) {
+//         for binding in bindings.iter() {
+//             assert!(
+//                 !keymap.binding_disabled(binding),
+//                 "{binding:?} should not be disabled in the keymap where was not added"
+//             );
+//             assert_eq!(
+//                 keymap.bindings_for_action(binding.action().id()).count(),
+//                 0,
+//                 "{binding:?} should have no actions in the keymap where was not added"
+//             );
+//         }
+//     }
+
+//     #[track_caller]
+//     fn assert_disabled(keymap: &Keymap, bindings: &[&Binding]) {
+//         for binding in bindings.iter() {
+//             assert!(
+//                 keymap.binding_disabled(binding),
+//                 "{binding:?} should be disabled in the keymap"
+//             );
+//             assert_eq!(
+//                 keymap.bindings_for_action(binding.action().id()).count(),
+//                 0,
+//                 "{binding:?} should have no actions in the keymap where it was disabled"
+//             );
+//         }
+//     }
+// }

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

@@ -0,0 +1,473 @@
+use crate::{Action, DispatchContext, Keymap, KeymapVersion, Keystroke};
+use parking_lot::Mutex;
+use smallvec::SmallVec;
+use std::sync::Arc;
+
+pub struct KeyMatcher {
+    pending_keystrokes: Vec<Keystroke>,
+    keymap: Arc<Mutex<Keymap>>,
+    keymap_version: KeymapVersion,
+}
+
+impl KeyMatcher {
+    pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
+        let keymap_version = keymap.lock().version();
+        Self {
+            pending_keystrokes: Vec::new(),
+            keymap_version,
+            keymap,
+        }
+    }
+
+    // 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.lock().bindings_for_action(action_id)
+    // }
+
+    pub fn clear_pending(&mut self) {
+        self.pending_keystrokes.clear();
+    }
+
+    pub fn has_pending_keystrokes(&self) -> bool {
+        !self.pending_keystrokes.is_empty()
+    }
+
+    /// Pushes a keystroke onto the matcher.
+    /// The result of the new keystroke is returned:
+    ///     KeyMatch::None =>
+    ///         No match is valid for this key given any pending keystrokes.
+    ///     KeyMatch::Pending =>
+    ///         There exist bindings which are still waiting for more keys.
+    ///     KeyMatch::Complete(matches) =>
+    ///         One or more bindings have received the necessary key presses.
+    ///         Bindings added later will take precedence over earlier bindings.
+    pub fn match_keystroke(
+        &mut self,
+        keystroke: &Keystroke,
+        context_stack: &[&DispatchContext],
+    ) -> KeyMatch {
+        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();
+            self.pending_keystrokes.clear();
+        }
+
+        let mut pending_key = None;
+
+        for binding in keymap.bindings().iter().rev() {
+            for candidate in keystroke.match_candidates() {
+                self.pending_keystrokes.push(candidate.clone());
+                match binding.match_keystrokes(&self.pending_keystrokes, context_stack) {
+                    KeyMatch::Some(action) => {
+                        self.pending_keystrokes.clear();
+                        return KeyMatch::Some(action);
+                    }
+                    KeyMatch::Pending => {
+                        pending_key.get_or_insert(candidate);
+                    }
+                    KeyMatch::None => {}
+                }
+                self.pending_keystrokes.pop();
+            }
+        }
+
+        if let Some(pending_key) = pending_key {
+            self.pending_keystrokes.push(pending_key);
+        }
+
+        if self.pending_keystrokes.is_empty() {
+            KeyMatch::None
+        } else {
+            KeyMatch::Pending
+        }
+    }
+
+    pub fn keystrokes_for_action(
+        &self,
+        action: &dyn Action,
+        contexts: &[&DispatchContext],
+    ) -> Option<SmallVec<[Keystroke; 2]>> {
+        self.keymap
+            .lock()
+            .bindings()
+            .iter()
+            .rev()
+            .find_map(|binding| binding.keystrokes_for_action(action, contexts))
+    }
+}
+
+pub enum KeyMatch {
+    None,
+    Pending,
+    Some(Box<dyn Action>),
+}
+
+impl KeyMatch {
+    pub fn is_some(&self) -> bool {
+        matches!(self, KeyMatch::Some(_))
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use anyhow::Result;
+//     use serde::Deserialize;
+
+//     use crate::{actions, impl_actions, keymap_matcher::ActionContext};
+
+//     use super::*;
+
+//     #[test]
+//     fn test_keymap_and_view_ordering() -> Result<()> {
+//         actions!(test, [EditorAction, ProjectPanelAction]);
+
+//         let mut editor = ActionContext::default();
+//         editor.add_identifier("Editor");
+
+//         let mut project_panel = ActionContext::default();
+//         project_panel.add_identifier("ProjectPanel");
+
+//         // Editor 'deeper' in than project panel
+//         let dispatch_path = vec![(2, editor), (1, project_panel)];
+
+//         // But editor actions 'higher' up in keymap
+//         let keymap = Keymap::new(vec![
+//             Binding::new("left", EditorAction, Some("Editor")),
+//             Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
+//         ]);
+
+//         let mut matcher = KeymapMatcher::new(keymap);
+
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![
+//                 (2, Box::new(EditorAction)),
+//                 (1, Box::new(ProjectPanelAction)),
+//             ]),
+//         );
+
+//         Ok(())
+//     }
+
+//     #[test]
+//     fn test_push_keystroke() -> Result<()> {
+//         actions!(test, [B, AB, C, D, DA, E, EF]);
+
+//         let mut context1 = ActionContext::default();
+//         context1.add_identifier("1");
+
+//         let mut context2 = ActionContext::default();
+//         context2.add_identifier("2");
+
+//         let dispatch_path = vec![(2, context2), (1, context1)];
+
+//         let keymap = Keymap::new(vec![
+//             Binding::new("a b", AB, Some("1")),
+//             Binding::new("b", B, Some("2")),
+//             Binding::new("c", C, Some("2")),
+//             Binding::new("d", D, Some("1")),
+//             Binding::new("d", D, Some("2")),
+//             Binding::new("d a", DA, Some("2")),
+//         ]);
+
+//         let mut matcher = KeymapMatcher::new(keymap);
+
+//         // Binding with pending prefix always takes precedence
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+//             KeyMatch::Pending,
+//         );
+//         // B alone doesn't match because a was pending, so AB is returned instead
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![(1, Box::new(AB))]),
+//         );
+//         assert!(!matcher.has_pending_keystrokes());
+
+//         // Without an a prefix, B is dispatched like expected
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![(2, Box::new(B))]),
+//         );
+//         assert!(!matcher.has_pending_keystrokes());
+
+//         // If a is prefixed, C will not be dispatched because there
+//         // was a pending binding for it
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+//             KeyMatch::Pending,
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
+//             KeyMatch::None,
+//         );
+//         assert!(!matcher.has_pending_keystrokes());
+
+//         // If a single keystroke matches multiple bindings in the tree
+//         // all of them are returned so that we can fallback if the action
+//         // handler decides to propagate the action
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
+//         );
+
+//         // If none of the d action handlers consume the binding, a pending
+//         // binding may then be used
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+//             KeyMatch::Matches(vec![(2, Box::new(DA))]),
+//         );
+//         assert!(!matcher.has_pending_keystrokes());
+
+//         Ok(())
+//     }
+
+//     #[test]
+//     fn test_keystroke_parsing() -> Result<()> {
+//         assert_eq!(
+//             Keystroke::parse("ctrl-p")?,
+//             Keystroke {
+//                 key: "p".into(),
+//                 ctrl: true,
+//                 alt: false,
+//                 shift: false,
+//                 cmd: false,
+//                 function: false,
+//                 ime_key: None,
+//             }
+//         );
+
+//         assert_eq!(
+//             Keystroke::parse("alt-shift-down")?,
+//             Keystroke {
+//                 key: "down".into(),
+//                 ctrl: false,
+//                 alt: true,
+//                 shift: true,
+//                 cmd: false,
+//                 function: false,
+//                 ime_key: None,
+//             }
+//         );
+
+//         assert_eq!(
+//             Keystroke::parse("shift-cmd--")?,
+//             Keystroke {
+//                 key: "-".into(),
+//                 ctrl: false,
+//                 alt: false,
+//                 shift: true,
+//                 cmd: true,
+//                 function: false,
+//                 ime_key: None,
+//             }
+//         );
+
+//         Ok(())
+//     }
+
+//     #[test]
+//     fn test_context_predicate_parsing() -> Result<()> {
+//         use KeymapContextPredicate::*;
+
+//         assert_eq!(
+//             KeymapContextPredicate::parse("a && (b == c || d != e)")?,
+//             And(
+//                 Box::new(Identifier("a".into())),
+//                 Box::new(Or(
+//                     Box::new(Equal("b".into(), "c".into())),
+//                     Box::new(NotEqual("d".into(), "e".into())),
+//                 ))
+//             )
+//         );
+
+//         assert_eq!(
+//             KeymapContextPredicate::parse("!a")?,
+//             Not(Box::new(Identifier("a".into())),)
+//         );
+
+//         Ok(())
+//     }
+
+//     #[test]
+//     fn test_context_predicate_eval() {
+//         let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
+
+//         let mut context = ActionContext::default();
+//         context.add_identifier("a");
+//         assert!(!predicate.eval(&[context]));
+
+//         let mut context = ActionContext::default();
+//         context.add_identifier("a");
+//         context.add_identifier("b");
+//         assert!(predicate.eval(&[context]));
+
+//         let mut context = ActionContext::default();
+//         context.add_identifier("a");
+//         context.add_key("c", "x");
+//         assert!(!predicate.eval(&[context]));
+
+//         let mut context = ActionContext::default();
+//         context.add_identifier("a");
+//         context.add_key("c", "d");
+//         assert!(predicate.eval(&[context]));
+
+//         let predicate = KeymapContextPredicate::parse("!a").unwrap();
+//         assert!(predicate.eval(&[ActionContext::default()]));
+//     }
+
+//     #[test]
+//     fn test_context_child_predicate_eval() {
+//         let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();
+//         let contexts = [
+//             context_set(&["e", "f"]),
+//             context_set(&["c", "d"]), // match this context
+//             context_set(&["a", "b"]),
+//         ];
+
+//         assert!(!predicate.eval(&contexts[0..]));
+//         assert!(predicate.eval(&contexts[1..]));
+//         assert!(!predicate.eval(&contexts[2..]));
+
+//         let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();
+//         let contexts = [
+//             context_set(&["f"]),
+//             context_set(&["e"]), // only match this context
+//             context_set(&["c"]),
+//             context_set(&["a", "b"]),
+//             context_set(&["e"]),
+//             context_set(&["c", "d"]),
+//             context_set(&["a", "b"]),
+//         ];
+
+//         assert!(!predicate.eval(&contexts[0..]));
+//         assert!(predicate.eval(&contexts[1..]));
+//         assert!(!predicate.eval(&contexts[2..]));
+//         assert!(!predicate.eval(&contexts[3..]));
+//         assert!(!predicate.eval(&contexts[4..]));
+//         assert!(!predicate.eval(&contexts[5..]));
+//         assert!(!predicate.eval(&contexts[6..]));
+
+//         fn context_set(names: &[&str]) -> ActionContext {
+//             let mut keymap = ActionContext::new();
+//             names
+//                 .iter()
+//                 .for_each(|name| keymap.add_identifier(name.to_string()));
+//             keymap
+//         }
+//     }
+
+//     #[test]
+//     fn test_matcher() -> Result<()> {
+//         #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
+//         pub struct A(pub String);
+//         impl_actions!(test, [A]);
+//         actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
+
+//         #[derive(Clone, Debug, Eq, PartialEq)]
+//         struct ActionArg {
+//             a: &'static str,
+//         }
+
+//         let keymap = Keymap::new(vec![
+//             Binding::new("a", A("x".to_string()), Some("a")),
+//             Binding::new("b", B, Some("a")),
+//             Binding::new("a b", Ab, Some("a || b")),
+//             Binding::new("$", Dollar, Some("a")),
+//             Binding::new("\"", Quote, Some("a")),
+//             Binding::new("alt-s", Ess, Some("a")),
+//             Binding::new("ctrl-`", Backtick, Some("a")),
+//         ]);
+
+//         let mut context_a = ActionContext::default();
+//         context_a.add_identifier("a");
+
+//         let mut context_b = ActionContext::default();
+//         context_b.add_identifier("b");
+
+//         let mut matcher = KeymapMatcher::new(keymap);
+
+//         // Basic match
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
+//         );
+//         matcher.clear_pending();
+
+//         // Multi-keystroke match
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
+//             KeyMatch::Pending
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Ab))])
+//         );
+//         matcher.clear_pending();
+
+//         // Failed matches don't interfere with matching subsequent keys
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
+//             KeyMatch::None
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
+//         );
+//         matcher.clear_pending();
+
+//         // Pending keystrokes are cleared when the context changes
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
+//             KeyMatch::Pending
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
+//             KeyMatch::None
+//         );
+//         matcher.clear_pending();
+
+//         let mut context_c = ActionContext::default();
+//         context_c.add_identifier("c");
+
+//         // Pending keystrokes are maintained per-view
+//         assert_eq!(
+//             matcher.match_keystroke(
+//                 Keystroke::parse("a")?,
+//                 vec![(1, context_b.clone()), (2, context_c.clone())]
+//             ),
+//             KeyMatch::Pending
+//         );
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Ab))])
+//         );
+
+//         // handle Czech $ (option + 4 key)
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Dollar))])
+//         );
+
+//         // handle Brazillian quote (quote key then space key)
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Quote))])
+//         );
+
+//         // handle ctrl+` on a brazillian keyboard
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Backtick))])
+//         );
+
+//         // handle alt-s on a US keyboard
+//         assert_eq!(
+//             matcher.match_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]),
+//             KeyMatch::Matches(vec![(1, Box::new(Ess))])
+//         );
+
+//         Ok(())
+//     }
+// }

crates/gpui2/src/platform.rs 🔗

@@ -0,0 +1,497 @@
+mod keystroke;
+#[cfg(target_os = "macos")]
+mod mac;
+#[cfg(any(test, feature = "test-support"))]
+mod test;
+
+use crate::{
+    AnyWindowHandle, Bounds, DevicePixels, Executor, Font, FontId, FontMetrics, FontRun,
+    GlobalPixels, GlyphId, InputEvent, LineLayout, Pixels, Point, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, Result, Scene, SharedString, Size,
+};
+use anyhow::anyhow;
+use async_task::Runnable;
+use futures::channel::oneshot;
+use seahash::SeaHasher;
+use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
+use std::hash::{Hash, Hasher};
+use std::time::Duration;
+use std::{
+    any::Any,
+    fmt::{self, Debug, Display},
+    ops::Range,
+    path::{Path, PathBuf},
+    rc::Rc,
+    str::FromStr,
+    sync::Arc,
+};
+
+pub use keystroke::*;
+#[cfg(target_os = "macos")]
+pub use mac::*;
+#[cfg(any(test, feature = "test-support"))]
+pub use test::*;
+pub use time::UtcOffset;
+
+#[cfg(target_os = "macos")]
+pub(crate) fn current_platform() -> Arc<dyn Platform> {
+    Arc::new(MacPlatform::new())
+}
+
+pub(crate) trait Platform: 'static {
+    fn executor(&self) -> Executor;
+    fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
+
+    fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
+    fn quit(&self);
+    fn restart(&self);
+    fn activate(&self, ignoring_other_apps: bool);
+    fn hide(&self);
+    fn hide_other_apps(&self);
+    fn unhide_other_apps(&self);
+
+    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
+    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
+    fn main_window(&self) -> Option<AnyWindowHandle>;
+    fn open_window(
+        &self,
+        handle: AnyWindowHandle,
+        options: WindowOptions,
+    ) -> Box<dyn PlatformWindow>;
+
+    fn set_display_link_output_callback(
+        &self,
+        display_id: DisplayId,
+        callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp)>,
+    );
+    fn start_display_link(&self, display_id: DisplayId);
+    fn stop_display_link(&self, display_id: DisplayId);
+    // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn PlatformWindow>;
+
+    fn open_url(&self, url: &str);
+    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
+    fn prompt_for_paths(
+        &self,
+        options: PathPromptOptions,
+    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
+    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
+    fn reveal_path(&self, path: &Path);
+
+    fn on_become_active(&self, callback: Box<dyn FnMut()>);
+    fn on_resign_active(&self, callback: Box<dyn FnMut()>);
+    fn on_quit(&self, callback: Box<dyn FnMut()>);
+    fn on_reopen(&self, callback: Box<dyn FnMut()>);
+    fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
+
+    fn os_name(&self) -> &'static str;
+    fn os_version(&self) -> Result<SemanticVersion>;
+    fn app_version(&self) -> Result<SemanticVersion>;
+    fn app_path(&self) -> Result<PathBuf>;
+    fn local_timezone(&self) -> UtcOffset;
+    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
+
+    fn set_cursor_style(&self, style: CursorStyle);
+    fn should_auto_hide_scrollbars(&self) -> bool;
+
+    fn write_to_clipboard(&self, item: ClipboardItem);
+    fn read_from_clipboard(&self) -> Option<ClipboardItem>;
+
+    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
+    fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
+    fn delete_credentials(&self, url: &str) -> Result<()>;
+}
+
+pub trait PlatformDisplay: Send + Sync + Debug {
+    fn id(&self) -> DisplayId;
+    fn as_any(&self) -> &dyn Any;
+    fn bounds(&self) -> Bounds<GlobalPixels>;
+}
+
+#[derive(PartialEq, Eq, Hash, Copy, Clone)]
+pub struct DisplayId(pub(crate) u32);
+
+impl Debug for DisplayId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "DisplayId({})", self.0)
+    }
+}
+
+unsafe impl Send for DisplayId {}
+
+pub(crate) trait PlatformWindow {
+    fn bounds(&self) -> WindowBounds;
+    fn content_size(&self) -> Size<Pixels>;
+    fn scale_factor(&self) -> f32;
+    fn titlebar_height(&self) -> Pixels;
+    fn appearance(&self) -> WindowAppearance;
+    fn display(&self) -> Rc<dyn PlatformDisplay>;
+    fn mouse_position(&self) -> Point<Pixels>;
+    fn as_any_mut(&mut self) -> &mut dyn Any;
+    fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
+    fn prompt(
+        &self,
+        level: WindowPromptLevel,
+        msg: &str,
+        answers: &[&str],
+    ) -> oneshot::Receiver<usize>;
+    fn activate(&self);
+    fn set_title(&mut self, title: &str);
+    fn set_edited(&mut self, edited: bool);
+    fn show_character_palette(&self);
+    fn minimize(&self);
+    fn zoom(&self);
+    fn toggle_full_screen(&self);
+    fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
+    fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
+    fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
+    fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>);
+    fn on_moved(&self, callback: Box<dyn FnMut()>);
+    fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
+    fn on_close(&self, callback: Box<dyn FnOnce()>);
+    fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
+    fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
+    fn draw(&self, scene: Scene);
+
+    fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
+}
+
+pub trait PlatformDispatcher: Send + Sync {
+    fn is_main_thread(&self) -> bool;
+    fn dispatch(&self, runnable: Runnable);
+    fn dispatch_on_main_thread(&self, runnable: Runnable);
+    fn dispatch_after(&self, duration: Duration, runnable: Runnable);
+    fn poll(&self) -> bool;
+
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_test(&self) -> Option<&TestDispatcher> {
+        None
+    }
+}
+
+pub trait PlatformTextSystem: Send + Sync {
+    fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
+    fn all_font_families(&self) -> Vec<String>;
+    fn font_id(&self, descriptor: &Font) -> Result<FontId>;
+    fn font_metrics(&self, font_id: FontId) -> FontMetrics;
+    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
+    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
+    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
+    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
+    fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)>;
+    fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
+    fn wrap_line(
+        &self,
+        text: &str,
+        font_id: FontId,
+        font_size: Pixels,
+        width: Pixels,
+    ) -> Vec<usize>;
+}
+
+#[derive(Clone, Debug)]
+pub struct AppMetadata {
+    pub os_name: &'static str,
+    pub os_version: Option<SemanticVersion>,
+    pub app_version: Option<SemanticVersion>,
+}
+
+#[derive(PartialEq, Eq, Hash, Clone)]
+pub enum AtlasKey {
+    Glyph(RenderGlyphParams),
+    Svg(RenderSvgParams),
+    Image(RenderImageParams),
+}
+
+impl AtlasKey {
+    pub(crate) fn texture_kind(&self) -> AtlasTextureKind {
+        match self {
+            AtlasKey::Glyph(params) => {
+                if params.is_emoji {
+                    AtlasTextureKind::Polychrome
+                } else {
+                    AtlasTextureKind::Monochrome
+                }
+            }
+            AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
+            AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
+        }
+    }
+}
+
+impl From<RenderGlyphParams> for AtlasKey {
+    fn from(params: RenderGlyphParams) -> Self {
+        Self::Glyph(params)
+    }
+}
+
+impl From<RenderSvgParams> for AtlasKey {
+    fn from(params: RenderSvgParams) -> Self {
+        Self::Svg(params)
+    }
+}
+
+impl From<RenderImageParams> for AtlasKey {
+    fn from(params: RenderImageParams) -> Self {
+        Self::Image(params)
+    }
+}
+
+pub trait PlatformAtlas: Send + Sync {
+    fn get_or_insert_with<'a>(
+        &self,
+        key: &AtlasKey,
+        build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
+    ) -> Result<AtlasTile>;
+
+    fn clear(&self);
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[repr(C)]
+pub struct AtlasTile {
+    pub(crate) texture_id: AtlasTextureId,
+    pub(crate) tile_id: TileId,
+    pub(crate) bounds: Bounds<DevicePixels>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+#[repr(C)]
+pub(crate) struct AtlasTextureId {
+    // We use u32 instead of usize for Metal Shader Language compatibility
+    pub(crate) index: u32,
+    pub(crate) kind: AtlasTextureKind,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+#[repr(C)]
+pub(crate) enum AtlasTextureKind {
+    Monochrome = 0,
+    Polychrome = 1,
+    Path = 2,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[repr(C)]
+pub(crate) struct TileId(pub(crate) u32);
+
+impl From<etagere::AllocId> for TileId {
+    fn from(id: etagere::AllocId) -> Self {
+        Self(id.serialize())
+    }
+}
+
+impl From<TileId> for etagere::AllocId {
+    fn from(id: TileId) -> Self {
+        Self::deserialize(id.0)
+    }
+}
+
+pub trait PlatformInputHandler {
+    fn selected_text_range(&self) -> Option<Range<usize>>;
+    fn marked_text_range(&self) -> Option<Range<usize>>;
+    fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String>;
+    fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range_utf16: Option<Range<usize>>,
+        new_text: &str,
+        new_selected_range: Option<Range<usize>>,
+    );
+    fn unmark_text(&mut self);
+    fn bounds_for_range(&self, range_utf16: Range<usize>) -> Option<Bounds<f32>>;
+}
+
+#[derive(Debug)]
+pub struct WindowOptions {
+    pub bounds: WindowBounds,
+    pub titlebar: Option<TitlebarOptions>,
+    pub center: bool,
+    pub focus: bool,
+    pub show: bool,
+    pub kind: WindowKind,
+    pub is_movable: bool,
+    pub display_id: Option<DisplayId>,
+}
+
+impl Default for WindowOptions {
+    fn default() -> Self {
+        Self {
+            bounds: WindowBounds::default(),
+            titlebar: Some(TitlebarOptions {
+                title: Default::default(),
+                appears_transparent: Default::default(),
+                traffic_light_position: Default::default(),
+            }),
+            center: false,
+            focus: true,
+            show: true,
+            kind: WindowKind::Normal,
+            is_movable: true,
+            display_id: None,
+        }
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct TitlebarOptions {
+    pub title: Option<SharedString>,
+    pub appears_transparent: bool,
+    pub traffic_light_position: Option<Point<Pixels>>,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum Appearance {
+    Light,
+    VibrantLight,
+    Dark,
+    VibrantDark,
+}
+
+impl Default for Appearance {
+    fn default() -> Self {
+        Self::Light
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum WindowKind {
+    Normal,
+    PopUp,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Default)]
+pub enum WindowBounds {
+    Fullscreen,
+    #[default]
+    Maximized,
+    Fixed(Bounds<GlobalPixels>),
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum WindowAppearance {
+    Light,
+    VibrantLight,
+    Dark,
+    VibrantDark,
+}
+
+impl Default for WindowAppearance {
+    fn default() -> Self {
+        Self::Light
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Default)]
+pub enum WindowPromptLevel {
+    #[default]
+    Info,
+    Warning,
+    Critical,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct PathPromptOptions {
+    pub files: bool,
+    pub directories: bool,
+    pub multiple: bool,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum PromptLevel {
+    Info,
+    Warning,
+    Critical,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum CursorStyle {
+    Arrow,
+    ResizeLeftRight,
+    ResizeUpDown,
+    PointingHand,
+    IBeam,
+}
+
+impl Default for CursorStyle {
+    fn default() -> Self {
+        Self::Arrow
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+pub struct SemanticVersion {
+    major: usize,
+    minor: usize,
+    patch: usize,
+}
+
+impl FromStr for SemanticVersion {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self> {
+        let mut components = s.trim().split('.');
+        let major = components
+            .next()
+            .ok_or_else(|| anyhow!("missing major version number"))?
+            .parse()?;
+        let minor = components
+            .next()
+            .ok_or_else(|| anyhow!("missing minor version number"))?
+            .parse()?;
+        let patch = components
+            .next()
+            .ok_or_else(|| anyhow!("missing patch version number"))?
+            .parse()?;
+        Ok(Self {
+            major,
+            minor,
+            patch,
+        })
+    }
+}
+
+impl Display for SemanticVersion {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ClipboardItem {
+    pub(crate) text: String,
+    pub(crate) metadata: Option<String>,
+}
+
+impl ClipboardItem {
+    pub fn new(text: String) -> Self {
+        Self {
+            text,
+            metadata: None,
+        }
+    }
+
+    pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
+        self.metadata = Some(serde_json::to_string(&metadata).unwrap());
+        self
+    }
+
+    pub fn text(&self) -> &String {
+        &self.text
+    }
+
+    pub fn metadata<T>(&self) -> Option<T>
+    where
+        T: for<'a> Deserialize<'a>,
+    {
+        self.metadata
+            .as_ref()
+            .and_then(|m| serde_json::from_str(m).ok())
+    }
+
+    pub(crate) fn text_hash(text: &str) -> u64 {
+        let mut hasher = SeaHasher::new();
+        text.hash(&mut hasher);
+        hasher.finish()
+    }
+}

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

@@ -0,0 +1,151 @@
+use anyhow::anyhow;
+use serde::Deserialize;
+use smallvec::SmallVec;
+use std::fmt::Write;
+
+#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
+pub struct Keystroke {
+    pub modifiers: Modifiers,
+    /// key is the character printed on the key that was pressed
+    /// e.g. for option-s, key is "s"
+    pub key: String,
+    /// ime_key is the character inserted by the IME engine when that key was pressed.
+    /// e.g. for option-s, ime_key is "ß"
+    pub ime_key: Option<String>,
+}
+
+impl Keystroke {
+    // When matching a key we cannot know whether the user intended to type
+    // the ime_key or the key. On some non-US keyboards keys we use in our
+    // bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
+    // and on some keyboards the IME handler converts a sequence of keys into a
+    // specific character (for example `"` is typed as `" space` on a brazillian keyboard).
+    pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
+        let mut possibilities = SmallVec::new();
+        match self.ime_key.as_ref() {
+            None => possibilities.push(self.clone()),
+            Some(ime_key) => {
+                possibilities.push(Keystroke {
+                    modifiers: Modifiers {
+                        control: self.modifiers.control,
+                        alt: false,
+                        shift: false,
+                        command: false,
+                        function: false,
+                    },
+                    key: ime_key.to_string(),
+                    ime_key: None,
+                });
+                possibilities.push(Keystroke {
+                    ime_key: None,
+                    ..self.clone()
+                });
+            }
+        }
+        possibilities
+    }
+
+    /// key syntax is:
+    /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
+    /// ime_key is only used for generating test events,
+    /// when matching a key with an ime_key set will be matched without it.
+    pub fn parse(source: &str) -> anyhow::Result<Self> {
+        let mut control = false;
+        let mut alt = false;
+        let mut shift = false;
+        let mut command = false;
+        let mut function = false;
+        let mut key = None;
+        let mut ime_key = None;
+
+        let mut components = source.split('-').peekable();
+        while let Some(component) = components.next() {
+            match component {
+                "ctrl" => control = true,
+                "alt" => alt = true,
+                "shift" => shift = true,
+                "cmd" => command = true,
+                "fn" => function = true,
+                _ => {
+                    if let Some(next) = components.peek() {
+                        if next.is_empty() && source.ends_with('-') {
+                            key = Some(String::from("-"));
+                            break;
+                        } else if next.len() > 1 && next.starts_with('>') {
+                            key = Some(String::from(component));
+                            ime_key = Some(String::from(&next[1..]));
+                            components.next();
+                        } else {
+                            return Err(anyhow!("Invalid keystroke `{}`", source));
+                        }
+                    } else {
+                        key = Some(String::from(component));
+                    }
+                }
+            }
+        }
+
+        let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
+
+        Ok(Keystroke {
+            modifiers: Modifiers {
+                control,
+                alt,
+                shift,
+                command,
+                function,
+            },
+            key,
+            ime_key,
+        })
+    }
+}
+
+impl std::fmt::Display for Keystroke {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if self.modifiers.control {
+            f.write_char('^')?;
+        }
+        if self.modifiers.alt {
+            f.write_char('⌥')?;
+        }
+        if self.modifiers.command {
+            f.write_char('⌘')?;
+        }
+        if self.modifiers.shift {
+            f.write_char('⇧')?;
+        }
+        let key = match self.key.as_str() {
+            "backspace" => '⌫',
+            "up" => '↑',
+            "down" => '↓',
+            "left" => '←',
+            "right" => '→',
+            "tab" => '⇥',
+            "escape" => '⎋',
+            key => {
+                if key.len() == 1 {
+                    key.chars().next().unwrap().to_ascii_uppercase()
+                } else {
+                    return f.write_str(key);
+                }
+            }
+        };
+        f.write_char(key)
+    }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
+pub struct Modifiers {
+    pub control: bool,
+    pub alt: bool,
+    pub shift: bool,
+    pub command: bool,
+    pub function: bool,
+}
+
+impl Modifiers {
+    pub fn modified(&self) -> bool {
+        self.control || self.alt || self.shift || self.command || self.function
+    }
+}

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

@@ -0,0 +1,165 @@
+///! Macos screen have a y axis that goings up from the bottom of the screen and
+///! an origin at the bottom left of the main display.
+mod dispatcher;
+mod display;
+mod display_linker;
+mod events;
+mod metal_atlas;
+mod metal_renderer;
+mod open_type;
+mod platform;
+mod text_system;
+mod window;
+mod window_appearence;
+
+use crate::{px, size, GlobalPixels, Pixels, Size};
+use anyhow::anyhow;
+use cocoa::{
+    base::{id, nil},
+    foundation::{NSAutoreleasePool, NSNotFound, NSRect, NSSize, NSString, NSUInteger, NSURL},
+};
+use metal_renderer::*;
+use objc::{
+    msg_send,
+    runtime::{BOOL, NO, YES},
+    sel, sel_impl,
+};
+use std::{
+    ffi::{c_char, CStr, OsStr},
+    ops::Range,
+    os::unix::prelude::OsStrExt,
+    path::PathBuf,
+};
+
+pub use dispatcher::*;
+pub use display::*;
+pub use display_linker::*;
+pub use metal_atlas::*;
+pub use platform::*;
+pub use text_system::*;
+pub use window::*;
+
+trait BoolExt {
+    fn to_objc(self) -> BOOL;
+}
+
+impl BoolExt for bool {
+    fn to_objc(self) -> BOOL {
+        if self {
+            YES
+        } else {
+            NO
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+struct NSRange {
+    pub location: NSUInteger,
+    pub length: NSUInteger,
+}
+
+impl NSRange {
+    fn invalid() -> Self {
+        Self {
+            location: NSNotFound as NSUInteger,
+            length: 0,
+        }
+    }
+
+    fn is_valid(&self) -> bool {
+        self.location != NSNotFound as NSUInteger
+    }
+
+    fn to_range(self) -> Option<Range<usize>> {
+        if self.is_valid() {
+            let start = self.location as usize;
+            let end = start + self.length as usize;
+            Some(start..end)
+        } else {
+            None
+        }
+    }
+}
+
+impl From<Range<usize>> for NSRange {
+    fn from(range: Range<usize>) -> Self {
+        NSRange {
+            location: range.start as NSUInteger,
+            length: range.len() as NSUInteger,
+        }
+    }
+}
+
+unsafe impl objc::Encode for NSRange {
+    fn encode() -> objc::Encoding {
+        let encoding = format!(
+            "{{NSRange={}{}}}",
+            NSUInteger::encode().as_str(),
+            NSUInteger::encode().as_str()
+        );
+        unsafe { objc::Encoding::from_str(&encoding) }
+    }
+}
+
+unsafe fn ns_string(string: &str) -> id {
+    NSString::alloc(nil).init_str(string).autorelease()
+}
+
+impl From<NSSize> for Size<Pixels> {
+    fn from(value: NSSize) -> Self {
+        Size {
+            width: px(value.width as f32),
+            height: px(value.height as f32),
+        }
+    }
+}
+
+pub trait NSRectExt {
+    fn size(&self) -> Size<Pixels>;
+    fn intersects(&self, other: Self) -> bool;
+}
+
+impl From<NSRect> for Size<Pixels> {
+    fn from(rect: NSRect) -> Self {
+        let NSSize { width, height } = rect.size;
+        size(width.into(), height.into())
+    }
+}
+
+impl From<NSRect> for Size<GlobalPixels> {
+    fn from(rect: NSRect) -> Self {
+        let NSSize { width, height } = rect.size;
+        size(width.into(), height.into())
+    }
+}
+
+// impl NSRectExt for NSRect {
+//     fn intersects(&self, other: Self) -> bool {
+//         self.size.width > 0.
+//             && self.size.height > 0.
+//             && other.size.width > 0.
+//             && other.size.height > 0.
+//             && self.origin.x <= other.origin.x + other.size.width
+//             && self.origin.x + self.size.width >= other.origin.x
+//             && self.origin.y <= other.origin.y + other.size.height
+//             && self.origin.y + self.size.height >= other.origin.y
+//     }
+// }
+
+// todo!
+#[allow(unused)]
+unsafe fn ns_url_to_path(url: id) -> crate::Result<PathBuf> {
+    let path: *mut c_char = msg_send![url, fileSystemRepresentation];
+    if path.is_null() {
+        Err(anyhow!(
+            "url is not a file path: {}",
+            CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy()
+        ))
+    } else {
+        Ok(PathBuf::from(OsStr::from_bytes(
+            CStr::from_ptr(path).to_bytes(),
+        )))
+    }
+}

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

@@ -0,0 +1,100 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+use crate::PlatformDispatcher;
+use async_task::Runnable;
+use objc::{
+    class, msg_send,
+    runtime::{BOOL, YES},
+    sel, sel_impl,
+};
+use std::{
+    ffi::c_void,
+    time::{Duration, SystemTime},
+};
+
+include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
+
+pub fn dispatch_get_main_queue() -> dispatch_queue_t {
+    unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
+}
+
+pub struct MacDispatcher;
+
+impl PlatformDispatcher for MacDispatcher {
+    fn is_main_thread(&self) -> bool {
+        let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
+        is_main_thread == YES
+    }
+
+    fn dispatch(&self, runnable: Runnable) {
+        unsafe {
+            dispatch_async_f(
+                dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
+                runnable.into_raw() as *mut c_void,
+                Some(trampoline),
+            );
+        }
+    }
+
+    fn dispatch_on_main_thread(&self, runnable: Runnable) {
+        unsafe {
+            dispatch_async_f(
+                dispatch_get_main_queue(),
+                runnable.into_raw() as *mut c_void,
+                Some(trampoline),
+            );
+        }
+    }
+
+    fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
+        let now = SystemTime::now();
+        let after_duration = now
+            .duration_since(SystemTime::UNIX_EPOCH)
+            .unwrap()
+            .as_nanos() as u64
+            + duration.as_nanos() as u64;
+        unsafe {
+            let queue =
+                dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0);
+            let when = dispatch_time(0, after_duration as i64);
+            dispatch_after_f(
+                when,
+                queue,
+                runnable.into_raw() as *mut c_void,
+                Some(trampoline),
+            );
+        }
+    }
+
+    fn poll(&self) -> bool {
+        false
+    }
+}
+
+extern "C" fn trampoline(runnable: *mut c_void) {
+    let task = unsafe { Runnable::from_raw(runnable as *mut ()) };
+    task.run();
+}
+
+// #include <dispatch/dispatch.h>
+
+// int main(void) {
+
+//     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+//         // Do some lengthy background work here...
+//         printf("Background Work\n");
+
+//         dispatch_async(dispatch_get_main_queue(), ^{
+//             // Once done, update your UI on the main queue here.
+//             printf("UI Updated\n");
+
+//         });
+//     });
+
+//     sleep(3);  // prevent the program from terminating immediately
+
+//     return 0;
+// }
+// ```

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

@@ -0,0 +1,101 @@
+use crate::{point, size, Bounds, DisplayId, GlobalPixels, PlatformDisplay};
+use core_graphics::{
+    display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList},
+    geometry::{CGPoint, CGRect, CGSize},
+};
+use std::any::Any;
+
+#[derive(Debug)]
+pub struct MacDisplay(pub(crate) CGDirectDisplayID);
+
+unsafe impl Send for MacDisplay {}
+
+impl MacDisplay {
+    /// Get the screen with the given UUID.
+    pub fn find_by_id(id: DisplayId) -> Option<Self> {
+        Self::all().find(|screen| screen.id() == id)
+    }
+
+    /// Get the primary screen - the one with the menu bar, and whose bottom left
+    /// corner is at the origin of the AppKit coordinate system.
+    pub fn primary() -> Self {
+        Self::all().next().unwrap()
+    }
+
+    pub fn all() -> impl Iterator<Item = Self> {
+        unsafe {
+            let mut display_count: u32 = 0;
+            let result = CGGetActiveDisplayList(0, std::ptr::null_mut(), &mut display_count);
+
+            if result == 0 {
+                let mut displays = Vec::with_capacity(display_count as usize);
+                CGGetActiveDisplayList(display_count, displays.as_mut_ptr(), &mut display_count);
+                displays.set_len(display_count as usize);
+
+                displays.into_iter().map(|display| MacDisplay(display))
+            } else {
+                panic!("Failed to get active display list");
+            }
+        }
+    }
+}
+
+/// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space.
+///
+/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
+/// with the Y axis pointing upwards.
+///
+/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
+/// screen, with the Y axis pointing downwards.
+pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds<GlobalPixels> {
+    let primary_screen_size = unsafe { CGDisplayBounds(MacDisplay::primary().id().0) }.size;
+
+    Bounds {
+        origin: point(
+            GlobalPixels(rect.origin.x as f32),
+            GlobalPixels(
+                primary_screen_size.height as f32 - rect.origin.y as f32 - rect.size.height as f32,
+            ),
+        ),
+        size: size(
+            GlobalPixels(rect.size.width as f32),
+            GlobalPixels(rect.size.height as f32),
+        ),
+    }
+}
+
+/// Convert the given rectangle from GPUI's coordinate system to CoreGraphics' native coordinate space.
+///
+/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
+/// with the Y axis pointing upwards.
+///
+/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
+/// screen, with the Y axis pointing downwards.
+pub(crate) fn display_bounds_to_native(bounds: Bounds<GlobalPixels>) -> CGRect {
+    let primary_screen_height = MacDisplay::primary().bounds().size.height;
+
+    CGRect::new(
+        &CGPoint::new(
+            bounds.origin.x.into(),
+            (primary_screen_height - bounds.origin.y - bounds.size.height).into(),
+        ),
+        &CGSize::new(bounds.size.width.into(), bounds.size.height.into()),
+    )
+}
+
+impl PlatformDisplay for MacDisplay {
+    fn id(&self) -> DisplayId {
+        DisplayId(self.0)
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn bounds(&self) -> Bounds<GlobalPixels> {
+        unsafe {
+            let native_bounds = CGDisplayBounds(self.0);
+            display_bounds_from_native(native_bounds)
+        }
+    }
+}

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

@@ -0,0 +1,274 @@
+use std::{
+    ffi::c_void,
+    mem,
+    sync::{Arc, Weak},
+};
+
+use crate::DisplayId;
+use collections::HashMap;
+use parking_lot::Mutex;
+pub use sys::CVTimeStamp as VideoTimestamp;
+
+pub(crate) struct MacDisplayLinker {
+    links: HashMap<DisplayId, MacDisplayLink>,
+}
+
+struct MacDisplayLink {
+    system_link: sys::DisplayLink,
+    _output_callback: Arc<OutputCallback>,
+}
+
+impl MacDisplayLinker {
+    pub fn new() -> Self {
+        MacDisplayLinker {
+            links: Default::default(),
+        }
+    }
+}
+
+type OutputCallback = Mutex<Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp)>>;
+
+impl MacDisplayLinker {
+    pub fn set_output_callback(
+        &mut self,
+        display_id: DisplayId,
+        output_callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp)>,
+    ) {
+        if let Some(mut system_link) = unsafe { sys::DisplayLink::on_display(display_id.0) } {
+            let callback = Arc::new(Mutex::new(output_callback));
+            let weak_callback_ptr: *const OutputCallback = Arc::downgrade(&callback).into_raw();
+            unsafe { system_link.set_output_callback(trampoline, weak_callback_ptr as *mut c_void) }
+
+            self.links.insert(
+                display_id,
+                MacDisplayLink {
+                    _output_callback: callback,
+                    system_link,
+                },
+            );
+        } else {
+            log::warn!("DisplayLink could not be obtained for {:?}", display_id);
+            return;
+        }
+    }
+
+    pub fn start(&mut self, display_id: DisplayId) {
+        if let Some(link) = self.links.get_mut(&display_id) {
+            unsafe {
+                link.system_link.start();
+            }
+        } else {
+            log::warn!("No DisplayLink callback registered for {:?}", display_id)
+        }
+    }
+
+    pub fn stop(&mut self, display_id: DisplayId) {
+        if let Some(link) = self.links.get_mut(&display_id) {
+            unsafe {
+                link.system_link.stop();
+            }
+        } else {
+            log::warn!("No DisplayLink callback registered for {:?}", display_id)
+        }
+    }
+}
+
+unsafe extern "C" fn trampoline(
+    _display_link_out: *mut sys::CVDisplayLink,
+    current_time: *const sys::CVTimeStamp,
+    output_time: *const sys::CVTimeStamp,
+    _flags_in: i64,
+    _flags_out: *mut i64,
+    user_data: *mut c_void,
+) -> i32 {
+    if let Some((current_time, output_time)) = current_time.as_ref().zip(output_time.as_ref()) {
+        let output_callback: Weak<OutputCallback> =
+            Weak::from_raw(user_data as *mut OutputCallback);
+        if let Some(output_callback) = output_callback.upgrade() {
+            (output_callback.lock())(current_time, output_time)
+        }
+        mem::forget(output_callback);
+    }
+    0
+}
+
+mod sys {
+    //! Derived from display-link crate under the fololwing license:
+    //! https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT
+    //! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc)
+    #![allow(dead_code, non_upper_case_globals)]
+
+    use foreign_types::{foreign_type, ForeignType};
+    use std::{
+        ffi::c_void,
+        fmt::{Debug, Formatter, Result},
+    };
+
+    #[derive(Debug)]
+    pub enum CVDisplayLink {}
+
+    foreign_type! {
+        type CType = CVDisplayLink;
+        fn drop = CVDisplayLinkRelease;
+        fn clone = CVDisplayLinkRetain;
+        pub struct DisplayLink;
+        pub struct DisplayLinkRef;
+    }
+
+    impl Debug for DisplayLink {
+        fn fmt(&self, formatter: &mut Formatter) -> Result {
+            formatter
+                .debug_tuple("DisplayLink")
+                .field(&self.as_ptr())
+                .finish()
+        }
+    }
+
+    #[repr(C)]
+    #[derive(Clone, Copy)]
+    pub struct CVTimeStamp {
+        pub version: u32,
+        pub video_time_scale: i32,
+        pub video_time: i64,
+        pub host_time: u64,
+        pub rate_scalar: f64,
+        pub video_refresh_period: i64,
+        pub smpte_time: CVSMPTETime,
+        pub flags: u64,
+        pub reserved: u64,
+    }
+
+    pub type CVTimeStampFlags = u64;
+
+    pub const kCVTimeStampVideoTimeValid: CVTimeStampFlags = 1 << 0;
+    pub const kCVTimeStampHostTimeValid: CVTimeStampFlags = 1 << 1;
+    pub const kCVTimeStampSMPTETimeValid: CVTimeStampFlags = 1 << 2;
+    pub const kCVTimeStampVideoRefreshPeriodValid: CVTimeStampFlags = 1 << 3;
+    pub const kCVTimeStampRateScalarValid: CVTimeStampFlags = 1 << 4;
+    pub const kCVTimeStampTopField: CVTimeStampFlags = 1 << 16;
+    pub const kCVTimeStampBottomField: CVTimeStampFlags = 1 << 17;
+    pub const kCVTimeStampVideoHostTimeValid: CVTimeStampFlags =
+        kCVTimeStampVideoTimeValid | kCVTimeStampHostTimeValid;
+    pub const kCVTimeStampIsInterlaced: CVTimeStampFlags =
+        kCVTimeStampTopField | kCVTimeStampBottomField;
+
+    #[repr(C)]
+    #[derive(Clone, Copy)]
+    pub struct CVSMPTETime {
+        pub subframes: i16,
+        pub subframe_divisor: i16,
+        pub counter: u32,
+        pub time_type: u32,
+        pub flags: u32,
+        pub hours: i16,
+        pub minutes: i16,
+        pub seconds: i16,
+        pub frames: i16,
+    }
+
+    pub type CVSMPTETimeType = u32;
+
+    pub const kCVSMPTETimeType24: CVSMPTETimeType = 0;
+    pub const kCVSMPTETimeType25: CVSMPTETimeType = 1;
+    pub const kCVSMPTETimeType30Drop: CVSMPTETimeType = 2;
+    pub const kCVSMPTETimeType30: CVSMPTETimeType = 3;
+    pub const kCVSMPTETimeType2997: CVSMPTETimeType = 4;
+    pub const kCVSMPTETimeType2997Drop: CVSMPTETimeType = 5;
+    pub const kCVSMPTETimeType60: CVSMPTETimeType = 6;
+    pub const kCVSMPTETimeType5994: CVSMPTETimeType = 7;
+
+    pub type CVSMPTETimeFlags = u32;
+
+    pub const kCVSMPTETimeValid: CVSMPTETimeFlags = 1 << 0;
+    pub const kCVSMPTETimeRunning: CVSMPTETimeFlags = 1 << 1;
+
+    pub type CVDisplayLinkOutputCallback = unsafe extern "C" fn(
+        display_link_out: *mut CVDisplayLink,
+        // A pointer to the current timestamp. This represents the timestamp when the callback is called.
+        current_time: *const CVTimeStamp,
+        // A pointer to the output timestamp. This represents the timestamp for when the frame will be displayed.
+        output_time: *const CVTimeStamp,
+        // Unused
+        flags_in: i64,
+        // Unused
+        flags_out: *mut i64,
+        // A pointer to app-defined data.
+        display_link_context: *mut c_void,
+    ) -> i32;
+
+    #[link(name = "CoreFoundation", kind = "framework")]
+    #[link(name = "CoreVideo", kind = "framework")]
+    #[allow(improper_ctypes)]
+    extern "C" {
+        pub fn CVDisplayLinkCreateWithActiveCGDisplays(
+            display_link_out: *mut *mut CVDisplayLink,
+        ) -> i32;
+        pub fn CVDisplayLinkCreateWithCGDisplay(
+            display_id: u32,
+            display_link_out: *mut *mut CVDisplayLink,
+        ) -> i32;
+        pub fn CVDisplayLinkSetOutputCallback(
+            display_link: &mut DisplayLinkRef,
+            callback: CVDisplayLinkOutputCallback,
+            user_info: *mut c_void,
+        ) -> i32;
+        pub fn CVDisplayLinkSetCurrentCGDisplay(
+            display_link: &mut DisplayLinkRef,
+            display_id: u32,
+        ) -> i32;
+        pub fn CVDisplayLinkStart(display_link: &mut DisplayLinkRef) -> i32;
+        pub fn CVDisplayLinkStop(display_link: &mut DisplayLinkRef) -> i32;
+        pub fn CVDisplayLinkRelease(display_link: *mut CVDisplayLink);
+        pub fn CVDisplayLinkRetain(display_link: *mut CVDisplayLink) -> *mut CVDisplayLink;
+    }
+
+    impl DisplayLink {
+        /// Apple docs: [CVDisplayLinkCreateWithActiveCGDisplays](https://developer.apple.com/documentation/corevideo/1456863-cvdisplaylinkcreatewithactivecgd?language=objc)
+        pub unsafe fn new() -> Option<Self> {
+            let mut display_link: *mut CVDisplayLink = 0 as _;
+            let code = CVDisplayLinkCreateWithActiveCGDisplays(&mut display_link);
+            if code == 0 {
+                Some(DisplayLink::from_ptr(display_link))
+            } else {
+                None
+            }
+        }
+
+        /// Apple docs: [CVDisplayLinkCreateWithCGDisplay](https://developer.apple.com/documentation/corevideo/1456981-cvdisplaylinkcreatewithcgdisplay?language=objc)
+        pub unsafe fn on_display(display_id: u32) -> Option<Self> {
+            let mut display_link: *mut CVDisplayLink = 0 as _;
+            let code = CVDisplayLinkCreateWithCGDisplay(display_id, &mut display_link);
+            if code == 0 {
+                Some(DisplayLink::from_ptr(display_link))
+            } else {
+                None
+            }
+        }
+    }
+
+    impl DisplayLinkRef {
+        /// Apple docs: [CVDisplayLinkSetOutputCallback](https://developer.apple.com/documentation/corevideo/1457096-cvdisplaylinksetoutputcallback?language=objc)
+        pub unsafe fn set_output_callback(
+            &mut self,
+            callback: CVDisplayLinkOutputCallback,
+            user_info: *mut c_void,
+        ) {
+            assert_eq!(CVDisplayLinkSetOutputCallback(self, callback, user_info), 0);
+        }
+
+        /// Apple docs: [CVDisplayLinkSetCurrentCGDisplay](https://developer.apple.com/documentation/corevideo/1456768-cvdisplaylinksetcurrentcgdisplay?language=objc)
+        pub unsafe fn set_current_display(&mut self, display_id: u32) {
+            assert_eq!(CVDisplayLinkSetCurrentCGDisplay(self, display_id), 0);
+        }
+
+        /// Apple docs: [CVDisplayLinkStart](https://developer.apple.com/documentation/corevideo/1457193-cvdisplaylinkstart?language=objc)
+        pub unsafe fn start(&mut self) {
+            assert_eq!(CVDisplayLinkStart(self), 0);
+        }
+
+        /// Apple docs: [CVDisplayLinkStop](https://developer.apple.com/documentation/corevideo/1457281-cvdisplaylinkstop?language=objc)
+        pub unsafe fn stop(&mut self) {
+            assert_eq!(CVDisplayLinkStop(self), 0);
+        }
+    }
+}

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

@@ -0,0 +1,357 @@
+use crate::{
+    point, px, InputEvent, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent,
+    MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection,
+    Pixels, ScrollDelta, ScrollWheelEvent, TouchPhase,
+};
+use cocoa::{
+    appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
+    base::{id, YES},
+    foundation::NSString as _,
+};
+use core_graphics::{
+    event::{CGEvent, CGEventFlags, CGKeyCode},
+    event_source::{CGEventSource, CGEventSourceStateID},
+};
+use ctor::ctor;
+use foreign_types::ForeignType;
+use objc::{class, msg_send, sel, sel_impl};
+use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr};
+
+const BACKSPACE_KEY: u16 = 0x7f;
+const SPACE_KEY: u16 = b' ' as u16;
+const ENTER_KEY: u16 = 0x0d;
+const NUMPAD_ENTER_KEY: u16 = 0x03;
+const ESCAPE_KEY: u16 = 0x1b;
+const TAB_KEY: u16 = 0x09;
+const SHIFT_TAB_KEY: u16 = 0x19;
+
+static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
+
+#[ctor]
+unsafe fn build_event_source() {
+    let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
+    EVENT_SOURCE = source.as_ptr();
+    mem::forget(source);
+}
+
+// todo!
+#[allow(unused)]
+pub fn key_to_native(key: &str) -> Cow<str> {
+    use cocoa::appkit::*;
+    let code = match key {
+        "space" => SPACE_KEY,
+        "backspace" => BACKSPACE_KEY,
+        "up" => NSUpArrowFunctionKey,
+        "down" => NSDownArrowFunctionKey,
+        "left" => NSLeftArrowFunctionKey,
+        "right" => NSRightArrowFunctionKey,
+        "pageup" => NSPageUpFunctionKey,
+        "pagedown" => NSPageDownFunctionKey,
+        "home" => NSHomeFunctionKey,
+        "end" => NSEndFunctionKey,
+        "delete" => NSDeleteFunctionKey,
+        "f1" => NSF1FunctionKey,
+        "f2" => NSF2FunctionKey,
+        "f3" => NSF3FunctionKey,
+        "f4" => NSF4FunctionKey,
+        "f5" => NSF5FunctionKey,
+        "f6" => NSF6FunctionKey,
+        "f7" => NSF7FunctionKey,
+        "f8" => NSF8FunctionKey,
+        "f9" => NSF9FunctionKey,
+        "f10" => NSF10FunctionKey,
+        "f11" => NSF11FunctionKey,
+        "f12" => NSF12FunctionKey,
+        _ => return Cow::Borrowed(key),
+    };
+    Cow::Owned(String::from_utf16(&[code]).unwrap())
+}
+
+unsafe fn read_modifiers(native_event: id) -> Modifiers {
+    let modifiers = native_event.modifierFlags();
+    let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
+    let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
+    let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
+    let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
+    let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
+
+    Modifiers {
+        control,
+        alt,
+        shift,
+        command,
+        function,
+    }
+}
+
+impl InputEvent {
+    pub unsafe fn from_native(native_event: id, window_height: Option<Pixels>) -> Option<Self> {
+        let event_type = native_event.eventType();
+
+        // Filter out event types that aren't in the NSEventType enum.
+        // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
+        match event_type as u64 {
+            0 | 21 | 32 | 33 | 35 | 36 | 37 => {
+                return None;
+            }
+            _ => {}
+        }
+
+        match event_type {
+            NSEventType::NSFlagsChanged => Some(Self::ModifiersChanged(ModifiersChangedEvent {
+                modifiers: read_modifiers(native_event),
+            })),
+            NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {
+                keystroke: parse_keystroke(native_event),
+                is_held: native_event.isARepeat() == YES,
+            })),
+            NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent {
+                keystroke: parse_keystroke(native_event),
+            })),
+            NSEventType::NSLeftMouseDown
+            | NSEventType::NSRightMouseDown
+            | NSEventType::NSOtherMouseDown => {
+                let button = match native_event.buttonNumber() {
+                    0 => MouseButton::Left,
+                    1 => MouseButton::Right,
+                    2 => MouseButton::Middle,
+                    3 => MouseButton::Navigate(NavigationDirection::Back),
+                    4 => MouseButton::Navigate(NavigationDirection::Forward),
+                    // Other mouse buttons aren't tracked currently
+                    _ => return None,
+                };
+                window_height.map(|window_height| {
+                    Self::MouseDown(MouseDownEvent {
+                        button,
+                        position: point(
+                            px(native_event.locationInWindow().x as f32),
+                            // MacOS screen coordinates are relative to bottom left
+                            window_height - px(native_event.locationInWindow().y as f32),
+                        ),
+                        modifiers: read_modifiers(native_event),
+                        click_count: native_event.clickCount() as usize,
+                    })
+                })
+            }
+            NSEventType::NSLeftMouseUp
+            | NSEventType::NSRightMouseUp
+            | NSEventType::NSOtherMouseUp => {
+                let button = match native_event.buttonNumber() {
+                    0 => MouseButton::Left,
+                    1 => MouseButton::Right,
+                    2 => MouseButton::Middle,
+                    3 => MouseButton::Navigate(NavigationDirection::Back),
+                    4 => MouseButton::Navigate(NavigationDirection::Forward),
+                    // Other mouse buttons aren't tracked currently
+                    _ => return None,
+                };
+
+                window_height.map(|window_height| {
+                    Self::MouseUp(MouseUpEvent {
+                        button,
+                        position: point(
+                            px(native_event.locationInWindow().x as f32),
+                            window_height - px(native_event.locationInWindow().y as f32),
+                        ),
+                        modifiers: read_modifiers(native_event),
+                        click_count: native_event.clickCount() as usize,
+                    })
+                })
+            }
+            NSEventType::NSScrollWheel => window_height.map(|window_height| {
+                let phase = match native_event.phase() {
+                    NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
+                        TouchPhase::Started
+                    }
+                    NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended,
+                    _ => TouchPhase::Moved,
+                };
+
+                let raw_data = point(
+                    native_event.scrollingDeltaX() as f32,
+                    native_event.scrollingDeltaY() as f32,
+                );
+
+                let delta = if native_event.hasPreciseScrollingDeltas() == YES {
+                    ScrollDelta::Pixels(raw_data.map(px))
+                } else {
+                    ScrollDelta::Lines(raw_data)
+                };
+
+                Self::ScrollWheel(ScrollWheelEvent {
+                    position: point(
+                        px(native_event.locationInWindow().x as f32),
+                        window_height - px(native_event.locationInWindow().y as f32),
+                    ),
+                    delta,
+                    touch_phase: phase,
+                    modifiers: read_modifiers(native_event),
+                })
+            }),
+            NSEventType::NSLeftMouseDragged
+            | NSEventType::NSRightMouseDragged
+            | NSEventType::NSOtherMouseDragged => {
+                let pressed_button = match native_event.buttonNumber() {
+                    0 => MouseButton::Left,
+                    1 => MouseButton::Right,
+                    2 => MouseButton::Middle,
+                    3 => MouseButton::Navigate(NavigationDirection::Back),
+                    4 => MouseButton::Navigate(NavigationDirection::Forward),
+                    // Other mouse buttons aren't tracked currently
+                    _ => return None,
+                };
+
+                window_height.map(|window_height| {
+                    Self::MouseMove(MouseMoveEvent {
+                        pressed_button: Some(pressed_button),
+                        position: point(
+                            px(native_event.locationInWindow().x as f32),
+                            window_height - px(native_event.locationInWindow().y as f32),
+                        ),
+                        modifiers: read_modifiers(native_event),
+                    })
+                })
+            }
+            NSEventType::NSMouseMoved => window_height.map(|window_height| {
+                Self::MouseMove(MouseMoveEvent {
+                    position: point(
+                        px(native_event.locationInWindow().x as f32),
+                        window_height - px(native_event.locationInWindow().y as f32),
+                    ),
+                    pressed_button: None,
+                    modifiers: read_modifiers(native_event),
+                })
+            }),
+            NSEventType::NSMouseExited => window_height.map(|window_height| {
+                Self::MouseExited(MouseExitEvent {
+                    position: point(
+                        px(native_event.locationInWindow().x as f32),
+                        window_height - px(native_event.locationInWindow().y as f32),
+                    ),
+
+                    pressed_button: None,
+                    modifiers: read_modifiers(native_event),
+                })
+            }),
+            _ => None,
+        }
+    }
+}
+
+unsafe fn parse_keystroke(native_event: id) -> Keystroke {
+    use cocoa::appkit::*;
+
+    let mut chars_ignoring_modifiers =
+        CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
+            .to_str()
+            .unwrap()
+            .to_string();
+    let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
+    let modifiers = native_event.modifierFlags();
+
+    let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
+    let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
+    let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
+    let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
+    let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
+        && first_char.map_or(true, |ch| {
+            !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)
+        });
+
+    #[allow(non_upper_case_globals)]
+    let key = match first_char {
+        Some(SPACE_KEY) => "space".to_string(),
+        Some(BACKSPACE_KEY) => "backspace".to_string(),
+        Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
+        Some(ESCAPE_KEY) => "escape".to_string(),
+        Some(TAB_KEY) => "tab".to_string(),
+        Some(SHIFT_TAB_KEY) => "tab".to_string(),
+        Some(NSUpArrowFunctionKey) => "up".to_string(),
+        Some(NSDownArrowFunctionKey) => "down".to_string(),
+        Some(NSLeftArrowFunctionKey) => "left".to_string(),
+        Some(NSRightArrowFunctionKey) => "right".to_string(),
+        Some(NSPageUpFunctionKey) => "pageup".to_string(),
+        Some(NSPageDownFunctionKey) => "pagedown".to_string(),
+        Some(NSHomeFunctionKey) => "home".to_string(),
+        Some(NSEndFunctionKey) => "end".to_string(),
+        Some(NSDeleteFunctionKey) => "delete".to_string(),
+        Some(NSF1FunctionKey) => "f1".to_string(),
+        Some(NSF2FunctionKey) => "f2".to_string(),
+        Some(NSF3FunctionKey) => "f3".to_string(),
+        Some(NSF4FunctionKey) => "f4".to_string(),
+        Some(NSF5FunctionKey) => "f5".to_string(),
+        Some(NSF6FunctionKey) => "f6".to_string(),
+        Some(NSF7FunctionKey) => "f7".to_string(),
+        Some(NSF8FunctionKey) => "f8".to_string(),
+        Some(NSF9FunctionKey) => "f9".to_string(),
+        Some(NSF10FunctionKey) => "f10".to_string(),
+        Some(NSF11FunctionKey) => "f11".to_string(),
+        Some(NSF12FunctionKey) => "f12".to_string(),
+        _ => {
+            let mut chars_ignoring_modifiers_and_shift =
+                chars_for_modified_key(native_event.keyCode(), false, false);
+
+            // Honor ⌘ when Dvorak-QWERTY is used.
+            let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
+            if command && chars_ignoring_modifiers_and_shift != chars_with_cmd {
+                chars_ignoring_modifiers =
+                    chars_for_modified_key(native_event.keyCode(), true, shift);
+                chars_ignoring_modifiers_and_shift = chars_with_cmd;
+            }
+
+            if shift {
+                if chars_ignoring_modifiers_and_shift
+                    == chars_ignoring_modifiers.to_ascii_lowercase()
+                {
+                    chars_ignoring_modifiers_and_shift
+                } else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers {
+                    shift = false;
+                    chars_ignoring_modifiers
+                } else {
+                    chars_ignoring_modifiers
+                }
+            } else {
+                chars_ignoring_modifiers
+            }
+        }
+    };
+
+    Keystroke {
+        modifiers: Modifiers {
+            control,
+            alt,
+            shift,
+            command,
+            function,
+        },
+        key,
+        ime_key: None,
+    }
+}
+
+fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
+    // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
+    // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
+    // an event with the given flags instead lets us access `characters`, which always
+    // returns a valid string.
+    let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
+    let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
+    mem::forget(source);
+
+    let mut flags = CGEventFlags::empty();
+    if cmd {
+        flags |= CGEventFlags::CGEventFlagCommand;
+    }
+    if shift {
+        flags |= CGEventFlags::CGEventFlagShift;
+    }
+    event.set_flags(flags);
+
+    unsafe {
+        let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
+        CStr::from_ptr(event.characters().UTF8String())
+            .to_str()
+            .unwrap()
+            .to_string()
+    }
+}

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

@@ -0,0 +1,256 @@
+use crate::{
+    AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
+    Point, Size,
+};
+use anyhow::Result;
+use collections::HashMap;
+use derive_more::{Deref, DerefMut};
+use etagere::BucketedAtlasAllocator;
+use metal::Device;
+use parking_lot::Mutex;
+use std::borrow::Cow;
+
+pub struct MetalAtlas(Mutex<MetalAtlasState>);
+
+impl MetalAtlas {
+    pub fn new(device: Device) -> Self {
+        MetalAtlas(Mutex::new(MetalAtlasState {
+            device: AssertSend(device),
+            monochrome_textures: Default::default(),
+            polychrome_textures: Default::default(),
+            path_textures: Default::default(),
+            tiles_by_key: Default::default(),
+        }))
+    }
+
+    pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> metal::Texture {
+        self.0.lock().texture(id).metal_texture.clone()
+    }
+
+    pub(crate) fn allocate(
+        &self,
+        size: Size<DevicePixels>,
+        texture_kind: AtlasTextureKind,
+    ) -> AtlasTile {
+        self.0.lock().allocate(size, texture_kind)
+    }
+
+    pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) {
+        let mut lock = self.0.lock();
+        let textures = match texture_kind {
+            AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
+            AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
+            AtlasTextureKind::Path => &mut lock.path_textures,
+        };
+        for texture in textures {
+            texture.clear();
+        }
+    }
+}
+
+struct MetalAtlasState {
+    device: AssertSend<Device>,
+    monochrome_textures: Vec<MetalAtlasTexture>,
+    polychrome_textures: Vec<MetalAtlasTexture>,
+    path_textures: Vec<MetalAtlasTexture>,
+    tiles_by_key: HashMap<AtlasKey, AtlasTile>,
+}
+
+impl PlatformAtlas for MetalAtlas {
+    fn get_or_insert_with<'a>(
+        &self,
+        key: &AtlasKey,
+        build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
+    ) -> Result<AtlasTile> {
+        let mut lock = self.0.lock();
+        if let Some(tile) = lock.tiles_by_key.get(key) {
+            return Ok(tile.clone());
+        } else {
+            let (size, bytes) = build()?;
+            let tile = lock.allocate(size, key.texture_kind());
+            let texture = lock.texture(tile.texture_id);
+            texture.upload(tile.bounds, &bytes);
+            lock.tiles_by_key.insert(key.clone(), tile.clone());
+            Ok(tile)
+        }
+    }
+
+    fn clear(&self) {
+        let mut lock = self.0.lock();
+        lock.tiles_by_key.clear();
+        for texture in &mut lock.monochrome_textures {
+            texture.clear();
+        }
+        for texture in &mut lock.polychrome_textures {
+            texture.clear();
+        }
+        for texture in &mut lock.path_textures {
+            texture.clear();
+        }
+    }
+}
+
+impl MetalAtlasState {
+    fn allocate(&mut self, size: Size<DevicePixels>, texture_kind: AtlasTextureKind) -> AtlasTile {
+        let textures = match texture_kind {
+            AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
+            AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+            AtlasTextureKind::Path => &mut self.path_textures,
+        };
+        textures
+            .iter_mut()
+            .rev()
+            .find_map(|texture| texture.allocate(size))
+            .unwrap_or_else(|| {
+                let texture = self.push_texture(size, texture_kind);
+                texture.allocate(size).unwrap()
+            })
+    }
+
+    fn push_texture(
+        &mut self,
+        min_size: Size<DevicePixels>,
+        kind: AtlasTextureKind,
+    ) -> &mut MetalAtlasTexture {
+        const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
+            width: DevicePixels(1024),
+            height: DevicePixels(1024),
+        };
+
+        let size = min_size.max(&DEFAULT_ATLAS_SIZE);
+        let texture_descriptor = metal::TextureDescriptor::new();
+        texture_descriptor.set_width(size.width.into());
+        texture_descriptor.set_height(size.height.into());
+        let pixel_format;
+        let usage;
+        match kind {
+            AtlasTextureKind::Monochrome => {
+                pixel_format = metal::MTLPixelFormat::A8Unorm;
+                usage = metal::MTLTextureUsage::ShaderRead;
+            }
+            AtlasTextureKind::Polychrome => {
+                pixel_format = metal::MTLPixelFormat::BGRA8Unorm;
+                usage = metal::MTLTextureUsage::ShaderRead;
+            }
+            AtlasTextureKind::Path => {
+                pixel_format = metal::MTLPixelFormat::R16Float;
+                usage = metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead;
+            }
+        }
+        texture_descriptor.set_pixel_format(pixel_format);
+        texture_descriptor.set_usage(usage);
+        let metal_texture = self.device.new_texture(&texture_descriptor);
+
+        let textures = match kind {
+            AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
+            AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+            AtlasTextureKind::Path => &mut self.path_textures,
+        };
+        let atlas_texture = MetalAtlasTexture {
+            id: AtlasTextureId {
+                index: textures.len() as u32,
+                kind,
+            },
+            allocator: etagere::BucketedAtlasAllocator::new(size.into()),
+            metal_texture: AssertSend(metal_texture),
+        };
+        textures.push(atlas_texture);
+        textures.last_mut().unwrap()
+    }
+
+    fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture {
+        let textures = match id.kind {
+            crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
+            crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
+            crate::AtlasTextureKind::Path => &self.path_textures,
+        };
+        &textures[id.index as usize]
+    }
+}
+
+struct MetalAtlasTexture {
+    id: AtlasTextureId,
+    allocator: BucketedAtlasAllocator,
+    metal_texture: AssertSend<metal::Texture>,
+}
+
+impl MetalAtlasTexture {
+    fn clear(&mut self) {
+        self.allocator.clear();
+    }
+
+    fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
+        let allocation = self.allocator.allocate(size.into())?;
+        let tile = AtlasTile {
+            texture_id: self.id,
+            tile_id: allocation.id.into(),
+            bounds: Bounds {
+                origin: allocation.rectangle.min.into(),
+                size,
+            },
+        };
+        Some(tile)
+    }
+
+    fn upload(&self, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
+        let region = metal::MTLRegion::new_2d(
+            bounds.origin.x.into(),
+            bounds.origin.y.into(),
+            bounds.size.width.into(),
+            bounds.size.height.into(),
+        );
+        self.metal_texture.replace_region(
+            region,
+            0,
+            bytes.as_ptr() as *const _,
+            u32::from(bounds.size.width.to_bytes(self.bytes_per_pixel())) as u64,
+        );
+    }
+
+    fn bytes_per_pixel(&self) -> u8 {
+        use metal::MTLPixelFormat::*;
+        match self.metal_texture.pixel_format() {
+            A8Unorm | R8Unorm => 1,
+            RGBA8Unorm | BGRA8Unorm => 4,
+            _ => unimplemented!(),
+        }
+    }
+}
+
+impl From<Size<DevicePixels>> for etagere::Size {
+    fn from(size: Size<DevicePixels>) -> Self {
+        etagere::Size::new(size.width.into(), size.height.into())
+    }
+}
+
+impl From<etagere::Point> for Point<DevicePixels> {
+    fn from(value: etagere::Point) -> Self {
+        Point {
+            x: DevicePixels::from(value.x),
+            y: DevicePixels::from(value.y),
+        }
+    }
+}
+
+impl From<etagere::Size> for Size<DevicePixels> {
+    fn from(size: etagere::Size) -> Self {
+        Size {
+            width: DevicePixels::from(size.width),
+            height: DevicePixels::from(size.height),
+        }
+    }
+}
+
+impl From<etagere::Rectangle> for Bounds<DevicePixels> {
+    fn from(rectangle: etagere::Rectangle) -> Self {
+        Bounds {
+            origin: rectangle.min.into(),
+            size: rectangle.size().into(),
+        }
+    }
+}
+
+#[derive(Deref, DerefMut)]
+struct AssertSend<T>(T);
+
+unsafe impl<T> Send for AssertSend<T> {}

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

@@ -0,0 +1,880 @@
+use crate::{
+    point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
+    Hsla, MetalAtlas, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch,
+    Quad, ScaledPixels, Scene, Shadow, Size, Underline,
+};
+use cocoa::{
+    base::{NO, YES},
+    foundation::NSUInteger,
+    quartzcore::AutoresizingMask,
+};
+use collections::HashMap;
+use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
+use objc::{self, msg_send, sel, sel_impl};
+use smallvec::SmallVec;
+use std::{ffi::c_void, mem, ptr, sync::Arc};
+
+const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
+const INSTANCE_BUFFER_SIZE: usize = 8192 * 1024; // This is an arbitrary decision. There's probably a more optimal value.
+
+pub(crate) struct MetalRenderer {
+    layer: metal::MetalLayer,
+    command_queue: CommandQueue,
+    paths_rasterization_pipeline_state: metal::RenderPipelineState,
+    path_sprites_pipeline_state: metal::RenderPipelineState,
+    shadows_pipeline_state: metal::RenderPipelineState,
+    quads_pipeline_state: metal::RenderPipelineState,
+    underlines_pipeline_state: metal::RenderPipelineState,
+    monochrome_sprites_pipeline_state: metal::RenderPipelineState,
+    polychrome_sprites_pipeline_state: metal::RenderPipelineState,
+    unit_vertices: metal::Buffer,
+    instances: metal::Buffer,
+    sprite_atlas: Arc<MetalAtlas>,
+}
+
+impl MetalRenderer {
+    pub fn new(is_opaque: bool) -> Self {
+        let device: metal::Device = if let Some(device) = metal::Device::system_default() {
+            device
+        } else {
+            log::error!("unable to access a compatible graphics device");
+            std::process::exit(1);
+        };
+
+        let layer = metal::MetalLayer::new();
+        layer.set_device(&device);
+        layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
+        layer.set_presents_with_transaction(true);
+        layer.set_opaque(is_opaque);
+        unsafe {
+            let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
+            let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
+            let _: () = msg_send![
+                &*layer,
+                setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
+                    | AutoresizingMask::HEIGHT_SIZABLE
+            ];
+        }
+
+        let library = device
+            .new_library_with_data(SHADERS_METALLIB)
+            .expect("error building metal library");
+
+        fn to_float2_bits(point: crate::PointF) -> u64 {
+            unsafe {
+                let mut output = mem::transmute::<_, u32>(point.y.to_bits()) as u64;
+                output <<= 32;
+                output |= mem::transmute::<_, u32>(point.x.to_bits()) as u64;
+                output
+            }
+        }
+
+        let unit_vertices = [
+            to_float2_bits(point(0., 0.)),
+            to_float2_bits(point(1., 0.)),
+            to_float2_bits(point(0., 1.)),
+            to_float2_bits(point(0., 1.)),
+            to_float2_bits(point(1., 0.)),
+            to_float2_bits(point(1., 1.)),
+        ];
+        let unit_vertices = device.new_buffer_with_data(
+            unit_vertices.as_ptr() as *const c_void,
+            (unit_vertices.len() * mem::size_of::<u64>()) as u64,
+            MTLResourceOptions::StorageModeManaged,
+        );
+        let instances = device.new_buffer(
+            INSTANCE_BUFFER_SIZE as u64,
+            MTLResourceOptions::StorageModeManaged,
+        );
+
+        let paths_rasterization_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "paths_rasterization",
+            "path_rasterization_vertex",
+            "path_rasterization_fragment",
+            MTLPixelFormat::R16Float,
+        );
+        let path_sprites_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "path_sprites",
+            "path_sprite_vertex",
+            "path_sprite_fragment",
+            MTLPixelFormat::BGRA8Unorm,
+        );
+        let shadows_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "shadows",
+            "shadow_vertex",
+            "shadow_fragment",
+            MTLPixelFormat::BGRA8Unorm,
+        );
+        let quads_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "quads",
+            "quad_vertex",
+            "quad_fragment",
+            MTLPixelFormat::BGRA8Unorm,
+        );
+        let underlines_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "underlines",
+            "underline_vertex",
+            "underline_fragment",
+            MTLPixelFormat::BGRA8Unorm,
+        );
+        let monochrome_sprites_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "monochrome_sprites",
+            "monochrome_sprite_vertex",
+            "monochrome_sprite_fragment",
+            MTLPixelFormat::BGRA8Unorm,
+        );
+        let polychrome_sprites_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "polychrome_sprites",
+            "polychrome_sprite_vertex",
+            "polychrome_sprite_fragment",
+            MTLPixelFormat::BGRA8Unorm,
+        );
+
+        let command_queue = device.new_command_queue();
+        let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
+
+        Self {
+            layer,
+            command_queue,
+            paths_rasterization_pipeline_state,
+            path_sprites_pipeline_state,
+            shadows_pipeline_state,
+            quads_pipeline_state,
+            underlines_pipeline_state,
+            monochrome_sprites_pipeline_state,
+            polychrome_sprites_pipeline_state,
+            unit_vertices,
+            instances,
+            sprite_atlas,
+        }
+    }
+
+    pub fn layer(&self) -> &metal::MetalLayerRef {
+        &*self.layer
+    }
+
+    pub fn sprite_atlas(&self) -> &Arc<MetalAtlas> {
+        &self.sprite_atlas
+    }
+
+    pub fn draw(&mut self, scene: &Scene) {
+        let layer = self.layer.clone();
+        let viewport_size = layer.drawable_size();
+        let viewport_size: Size<DevicePixels> = size(
+            (viewport_size.width.ceil() as i32).into(),
+            (viewport_size.height.ceil() as i32).into(),
+        );
+        let drawable = if let Some(drawable) = layer.next_drawable() {
+            drawable
+        } else {
+            log::error!(
+                "failed to retrieve next drawable, drawable size: {:?}",
+                viewport_size
+            );
+            return;
+        };
+        let command_queue = self.command_queue.clone();
+        let command_buffer = command_queue.new_command_buffer();
+        let mut instance_offset = 0;
+
+        let path_tiles = self.rasterize_paths(scene.paths(), &mut instance_offset, &command_buffer);
+
+        let render_pass_descriptor = metal::RenderPassDescriptor::new();
+        let color_attachment = render_pass_descriptor
+            .color_attachments()
+            .object_at(0)
+            .unwrap();
+
+        color_attachment.set_texture(Some(drawable.texture()));
+        color_attachment.set_load_action(metal::MTLLoadAction::Clear);
+        color_attachment.set_store_action(metal::MTLStoreAction::Store);
+        let alpha = if self.layer.is_opaque() { 1. } else { 0. };
+        color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha));
+        let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
+
+        command_encoder.set_viewport(metal::MTLViewport {
+            originX: 0.0,
+            originY: 0.0,
+            width: i32::from(viewport_size.width) as f64,
+            height: i32::from(viewport_size.height) as f64,
+            znear: 0.0,
+            zfar: 1.0,
+        });
+        for batch in scene.batches() {
+            match batch {
+                PrimitiveBatch::Shadows(shadows) => {
+                    self.draw_shadows(
+                        shadows,
+                        &mut instance_offset,
+                        viewport_size,
+                        command_encoder,
+                    );
+                }
+                PrimitiveBatch::Quads(quads) => {
+                    self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder);
+                }
+                PrimitiveBatch::Paths(paths) => {
+                    self.draw_paths(
+                        paths,
+                        &path_tiles,
+                        &mut instance_offset,
+                        viewport_size,
+                        command_encoder,
+                    );
+                }
+                PrimitiveBatch::Underlines(underlines) => {
+                    self.draw_underlines(
+                        underlines,
+                        &mut instance_offset,
+                        viewport_size,
+                        command_encoder,
+                    );
+                }
+                PrimitiveBatch::MonochromeSprites {
+                    texture_id,
+                    sprites,
+                } => {
+                    self.draw_monochrome_sprites(
+                        texture_id,
+                        sprites,
+                        &mut instance_offset,
+                        viewport_size,
+                        command_encoder,
+                    );
+                }
+                PrimitiveBatch::PolychromeSprites {
+                    texture_id,
+                    sprites,
+                } => {
+                    self.draw_polychrome_sprites(
+                        texture_id,
+                        sprites,
+                        &mut instance_offset,
+                        viewport_size,
+                        command_encoder,
+                    );
+                }
+            }
+        }
+
+        command_encoder.end_encoding();
+
+        self.instances.did_modify_range(NSRange {
+            location: 0,
+            length: instance_offset as NSUInteger,
+        });
+
+        command_buffer.commit();
+        self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
+        command_buffer.wait_until_completed();
+        drawable.present();
+    }
+
+    fn rasterize_paths(
+        &mut self,
+        paths: &[Path<ScaledPixels>],
+        offset: &mut usize,
+        command_buffer: &metal::CommandBufferRef,
+    ) -> HashMap<PathId, AtlasTile> {
+        let mut tiles = HashMap::default();
+        let mut vertices_by_texture_id = HashMap::default();
+        for path in paths {
+            let clipped_bounds = path.bounds.intersect(&path.content_mask.bounds);
+
+            let tile = self
+                .sprite_atlas
+                .allocate(clipped_bounds.size.map(Into::into), AtlasTextureKind::Path);
+            vertices_by_texture_id
+                .entry(tile.texture_id)
+                .or_insert(Vec::new())
+                .extend(path.vertices.iter().map(|vertex| PathVertex {
+                    xy_position: vertex.xy_position - path.bounds.origin
+                        + tile.bounds.origin.map(Into::into),
+                    st_position: vertex.st_position,
+                    content_mask: ContentMask {
+                        bounds: tile.bounds.map(Into::into),
+                    },
+                }));
+            tiles.insert(path.id, tile);
+        }
+
+        for (texture_id, vertices) in vertices_by_texture_id {
+            align_offset(offset);
+            let next_offset = *offset + vertices.len() * mem::size_of::<PathVertex<ScaledPixels>>();
+            assert!(
+                next_offset <= INSTANCE_BUFFER_SIZE,
+                "instance buffer exhausted"
+            );
+
+            let render_pass_descriptor = metal::RenderPassDescriptor::new();
+            let color_attachment = render_pass_descriptor
+                .color_attachments()
+                .object_at(0)
+                .unwrap();
+
+            let texture = self.sprite_atlas.metal_texture(texture_id);
+            color_attachment.set_texture(Some(&texture));
+            color_attachment.set_load_action(metal::MTLLoadAction::Clear);
+            color_attachment.set_store_action(metal::MTLStoreAction::Store);
+            color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
+            let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
+            command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
+            command_encoder.set_vertex_buffer(
+                PathRasterizationInputIndex::Vertices as u64,
+                Some(&self.instances),
+                *offset as u64,
+            );
+            let texture_size = Size {
+                width: DevicePixels::from(texture.width()),
+                height: DevicePixels::from(texture.height()),
+            };
+            command_encoder.set_vertex_bytes(
+                PathRasterizationInputIndex::AtlasTextureSize as u64,
+                mem::size_of_val(&texture_size) as u64,
+                &texture_size as *const Size<DevicePixels> as *const _,
+            );
+
+            let vertices_bytes_len = mem::size_of::<PathVertex<ScaledPixels>>() * vertices.len();
+            let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+            unsafe {
+                ptr::copy_nonoverlapping(
+                    vertices.as_ptr() as *const u8,
+                    buffer_contents,
+                    vertices_bytes_len,
+                );
+            }
+
+            command_encoder.draw_primitives(
+                metal::MTLPrimitiveType::Triangle,
+                0,
+                vertices.len() as u64,
+            );
+            command_encoder.end_encoding();
+            *offset = next_offset;
+        }
+
+        tiles
+    }
+
+    fn draw_shadows(
+        &mut self,
+        shadows: &[Shadow],
+        offset: &mut usize,
+        viewport_size: Size<DevicePixels>,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if shadows.is_empty() {
+            return;
+        }
+        align_offset(offset);
+
+        command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            ShadowInputIndex::Vertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_buffer(
+            ShadowInputIndex::Shadows as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_fragment_buffer(
+            ShadowInputIndex::Shadows as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+
+        command_encoder.set_vertex_bytes(
+            ShadowInputIndex::ViewportSize as u64,
+            mem::size_of_val(&viewport_size) as u64,
+            &viewport_size as *const Size<DevicePixels> as *const _,
+        );
+
+        let shadow_bytes_len = mem::size_of::<Shadow>() * shadows.len();
+        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        unsafe {
+            ptr::copy_nonoverlapping(
+                shadows.as_ptr() as *const u8,
+                buffer_contents,
+                shadow_bytes_len,
+            );
+        }
+
+        let next_offset = *offset + shadow_bytes_len;
+        assert!(
+            next_offset <= INSTANCE_BUFFER_SIZE,
+            "instance buffer exhausted"
+        );
+
+        command_encoder.draw_primitives_instanced(
+            metal::MTLPrimitiveType::Triangle,
+            0,
+            6,
+            shadows.len() as u64,
+        );
+        *offset = next_offset;
+    }
+
+    fn draw_quads(
+        &mut self,
+        quads: &[Quad],
+        offset: &mut usize,
+        viewport_size: Size<DevicePixels>,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if quads.is_empty() {
+            return;
+        }
+        align_offset(offset);
+
+        command_encoder.set_render_pipeline_state(&self.quads_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            QuadInputIndex::Vertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_buffer(
+            QuadInputIndex::Quads as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_fragment_buffer(
+            QuadInputIndex::Quads as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+
+        command_encoder.set_vertex_bytes(
+            QuadInputIndex::ViewportSize as u64,
+            mem::size_of_val(&viewport_size) as u64,
+            &viewport_size as *const Size<DevicePixels> as *const _,
+        );
+
+        let quad_bytes_len = mem::size_of::<Quad>() * quads.len();
+        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        unsafe {
+            ptr::copy_nonoverlapping(quads.as_ptr() as *const u8, buffer_contents, quad_bytes_len);
+        }
+
+        let next_offset = *offset + quad_bytes_len;
+        assert!(
+            next_offset <= INSTANCE_BUFFER_SIZE,
+            "instance buffer exhausted"
+        );
+
+        command_encoder.draw_primitives_instanced(
+            metal::MTLPrimitiveType::Triangle,
+            0,
+            6,
+            quads.len() as u64,
+        );
+        *offset = next_offset;
+    }
+
+    fn draw_paths(
+        &mut self,
+        paths: &[Path<ScaledPixels>],
+        tiles_by_path_id: &HashMap<PathId, AtlasTile>,
+        offset: &mut usize,
+        viewport_size: Size<DevicePixels>,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if paths.is_empty() {
+            return;
+        }
+
+        command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            SpriteInputIndex::Vertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_bytes(
+            SpriteInputIndex::ViewportSize as u64,
+            mem::size_of_val(&viewport_size) as u64,
+            &viewport_size as *const Size<DevicePixels> as *const _,
+        );
+
+        let mut prev_texture_id = None;
+        let mut sprites = SmallVec::<[_; 1]>::new();
+        let mut paths_and_tiles = paths
+            .into_iter()
+            .map(|path| (path, tiles_by_path_id.get(&path.id).unwrap()))
+            .peekable();
+
+        loop {
+            if let Some((path, tile)) = paths_and_tiles.peek() {
+                if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) {
+                    prev_texture_id = Some(tile.texture_id);
+                    sprites.push(PathSprite {
+                        bounds: Bounds {
+                            origin: path.bounds.origin.map(|p| p.floor()),
+                            size: tile.bounds.size.map(Into::into),
+                        },
+                        color: path.color,
+                        tile: (*tile).clone(),
+                    });
+                    paths_and_tiles.next();
+                    continue;
+                }
+            }
+
+            if sprites.is_empty() {
+                break;
+            } else {
+                align_offset(offset);
+                let texture_id = prev_texture_id.take().unwrap();
+                let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id);
+                let texture_size = size(
+                    DevicePixels(texture.width() as i32),
+                    DevicePixels(texture.height() as i32),
+                );
+
+                command_encoder.set_vertex_buffer(
+                    SpriteInputIndex::Sprites as u64,
+                    Some(&self.instances),
+                    *offset as u64,
+                );
+                command_encoder.set_vertex_bytes(
+                    SpriteInputIndex::AtlasTextureSize as u64,
+                    mem::size_of_val(&texture_size) as u64,
+                    &texture_size as *const Size<DevicePixels> as *const _,
+                );
+                command_encoder.set_fragment_buffer(
+                    SpriteInputIndex::Sprites as u64,
+                    Some(&self.instances),
+                    *offset as u64,
+                );
+                command_encoder
+                    .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
+
+                let sprite_bytes_len = mem::size_of::<MonochromeSprite>() * sprites.len();
+                let buffer_contents =
+                    unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+                unsafe {
+                    ptr::copy_nonoverlapping(
+                        sprites.as_ptr() as *const u8,
+                        buffer_contents,
+                        sprite_bytes_len,
+                    );
+                }
+
+                let next_offset = *offset + sprite_bytes_len;
+                assert!(
+                    next_offset <= INSTANCE_BUFFER_SIZE,
+                    "instance buffer exhausted"
+                );
+
+                command_encoder.draw_primitives_instanced(
+                    metal::MTLPrimitiveType::Triangle,
+                    0,
+                    6,
+                    sprites.len() as u64,
+                );
+                *offset = next_offset;
+                sprites.clear();
+            }
+        }
+    }
+
+    fn draw_underlines(
+        &mut self,
+        underlines: &[Underline],
+        offset: &mut usize,
+        viewport_size: Size<DevicePixels>,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if underlines.is_empty() {
+            return;
+        }
+        align_offset(offset);
+
+        command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            UnderlineInputIndex::Vertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_buffer(
+            UnderlineInputIndex::Underlines as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_fragment_buffer(
+            UnderlineInputIndex::Underlines as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+
+        command_encoder.set_vertex_bytes(
+            UnderlineInputIndex::ViewportSize as u64,
+            mem::size_of_val(&viewport_size) as u64,
+            &viewport_size as *const Size<DevicePixels> as *const _,
+        );
+
+        let quad_bytes_len = mem::size_of::<Underline>() * underlines.len();
+        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        unsafe {
+            ptr::copy_nonoverlapping(
+                underlines.as_ptr() as *const u8,
+                buffer_contents,
+                quad_bytes_len,
+            );
+        }
+
+        let next_offset = *offset + quad_bytes_len;
+        assert!(
+            next_offset <= INSTANCE_BUFFER_SIZE,
+            "instance buffer exhausted"
+        );
+
+        command_encoder.draw_primitives_instanced(
+            metal::MTLPrimitiveType::Triangle,
+            0,
+            6,
+            underlines.len() as u64,
+        );
+        *offset = next_offset;
+    }
+
+    fn draw_monochrome_sprites(
+        &mut self,
+        texture_id: AtlasTextureId,
+        sprites: &[MonochromeSprite],
+        offset: &mut usize,
+        viewport_size: Size<DevicePixels>,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if sprites.is_empty() {
+            return;
+        }
+        align_offset(offset);
+
+        let texture = self.sprite_atlas.metal_texture(texture_id);
+        let texture_size = size(
+            DevicePixels(texture.width() as i32),
+            DevicePixels(texture.height() as i32),
+        );
+        command_encoder.set_render_pipeline_state(&self.monochrome_sprites_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            SpriteInputIndex::Vertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_buffer(
+            SpriteInputIndex::Sprites as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_vertex_bytes(
+            SpriteInputIndex::ViewportSize as u64,
+            mem::size_of_val(&viewport_size) as u64,
+            &viewport_size as *const Size<DevicePixels> as *const _,
+        );
+        command_encoder.set_vertex_bytes(
+            SpriteInputIndex::AtlasTextureSize as u64,
+            mem::size_of_val(&texture_size) as u64,
+            &texture_size as *const Size<DevicePixels> as *const _,
+        );
+        command_encoder.set_fragment_buffer(
+            SpriteInputIndex::Sprites as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
+
+        let sprite_bytes_len = mem::size_of::<MonochromeSprite>() * sprites.len();
+        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        unsafe {
+            ptr::copy_nonoverlapping(
+                sprites.as_ptr() as *const u8,
+                buffer_contents,
+                sprite_bytes_len,
+            );
+        }
+
+        let next_offset = *offset + sprite_bytes_len;
+        assert!(
+            next_offset <= INSTANCE_BUFFER_SIZE,
+            "instance buffer exhausted"
+        );
+
+        command_encoder.draw_primitives_instanced(
+            metal::MTLPrimitiveType::Triangle,
+            0,
+            6,
+            sprites.len() as u64,
+        );
+        *offset = next_offset;
+    }
+
+    fn draw_polychrome_sprites(
+        &mut self,
+        texture_id: AtlasTextureId,
+        sprites: &[PolychromeSprite],
+        offset: &mut usize,
+        viewport_size: Size<DevicePixels>,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if sprites.is_empty() {
+            return;
+        }
+        align_offset(offset);
+
+        let texture = self.sprite_atlas.metal_texture(texture_id);
+        let texture_size = size(
+            DevicePixels(texture.width() as i32),
+            DevicePixels(texture.height() as i32),
+        );
+        command_encoder.set_render_pipeline_state(&self.polychrome_sprites_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            SpriteInputIndex::Vertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_buffer(
+            SpriteInputIndex::Sprites as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_vertex_bytes(
+            SpriteInputIndex::ViewportSize as u64,
+            mem::size_of_val(&viewport_size) as u64,
+            &viewport_size as *const Size<DevicePixels> as *const _,
+        );
+        command_encoder.set_vertex_bytes(
+            SpriteInputIndex::AtlasTextureSize as u64,
+            mem::size_of_val(&texture_size) as u64,
+            &texture_size as *const Size<DevicePixels> as *const _,
+        );
+        command_encoder.set_fragment_buffer(
+            SpriteInputIndex::Sprites as u64,
+            Some(&self.instances),
+            *offset as u64,
+        );
+        command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
+
+        let sprite_bytes_len = mem::size_of::<PolychromeSprite>() * sprites.len();
+        let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+        unsafe {
+            ptr::copy_nonoverlapping(
+                sprites.as_ptr() as *const u8,
+                buffer_contents,
+                sprite_bytes_len,
+            );
+        }
+
+        let next_offset = *offset + sprite_bytes_len;
+        assert!(
+            next_offset <= INSTANCE_BUFFER_SIZE,
+            "instance buffer exhausted"
+        );
+
+        command_encoder.draw_primitives_instanced(
+            metal::MTLPrimitiveType::Triangle,
+            0,
+            6,
+            sprites.len() as u64,
+        );
+        *offset = next_offset;
+    }
+}
+
+fn build_pipeline_state(
+    device: &metal::DeviceRef,
+    library: &metal::LibraryRef,
+    label: &str,
+    vertex_fn_name: &str,
+    fragment_fn_name: &str,
+    pixel_format: metal::MTLPixelFormat,
+) -> metal::RenderPipelineState {
+    let vertex_fn = library
+        .get_function(vertex_fn_name, None)
+        .expect("error locating vertex function");
+    let fragment_fn = library
+        .get_function(fragment_fn_name, None)
+        .expect("error locating fragment function");
+
+    let descriptor = metal::RenderPipelineDescriptor::new();
+    descriptor.set_label(label);
+    descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
+    descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
+    let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
+    color_attachment.set_pixel_format(pixel_format);
+    color_attachment.set_blending_enabled(true);
+    color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
+    color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
+    color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
+    color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
+    color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
+    color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
+    descriptor.set_depth_attachment_pixel_format(MTLPixelFormat::Invalid);
+
+    device
+        .new_render_pipeline_state(&descriptor)
+        .expect("could not create render pipeline state")
+}
+
+// Align to multiples of 256 make Metal happy.
+fn align_offset(offset: &mut usize) {
+    *offset = ((*offset + 255) / 256) * 256;
+}
+
+#[repr(C)]
+enum ShadowInputIndex {
+    Vertices = 0,
+    Shadows = 1,
+    ViewportSize = 2,
+}
+
+#[repr(C)]
+enum QuadInputIndex {
+    Vertices = 0,
+    Quads = 1,
+    ViewportSize = 2,
+}
+
+#[repr(C)]
+enum UnderlineInputIndex {
+    Vertices = 0,
+    Underlines = 1,
+    ViewportSize = 2,
+}
+
+#[repr(C)]
+enum SpriteInputIndex {
+    Vertices = 0,
+    Sprites = 1,
+    ViewportSize = 2,
+    AtlasTextureSize = 3,
+    AtlasTexture = 4,
+}
+
+#[repr(C)]
+enum PathRasterizationInputIndex {
+    Vertices = 0,
+    AtlasTextureSize = 1,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+#[repr(C)]
+pub struct PathSprite {
+    pub bounds: Bounds<ScaledPixels>,
+    pub color: Hsla,
+    pub tile: AtlasTile,
+}

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

@@ -0,0 +1,394 @@
+#![allow(unused, non_upper_case_globals)]
+
+use crate::FontFeatures;
+use cocoa::appkit::CGFloat;
+use core_foundation::{base::TCFType, number::CFNumber};
+use core_graphics::geometry::CGAffineTransform;
+use core_text::{
+    font::{CTFont, CTFontRef},
+    font_descriptor::{
+        CTFontDescriptor, CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorRef,
+    },
+};
+use font_kit::font::Font;
+use std::ptr;
+
+const kCaseSensitiveLayoutOffSelector: i32 = 1;
+const kCaseSensitiveLayoutOnSelector: i32 = 0;
+const kCaseSensitiveLayoutType: i32 = 33;
+const kCaseSensitiveSpacingOffSelector: i32 = 3;
+const kCaseSensitiveSpacingOnSelector: i32 = 2;
+const kCharacterAlternativesType: i32 = 17;
+const kCommonLigaturesOffSelector: i32 = 3;
+const kCommonLigaturesOnSelector: i32 = 2;
+const kContextualAlternatesOffSelector: i32 = 1;
+const kContextualAlternatesOnSelector: i32 = 0;
+const kContextualAlternatesType: i32 = 36;
+const kContextualLigaturesOffSelector: i32 = 19;
+const kContextualLigaturesOnSelector: i32 = 18;
+const kContextualSwashAlternatesOffSelector: i32 = 5;
+const kContextualSwashAlternatesOnSelector: i32 = 4;
+const kDefaultLowerCaseSelector: i32 = 0;
+const kDefaultUpperCaseSelector: i32 = 0;
+const kDiagonalFractionsSelector: i32 = 2;
+const kFractionsType: i32 = 11;
+const kHistoricalLigaturesOffSelector: i32 = 21;
+const kHistoricalLigaturesOnSelector: i32 = 20;
+const kHojoCharactersSelector: i32 = 12;
+const kInferiorsSelector: i32 = 2;
+const kJIS2004CharactersSelector: i32 = 11;
+const kLigaturesType: i32 = 1;
+const kLowerCasePetiteCapsSelector: i32 = 2;
+const kLowerCaseSmallCapsSelector: i32 = 1;
+const kLowerCaseType: i32 = 37;
+const kLowerCaseNumbersSelector: i32 = 0;
+const kMathematicalGreekOffSelector: i32 = 11;
+const kMathematicalGreekOnSelector: i32 = 10;
+const kMonospacedNumbersSelector: i32 = 0;
+const kNLCCharactersSelector: i32 = 13;
+const kNoFractionsSelector: i32 = 0;
+const kNormalPositionSelector: i32 = 0;
+const kNoStyleOptionsSelector: i32 = 0;
+const kNumberCaseType: i32 = 21;
+const kNumberSpacingType: i32 = 6;
+const kOrdinalsSelector: i32 = 3;
+const kProportionalNumbersSelector: i32 = 1;
+const kQuarterWidthTextSelector: i32 = 4;
+const kScientificInferiorsSelector: i32 = 4;
+const kSlashedZeroOffSelector: i32 = 5;
+const kSlashedZeroOnSelector: i32 = 4;
+const kStyleOptionsType: i32 = 19;
+const kStylisticAltEighteenOffSelector: i32 = 37;
+const kStylisticAltEighteenOnSelector: i32 = 36;
+const kStylisticAltEightOffSelector: i32 = 17;
+const kStylisticAltEightOnSelector: i32 = 16;
+const kStylisticAltElevenOffSelector: i32 = 23;
+const kStylisticAltElevenOnSelector: i32 = 22;
+const kStylisticAlternativesType: i32 = 35;
+const kStylisticAltFifteenOffSelector: i32 = 31;
+const kStylisticAltFifteenOnSelector: i32 = 30;
+const kStylisticAltFiveOffSelector: i32 = 11;
+const kStylisticAltFiveOnSelector: i32 = 10;
+const kStylisticAltFourOffSelector: i32 = 9;
+const kStylisticAltFourOnSelector: i32 = 8;
+const kStylisticAltFourteenOffSelector: i32 = 29;
+const kStylisticAltFourteenOnSelector: i32 = 28;
+const kStylisticAltNineOffSelector: i32 = 19;
+const kStylisticAltNineOnSelector: i32 = 18;
+const kStylisticAltNineteenOffSelector: i32 = 39;
+const kStylisticAltNineteenOnSelector: i32 = 38;
+const kStylisticAltOneOffSelector: i32 = 3;
+const kStylisticAltOneOnSelector: i32 = 2;
+const kStylisticAltSevenOffSelector: i32 = 15;
+const kStylisticAltSevenOnSelector: i32 = 14;
+const kStylisticAltSeventeenOffSelector: i32 = 35;
+const kStylisticAltSeventeenOnSelector: i32 = 34;
+const kStylisticAltSixOffSelector: i32 = 13;
+const kStylisticAltSixOnSelector: i32 = 12;
+const kStylisticAltSixteenOffSelector: i32 = 33;
+const kStylisticAltSixteenOnSelector: i32 = 32;
+const kStylisticAltTenOffSelector: i32 = 21;
+const kStylisticAltTenOnSelector: i32 = 20;
+const kStylisticAltThirteenOffSelector: i32 = 27;
+const kStylisticAltThirteenOnSelector: i32 = 26;
+const kStylisticAltThreeOffSelector: i32 = 7;
+const kStylisticAltThreeOnSelector: i32 = 6;
+const kStylisticAltTwelveOffSelector: i32 = 25;
+const kStylisticAltTwelveOnSelector: i32 = 24;
+const kStylisticAltTwentyOffSelector: i32 = 41;
+const kStylisticAltTwentyOnSelector: i32 = 40;
+const kStylisticAltTwoOffSelector: i32 = 5;
+const kStylisticAltTwoOnSelector: i32 = 4;
+const kSuperiorsSelector: i32 = 1;
+const kSwashAlternatesOffSelector: i32 = 3;
+const kSwashAlternatesOnSelector: i32 = 2;
+const kTitlingCapsSelector: i32 = 4;
+const kTypographicExtrasType: i32 = 14;
+const kVerticalFractionsSelector: i32 = 1;
+const kVerticalPositionType: i32 = 10;
+
+pub fn apply_features(font: &mut Font, features: FontFeatures) {
+    // See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc
+    // for a reference implementation.
+    toggle_open_type_feature(
+        font,
+        features.calt(),
+        kContextualAlternatesType,
+        kContextualAlternatesOnSelector,
+        kContextualAlternatesOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.case(),
+        kCaseSensitiveLayoutType,
+        kCaseSensitiveLayoutOnSelector,
+        kCaseSensitiveLayoutOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.cpsp(),
+        kCaseSensitiveLayoutType,
+        kCaseSensitiveSpacingOnSelector,
+        kCaseSensitiveSpacingOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.frac(),
+        kFractionsType,
+        kDiagonalFractionsSelector,
+        kNoFractionsSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.liga(),
+        kLigaturesType,
+        kCommonLigaturesOnSelector,
+        kCommonLigaturesOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.onum(),
+        kNumberCaseType,
+        kLowerCaseNumbersSelector,
+        2,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ordn(),
+        kVerticalPositionType,
+        kOrdinalsSelector,
+        kNormalPositionSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.pnum(),
+        kNumberSpacingType,
+        kProportionalNumbersSelector,
+        4,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss01(),
+        kStylisticAlternativesType,
+        kStylisticAltOneOnSelector,
+        kStylisticAltOneOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss02(),
+        kStylisticAlternativesType,
+        kStylisticAltTwoOnSelector,
+        kStylisticAltTwoOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss03(),
+        kStylisticAlternativesType,
+        kStylisticAltThreeOnSelector,
+        kStylisticAltThreeOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss04(),
+        kStylisticAlternativesType,
+        kStylisticAltFourOnSelector,
+        kStylisticAltFourOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss05(),
+        kStylisticAlternativesType,
+        kStylisticAltFiveOnSelector,
+        kStylisticAltFiveOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss06(),
+        kStylisticAlternativesType,
+        kStylisticAltSixOnSelector,
+        kStylisticAltSixOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss07(),
+        kStylisticAlternativesType,
+        kStylisticAltSevenOnSelector,
+        kStylisticAltSevenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss08(),
+        kStylisticAlternativesType,
+        kStylisticAltEightOnSelector,
+        kStylisticAltEightOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss09(),
+        kStylisticAlternativesType,
+        kStylisticAltNineOnSelector,
+        kStylisticAltNineOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss10(),
+        kStylisticAlternativesType,
+        kStylisticAltTenOnSelector,
+        kStylisticAltTenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss11(),
+        kStylisticAlternativesType,
+        kStylisticAltElevenOnSelector,
+        kStylisticAltElevenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss12(),
+        kStylisticAlternativesType,
+        kStylisticAltTwelveOnSelector,
+        kStylisticAltTwelveOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss13(),
+        kStylisticAlternativesType,
+        kStylisticAltThirteenOnSelector,
+        kStylisticAltThirteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss14(),
+        kStylisticAlternativesType,
+        kStylisticAltFourteenOnSelector,
+        kStylisticAltFourteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss15(),
+        kStylisticAlternativesType,
+        kStylisticAltFifteenOnSelector,
+        kStylisticAltFifteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss16(),
+        kStylisticAlternativesType,
+        kStylisticAltSixteenOnSelector,
+        kStylisticAltSixteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss17(),
+        kStylisticAlternativesType,
+        kStylisticAltSeventeenOnSelector,
+        kStylisticAltSeventeenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss18(),
+        kStylisticAlternativesType,
+        kStylisticAltEighteenOnSelector,
+        kStylisticAltEighteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss19(),
+        kStylisticAlternativesType,
+        kStylisticAltNineteenOnSelector,
+        kStylisticAltNineteenOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.ss20(),
+        kStylisticAlternativesType,
+        kStylisticAltTwentyOnSelector,
+        kStylisticAltTwentyOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.subs(),
+        kVerticalPositionType,
+        kInferiorsSelector,
+        kNormalPositionSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.sups(),
+        kVerticalPositionType,
+        kSuperiorsSelector,
+        kNormalPositionSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.swsh(),
+        kContextualAlternatesType,
+        kSwashAlternatesOnSelector,
+        kSwashAlternatesOffSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.titl(),
+        kStyleOptionsType,
+        kTitlingCapsSelector,
+        kNoStyleOptionsSelector,
+    );
+    toggle_open_type_feature(
+        font,
+        features.tnum(),
+        kNumberSpacingType,
+        kMonospacedNumbersSelector,
+        4,
+    );
+    toggle_open_type_feature(
+        font,
+        features.zero(),
+        kTypographicExtrasType,
+        kSlashedZeroOnSelector,
+        kSlashedZeroOffSelector,
+    );
+}
+
+fn toggle_open_type_feature(
+    font: &mut Font,
+    enabled: Option<bool>,
+    type_identifier: i32,
+    on_selector_identifier: i32,
+    off_selector_identifier: i32,
+) {
+    if let Some(enabled) = enabled {
+        let native_font = font.native_font();
+        unsafe {
+            let selector_identifier = if enabled {
+                on_selector_identifier
+            } else {
+                off_selector_identifier
+            };
+            let new_descriptor = CTFontDescriptorCreateCopyWithFeature(
+                native_font.copy_descriptor().as_concrete_TypeRef(),
+                CFNumber::from(type_identifier).as_concrete_TypeRef(),
+                CFNumber::from(selector_identifier).as_concrete_TypeRef(),
+            );
+            let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
+            let new_font = CTFontCreateCopyWithAttributes(
+                font.native_font().as_concrete_TypeRef(),
+                0.0,
+                ptr::null(),
+                new_descriptor.as_concrete_TypeRef(),
+            );
+            let new_font = CTFont::wrap_under_create_rule(new_font);
+            *font = Font::from_native_font(new_font);
+        }
+    }
+}
+
+#[link(name = "CoreText", kind = "framework")]
+extern "C" {
+    fn CTFontCreateCopyWithAttributes(
+        font: CTFontRef,
+        size: CGFloat,
+        matrix: *const CGAffineTransform,
+        attributes: CTFontDescriptorRef,
+    ) -> CTFontRef;
+}

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

@@ -0,0 +1,1149 @@
+use super::BoolExt;
+use crate::{
+    AnyWindowHandle, ClipboardItem, CursorStyle, DisplayId, Executor, InputEvent, MacDispatcher,
+    MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, PathPromptOptions, Platform,
+    PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp,
+    WindowOptions,
+};
+use anyhow::anyhow;
+use block::ConcreteBlock;
+use cocoa::{
+    appkit::{
+        NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
+        NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow,
+    },
+    base::{id, nil, BOOL, YES},
+    foundation::{
+        NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
+        NSUInteger, NSURL,
+    },
+};
+use core_foundation::{
+    base::{CFType, CFTypeRef, OSStatus, TCFType as _},
+    boolean::CFBoolean,
+    data::CFData,
+    dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
+    string::{CFString, CFStringRef},
+};
+use ctor::ctor;
+use futures::channel::oneshot;
+use objc::{
+    class,
+    declare::ClassDecl,
+    msg_send,
+    runtime::{Class, Object, Sel},
+    sel, sel_impl,
+};
+use parking_lot::Mutex;
+use ptr::null_mut;
+use std::{
+    cell::Cell,
+    convert::TryInto,
+    ffi::{c_void, CStr, OsStr},
+    os::{raw::c_char, unix::ffi::OsStrExt},
+    path::{Path, PathBuf},
+    process::Command,
+    ptr,
+    rc::Rc,
+    slice, str,
+    sync::Arc,
+};
+use time::UtcOffset;
+
+#[allow(non_upper_case_globals)]
+const NSUTF8StringEncoding: NSUInteger = 4;
+
+#[allow(non_upper_case_globals)]
+pub const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
+
+const MAC_PLATFORM_IVAR: &str = "platform";
+static mut APP_CLASS: *const Class = ptr::null();
+static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
+
+#[ctor]
+unsafe fn build_classes() {
+    APP_CLASS = {
+        let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
+        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
+        decl.add_method(
+            sel!(sendEvent:),
+            send_event as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.register()
+    };
+
+    APP_DELEGATE_CLASS = {
+        let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
+        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
+        decl.add_method(
+            sel!(applicationDidFinishLaunching:),
+            did_finish_launching as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(applicationShouldHandleReopen:hasVisibleWindows:),
+            should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool),
+        );
+        decl.add_method(
+            sel!(applicationDidBecomeActive:),
+            did_become_active as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(applicationDidResignActive:),
+            did_resign_active as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(applicationWillTerminate:),
+            will_terminate as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(handleGPUIMenuItem:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        // Add menu item handlers so that OS save panels have the correct key commands
+        decl.add_method(
+            sel!(cut:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(copy:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(paste:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(selectAll:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(undo:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(redo:),
+            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(validateMenuItem:),
+            validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
+        );
+        decl.add_method(
+            sel!(menuWillOpen:),
+            menu_will_open as extern "C" fn(&mut Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(application:openURLs:),
+            open_urls as extern "C" fn(&mut Object, Sel, id, id),
+        );
+        decl.register()
+    }
+}
+
+pub struct MacPlatform(Mutex<MacPlatformState>);
+
+pub struct MacPlatformState {
+    executor: Executor,
+    text_system: Arc<MacTextSystem>,
+    display_linker: MacDisplayLinker,
+    pasteboard: id,
+    text_hash_pasteboard_type: id,
+    metadata_pasteboard_type: id,
+    become_active: Option<Box<dyn FnMut()>>,
+    resign_active: Option<Box<dyn FnMut()>>,
+    reopen: Option<Box<dyn FnMut()>>,
+    quit: Option<Box<dyn FnMut()>>,
+    event: Option<Box<dyn FnMut(InputEvent) -> bool>>,
+    // menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
+    // validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
+    will_open_menu: Option<Box<dyn FnMut()>>,
+    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
+    finish_launching: Option<Box<dyn FnOnce()>>,
+    // menu_actions: Vec<Box<dyn Action>>,
+}
+
+impl MacPlatform {
+    pub fn new() -> Self {
+        Self(Mutex::new(MacPlatformState {
+            executor: Executor::new(Arc::new(MacDispatcher)),
+            text_system: Arc::new(MacTextSystem::new()),
+            display_linker: MacDisplayLinker::new(),
+            pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
+            text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
+            metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
+            become_active: None,
+            resign_active: None,
+            reopen: None,
+            quit: None,
+            event: None,
+            will_open_menu: None,
+            open_urls: None,
+            finish_launching: None,
+            // menu_command: None,
+            // validate_menu_command: None,
+            // menu_actions: Default::default(),
+        }))
+    }
+
+    unsafe fn read_from_pasteboard(&self, pasteboard: *mut Object, kind: id) -> Option<&[u8]> {
+        let data = pasteboard.dataForType(kind);
+        if data == nil {
+            None
+        } else {
+            Some(slice::from_raw_parts(
+                data.bytes() as *mut u8,
+                data.length() as usize,
+            ))
+        }
+    }
+
+    // unsafe fn create_menu_bar(
+    //     &self,
+    //     menus: Vec<Menu>,
+    //     delegate: id,
+    //     actions: &mut Vec<Box<dyn Action>>,
+    //     keystroke_matcher: &KeymapMatcher,
+    // ) -> id {
+    //     let application_menu = NSMenu::new(nil).autorelease();
+    //     application_menu.setDelegate_(delegate);
+
+    //     for menu_config in menus {
+    //         let menu = NSMenu::new(nil).autorelease();
+    //         menu.setTitle_(ns_string(menu_config.name));
+    //         menu.setDelegate_(delegate);
+
+    //         for item_config in menu_config.items {
+    //             menu.addItem_(self.create_menu_item(
+    //                 item_config,
+    //                 delegate,
+    //                 actions,
+    //                 keystroke_matcher,
+    //             ));
+    //         }
+
+    //         let menu_item = NSMenuItem::new(nil).autorelease();
+    //         menu_item.setSubmenu_(menu);
+    //         application_menu.addItem_(menu_item);
+
+    //         if menu_config.name == "Window" {
+    //             let app: id = msg_send![APP_CLASS, sharedApplication];
+    //             app.setWindowsMenu_(menu);
+    //         }
+    //     }
+
+    //     application_menu
+    // }
+
+    // unsafe fn create_menu_item(
+    //     &self,
+    //     item: MenuItem,
+    //     delegate: id,
+    //     actions: &mut Vec<Box<dyn Action>>,
+    //     keystroke_matcher: &KeymapMatcher,
+    // ) -> id {
+    //     match item {
+    //         MenuItem::Separator => NSMenuItem::separatorItem(nil),
+    //         MenuItem::Action {
+    //             name,
+    //             action,
+    //             os_action,
+    //         } => {
+    //             // TODO
+    //             let keystrokes = keystroke_matcher
+    //                 .bindings_for_action(action.id())
+    //                 .find(|binding| binding.action().eq(action.as_ref()))
+    //                 .map(|binding| binding.keystrokes());
+    //             let selector = match os_action {
+    //                 Some(crate::OsAction::Cut) => selector("cut:"),
+    //                 Some(crate::OsAction::Copy) => selector("copy:"),
+    //                 Some(crate::OsAction::Paste) => selector("paste:"),
+    //                 Some(crate::OsAction::SelectAll) => selector("selectAll:"),
+    //                 Some(crate::OsAction::Undo) => selector("undo:"),
+    //                 Some(crate::OsAction::Redo) => selector("redo:"),
+    //                 None => selector("handleGPUIMenuItem:"),
+    //             };
+
+    //             let item;
+    //             if let Some(keystrokes) = keystrokes {
+    //                 if keystrokes.len() == 1 {
+    //                     let keystroke = &keystrokes[0];
+    //                     let mut mask = NSEventModifierFlags::empty();
+    //                     for (modifier, flag) in &[
+    //                         (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
+    //                         (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
+    //                         (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
+    //                         (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
+    //                     ] {
+    //                         if *modifier {
+    //                             mask |= *flag;
+    //                         }
+    //                     }
+
+    //                     item = NSMenuItem::alloc(nil)
+    //                         .initWithTitle_action_keyEquivalent_(
+    //                             ns_string(name),
+    //                             selector,
+    //                             ns_string(key_to_native(&keystroke.key).as_ref()),
+    //                         )
+    //                         .autorelease();
+    //                     item.setKeyEquivalentModifierMask_(mask);
+    //                 }
+    //                 // For multi-keystroke bindings, render the keystroke as part of the title.
+    //                 else {
+    //                     use std::fmt::Write;
+
+    //                     let mut name = format!("{name} [");
+    //                     for (i, keystroke) in keystrokes.iter().enumerate() {
+    //                         if i > 0 {
+    //                             name.push(' ');
+    //                         }
+    //                         write!(&mut name, "{}", keystroke).unwrap();
+    //                     }
+    //                     name.push(']');
+
+    //                     item = NSMenuItem::alloc(nil)
+    //                         .initWithTitle_action_keyEquivalent_(
+    //                             ns_string(&name),
+    //                             selector,
+    //                             ns_string(""),
+    //                         )
+    //                         .autorelease();
+    //                 }
+    //             } else {
+    //                 item = NSMenuItem::alloc(nil)
+    //                     .initWithTitle_action_keyEquivalent_(
+    //                         ns_string(name),
+    //                         selector,
+    //                         ns_string(""),
+    //                     )
+    //                     .autorelease();
+    //             }
+
+    //             let tag = actions.len() as NSInteger;
+    //             let _: () = msg_send![item, setTag: tag];
+    //             actions.push(action);
+    //             item
+    //         }
+    //         MenuItem::Submenu(Menu { name, items }) => {
+    //             let item = NSMenuItem::new(nil).autorelease();
+    //             let submenu = NSMenu::new(nil).autorelease();
+    //             submenu.setDelegate_(delegate);
+    //             for item in items {
+    //                 submenu.addItem_(self.create_menu_item(
+    //                     item,
+    //                     delegate,
+    //                     actions,
+    //                     keystroke_matcher,
+    //                 ));
+    //             }
+    //             item.setSubmenu_(submenu);
+    //             item.setTitle_(ns_string(name));
+    //             item
+    //         }
+    //     }
+    // }
+}
+
+impl Platform for MacPlatform {
+    fn executor(&self) -> Executor {
+        self.0.lock().executor.clone()
+    }
+
+    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
+        self.0.lock().text_system.clone()
+    }
+
+    fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
+        self.0.lock().finish_launching = Some(on_finish_launching);
+
+        unsafe {
+            let app: id = msg_send![APP_CLASS, sharedApplication];
+            let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
+            app.setDelegate_(app_delegate);
+
+            let self_ptr = self as *const Self as *const c_void;
+            (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
+            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
+
+            let pool = NSAutoreleasePool::new(nil);
+            app.run();
+            pool.drain();
+
+            (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
+            (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
+        }
+    }
+
+    fn quit(&self) {
+        // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks
+        // synchronously before this method terminates. If we call `Platform::quit` while holding a
+        // borrow of the app state (which most of the time we will do), we will end up
+        // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve
+        // this, we make quitting the application asynchronous so that we aren't holding borrows to
+        // the app state on the stack when we actually terminate the app.
+
+        use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue};
+
+        unsafe {
+            dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit));
+        }
+
+        unsafe extern "C" fn quit(_: *mut c_void) {
+            let app = NSApplication::sharedApplication(nil);
+            let _: () = msg_send![app, terminate: nil];
+        }
+    }
+
+    fn restart(&self) {
+        use std::os::unix::process::CommandExt as _;
+
+        let app_pid = std::process::id().to_string();
+        let app_path = self
+            .app_path()
+            .ok()
+            // When the app is not bundled, `app_path` returns the
+            // directory containing the executable. Disregard this
+            // and get the path to the executable itself.
+            .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path))
+            .unwrap_or_else(|| std::env::current_exe().unwrap());
+
+        // Wait until this process has exited and then re-open this path.
+        let script = r#"
+            while kill -0 $0 2> /dev/null; do
+                sleep 0.1
+            done
+            open "$1"
+        "#;
+
+        let restart_process = Command::new("/bin/bash")
+            .arg("-c")
+            .arg(script)
+            .arg(app_pid)
+            .arg(app_path)
+            .process_group(0)
+            .spawn();
+
+        match restart_process {
+            Ok(_) => self.quit(),
+            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
+        }
+    }
+
+    fn activate(&self, ignoring_other_apps: bool) {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
+        }
+    }
+
+    fn hide(&self) {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let _: () = msg_send![app, hide: nil];
+        }
+    }
+
+    fn hide_other_apps(&self) {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let _: () = msg_send![app, hideOtherApplications: nil];
+        }
+    }
+
+    fn unhide_other_apps(&self) {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let _: () = msg_send![app, unhideAllApplications: nil];
+        }
+    }
+
+    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
+        MacDisplay::all()
+            .into_iter()
+            .map(|screen| Rc::new(screen) as Rc<_>)
+            .collect()
+    }
+
+    // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn platform::Window> {
+    //     Box::new(StatusItem::add(self.fonts()))
+    // }
+
+    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
+        MacDisplay::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
+    }
+
+    fn main_window(&self) -> Option<AnyWindowHandle> {
+        MacWindow::main_window()
+    }
+
+    fn open_window(
+        &self,
+        handle: AnyWindowHandle,
+        options: WindowOptions,
+    ) -> Box<dyn PlatformWindow> {
+        Box::new(MacWindow::open(handle, options, self.executor()))
+    }
+
+    fn set_display_link_output_callback(
+        &self,
+        display_id: DisplayId,
+        callback: Box<dyn FnMut(&VideoTimestamp, &VideoTimestamp)>,
+    ) {
+        self.0
+            .lock()
+            .display_linker
+            .set_output_callback(display_id, callback);
+    }
+
+    fn start_display_link(&self, display_id: DisplayId) {
+        self.0.lock().display_linker.start(display_id);
+    }
+
+    fn stop_display_link(&self, display_id: DisplayId) {
+        self.0.lock().display_linker.stop(display_id);
+    }
+
+    fn open_url(&self, url: &str) {
+        unsafe {
+            let url = NSURL::alloc(nil)
+                .initWithString_(ns_string(url))
+                .autorelease();
+            let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
+            msg_send![workspace, openURL: url]
+        }
+    }
+
+    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
+        self.0.lock().open_urls = Some(callback);
+    }
+
+    fn prompt_for_paths(
+        &self,
+        options: PathPromptOptions,
+    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+        unsafe {
+            let panel = NSOpenPanel::openPanel(nil);
+            panel.setCanChooseDirectories_(options.directories.to_objc());
+            panel.setCanChooseFiles_(options.files.to_objc());
+            panel.setAllowsMultipleSelection_(options.multiple.to_objc());
+            panel.setResolvesAliases_(false.to_objc());
+            let (done_tx, done_rx) = oneshot::channel();
+            let done_tx = Cell::new(Some(done_tx));
+            let block = ConcreteBlock::new(move |response: NSModalResponse| {
+                let result = if response == NSModalResponse::NSModalResponseOk {
+                    let mut result = Vec::new();
+                    let urls = panel.URLs();
+                    for i in 0..urls.count() {
+                        let url = urls.objectAtIndex(i);
+                        if url.isFileURL() == YES {
+                            if let Ok(path) = ns_url_to_path(url) {
+                                result.push(path)
+                            }
+                        }
+                    }
+                    Some(result)
+                } else {
+                    None
+                };
+
+                if let Some(done_tx) = done_tx.take() {
+                    let _ = done_tx.send(result);
+                }
+            });
+            let block = block.copy();
+            let _: () = msg_send![panel, beginWithCompletionHandler: block];
+            done_rx
+        }
+    }
+
+    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
+        unsafe {
+            let panel = NSSavePanel::savePanel(nil);
+            let path = ns_string(directory.to_string_lossy().as_ref());
+            let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
+            panel.setDirectoryURL(url);
+
+            let (done_tx, done_rx) = oneshot::channel();
+            let done_tx = Cell::new(Some(done_tx));
+            let block = ConcreteBlock::new(move |response: NSModalResponse| {
+                let mut result = None;
+                if response == NSModalResponse::NSModalResponseOk {
+                    let url = panel.URL();
+                    if url.isFileURL() == YES {
+                        result = ns_url_to_path(panel.URL()).ok()
+                    }
+                }
+
+                if let Some(done_tx) = done_tx.take() {
+                    let _ = done_tx.send(result);
+                }
+            });
+            let block = block.copy();
+            let _: () = msg_send![panel, beginWithCompletionHandler: block];
+            done_rx
+        }
+    }
+
+    fn reveal_path(&self, path: &Path) {
+        unsafe {
+            let path = path.to_path_buf();
+            self.0
+                .lock()
+                .executor
+                .spawn_on_main_local(async move {
+                    let full_path = ns_string(path.to_str().unwrap_or(""));
+                    let root_full_path = ns_string("");
+                    let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
+                    let _: BOOL = msg_send![
+                        workspace,
+                        selectFile: full_path
+                        inFileViewerRootedAtPath: root_full_path
+                    ];
+                })
+                .detach();
+        }
+    }
+
+    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
+        self.0.lock().become_active = Some(callback);
+    }
+
+    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
+        self.0.lock().resign_active = Some(callback);
+    }
+
+    fn on_quit(&self, callback: Box<dyn FnMut()>) {
+        self.0.lock().quit = Some(callback);
+    }
+
+    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
+        self.0.lock().reopen = Some(callback);
+    }
+
+    fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>) {
+        self.0.lock().event = Some(callback);
+    }
+
+    fn os_name(&self) -> &'static str {
+        "macOS"
+    }
+
+    fn os_version(&self) -> Result<SemanticVersion> {
+        unsafe {
+            let process_info = NSProcessInfo::processInfo(nil);
+            let version = process_info.operatingSystemVersion();
+            Ok(SemanticVersion {
+                major: version.majorVersion as usize,
+                minor: version.minorVersion as usize,
+                patch: version.patchVersion as usize,
+            })
+        }
+    }
+
+    fn app_version(&self) -> Result<SemanticVersion> {
+        unsafe {
+            let bundle: id = NSBundle::mainBundle();
+            if bundle.is_null() {
+                Err(anyhow!("app is not running inside a bundle"))
+            } else {
+                let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")];
+                let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
+                let bytes = version.UTF8String() as *const u8;
+                let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap();
+                version.parse()
+            }
+        }
+    }
+
+    fn app_path(&self) -> Result<PathBuf> {
+        unsafe {
+            let bundle: id = NSBundle::mainBundle();
+            if bundle.is_null() {
+                Err(anyhow!("app is not running inside a bundle"))
+            } else {
+                Ok(path_from_objc(msg_send![bundle, bundlePath]))
+            }
+        }
+    }
+
+    fn local_timezone(&self) -> UtcOffset {
+        unsafe {
+            let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
+            let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT];
+            UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
+        }
+    }
+
+    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
+        unsafe {
+            let bundle: id = NSBundle::mainBundle();
+            if bundle.is_null() {
+                Err(anyhow!("app is not running inside a bundle"))
+            } else {
+                let name = ns_string(name);
+                let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name];
+                if url.is_null() {
+                    Err(anyhow!("resource not found"))
+                } else {
+                    ns_url_to_path(url)
+                }
+            }
+        }
+    }
+
+    // fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
+    //     self.0.lock().menu_command = Some(callback);
+    // }
+
+    // fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
+    //     self.0.lock().will_open_menu = Some(callback);
+    // }
+
+    // fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
+    //     self.0.lock().validate_menu_command = Some(callback);
+    // }
+
+    // fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
+    //     unsafe {
+    //         let app: id = msg_send![APP_CLASS, sharedApplication];
+    //         let mut state = self.0.lock();
+    //         let actions = &mut state.menu_actions;
+    //         app.setMainMenu_(self.create_menu_bar(
+    //             menus,
+    //             app.delegate(),
+    //             actions,
+    //             keystroke_matcher,
+    //         ));
+    //     }
+    // }
+
+    fn set_cursor_style(&self, style: CursorStyle) {
+        unsafe {
+            let new_cursor: id = match style {
+                CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
+                CursorStyle::ResizeLeftRight => {
+                    msg_send![class!(NSCursor), resizeLeftRightCursor]
+                }
+                CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
+                CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
+                CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
+            };
+
+            let old_cursor: id = msg_send![class!(NSCursor), currentCursor];
+            if new_cursor != old_cursor {
+                let _: () = msg_send![new_cursor, set];
+            }
+        }
+    }
+
+    fn should_auto_hide_scrollbars(&self) -> bool {
+        #[allow(non_upper_case_globals)]
+        const NSScrollerStyleOverlay: NSInteger = 1;
+
+        unsafe {
+            let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle];
+            style == NSScrollerStyleOverlay
+        }
+    }
+
+    fn write_to_clipboard(&self, item: ClipboardItem) {
+        let state = self.0.lock();
+        unsafe {
+            state.pasteboard.clearContents();
+
+            let text_bytes = NSData::dataWithBytes_length_(
+                nil,
+                item.text.as_ptr() as *const c_void,
+                item.text.len() as u64,
+            );
+            state
+                .pasteboard
+                .setData_forType(text_bytes, NSPasteboardTypeString);
+
+            if let Some(metadata) = item.metadata.as_ref() {
+                let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes();
+                let hash_bytes = NSData::dataWithBytes_length_(
+                    nil,
+                    hash_bytes.as_ptr() as *const c_void,
+                    hash_bytes.len() as u64,
+                );
+                state
+                    .pasteboard
+                    .setData_forType(hash_bytes, state.text_hash_pasteboard_type);
+
+                let metadata_bytes = NSData::dataWithBytes_length_(
+                    nil,
+                    metadata.as_ptr() as *const c_void,
+                    metadata.len() as u64,
+                );
+                state
+                    .pasteboard
+                    .setData_forType(metadata_bytes, state.metadata_pasteboard_type);
+            }
+        }
+    }
+
+    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
+        let state = self.0.lock();
+        unsafe {
+            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.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.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) {
+                    if hash == ClipboardItem::text_hash(&text) {
+                        Some(ClipboardItem {
+                            text,
+                            metadata: Some(metadata),
+                        })
+                    } else {
+                        Some(ClipboardItem {
+                            text,
+                            metadata: None,
+                        })
+                    }
+                } else {
+                    Some(ClipboardItem {
+                        text,
+                        metadata: None,
+                    })
+                }
+            } else {
+                None
+            }
+        }
+    }
+
+    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
+        let url = CFString::from(url);
+        let username = CFString::from(username);
+        let password = CFData::from_buffer(password);
+
+        unsafe {
+            use security::*;
+
+            // First, check if there are already credentials for the given server. If so, then
+            // update the username and password.
+            let mut verb = "updating";
+            let mut query_attrs = CFMutableDictionary::with_capacity(2);
+            query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+            query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+
+            let mut attrs = CFMutableDictionary::with_capacity(4);
+            attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+            attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+            attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
+            attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
+
+            let mut status = SecItemUpdate(
+                query_attrs.as_concrete_TypeRef(),
+                attrs.as_concrete_TypeRef(),
+            );
+
+            // If there were no existing credentials for the given server, then create them.
+            if status == errSecItemNotFound {
+                verb = "creating";
+                status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
+            }
+
+            if status != errSecSuccess {
+                return Err(anyhow!("{} password failed: {}", verb, status));
+            }
+        }
+        Ok(())
+    }
+
+    fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
+        let url = CFString::from(url);
+        let cf_true = CFBoolean::true_value().as_CFTypeRef();
+
+        unsafe {
+            use security::*;
+
+            // Find any credentials for the given server URL.
+            let mut attrs = CFMutableDictionary::with_capacity(5);
+            attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+            attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+            attrs.set(kSecReturnAttributes as *const _, cf_true);
+            attrs.set(kSecReturnData as *const _, cf_true);
+
+            let mut result = CFTypeRef::from(ptr::null());
+            let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
+            match status {
+                security::errSecSuccess => {}
+                security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
+                _ => return Err(anyhow!("reading password failed: {}", status)),
+            }
+
+            let result = CFType::wrap_under_create_rule(result)
+                .downcast::<CFDictionary>()
+                .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
+            let username = result
+                .find(kSecAttrAccount as *const _)
+                .ok_or_else(|| anyhow!("account was missing from keychain item"))?;
+            let username = CFType::wrap_under_get_rule(*username)
+                .downcast::<CFString>()
+                .ok_or_else(|| anyhow!("account was not a string"))?;
+            let password = result
+                .find(kSecValueData as *const _)
+                .ok_or_else(|| anyhow!("password was missing from keychain item"))?;
+            let password = CFType::wrap_under_get_rule(*password)
+                .downcast::<CFData>()
+                .ok_or_else(|| anyhow!("password was not a string"))?;
+
+            Ok(Some((username.to_string(), password.bytes().to_vec())))
+        }
+    }
+
+    fn delete_credentials(&self, url: &str) -> Result<()> {
+        let url = CFString::from(url);
+
+        unsafe {
+            use security::*;
+
+            let mut query_attrs = CFMutableDictionary::with_capacity(2);
+            query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+            query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+
+            let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
+
+            if status != errSecSuccess {
+                return Err(anyhow!("delete password failed: {}", status));
+            }
+        }
+        Ok(())
+    }
+}
+
+unsafe fn path_from_objc(path: id) -> PathBuf {
+    let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
+    let bytes = path.UTF8String() as *const u8;
+    let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap();
+    PathBuf::from(path)
+}
+
+unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform {
+    let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
+    assert!(!platform_ptr.is_null());
+    &*(platform_ptr as *const MacPlatform)
+}
+
+extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
+    unsafe {
+        if let Some(event) = InputEvent::from_native(native_event, None) {
+            let platform = get_foreground_platform(this);
+            if let Some(callback) = platform.0.lock().event.as_mut() {
+                if !callback(event) {
+                    return;
+                }
+            }
+        }
+        msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
+    }
+}
+
+extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
+    unsafe {
+        let app: id = msg_send![APP_CLASS, sharedApplication];
+        app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
+
+        let platform = get_foreground_platform(this);
+        let callback = platform.0.lock().finish_launching.take();
+        if let Some(callback) = callback {
+            callback();
+        }
+    }
+}
+
+extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
+    if !has_open_windows {
+        let platform = unsafe { get_foreground_platform(this) };
+        if let Some(callback) = platform.0.lock().reopen.as_mut() {
+            callback();
+        }
+    }
+}
+
+extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
+    let platform = unsafe { get_foreground_platform(this) };
+    if let Some(callback) = platform.0.lock().become_active.as_mut() {
+        callback();
+    }
+}
+
+extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
+    let platform = unsafe { get_foreground_platform(this) };
+    if let Some(callback) = platform.0.lock().resign_active.as_mut() {
+        callback();
+    }
+}
+
+extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
+    let platform = unsafe { get_foreground_platform(this) };
+    if let Some(callback) = platform.0.lock().quit.as_mut() {
+        callback();
+    }
+}
+
+extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
+    let urls = unsafe {
+        (0..urls.count())
+            .into_iter()
+            .filter_map(|i| {
+                let url = urls.objectAtIndex(i);
+                match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() {
+                    Ok(string) => Some(string.to_string()),
+                    Err(err) => {
+                        log::error!("error converting path to string: {}", err);
+                        None
+                    }
+                }
+            })
+            .collect::<Vec<_>>()
+    };
+    let platform = unsafe { get_foreground_platform(this) };
+    if let Some(callback) = platform.0.lock().open_urls.as_mut() {
+        callback(urls);
+    }
+}
+
+extern "C" fn handle_menu_item(__this: &mut Object, _: Sel, __item: id) {
+    todo!()
+    // unsafe {
+    //     let platform = get_foreground_platform(this);
+    //     let mut platform = platform.0.lock();
+    //     if let Some(mut callback) = platform.menu_command.take() {
+    //         let tag: NSInteger = msg_send![item, tag];
+    //         let index = tag as usize;
+    //         if let Some(action) = platform.menu_actions.get(index) {
+    //             callback(action.as_ref());
+    //         }
+    //         platform.menu_command = Some(callback);
+    //     }
+    // }
+}
+
+extern "C" fn validate_menu_item(__this: &mut Object, _: Sel, __item: id) -> bool {
+    todo!()
+    // unsafe {
+    //     let mut result = false;
+    //     let platform = get_foreground_platform(this);
+    //     let mut platform = platform.0.lock();
+    //     if let Some(mut callback) = platform.validate_menu_command.take() {
+    //         let tag: NSInteger = msg_send![item, tag];
+    //         let index = tag as usize;
+    //         if let Some(action) = platform.menu_actions.get(index) {
+    //             result = callback(action.as_ref());
+    //         }
+    //         platform.validate_menu_command = Some(callback);
+    //     }
+    //     result
+    // }
+}
+
+extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
+    unsafe {
+        let platform = get_foreground_platform(this);
+        let mut platform = platform.0.lock();
+        if let Some(mut callback) = platform.will_open_menu.take() {
+            callback();
+            platform.will_open_menu = Some(callback);
+        }
+    }
+}
+
+unsafe fn ns_string(string: &str) -> id {
+    NSString::alloc(nil).init_str(string).autorelease()
+}
+
+unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
+    let path: *mut c_char = msg_send![url, fileSystemRepresentation];
+    if path.is_null() {
+        Err(anyhow!(
+            "url is not a file path: {}",
+            CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy()
+        ))
+    } else {
+        Ok(PathBuf::from(OsStr::from_bytes(
+            CStr::from_ptr(path).to_bytes(),
+        )))
+    }
+}
+
+mod security {
+    #![allow(non_upper_case_globals)]
+    use super::*;
+
+    #[link(name = "Security", kind = "framework")]
+    extern "C" {
+        pub static kSecClass: CFStringRef;
+        pub static kSecClassInternetPassword: CFStringRef;
+        pub static kSecAttrServer: CFStringRef;
+        pub static kSecAttrAccount: CFStringRef;
+        pub static kSecValueData: CFStringRef;
+        pub static kSecReturnAttributes: CFStringRef;
+        pub static kSecReturnData: CFStringRef;
+
+        pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
+        pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
+        pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus;
+        pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
+    }
+
+    pub const errSecSuccess: OSStatus = 0;
+    pub const errSecUserCanceled: OSStatus = -128;
+    pub const errSecItemNotFound: OSStatus = -25300;
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::ClipboardItem;
+
+    use super::*;
+
+    #[test]
+    fn test_clipboard() {
+        let platform = build_platform();
+        assert_eq!(platform.read_from_clipboard(), None);
+
+        let item = ClipboardItem::new("1".to_string());
+        platform.write_to_clipboard(item.clone());
+        assert_eq!(platform.read_from_clipboard(), Some(item));
+
+        let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]);
+        platform.write_to_clipboard(item.clone());
+        assert_eq!(platform.read_from_clipboard(), Some(item));
+
+        let text_from_other_app = "text from other app";
+        unsafe {
+            let bytes = NSData::dataWithBytes_length_(
+                nil,
+                text_from_other_app.as_ptr() as *const c_void,
+                text_from_other_app.len() as u64,
+            );
+            platform
+                .0
+                .lock()
+                .pasteboard
+                .setData_forType(bytes, NSPasteboardTypeString);
+        }
+        assert_eq!(
+            platform.read_from_clipboard(),
+            Some(ClipboardItem::new(text_from_other_app.to_string()))
+        );
+    }
+
+    fn build_platform() -> MacPlatform {
+        let platform = MacPlatform::new();
+        platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
+        platform
+    }
+}

crates/gpui2/src/platform/mac/shaders.metal 🔗

@@ -0,0 +1,553 @@
+#include <metal_stdlib>
+#include <simd/simd.h>
+
+using namespace metal;
+
+float4 hsla_to_rgba(Hsla hsla);
+float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
+                          Bounds_ScaledPixels clip_bounds,
+                          constant Size_DevicePixels *viewport_size);
+float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
+                        constant Size_DevicePixels *atlas_size);
+float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
+               Corners_ScaledPixels corner_radii);
+float gaussian(float x, float sigma);
+float2 erf(float2 x);
+float blur_along_x(float x, float y, float sigma, float corner,
+                   float2 half_size);
+
+struct QuadVertexOutput {
+  float4 position [[position]];
+  float4 background_color [[flat]];
+  float4 border_color [[flat]];
+  uint quad_id [[flat]];
+};
+
+vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
+                                    uint quad_id [[instance_id]],
+                                    constant float2 *unit_vertices
+                                    [[buffer(QuadInputIndex_Vertices)]],
+                                    constant Quad *quads
+                                    [[buffer(QuadInputIndex_Quads)]],
+                                    constant Size_DevicePixels *viewport_size
+                                    [[buffer(QuadInputIndex_ViewportSize)]]) {
+  float2 unit_vertex = unit_vertices[unit_vertex_id];
+  Quad quad = quads[quad_id];
+  float4 device_position = to_device_position(
+      unit_vertex, quad.bounds, quad.content_mask.bounds, viewport_size);
+  float4 background_color = hsla_to_rgba(quad.background);
+  float4 border_color = hsla_to_rgba(quad.border_color);
+  return QuadVertexOutput{device_position, background_color, border_color,
+                          quad_id};
+}
+
+fragment float4 quad_fragment(QuadVertexOutput input [[stage_in]],
+                              constant Quad *quads
+                              [[buffer(QuadInputIndex_Quads)]]) {
+  Quad quad = quads[input.quad_id];
+  float2 half_size =
+      float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
+  float2 center =
+      float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
+  float2 center_to_point = input.position.xy - center;
+  float corner_radius;
+  if (center_to_point.x < 0.) {
+    if (center_to_point.y < 0.) {
+      corner_radius = quad.corner_radii.top_left;
+    } else {
+      corner_radius = quad.corner_radii.bottom_left;
+    }
+  } else {
+    if (center_to_point.y < 0.) {
+      corner_radius = quad.corner_radii.top_right;
+    } else {
+      corner_radius = quad.corner_radii.bottom_right;
+    }
+  }
+
+  float2 rounded_edge_to_point =
+      fabs(center_to_point) - half_size + corner_radius;
+  float distance =
+      length(max(0., rounded_edge_to_point)) +
+      min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
+      corner_radius;
+
+  float vertical_border = center_to_point.x <= 0. ? quad.border_widths.left
+                                                  : quad.border_widths.right;
+  float horizontal_border = center_to_point.y <= 0. ? quad.border_widths.top
+                                                    : quad.border_widths.bottom;
+  float2 inset_size =
+      half_size - corner_radius - float2(vertical_border, horizontal_border);
+  float2 point_to_inset_corner = fabs(center_to_point) - inset_size;
+  float border_width;
+  if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) {
+    border_width = 0.;
+  } else if (point_to_inset_corner.y > point_to_inset_corner.x) {
+    border_width = horizontal_border;
+  } else {
+    border_width = vertical_border;
+  }
+
+  float4 color;
+  if (border_width == 0.) {
+    color = input.background_color;
+  } else {
+    float inset_distance = distance + border_width;
+
+    // Decrease border's opacity as we move inside the background.
+    input.border_color.a *= 1. - saturate(0.5 - inset_distance);
+
+    // Alpha-blend the border and the background.
+    float output_alpha =
+        quad.border_color.a + quad.background.a * (1. - quad.border_color.a);
+    float3 premultiplied_border_rgb =
+        input.border_color.rgb * quad.border_color.a;
+    float3 premultiplied_background_rgb =
+        input.background_color.rgb * input.background_color.a;
+    float3 premultiplied_output_rgb =
+        premultiplied_border_rgb +
+        premultiplied_background_rgb * (1. - input.border_color.a);
+    color = float4(premultiplied_output_rgb, output_alpha);
+  }
+
+  return color * float4(1., 1., 1., saturate(0.5 - distance));
+}
+
+struct ShadowVertexOutput {
+  float4 position [[position]];
+  float4 color [[flat]];
+  uint shadow_id [[flat]];
+};
+
+vertex ShadowVertexOutput shadow_vertex(
+    uint unit_vertex_id [[vertex_id]], uint shadow_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(ShadowInputIndex_Vertices)]],
+    constant Shadow *shadows [[buffer(ShadowInputIndex_Shadows)]],
+    constant Size_DevicePixels *viewport_size
+    [[buffer(ShadowInputIndex_ViewportSize)]]) {
+  float2 unit_vertex = unit_vertices[unit_vertex_id];
+  Shadow shadow = shadows[shadow_id];
+
+  float margin = 3. * shadow.blur_radius;
+  // Set the bounds of the shadow and adjust its size based on the shadow's
+  // spread radius to achieve the spreading effect
+  Bounds_ScaledPixels bounds = shadow.bounds;
+  bounds.origin.x -= margin;
+  bounds.origin.y -= margin;
+  bounds.size.width += 2. * margin;
+  bounds.size.height += 2. * margin;
+
+  float4 device_position = to_device_position(
+      unit_vertex, bounds, shadow.content_mask.bounds, viewport_size);
+  float4 color = hsla_to_rgba(shadow.color);
+
+  return ShadowVertexOutput{
+      device_position,
+      color,
+      shadow_id,
+  };
+}
+
+fragment float4 shadow_fragment(ShadowVertexOutput input [[stage_in]],
+                                constant Shadow *shadows
+                                [[buffer(ShadowInputIndex_Shadows)]]) {
+  Shadow shadow = shadows[input.shadow_id];
+
+  float2 origin = float2(shadow.bounds.origin.x, shadow.bounds.origin.y);
+  float2 size = float2(shadow.bounds.size.width, shadow.bounds.size.height);
+  float2 half_size = size / 2.;
+  float2 center = origin + half_size;
+  float2 point = input.position.xy - center;
+  float corner_radius;
+  if (point.x < 0.) {
+    if (point.y < 0.) {
+      corner_radius = shadow.corner_radii.top_left;
+    } else {
+      corner_radius = shadow.corner_radii.bottom_left;
+    }
+  } else {
+    if (point.y < 0.) {
+      corner_radius = shadow.corner_radii.top_right;
+    } else {
+      corner_radius = shadow.corner_radii.bottom_right;
+    }
+  }
+
+  // The signal is only non-zero in a limited range, so don't waste samples
+  float low = point.y - half_size.y;
+  float high = point.y + half_size.y;
+  float start = clamp(-3. * shadow.blur_radius, low, high);
+  float end = clamp(3. * shadow.blur_radius, low, high);
+
+  // Accumulate samples (we can get away with surprisingly few samples)
+  float step = (end - start) / 4.;
+  float y = start + step * 0.5;
+  float alpha = 0.;
+  for (int i = 0; i < 4; i++) {
+    alpha += blur_along_x(point.x, point.y - y, shadow.blur_radius,
+                          corner_radius, half_size) *
+             gaussian(y, shadow.blur_radius) * step;
+    y += step;
+  }
+
+  return input.color * float4(1., 1., 1., alpha);
+}
+
+struct UnderlineVertexOutput {
+  float4 position [[position]];
+  float4 color [[flat]];
+  uint underline_id [[flat]];
+};
+
+vertex UnderlineVertexOutput underline_vertex(
+    uint unit_vertex_id [[vertex_id]], uint underline_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(UnderlineInputIndex_Vertices)]],
+    constant Underline *underlines [[buffer(UnderlineInputIndex_Underlines)]],
+    constant Size_DevicePixels *viewport_size
+    [[buffer(ShadowInputIndex_ViewportSize)]]) {
+  float2 unit_vertex = unit_vertices[unit_vertex_id];
+  Underline underline = underlines[underline_id];
+  float4 device_position =
+      to_device_position(unit_vertex, underline.bounds,
+                         underline.content_mask.bounds, viewport_size);
+  float4 color = hsla_to_rgba(underline.color);
+  return UnderlineVertexOutput{device_position, color, underline_id};
+}
+
+fragment float4 underline_fragment(UnderlineVertexOutput input [[stage_in]],
+                                   constant Underline *underlines
+                                   [[buffer(UnderlineInputIndex_Underlines)]]) {
+  Underline underline = underlines[input.underline_id];
+  if (underline.wavy) {
+    float half_thickness = underline.thickness * 0.5;
+    float2 origin =
+        float2(underline.bounds.origin.x, underline.bounds.origin.y);
+    float2 st = ((input.position.xy - origin) / underline.bounds.size.height) -
+                float2(0., 0.5);
+    float frequency = (M_PI_F * (3. * underline.thickness)) / 8.;
+    float amplitude = 1. / (2. * underline.thickness);
+    float sine = sin(st.x * frequency) * amplitude;
+    float dSine = cos(st.x * frequency) * amplitude * frequency;
+    float distance = (st.y - sine) / sqrt(1. + dSine * dSine);
+    float distance_in_pixels = distance * underline.bounds.size.height;
+    float distance_from_top_border = distance_in_pixels - half_thickness;
+    float distance_from_bottom_border = distance_in_pixels + half_thickness;
+    float alpha = saturate(
+        0.5 - max(-distance_from_bottom_border, distance_from_top_border));
+    return input.color * float4(1., 1., 1., alpha);
+  } else {
+    return input.color;
+  }
+}
+
+struct MonochromeSpriteVertexOutput {
+  float4 position [[position]];
+  float2 tile_position;
+  float4 color [[flat]];
+  uint sprite_id [[flat]];
+};
+
+vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex(
+    uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]],
+    constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
+    constant Size_DevicePixels *viewport_size
+    [[buffer(SpriteInputIndex_ViewportSize)]],
+    constant Size_DevicePixels *atlas_size
+    [[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
+
+  float2 unit_vertex = unit_vertices[unit_vertex_id];
+  MonochromeSprite sprite = sprites[sprite_id];
+  // Don't apply content mask at the vertex level because we don't have time
+  // to make sampling from the texture match the mask.
+  float4 device_position = to_device_position(unit_vertex, sprite.bounds,
+                                              sprite.bounds, viewport_size);
+  float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
+  float4 color = hsla_to_rgba(sprite.color);
+  return MonochromeSpriteVertexOutput{device_position, tile_position, color,
+                                      sprite_id};
+}
+
+fragment float4 monochrome_sprite_fragment(
+    MonochromeSpriteVertexOutput input [[stage_in]],
+    constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
+    texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
+  MonochromeSprite sprite = sprites[input.sprite_id];
+  constexpr sampler atlas_texture_sampler(mag_filter::linear,
+                                          min_filter::linear);
+  float4 sample =
+      atlas_texture.sample(atlas_texture_sampler, input.tile_position);
+  float clip_distance = quad_sdf(input.position.xy, sprite.content_mask.bounds,
+                                 Corners_ScaledPixels{0., 0., 0., 0.});
+  float4 color = input.color;
+  color.a *= sample.a * saturate(0.5 - clip_distance);
+  return color;
+}
+
+struct PolychromeSpriteVertexOutput {
+  float4 position [[position]];
+  float2 tile_position;
+  uint sprite_id [[flat]];
+};
+
+vertex PolychromeSpriteVertexOutput polychrome_sprite_vertex(
+    uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]],
+    constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
+    constant Size_DevicePixels *viewport_size
+    [[buffer(SpriteInputIndex_ViewportSize)]],
+    constant Size_DevicePixels *atlas_size
+    [[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
+
+  float2 unit_vertex = unit_vertices[unit_vertex_id];
+  PolychromeSprite sprite = sprites[sprite_id];
+  // Don't apply content mask at the vertex level because we don't have time
+  // to make sampling from the texture match the mask.
+  float4 device_position = to_device_position(unit_vertex, sprite.bounds,
+                                              sprite.bounds, viewport_size);
+  float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
+  return PolychromeSpriteVertexOutput{device_position, tile_position,
+                                      sprite_id};
+}
+
+fragment float4 polychrome_sprite_fragment(
+    PolychromeSpriteVertexOutput input [[stage_in]],
+    constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
+    texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
+  PolychromeSprite sprite = sprites[input.sprite_id];
+  constexpr sampler atlas_texture_sampler(mag_filter::linear,
+                                          min_filter::linear);
+  float4 sample =
+      atlas_texture.sample(atlas_texture_sampler, input.tile_position);
+  float quad_distance =
+      quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii);
+  float clip_distance = quad_sdf(input.position.xy, sprite.content_mask.bounds,
+                                 Corners_ScaledPixels{0., 0., 0., 0.});
+  float distance = max(quad_distance, clip_distance);
+
+  float4 color = sample;
+  if (sprite.grayscale) {
+    float grayscale = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
+    color.r = grayscale;
+    color.g = grayscale;
+    color.b = grayscale;
+  }
+  color.a *= saturate(0.5 - distance);
+  return color;
+}
+
+struct PathRasterizationVertexOutput {
+  float4 position [[position]];
+  float2 st_position;
+  float clip_rect_distance [[clip_distance]][4];
+};
+
+struct PathRasterizationFragmentInput {
+  float4 position [[position]];
+  float2 st_position;
+};
+
+vertex PathRasterizationVertexOutput path_rasterization_vertex(
+    uint vertex_id [[vertex_id]],
+    constant PathVertex_ScaledPixels *vertices
+    [[buffer(PathRasterizationInputIndex_Vertices)]],
+    constant Size_DevicePixels *atlas_size
+    [[buffer(PathRasterizationInputIndex_AtlasTextureSize)]]) {
+  PathVertex_ScaledPixels v = vertices[vertex_id];
+  float2 vertex_position = float2(v.xy_position.x, v.xy_position.y);
+  float2 viewport_size = float2(atlas_size->width, atlas_size->height);
+  return PathRasterizationVertexOutput{
+      float4(vertex_position / viewport_size * float2(2., -2.) +
+                 float2(-1., 1.),
+             0., 1.),
+      float2(v.st_position.x, v.st_position.y),
+      {v.xy_position.x - v.content_mask.bounds.origin.x,
+       v.content_mask.bounds.origin.x + v.content_mask.bounds.size.width -
+           v.xy_position.x,
+       v.xy_position.y - v.content_mask.bounds.origin.y,
+       v.content_mask.bounds.origin.y + v.content_mask.bounds.size.height -
+           v.xy_position.y}};
+}
+
+fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput input
+                                            [[stage_in]]) {
+  float2 dx = dfdx(input.st_position);
+  float2 dy = dfdy(input.st_position);
+  float2 gradient = float2((2. * input.st_position.x) * dx.x - dx.y,
+                           (2. * input.st_position.x) * dy.x - dy.y);
+  float f = (input.st_position.x * input.st_position.x) - input.st_position.y;
+  float distance = f / length(gradient);
+  float alpha = saturate(0.5 - distance);
+  return float4(alpha, 0., 0., 1.);
+}
+
+struct PathSpriteVertexOutput {
+  float4 position [[position]];
+  float2 tile_position;
+  float4 color [[flat]];
+  uint sprite_id [[flat]];
+};
+
+vertex PathSpriteVertexOutput path_sprite_vertex(
+    uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]],
+    constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
+    constant Size_DevicePixels *viewport_size
+    [[buffer(SpriteInputIndex_ViewportSize)]],
+    constant Size_DevicePixels *atlas_size
+    [[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
+
+  float2 unit_vertex = unit_vertices[unit_vertex_id];
+  PathSprite sprite = sprites[sprite_id];
+  // Don't apply content mask because it was already accounted for when
+  // rasterizing the path.
+  float4 device_position = to_device_position(unit_vertex, sprite.bounds,
+                                              sprite.bounds, viewport_size);
+  float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
+  float4 color = hsla_to_rgba(sprite.color);
+  return PathSpriteVertexOutput{device_position, tile_position, color,
+                                sprite_id};
+}
+
+fragment float4 path_sprite_fragment(
+    PathSpriteVertexOutput input [[stage_in]],
+    constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
+    texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
+  PathSprite sprite = sprites[input.sprite_id];
+  constexpr sampler atlas_texture_sampler(mag_filter::linear,
+                                          min_filter::linear);
+  float4 sample =
+      atlas_texture.sample(atlas_texture_sampler, input.tile_position);
+  float mask = 1. - abs(1. - fmod(sample.r, 2.));
+  float4 color = input.color;
+  color.a *= mask;
+  return color;
+}
+
+float4 hsla_to_rgba(Hsla hsla) {
+  float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
+  float s = hsla.s;
+  float l = hsla.l;
+  float a = hsla.a;
+
+  float c = (1.0 - fabs(2.0 * l - 1.0)) * s;
+  float x = c * (1.0 - fabs(fmod(h, 2.0) - 1.0));
+  float m = l - c / 2.0;
+
+  float r = 0.0;
+  float g = 0.0;
+  float b = 0.0;
+
+  if (h >= 0.0 && h < 1.0) {
+    r = c;
+    g = x;
+    b = 0.0;
+  } else if (h >= 1.0 && h < 2.0) {
+    r = x;
+    g = c;
+    b = 0.0;
+  } else if (h >= 2.0 && h < 3.0) {
+    r = 0.0;
+    g = c;
+    b = x;
+  } else if (h >= 3.0 && h < 4.0) {
+    r = 0.0;
+    g = x;
+    b = c;
+  } else if (h >= 4.0 && h < 5.0) {
+    r = x;
+    g = 0.0;
+    b = c;
+  } else {
+    r = c;
+    g = 0.0;
+    b = x;
+  }
+
+  float4 rgba;
+  rgba.x = (r + m);
+  rgba.y = (g + m);
+  rgba.z = (b + m);
+  rgba.w = a;
+  return rgba;
+}
+
+float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
+                          Bounds_ScaledPixels clip_bounds,
+                          constant Size_DevicePixels *input_viewport_size) {
+  float2 position =
+      unit_vertex * float2(bounds.size.width, bounds.size.height) +
+      float2(bounds.origin.x, bounds.origin.y);
+  position.x = max(clip_bounds.origin.x, position.x);
+  position.x = min(clip_bounds.origin.x + clip_bounds.size.width, position.x);
+  position.y = max(clip_bounds.origin.y, position.y);
+  position.y = min(clip_bounds.origin.y + clip_bounds.size.height, position.y);
+
+  float2 viewport_size = float2((float)input_viewport_size->width,
+                                (float)input_viewport_size->height);
+  float2 device_position =
+      position / viewport_size * float2(2., -2.) + float2(-1., 1.);
+  return float4(device_position, 0., 1.);
+}
+
+float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
+                        constant Size_DevicePixels *atlas_size) {
+  float2 tile_origin = float2(tile.bounds.origin.x, tile.bounds.origin.y);
+  float2 tile_size = float2(tile.bounds.size.width, tile.bounds.size.height);
+  return (tile_origin + unit_vertex * tile_size) /
+         float2((float)atlas_size->width, (float)atlas_size->height);
+}
+
+float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
+               Corners_ScaledPixels corner_radii) {
+  float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
+  float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
+  float2 center_to_point = point - center;
+  float corner_radius;
+  if (center_to_point.x < 0.) {
+    if (center_to_point.y < 0.) {
+      corner_radius = corner_radii.top_left;
+    } else {
+      corner_radius = corner_radii.bottom_left;
+    }
+  } else {
+    if (center_to_point.y < 0.) {
+      corner_radius = corner_radii.top_right;
+    } else {
+      corner_radius = corner_radii.bottom_right;
+    }
+  }
+
+  float2 rounded_edge_to_point =
+      abs(center_to_point) - half_size + corner_radius;
+  float distance =
+      length(max(0., rounded_edge_to_point)) +
+      min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
+      corner_radius;
+
+  return distance;
+}
+
+// A standard gaussian function, used for weighting samples
+float gaussian(float x, float sigma) {
+  return exp(-(x * x) / (2. * sigma * sigma)) / (sqrt(2. * M_PI_F) * sigma);
+}
+
+// This approximates the error function, needed for the gaussian integral
+float2 erf(float2 x) {
+  float2 s = sign(x);
+  float2 a = abs(x);
+  x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
+  x *= x;
+  return s - s / (x * x);
+}
+
+float blur_along_x(float x, float y, float sigma, float corner,
+                   float2 half_size) {
+  float delta = min(half_size.y - corner - abs(y), 0.);
+  float curved =
+      half_size.x - corner + sqrt(max(0., corner * corner - delta * delta));
+  float2 integral =
+      0.5 + 0.5 * erf((x + float2(-curved, curved)) * (sqrt(0.5) / sigma));
+  return integral.y - integral.x;
+}

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

@@ -0,0 +1,752 @@
+use crate::{
+    point, px, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun,
+    FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point,
+    RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS,
+};
+use anyhow::anyhow;
+use cocoa::appkit::{CGFloat, CGPoint};
+use collections::HashMap;
+use core_foundation::{
+    array::CFIndex,
+    attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
+    base::{CFRange, TCFType},
+    string::CFString,
+};
+use core_graphics::{
+    base::{kCGImageAlphaPremultipliedLast, CGGlyph},
+    color_space::CGColorSpace,
+    context::CGContext,
+};
+use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
+use font_kit::{
+    font::Font as FontKitFont,
+    handle::Handle,
+    hinting::HintingOptions,
+    metrics::Metrics,
+    properties::{Style as FontkitStyle, Weight as FontkitWeight},
+    source::SystemSource,
+    sources::mem::MemSource,
+};
+use parking_lot::{RwLock, RwLockUpgradableReadGuard};
+use pathfinder_geometry::{
+    rect::{RectF, RectI},
+    transform2d::Transform2F,
+    vector::{Vector2F, Vector2I},
+};
+use smallvec::SmallVec;
+use std::{char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
+
+use super::open_type;
+
+#[allow(non_upper_case_globals)]
+const kCGImageAlphaOnly: u32 = 7;
+
+pub struct MacTextSystem(RwLock<MacTextSystemState>);
+
+struct MacTextSystemState {
+    memory_source: MemSource,
+    system_source: SystemSource,
+    fonts: Vec<FontKitFont>,
+    font_selections: HashMap<Font, FontId>,
+    font_ids_by_postscript_name: HashMap<String, FontId>,
+    font_ids_by_family_name: HashMap<SharedString, SmallVec<[FontId; 4]>>,
+    postscript_names_by_font_id: HashMap<FontId, String>,
+}
+
+impl MacTextSystem {
+    pub fn new() -> Self {
+        Self(RwLock::new(MacTextSystemState {
+            memory_source: MemSource::empty(),
+            system_source: SystemSource::new(),
+            fonts: Vec::new(),
+            font_selections: HashMap::default(),
+            font_ids_by_postscript_name: HashMap::default(),
+            font_ids_by_family_name: HashMap::default(),
+            postscript_names_by_font_id: HashMap::default(),
+        }))
+    }
+}
+
+impl Default for MacTextSystem {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl PlatformTextSystem for MacTextSystem {
+    fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
+        self.0.write().add_fonts(fonts)
+    }
+
+    fn all_font_families(&self) -> Vec<String> {
+        self.0
+            .read()
+            .system_source
+            .all_families()
+            .expect("core text should never return an error")
+    }
+
+    fn font_id(&self, font: &Font) -> Result<FontId> {
+        let lock = self.0.upgradable_read();
+        if let Some(font_id) = lock.font_selections.get(font) {
+            Ok(*font_id)
+        } else {
+            let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
+            let candidates = if let Some(font_ids) = lock.font_ids_by_family_name.get(&font.family)
+            {
+                font_ids.as_slice()
+            } else {
+                let font_ids = lock.load_family(&font.family, font.features)?;
+                lock.font_ids_by_family_name
+                    .insert(font.family.clone(), font_ids);
+                lock.font_ids_by_family_name[&font.family].as_ref()
+            };
+
+            let candidate_properties = candidates
+                .iter()
+                .map(|font_id| lock.fonts[font_id.0].properties())
+                .collect::<SmallVec<[_; 4]>>();
+
+            let ix = font_kit::matching::find_best_match(
+                &candidate_properties,
+                &font_kit::properties::Properties {
+                    style: font.style.into(),
+                    weight: font.weight.into(),
+                    stretch: Default::default(),
+                },
+            )?;
+
+            Ok(candidates[ix])
+        }
+    }
+
+    fn font_metrics(&self, font_id: FontId) -> FontMetrics {
+        self.0.read().fonts[font_id.0].metrics().into()
+    }
+
+    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
+        Ok(self.0.read().fonts[font_id.0]
+            .typographic_bounds(glyph_id.into())?
+            .into())
+    }
+
+    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
+        self.0.read().advance(font_id, glyph_id)
+    }
+
+    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
+        self.0.read().glyph_for_char(font_id, ch)
+    }
+
+    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
+        self.0.read().raster_bounds(params)
+    }
+
+    fn rasterize_glyph(
+        &self,
+        glyph_id: &RenderGlyphParams,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
+        self.0.read().rasterize_glyph(glyph_id)
+    }
+
+    fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
+        self.0.write().layout_line(text, font_size, font_runs)
+    }
+
+    fn wrap_line(
+        &self,
+        text: &str,
+        font_id: FontId,
+        font_size: Pixels,
+        width: Pixels,
+    ) -> Vec<usize> {
+        self.0.read().wrap_line(text, font_id, font_size, width)
+    }
+}
+
+impl MacTextSystemState {
+    fn add_fonts(&mut self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
+        self.memory_source.add_fonts(
+            fonts
+                .iter()
+                .map(|bytes| Handle::from_memory(bytes.clone(), 0)),
+        )?;
+        Ok(())
+    }
+
+    fn load_family(
+        &mut self,
+        name: &SharedString,
+        features: FontFeatures,
+    ) -> Result<SmallVec<[FontId; 4]>> {
+        let mut font_ids = SmallVec::new();
+        let family = self
+            .memory_source
+            .select_family_by_name(name.as_ref())
+            .or_else(|_| self.system_source.select_family_by_name(name.as_ref()))?;
+        for font in family.fonts() {
+            let mut font = font.load()?;
+            open_type::apply_features(&mut font, features);
+            let font_id = FontId(self.fonts.len());
+            font_ids.push(font_id);
+            let postscript_name = font.postscript_name().unwrap();
+            self.font_ids_by_postscript_name
+                .insert(postscript_name.clone(), font_id);
+            self.postscript_names_by_font_id
+                .insert(font_id, postscript_name);
+            self.fonts.push(font);
+        }
+        Ok(font_ids)
+    }
+
+    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
+        Ok(self.fonts[font_id.0].advance(glyph_id.into())?.into())
+    }
+
+    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
+        self.fonts[font_id.0].glyph_for_char(ch).map(Into::into)
+    }
+
+    fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId {
+        let postscript_name = requested_font.postscript_name();
+        if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) {
+            *font_id
+        } else {
+            let font_id = FontId(self.fonts.len());
+            self.font_ids_by_postscript_name
+                .insert(postscript_name.clone(), font_id);
+            self.postscript_names_by_font_id
+                .insert(font_id, postscript_name);
+            self.fonts
+                .push(font_kit::font::Font::from_core_graphics_font(
+                    requested_font.copy_to_CGFont(),
+                ));
+            font_id
+        }
+    }
+
+    fn is_emoji(&self, font_id: FontId) -> bool {
+        self.postscript_names_by_font_id
+            .get(&font_id)
+            .map_or(false, |postscript_name| {
+                postscript_name == "AppleColorEmoji"
+            })
+    }
+
+    fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
+        let font = &self.fonts[params.font_id.0];
+        let scale = Transform2F::from_scale(params.scale_factor);
+        Ok(font
+            .raster_bounds(
+                params.glyph_id.into(),
+                params.font_size.into(),
+                scale,
+                HintingOptions::None,
+                font_kit::canvas::RasterizationOptions::GrayscaleAa,
+            )?
+            .into())
+    }
+
+    fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)> {
+        let glyph_bounds = self.raster_bounds(params)?;
+        if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
+            Err(anyhow!("glyph bounds are empty"))
+        } else {
+            // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing.
+            let mut bitmap_size = glyph_bounds.size;
+            if params.subpixel_variant.x > 0 {
+                bitmap_size.width += DevicePixels(1);
+            }
+            if params.subpixel_variant.y > 0 {
+                bitmap_size.height += DevicePixels(1);
+            }
+
+            let mut bytes;
+            let cx;
+            if params.is_emoji {
+                bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize];
+                cx = CGContext::create_bitmap_context(
+                    Some(bytes.as_mut_ptr() as *mut _),
+                    bitmap_size.width.0 as usize,
+                    bitmap_size.height.0 as usize,
+                    8,
+                    bitmap_size.width.0 as usize * 4,
+                    &CGColorSpace::create_device_rgb(),
+                    kCGImageAlphaPremultipliedLast,
+                );
+            } else {
+                bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize];
+                cx = CGContext::create_bitmap_context(
+                    Some(bytes.as_mut_ptr() as *mut _),
+                    bitmap_size.width.0 as usize,
+                    bitmap_size.height.0 as usize,
+                    8,
+                    bitmap_size.width.0 as usize,
+                    &CGColorSpace::create_device_gray(),
+                    kCGImageAlphaOnly,
+                );
+            }
+
+            // Move the origin to bottom left and account for scaling, this
+            // makes drawing text consistent with the font-kit's raster_bounds.
+            cx.translate(
+                -glyph_bounds.origin.x.0 as CGFloat,
+                (glyph_bounds.origin.y.0 + glyph_bounds.size.height.0) as CGFloat,
+            );
+            cx.scale(
+                params.scale_factor as CGFloat,
+                params.scale_factor as CGFloat,
+            );
+
+            let subpixel_shift = params
+                .subpixel_variant
+                .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32);
+            cx.set_allows_font_subpixel_positioning(true);
+            cx.set_should_subpixel_position_fonts(true);
+            cx.set_allows_font_subpixel_quantization(false);
+            cx.set_should_subpixel_quantize_fonts(false);
+            self.fonts[params.font_id.0]
+                .native_font()
+                .clone_with_font_size(f32::from(params.font_size) as CGFloat)
+                .draw_glyphs(
+                    &[u32::from(params.glyph_id) as CGGlyph],
+                    &[CGPoint::new(
+                        (subpixel_shift.x / params.scale_factor) as CGFloat,
+                        (subpixel_shift.y / params.scale_factor) as CGFloat,
+                    )],
+                    cx,
+                );
+
+            if params.is_emoji {
+                // Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
+                for pixel in bytes.chunks_exact_mut(4) {
+                    pixel.swap(0, 2);
+                    let a = pixel[3] as f32 / 255.;
+                    pixel[0] = (pixel[0] as f32 / a) as u8;
+                    pixel[1] = (pixel[1] as f32 / a) as u8;
+                    pixel[2] = (pixel[2] as f32 / a) as u8;
+                }
+            }
+
+            Ok((bitmap_size.into(), bytes))
+        }
+    }
+
+    fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
+        // Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
+        let mut string = CFMutableAttributedString::new();
+        {
+            string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+            let utf16_line_len = string.char_len() as usize;
+
+            let mut ix_converter = StringIndexConverter::new(text);
+            for run in font_runs {
+                let utf8_end = ix_converter.utf8_ix + run.len;
+                let utf16_start = ix_converter.utf16_ix;
+
+                if utf16_start >= utf16_line_len {
+                    break;
+                }
+
+                ix_converter.advance_to_utf8_ix(utf8_end);
+                let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len);
+
+                let cf_range =
+                    CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
+
+                let font: &FontKitFont = &self.fonts[run.font_id.0];
+                unsafe {
+                    string.set_attribute(
+                        cf_range,
+                        kCTFontAttributeName,
+                        &font.native_font().clone_with_font_size(font_size.into()),
+                    );
+                }
+
+                if utf16_end == utf16_line_len {
+                    break;
+                }
+            }
+        }
+
+        // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets.
+        let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
+
+        let mut runs = Vec::new();
+        for run in line.glyph_runs().into_iter() {
+            let attributes = run.attributes().unwrap();
+            let font = unsafe {
+                attributes
+                    .get(kCTFontAttributeName)
+                    .downcast::<CTFont>()
+                    .unwrap()
+            };
+            let font_id = self.id_for_native_font(font);
+
+            let mut ix_converter = StringIndexConverter::new(text);
+            let mut glyphs = SmallVec::new();
+            for ((glyph_id, position), glyph_utf16_ix) in run
+                .glyphs()
+                .iter()
+                .zip(run.positions().iter())
+                .zip(run.string_indices().iter())
+            {
+                let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap();
+                ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
+                glyphs.push(ShapedGlyph {
+                    id: (*glyph_id).into(),
+                    position: point(position.x as f32, position.y as f32).map(px),
+                    index: ix_converter.utf8_ix,
+                    is_emoji: self.is_emoji(font_id),
+                });
+            }
+
+            runs.push(ShapedRun { font_id, glyphs })
+        }
+
+        let typographic_bounds = line.get_typographic_bounds();
+        LineLayout {
+            width: typographic_bounds.width.into(),
+            ascent: typographic_bounds.ascent.into(),
+            descent: typographic_bounds.descent.into(),
+            runs,
+            font_size,
+        }
+    }
+
+    fn wrap_line(
+        &self,
+        text: &str,
+        font_id: FontId,
+        font_size: Pixels,
+        width: Pixels,
+    ) -> Vec<usize> {
+        let mut string = CFMutableAttributedString::new();
+        string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+        let cf_range = CFRange::init(0, text.encode_utf16().count() as isize);
+        let font = &self.fonts[font_id.0];
+        unsafe {
+            string.set_attribute(
+                cf_range,
+                kCTFontAttributeName,
+                &font.native_font().clone_with_font_size(font_size.into()),
+            );
+
+            let typesetter = CTTypesetterCreateWithAttributedString(string.as_concrete_TypeRef());
+            let mut ix_converter = StringIndexConverter::new(text);
+            let mut break_indices = Vec::new();
+            while ix_converter.utf8_ix < text.len() {
+                let utf16_len = CTTypesetterSuggestLineBreak(
+                    typesetter,
+                    ix_converter.utf16_ix as isize,
+                    width.into(),
+                ) as usize;
+                ix_converter.advance_to_utf16_ix(ix_converter.utf16_ix + utf16_len);
+                if ix_converter.utf8_ix >= text.len() {
+                    break;
+                }
+                break_indices.push(ix_converter.utf8_ix as usize);
+            }
+            break_indices
+        }
+    }
+}
+
+#[derive(Clone)]
+struct StringIndexConverter<'a> {
+    text: &'a str,
+    utf8_ix: usize,
+    utf16_ix: usize,
+}
+
+impl<'a> StringIndexConverter<'a> {
+    fn new(text: &'a str) -> Self {
+        Self {
+            text,
+            utf8_ix: 0,
+            utf16_ix: 0,
+        }
+    }
+
+    fn advance_to_utf8_ix(&mut self, utf8_target: usize) {
+        for (ix, c) in self.text[self.utf8_ix..].char_indices() {
+            if self.utf8_ix + ix >= utf8_target {
+                self.utf8_ix += ix;
+                return;
+            }
+            self.utf16_ix += c.len_utf16();
+        }
+        self.utf8_ix = self.text.len();
+    }
+
+    fn advance_to_utf16_ix(&mut self, utf16_target: usize) {
+        for (ix, c) in self.text[self.utf8_ix..].char_indices() {
+            if self.utf16_ix >= utf16_target {
+                self.utf8_ix += ix;
+                return;
+            }
+            self.utf16_ix += c.len_utf16();
+        }
+        self.utf8_ix = self.text.len();
+    }
+}
+
+#[repr(C)]
+pub struct __CFTypesetter(c_void);
+
+pub type CTTypesetterRef = *const __CFTypesetter;
+
+#[link(name = "CoreText", kind = "framework")]
+extern "C" {
+    fn CTTypesetterCreateWithAttributedString(string: CFAttributedStringRef) -> CTTypesetterRef;
+
+    fn CTTypesetterSuggestLineBreak(
+        typesetter: CTTypesetterRef,
+        start_index: CFIndex,
+        width: f64,
+    ) -> CFIndex;
+}
+
+impl From<Metrics> for FontMetrics {
+    fn from(metrics: Metrics) -> Self {
+        FontMetrics {
+            units_per_em: metrics.units_per_em,
+            ascent: metrics.ascent,
+            descent: metrics.descent,
+            line_gap: metrics.line_gap,
+            underline_position: metrics.underline_position,
+            underline_thickness: metrics.underline_thickness,
+            cap_height: metrics.cap_height,
+            x_height: metrics.x_height,
+            bounding_box: metrics.bounding_box.into(),
+        }
+    }
+}
+
+impl From<RectF> for Bounds<f32> {
+    fn from(rect: RectF) -> Self {
+        Bounds {
+            origin: point(rect.origin_x(), rect.origin_y()),
+            size: size(rect.width(), rect.height()),
+        }
+    }
+}
+
+impl From<RectI> for Bounds<DevicePixels> {
+    fn from(rect: RectI) -> Self {
+        Bounds {
+            origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
+            size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
+        }
+    }
+}
+
+impl From<Vector2I> for Size<DevicePixels> {
+    fn from(value: Vector2I) -> Self {
+        size(value.x().into(), value.y().into())
+    }
+}
+
+impl From<RectI> for Bounds<i32> {
+    fn from(rect: RectI) -> Self {
+        Bounds {
+            origin: point(rect.origin_x(), rect.origin_y()),
+            size: size(rect.width(), rect.height()),
+        }
+    }
+}
+
+impl From<Point<u32>> for Vector2I {
+    fn from(size: Point<u32>) -> Self {
+        Vector2I::new(size.x as i32, size.y as i32)
+    }
+}
+
+impl From<Vector2F> for Size<f32> {
+    fn from(vec: Vector2F) -> Self {
+        size(vec.x(), vec.y())
+    }
+}
+
+impl From<FontWeight> for FontkitWeight {
+    fn from(value: FontWeight) -> Self {
+        FontkitWeight(value.0)
+    }
+}
+
+impl From<FontStyle> for FontkitStyle {
+    fn from(style: FontStyle) -> Self {
+        match style {
+            FontStyle::Normal => FontkitStyle::Normal,
+            FontStyle::Italic => FontkitStyle::Italic,
+            FontStyle::Oblique => FontkitStyle::Oblique,
+        }
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use crate::AppContext;
+//     use font_kit::properties::{Style, Weight};
+//     use platform::FontSystem as _;
+
+//     #[crate::test(self, retries = 5)]
+//     fn test_layout_str(_: &mut AppContext) {
+//         // This is failing intermittently on CI and we don't have time to figure it out
+//         let fonts = FontSystem::new();
+//         let menlo = fonts.load_family("Menlo", &Default::default()).unwrap();
+//         let menlo_regular = RunStyle {
+//             font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
+//             color: Default::default(),
+//             underline: Default::default(),
+//         };
+//         let menlo_italic = RunStyle {
+//             font_id: fonts
+//                 .select_font(&menlo, Properties::new().style(Style::Italic))
+//                 .unwrap(),
+//             color: Default::default(),
+//             underline: Default::default(),
+//         };
+//         let menlo_bold = RunStyle {
+//             font_id: fonts
+//                 .select_font(&menlo, Properties::new().weight(Weight::BOLD))
+//                 .unwrap(),
+//             color: Default::default(),
+//             underline: Default::default(),
+//         };
+//         assert_ne!(menlo_regular, menlo_italic);
+//         assert_ne!(menlo_regular, menlo_bold);
+//         assert_ne!(menlo_italic, menlo_bold);
+
+//         let line = fonts.layout_line(
+//             "hello world",
+//             16.0,
+//             &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)],
+//         );
+//         assert_eq!(line.runs.len(), 3);
+//         assert_eq!(line.runs[0].font_id, menlo_bold.font_id);
+//         assert_eq!(line.runs[0].glyphs.len(), 2);
+//         assert_eq!(line.runs[1].font_id, menlo_italic.font_id);
+//         assert_eq!(line.runs[1].glyphs.len(), 4);
+//         assert_eq!(line.runs[2].font_id, menlo_regular.font_id);
+//         assert_eq!(line.runs[2].glyphs.len(), 5);
+//     }
+
+//     #[test]
+//     fn test_glyph_offsets() -> crate::Result<()> {
+//         let fonts = FontSystem::new();
+//         let zapfino = fonts.load_family("Zapfino", &Default::default())?;
+//         let zapfino_regular = RunStyle {
+//             font_id: fonts.select_font(&zapfino, &Properties::new())?,
+//             color: Default::default(),
+//             underline: Default::default(),
+//         };
+//         let menlo = fonts.load_family("Menlo", &Default::default())?;
+//         let menlo_regular = RunStyle {
+//             font_id: fonts.select_font(&menlo, &Properties::new())?,
+//             color: Default::default(),
+//             underline: Default::default(),
+//         };
+
+//         let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
+//         let line = fonts.layout_line(
+//             text,
+//             16.0,
+//             &[
+//                 (9, zapfino_regular),
+//                 (13, menlo_regular),
+//                 (text.len() - 22, zapfino_regular),
+//             ],
+//         );
+//         assert_eq!(
+//             line.runs
+//                 .iter()
+//                 .flat_map(|r| r.glyphs.iter())
+//                 .map(|g| g.index)
+//                 .collect::<Vec<_>>(),
+//             vec![0, 2, 4, 5, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22, 23, 24, 26, 27, 28, 29, 36, 37],
+//         );
+//         Ok(())
+//     }
+
+//     #[test]
+//     #[ignore]
+//     fn test_rasterize_glyph() {
+//         use std::{fs::File, io::BufWriter, path::Path};
+
+//         let fonts = FontSystem::new();
+//         let font_ids = fonts.load_family("Fira Code", &Default::default()).unwrap();
+//         let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
+//         let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
+
+//         const VARIANTS: usize = 1;
+//         for i in 0..VARIANTS {
+//             let variant = i as f32 / VARIANTS as f32;
+//             let (bounds, bytes) = fonts
+//                 .rasterize_glyph(
+//                     font_id,
+//                     16.0,
+//                     glyph_id,
+//                     vec2f(variant, variant),
+//                     2.,
+//                     RasterizationOptions::Alpha,
+//                 )
+//                 .unwrap();
+
+//             let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
+//             let path = Path::new(&name);
+//             let file = File::create(path).unwrap();
+//             let w = &mut BufWriter::new(file);
+
+//             let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32);
+//             encoder.set_color(png::ColorType::Grayscale);
+//             encoder.set_depth(png::BitDepth::Eight);
+//             let mut writer = encoder.write_header().unwrap();
+//             writer.write_image_data(&bytes).unwrap();
+//         }
+//     }
+
+//     #[test]
+//     fn test_wrap_line() {
+//         let fonts = FontSystem::new();
+//         let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
+//         let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
+
+//         let line = "one two three four five\n";
+//         let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
+//         assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
+
+//         let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
+//         let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
+//         assert_eq!(
+//             wrap_boundaries,
+//             &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
+//         );
+//     }
+
+//     #[test]
+//     fn test_layout_line_bom_char() {
+//         let fonts = FontSystem::new();
+//         let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
+//         let style = RunStyle {
+//             font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
+//             color: Default::default(),
+//             underline: Default::default(),
+//         };
+
+//         let line = "\u{feff}";
+//         let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
+//         assert_eq!(layout.len, line.len());
+//         assert!(layout.runs.is_empty());
+
+//         let line = "a\u{feff}b";
+//         let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
+//         assert_eq!(layout.len, line.len());
+//         assert_eq!(layout.runs.len(), 1);
+//         assert_eq!(layout.runs[0].glyphs.len(), 2);
+//         assert_eq!(layout.runs[0].glyphs[0].id, 68); // a
+//                                                      // There's no glyph for \u{feff}
+//         assert_eq!(layout.runs[0].glyphs[1].id, 69); // b
+//     }
+// }

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

@@ -0,0 +1,1755 @@
+use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
+use crate::{
+    display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, Executor, ExternalPaths,
+    FileDropEvent, GlobalPixels, InputEvent, KeyDownEvent, Keystroke, Modifiers,
+    ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
+    PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Scene, Size,
+    Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions, WindowPromptLevel,
+};
+use block::ConcreteBlock;
+use cocoa::{
+    appkit::{
+        CGPoint, NSApplication, NSBackingStoreBuffered, NSFilenamesPboardType, NSPasteboard,
+        NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton,
+        NSWindowCollectionBehavior, NSWindowStyleMask, NSWindowTitleVisibility,
+    },
+    base::{id, nil},
+    foundation::{
+        NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSPoint, NSRect,
+        NSSize, NSString, NSUInteger,
+    },
+};
+use core_graphics::display::CGRect;
+use ctor::ctor;
+use foreign_types::ForeignTypeRef;
+use futures::channel::oneshot;
+use objc::{
+    class,
+    declare::ClassDecl,
+    msg_send,
+    runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES},
+    sel, sel_impl,
+};
+use parking_lot::Mutex;
+use smallvec::SmallVec;
+use std::{
+    any::Any,
+    cell::{Cell, RefCell},
+    ffi::{c_void, CStr},
+    mem,
+    ops::Range,
+    os::raw::c_char,
+    path::PathBuf,
+    ptr,
+    rc::Rc,
+    sync::{Arc, Weak},
+    time::Duration,
+};
+
+const WINDOW_STATE_IVAR: &str = "windowState";
+
+static mut WINDOW_CLASS: *const Class = ptr::null();
+static mut PANEL_CLASS: *const Class = ptr::null();
+static mut VIEW_CLASS: *const Class = ptr::null();
+
+#[allow(non_upper_case_globals)]
+const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
+    unsafe { NSWindowStyleMask::from_bits_unchecked(1 << 7) };
+#[allow(non_upper_case_globals)]
+const NSNormalWindowLevel: NSInteger = 0;
+#[allow(non_upper_case_globals)]
+const NSPopUpWindowLevel: NSInteger = 101;
+#[allow(non_upper_case_globals)]
+const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
+#[allow(non_upper_case_globals)]
+const NSTrackingMouseMoved: NSUInteger = 0x02;
+#[allow(non_upper_case_globals)]
+const NSTrackingActiveAlways: NSUInteger = 0x80;
+#[allow(non_upper_case_globals)]
+const NSTrackingInVisibleRect: NSUInteger = 0x200;
+#[allow(non_upper_case_globals)]
+const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
+#[allow(non_upper_case_globals)]
+const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
+// https://developer.apple.com/documentation/appkit/nsdragoperation
+#[allow(non_upper_case_globals)]
+type NSDragOperation = NSUInteger;
+#[allow(non_upper_case_globals)]
+const NSDragOperationNone: NSDragOperation = 0;
+#[allow(non_upper_case_globals)]
+const NSDragOperationCopy: NSDragOperation = 1;
+
+#[ctor]
+unsafe fn build_classes() {
+    ::util::gpui2_loaded();
+
+    WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
+    PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
+    VIEW_CLASS = {
+        let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
+        decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
+
+        decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
+
+        decl.add_method(
+            sel!(performKeyEquivalent:),
+            handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL,
+        );
+        decl.add_method(
+            sel!(keyDown:),
+            handle_key_down as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(mouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(mouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(rightMouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(rightMouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(otherMouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(otherMouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(mouseMoved:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(mouseExited:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(mouseDragged:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(scrollWheel:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(flagsChanged:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(cancelOperation:),
+            cancel_operation as extern "C" fn(&Object, Sel, id),
+        );
+
+        decl.add_method(
+            sel!(makeBackingLayer),
+            make_backing_layer as extern "C" fn(&Object, Sel) -> id,
+        );
+
+        decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
+        decl.add_method(
+            sel!(viewDidChangeBackingProperties),
+            view_did_change_backing_properties as extern "C" fn(&Object, Sel),
+        );
+        decl.add_method(
+            sel!(setFrameSize:),
+            set_frame_size as extern "C" fn(&Object, Sel, NSSize),
+        );
+        decl.add_method(
+            sel!(displayLayer:),
+            display_layer as extern "C" fn(&Object, Sel, id),
+        );
+
+        decl.add_protocol(Protocol::get("NSTextInputClient").unwrap());
+        decl.add_method(
+            sel!(validAttributesForMarkedText),
+            valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id,
+        );
+        decl.add_method(
+            sel!(hasMarkedText),
+            has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
+        );
+        decl.add_method(
+            sel!(markedRange),
+            marked_range as extern "C" fn(&Object, Sel) -> NSRange,
+        );
+        decl.add_method(
+            sel!(selectedRange),
+            selected_range as extern "C" fn(&Object, Sel) -> NSRange,
+        );
+        decl.add_method(
+            sel!(firstRectForCharacterRange:actualRange:),
+            first_rect_for_character_range as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect,
+        );
+        decl.add_method(
+            sel!(insertText:replacementRange:),
+            insert_text as extern "C" fn(&Object, Sel, id, NSRange),
+        );
+        decl.add_method(
+            sel!(setMarkedText:selectedRange:replacementRange:),
+            set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange),
+        );
+        decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel));
+        decl.add_method(
+            sel!(attributedSubstringForProposedRange:actualRange:),
+            attributed_substring_for_proposed_range
+                as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id,
+        );
+        decl.add_method(
+            sel!(viewDidChangeEffectiveAppearance),
+            view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
+        );
+
+        // Suppress beep on keystrokes with modifier keys.
+        decl.add_method(
+            sel!(doCommandBySelector:),
+            do_command_by_selector as extern "C" fn(&Object, Sel, Sel),
+        );
+
+        decl.add_method(
+            sel!(acceptsFirstMouse:),
+            accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
+        );
+
+        decl.register()
+    };
+}
+
+pub fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point<Pixels> {
+    point(
+        px(position.x as f32),
+        // MacOS screen coordinates are relative to bottom left
+        window_height - px(position.y as f32),
+    )
+}
+
+unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class {
+    let mut decl = ClassDecl::new(name, superclass).unwrap();
+    decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
+    decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
+    decl.add_method(
+        sel!(canBecomeMainWindow),
+        yes as extern "C" fn(&Object, Sel) -> BOOL,
+    );
+    decl.add_method(
+        sel!(canBecomeKeyWindow),
+        yes as extern "C" fn(&Object, Sel) -> BOOL,
+    );
+    decl.add_method(
+        sel!(windowDidResize:),
+        window_did_resize as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowWillEnterFullScreen:),
+        window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowWillExitFullScreen:),
+        window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowDidMove:),
+        window_did_move as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowDidBecomeKey:),
+        window_did_change_key_status as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowDidResignKey:),
+        window_did_change_key_status as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowShouldClose:),
+        window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
+    );
+    decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
+
+    decl.add_method(
+        sel!(draggingEntered:),
+        dragging_entered as extern "C" fn(&Object, Sel, id) -> NSDragOperation,
+    );
+    decl.add_method(
+        sel!(draggingUpdated:),
+        dragging_updated as extern "C" fn(&Object, Sel, id) -> NSDragOperation,
+    );
+    decl.add_method(
+        sel!(draggingExited:),
+        dragging_exited as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(performDragOperation:),
+        perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
+    );
+    decl.add_method(
+        sel!(concludeDragOperation:),
+        conclude_drag_operation as extern "C" fn(&Object, Sel, id),
+    );
+
+    decl.register()
+}
+
+///Used to track what the IME does when we send it a keystroke.
+///This is only used to handle the case where the IME mysteriously
+///swallows certain keys.
+///
+///Basically a direct copy of the approach that WezTerm uses in:
+///github.com/wez/wezterm : d5755f3e : window/src/os/macos/window.rs
+enum ImeState {
+    Continue,
+    Acted,
+    None,
+}
+
+struct InsertText {
+    replacement_range: Option<Range<usize>>,
+    text: String,
+}
+
+struct MacWindowState {
+    handle: AnyWindowHandle,
+    executor: Executor,
+    native_window: id,
+    renderer: MetalRenderer,
+    scene_to_render: Option<Scene>,
+    kind: WindowKind,
+    event_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
+    activate_callback: Option<Box<dyn FnMut(bool)>>,
+    resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
+    fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
+    moved_callback: Option<Box<dyn FnMut()>>,
+    should_close_callback: Option<Box<dyn FnMut() -> bool>>,
+    close_callback: Option<Box<dyn FnOnce()>>,
+    appearance_changed_callback: Option<Box<dyn FnMut()>>,
+    input_handler: Option<Box<dyn PlatformInputHandler>>,
+    pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
+    last_key_equivalent: Option<KeyDownEvent>,
+    synthetic_drag_counter: usize,
+    last_fresh_keydown: Option<Keystroke>,
+    traffic_light_position: Option<Point<Pixels>>,
+    previous_modifiers_changed_event: Option<InputEvent>,
+    // State tracking what the IME did after the last request
+    ime_state: ImeState,
+    // Retains the last IME Text
+    ime_text: Option<String>,
+}
+
+impl MacWindowState {
+    fn move_traffic_light(&self) {
+        if let Some(traffic_light_position) = self.traffic_light_position {
+            let titlebar_height = self.titlebar_height();
+
+            unsafe {
+                let close_button: id = msg_send![
+                    self.native_window,
+                    standardWindowButton: NSWindowButton::NSWindowCloseButton
+                ];
+                let min_button: id = msg_send![
+                    self.native_window,
+                    standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton
+                ];
+                let zoom_button: id = msg_send![
+                    self.native_window,
+                    standardWindowButton: NSWindowButton::NSWindowZoomButton
+                ];
+
+                let mut close_button_frame: CGRect = msg_send![close_button, frame];
+                let mut min_button_frame: CGRect = msg_send![min_button, frame];
+                let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
+                let mut origin = point(
+                    traffic_light_position.x,
+                    titlebar_height
+                        - traffic_light_position.y
+                        - px(close_button_frame.size.height as f32),
+                );
+                let button_spacing =
+                    px((min_button_frame.origin.x - close_button_frame.origin.x) as f32);
+
+                close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
+                let _: () = msg_send![close_button, setFrame: close_button_frame];
+                origin.x += button_spacing;
+
+                min_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
+                let _: () = msg_send![min_button, setFrame: min_button_frame];
+                origin.x += button_spacing;
+
+                zoom_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
+                let _: () = msg_send![zoom_button, setFrame: zoom_button_frame];
+                origin.x += button_spacing;
+            }
+        }
+    }
+
+    fn is_fullscreen(&self) -> bool {
+        unsafe {
+            let style_mask = self.native_window.styleMask();
+            style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask)
+        }
+    }
+
+    fn bounds(&self) -> WindowBounds {
+        unsafe {
+            if self.is_fullscreen() {
+                return WindowBounds::Fullscreen;
+            }
+
+            let frame = self.frame();
+            let screen_size = self.native_window.screen().visibleFrame().into();
+            if frame.size == screen_size {
+                WindowBounds::Maximized
+            } else {
+                WindowBounds::Fixed(frame)
+            }
+        }
+    }
+
+    fn frame(&self) -> Bounds<GlobalPixels> {
+        unsafe {
+            let frame = NSWindow::frame(self.native_window);
+            display_bounds_from_native(mem::transmute::<NSRect, CGRect>(frame))
+        }
+    }
+
+    fn content_size(&self) -> Size<Pixels> {
+        let NSSize { width, height, .. } =
+            unsafe { NSView::frame(self.native_window.contentView()) }.size;
+        size(px(width as f32), px(height as f32))
+    }
+
+    fn scale_factor(&self) -> f32 {
+        get_scale_factor(self.native_window)
+    }
+
+    fn titlebar_height(&self) -> Pixels {
+        unsafe {
+            let frame = NSWindow::frame(self.native_window);
+            let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
+            px((frame.size.height - content_layout_rect.size.height) as f32)
+        }
+    }
+
+    fn to_screen_ns_point(&self, point: Point<Pixels>) -> NSPoint {
+        unsafe {
+            let point = NSPoint::new(
+                point.x.into(),
+                (self.content_size().height - point.y).into(),
+            );
+            msg_send![self.native_window, convertPointToScreen: point]
+        }
+    }
+}
+
+unsafe impl Send for MacWindowState {}
+
+pub struct MacWindow(Arc<Mutex<MacWindowState>>);
+
+impl MacWindow {
+    pub fn open(handle: AnyWindowHandle, options: WindowOptions, executor: Executor) -> Self {
+        unsafe {
+            let pool = NSAutoreleasePool::new(nil);
+
+            let mut style_mask;
+            if let Some(titlebar) = options.titlebar.as_ref() {
+                style_mask = NSWindowStyleMask::NSClosableWindowMask
+                    | NSWindowStyleMask::NSMiniaturizableWindowMask
+                    | NSWindowStyleMask::NSResizableWindowMask
+                    | NSWindowStyleMask::NSTitledWindowMask;
+
+                if titlebar.appears_transparent {
+                    style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
+                }
+            } else {
+                style_mask = NSWindowStyleMask::NSTitledWindowMask
+                    | NSWindowStyleMask::NSFullSizeContentViewWindowMask;
+            }
+
+            let native_window: id = match options.kind {
+                WindowKind::Normal => msg_send![WINDOW_CLASS, alloc],
+                WindowKind::PopUp => {
+                    style_mask |= NSWindowStyleMaskNonactivatingPanel;
+                    msg_send![PANEL_CLASS, alloc]
+                }
+            };
+
+            let display = options
+                .display_id
+                .and_then(|display_id| MacDisplay::all().find(|display| display.id() == display_id))
+                .unwrap_or_else(|| MacDisplay::primary());
+
+            let mut target_screen = nil;
+            let screens = NSScreen::screens(nil);
+            let count: u64 = cocoa::foundation::NSArray::count(screens);
+            for i in 0..count {
+                let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i);
+                let device_description = NSScreen::deviceDescription(screen);
+                let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
+                let screen_number = device_description.objectForKey_(screen_number_key);
+                let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue];
+                if screen_number as u32 == display.id().0 {
+                    target_screen = screen;
+                    break;
+                }
+            }
+
+            let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_(
+                NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.)),
+                style_mask,
+                NSBackingStoreBuffered,
+                NO,
+                target_screen,
+            );
+            assert!(!native_window.is_null());
+            let () = msg_send![
+                native_window,
+                registerForDraggedTypes:
+                    NSArray::arrayWithObject(nil, NSFilenamesPboardType)
+            ];
+
+            let screen = native_window.screen();
+            match options.bounds {
+                WindowBounds::Fullscreen => {
+                    native_window.toggleFullScreen_(nil);
+                }
+                WindowBounds::Maximized => {
+                    native_window.setFrame_display_(screen.visibleFrame(), YES);
+                }
+                WindowBounds::Fixed(bounds) => {
+                    let display_bounds = display.bounds();
+                    let frame = if bounds.intersects(&display_bounds) {
+                        display_bounds_to_native(bounds)
+                    } else {
+                        display_bounds_to_native(display_bounds)
+                    };
+                    native_window.setFrame_display_(mem::transmute::<CGRect, NSRect>(frame), YES);
+                }
+            }
+
+            let native_view: id = msg_send![VIEW_CLASS, alloc];
+            let native_view = NSView::init(native_view);
+
+            assert!(!native_view.is_null());
+
+            let window = Self(Arc::new(Mutex::new(MacWindowState {
+                handle,
+                executor,
+                native_window,
+                renderer: MetalRenderer::new(true),
+                scene_to_render: None,
+                kind: options.kind,
+                event_callback: None,
+                activate_callback: None,
+                resize_callback: None,
+                fullscreen_callback: None,
+                moved_callback: None,
+                should_close_callback: None,
+                close_callback: None,
+                appearance_changed_callback: None,
+                input_handler: None,
+                pending_key_down: None,
+                last_key_equivalent: None,
+                synthetic_drag_counter: 0,
+                last_fresh_keydown: None,
+                traffic_light_position: options
+                    .titlebar
+                    .as_ref()
+                    .and_then(|titlebar| titlebar.traffic_light_position),
+                previous_modifiers_changed_event: None,
+                ime_state: ImeState::None,
+                ime_text: None,
+            })));
+
+            (*native_window).set_ivar(
+                WINDOW_STATE_IVAR,
+                Arc::into_raw(window.0.clone()) as *const c_void,
+            );
+            native_window.setDelegate_(native_window);
+            (*native_view).set_ivar(
+                WINDOW_STATE_IVAR,
+                Arc::into_raw(window.0.clone()) as *const c_void,
+            );
+
+            if let Some(title) = options
+                .titlebar
+                .as_ref()
+                .and_then(|t| t.title.as_ref().map(AsRef::as_ref))
+            {
+                native_window.setTitle_(NSString::alloc(nil).init_str(title));
+            }
+
+            native_window.setMovable_(options.is_movable as BOOL);
+
+            if options
+                .titlebar
+                .map_or(true, |titlebar| titlebar.appears_transparent)
+            {
+                native_window.setTitlebarAppearsTransparent_(YES);
+                native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
+            }
+
+            native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
+            native_view.setWantsBestResolutionOpenGLSurface_(YES);
+
+            // From winit crate: On Mojave, views automatically become layer-backed shortly after
+            // being added to a native_window. Changing the layer-backedness of a view breaks the
+            // association between the view and its associated OpenGL context. To work around this,
+            // on we explicitly make the view layer-backed up front so that AppKit doesn't do it
+            // itself and break the association with its context.
+            native_view.setWantsLayer(YES);
+            let _: () = msg_send![
+                native_view,
+                setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
+            ];
+
+            native_window.setContentView_(native_view.autorelease());
+            native_window.makeFirstResponder_(native_view);
+
+            if options.center {
+                native_window.center();
+            }
+
+            match options.kind {
+                WindowKind::Normal => {
+                    native_window.setLevel_(NSNormalWindowLevel);
+                    native_window.setAcceptsMouseMovedEvents_(YES);
+                }
+                WindowKind::PopUp => {
+                    // Use a tracking area to allow receiving MouseMoved events even when
+                    // the window or application aren't active, which is often the case
+                    // e.g. for notification windows.
+                    let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
+                    let _: () = msg_send![
+                        tracking_area,
+                        initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
+                        options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
+                        owner: native_view
+                        userInfo: nil
+                    ];
+                    let _: () =
+                        msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
+
+                    native_window.setLevel_(NSPopUpWindowLevel);
+                    let _: () = msg_send![
+                        native_window,
+                        setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow
+                    ];
+                    native_window.setCollectionBehavior_(
+                        NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces |
+                        NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
+                    );
+                }
+            }
+            if options.focus {
+                native_window.makeKeyAndOrderFront_(nil);
+            } else if options.show {
+                native_window.orderFront_(nil);
+            }
+
+            window.0.lock().move_traffic_light();
+            pool.drain();
+
+            window
+        }
+    }
+
+    pub fn main_window() -> Option<AnyWindowHandle> {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let main_window: id = msg_send![app, mainWindow];
+            if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
+                let handle = get_window_state(&*main_window).lock().handle;
+                Some(handle)
+            } else {
+                None
+            }
+        }
+    }
+}
+
+impl Drop for MacWindow {
+    fn drop(&mut self) {
+        let this = self.0.clone();
+        let executor = self.0.lock().executor.clone();
+        executor
+            .run_on_main(move || unsafe {
+                this.lock().native_window.close();
+            })
+            .detach();
+    }
+}
+
+impl PlatformWindow for MacWindow {
+    fn bounds(&self) -> WindowBounds {
+        self.0.as_ref().lock().bounds()
+    }
+
+    fn content_size(&self) -> Size<Pixels> {
+        self.0.as_ref().lock().content_size().into()
+    }
+
+    fn scale_factor(&self) -> f32 {
+        self.0.as_ref().lock().scale_factor()
+    }
+
+    fn titlebar_height(&self) -> Pixels {
+        self.0.as_ref().lock().titlebar_height()
+    }
+
+    fn appearance(&self) -> WindowAppearance {
+        unsafe {
+            let appearance: id = msg_send![self.0.lock().native_window, effectiveAppearance];
+            WindowAppearance::from_native(appearance)
+        }
+    }
+
+    fn display(&self) -> Rc<dyn PlatformDisplay> {
+        unsafe {
+            let screen = self.0.lock().native_window.screen();
+            let device_description: id = msg_send![screen, deviceDescription];
+            let screen_number: id = NSDictionary::valueForKey_(
+                device_description,
+                NSString::alloc(nil).init_str("NSScreenNumber"),
+            );
+
+            let screen_number: u32 = msg_send![screen_number, unsignedIntValue];
+
+            Rc::new(MacDisplay(screen_number))
+        }
+    }
+
+    fn mouse_position(&self) -> Point<Pixels> {
+        let position = unsafe {
+            self.0
+                .lock()
+                .native_window
+                .mouseLocationOutsideOfEventStream()
+        };
+        convert_mouse_position(position, self.content_size().height)
+    }
+
+    fn as_any_mut(&mut self) -> &mut dyn Any {
+        self
+    }
+
+    fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>) {
+        self.0.as_ref().lock().input_handler = Some(input_handler);
+    }
+
+    fn prompt(
+        &self,
+        level: WindowPromptLevel,
+        msg: &str,
+        answers: &[&str],
+    ) -> oneshot::Receiver<usize> {
+        // macOs applies overrides to modal window buttons after they are added.
+        // Two most important for this logic are:
+        // * Buttons with "Cancel" title will be displayed as the last buttons in the modal
+        // * Last button added to the modal via `addButtonWithTitle` stays focused
+        // * Focused buttons react on "space"/" " keypresses
+        // * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus
+        //
+        // See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion
+        // ```
+        // By default, the first button has a key equivalent of Return,
+        // any button with a title of “Cancel” has a key equivalent of Escape,
+        // and any button with the title “Don’t Save” has a key equivalent of Command-D (but only if it’s not the first button).
+        // ```
+        //
+        // To avoid situations when the last element added is "Cancel" and it gets the focus
+        // (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button
+        // last, so it gets focus and a Space shortcut.
+        // This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key.
+        let latest_non_cancel_label = answers
+            .iter()
+            .enumerate()
+            .rev()
+            .find(|(_, &label)| label != "Cancel")
+            .filter(|&(label_index, _)| label_index > 0);
+
+        unsafe {
+            let alert: id = msg_send![class!(NSAlert), alloc];
+            let alert: id = msg_send![alert, init];
+            let alert_style = match level {
+                WindowPromptLevel::Info => 1,
+                WindowPromptLevel::Warning => 0,
+                WindowPromptLevel::Critical => 2,
+            };
+            let _: () = msg_send![alert, setAlertStyle: alert_style];
+            let _: () = msg_send![alert, setMessageText: ns_string(msg)];
+
+            for (ix, answer) in answers
+                .iter()
+                .enumerate()
+                .filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix))
+            {
+                let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
+                let _: () = msg_send![button, setTag: ix as NSInteger];
+            }
+            if let Some((ix, answer)) = latest_non_cancel_label {
+                let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
+                let _: () = msg_send![button, setTag: ix as NSInteger];
+            }
+
+            let (done_tx, done_rx) = oneshot::channel();
+            let done_tx = Cell::new(Some(done_tx));
+            let block = ConcreteBlock::new(move |answer: NSInteger| {
+                if let Some(done_tx) = done_tx.take() {
+                    let _ = done_tx.send(answer.try_into().unwrap());
+                }
+            });
+            let block = block.copy();
+            let native_window = self.0.lock().native_window;
+            let executor = self.0.lock().executor.clone();
+            executor
+                .spawn_on_main_local(async move {
+                    let _: () = msg_send![
+                        alert,
+                        beginSheetModalForWindow: native_window
+                        completionHandler: block
+                    ];
+                })
+                .detach();
+
+            done_rx
+        }
+    }
+
+    fn activate(&self) {
+        let window = self.0.lock().native_window;
+        let executor = self.0.lock().executor.clone();
+        executor
+            .spawn_on_main_local(async move {
+                unsafe {
+                    let _: () = msg_send![window, makeKeyAndOrderFront: nil];
+                }
+            })
+            .detach();
+    }
+
+    fn set_title(&mut self, title: &str) {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let window = self.0.lock().native_window;
+            let title = ns_string(title);
+            let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
+            let _: () = msg_send![window, setTitle: title];
+            self.0.lock().move_traffic_light();
+        }
+    }
+
+    fn set_edited(&mut self, edited: bool) {
+        unsafe {
+            let window = self.0.lock().native_window;
+            msg_send![window, setDocumentEdited: edited as BOOL]
+        }
+
+        // Changing the document edited state resets the traffic light position,
+        // so we have to move it again.
+        self.0.lock().move_traffic_light();
+    }
+
+    fn show_character_palette(&self) {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let window = self.0.lock().native_window;
+            let _: () = msg_send![app, orderFrontCharacterPalette: window];
+        }
+    }
+
+    fn minimize(&self) {
+        let window = self.0.lock().native_window;
+        unsafe {
+            window.miniaturize_(nil);
+        }
+    }
+
+    fn zoom(&self) {
+        let this = self.0.lock();
+        let window = this.native_window;
+        this.executor
+            .spawn_on_main_local(async move {
+                unsafe {
+                    window.zoom_(nil);
+                }
+            })
+            .detach();
+    }
+
+    fn toggle_full_screen(&self) {
+        let this = self.0.lock();
+        let window = this.native_window;
+        this.executor
+            .spawn_on_main_local(async move {
+                unsafe {
+                    window.toggleFullScreen_(nil);
+                }
+            })
+            .detach();
+    }
+
+    fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>) {
+        self.0.as_ref().lock().event_callback = Some(callback);
+    }
+
+    fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
+        self.0.as_ref().lock().activate_callback = Some(callback);
+    }
+
+    fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
+        self.0.as_ref().lock().resize_callback = Some(callback);
+    }
+
+    fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
+        self.0.as_ref().lock().fullscreen_callback = Some(callback);
+    }
+
+    fn on_moved(&self, callback: Box<dyn FnMut()>) {
+        self.0.as_ref().lock().moved_callback = Some(callback);
+    }
+
+    fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
+        self.0.as_ref().lock().should_close_callback = Some(callback);
+    }
+
+    fn on_close(&self, callback: Box<dyn FnOnce()>) {
+        self.0.as_ref().lock().close_callback = Some(callback);
+    }
+
+    fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
+        self.0.lock().appearance_changed_callback = Some(callback);
+    }
+
+    fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool {
+        let self_borrow = self.0.lock();
+        let self_handle = self_borrow.handle;
+
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+
+            // Convert back to screen coordinates
+            let screen_point = self_borrow.to_screen_ns_point(position);
+
+            let window_number: NSInteger = msg_send![class!(NSWindow), windowNumberAtPoint:screen_point belowWindowWithWindowNumber:0];
+            let top_most_window: id = msg_send![app, windowWithWindowNumber: window_number];
+
+            let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS];
+            let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS];
+            if is_panel == YES || is_window == YES {
+                let topmost_window = get_window_state(&*top_most_window).lock().handle;
+                topmost_window == self_handle
+            } else {
+                // Someone else's window is on top
+                false
+            }
+        }
+    }
+
+    fn draw(&self, scene: Scene) {
+        let mut this = self.0.lock();
+        this.scene_to_render = Some(scene);
+        unsafe {
+            let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES];
+        }
+    }
+
+    fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
+        self.0.lock().renderer.sprite_atlas().clone()
+    }
+}
+
+fn get_scale_factor(native_window: id) -> f32 {
+    unsafe {
+        let screen: id = msg_send![native_window, screen];
+        NSScreen::backingScaleFactor(screen) as f32
+    }
+}
+
+unsafe fn get_window_state(object: &Object) -> Arc<Mutex<MacWindowState>> {
+    let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
+    let rc1 = Arc::from_raw(raw as *mut Mutex<MacWindowState>);
+    let rc2 = rc1.clone();
+    mem::forget(rc1);
+    rc2
+}
+
+unsafe fn drop_window_state(object: &Object) {
+    let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
+    Rc::from_raw(raw as *mut RefCell<MacWindowState>);
+}
+
+extern "C" fn yes(_: &Object, _: Sel) -> BOOL {
+    YES
+}
+
+extern "C" fn dealloc_window(this: &Object, _: Sel) {
+    unsafe {
+        drop_window_state(this);
+        let _: () = msg_send![super(this, class!(NSWindow)), dealloc];
+    }
+}
+
+extern "C" fn dealloc_view(this: &Object, _: Sel) {
+    unsafe {
+        drop_window_state(this);
+        let _: () = msg_send![super(this, class!(NSView)), dealloc];
+    }
+}
+
+extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL {
+    handle_key_event(this, native_event, true)
+}
+
+extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
+    handle_key_event(this, native_event, false);
+}
+
+extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.as_ref().lock();
+
+    let window_height = lock.content_size().height;
+    let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
+
+    if let Some(InputEvent::KeyDown(event)) = event {
+        // For certain keystrokes, macOS will first dispatch a "key equivalent" event.
+        // If that event isn't handled, it will then dispatch a "key down" event. GPUI
+        // makes no distinction between these two types of events, so we need to ignore
+        // the "key down" event if we've already just processed its "key equivalent" version.
+        if key_equivalent {
+            lock.last_key_equivalent = Some(event.clone());
+        } else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
+            return NO;
+        }
+
+        let keydown = event.keystroke.clone();
+        let fn_modifier = keydown.modifiers.function;
+        // Ignore events from held-down keys after some of the initially-pressed keys
+        // were released.
+        if event.is_held {
+            if lock.last_fresh_keydown.as_ref() != Some(&keydown) {
+                return YES;
+            }
+        } else {
+            lock.last_fresh_keydown = Some(keydown);
+        }
+        lock.pending_key_down = Some((event, None));
+        drop(lock);
+
+        // Send the event to the input context for IME handling, unless the `fn` modifier is
+        // being pressed.
+        if !fn_modifier {
+            unsafe {
+                let input_context: id = msg_send![this, inputContext];
+                let _: BOOL = msg_send![input_context, handleEvent: native_event];
+            }
+        }
+
+        let mut handled = false;
+        let mut lock = window_state.lock();
+        let ime_text = lock.ime_text.clone();
+        if let Some((event, insert_text)) = lock.pending_key_down.take() {
+            let is_held = event.is_held;
+            if let Some(mut callback) = lock.event_callback.take() {
+                drop(lock);
+
+                let is_composing =
+                    with_input_handler(this, |input_handler| input_handler.marked_text_range())
+                        .flatten()
+                        .is_some();
+                if !is_composing {
+                    // if the IME has changed the key, we'll first emit an event with the character
+                    // generated by the IME system; then fallback to the keystroke if that is not
+                    // handled.
+                    // cases that we have working:
+                    // - " on a brazillian layout by typing <quote><space>
+                    // - ctrl-` on a brazillian layout by typing <ctrl-`>
+                    // - $ on a czech QWERTY layout by typing <alt-4>
+                    // - 4 on a czech QWERTY layout by typing <shift-4>
+                    // - ctrl-4 on a czech QWERTY layout by typing <ctrl-alt-4> (or <ctrl-shift-4>)
+                    if ime_text.is_some() && ime_text.as_ref() != Some(&event.keystroke.key) {
+                        let event_with_ime_text = KeyDownEvent {
+                            is_held: false,
+                            keystroke: Keystroke {
+                                // we match ctrl because some use-cases need it.
+                                // we don't match alt because it's often used to generate the optional character
+                                // we don't match shift because we're not here with letters (usually)
+                                // we don't match cmd/fn because they don't seem to use IME
+                                modifiers: Default::default(),
+                                key: ime_text.clone().unwrap(),
+                                ime_key: None, // todo!("handle IME key")
+                            },
+                        };
+                        handled = callback(InputEvent::KeyDown(event_with_ime_text));
+                    }
+                    if !handled {
+                        // empty key happens when you type a deadkey in input composition.
+                        // (e.g. on a brazillian keyboard typing quote is a deadkey)
+                        if !event.keystroke.key.is_empty() {
+                            handled = callback(InputEvent::KeyDown(event));
+                        }
+                    }
+                }
+
+                if !handled {
+                    if let Some(insert) = insert_text {
+                        handled = true;
+                        with_input_handler(this, |input_handler| {
+                            input_handler
+                                .replace_text_in_range(insert.replacement_range, &insert.text)
+                        });
+                    } else if !is_composing && is_held {
+                        if let Some(last_insert_text) = ime_text {
+                            //MacOS IME is a bit funky, and even when you've told it there's nothing to
+                            //inter it will still swallow certain keys (e.g. 'f', 'j') and not others
+                            //(e.g. 'n'). This is a problem for certain kinds of views, like the terminal
+                            with_input_handler(this, |input_handler| {
+                                if input_handler.selected_text_range().is_none() {
+                                    handled = true;
+                                    input_handler.replace_text_in_range(None, &last_insert_text)
+                                }
+                            });
+                        }
+                    }
+                }
+
+                window_state.lock().event_callback = Some(callback);
+            }
+        } else {
+            handled = true;
+        }
+
+        handled as BOOL
+    } else {
+        NO
+    }
+}
+
+extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
+    let window_state = unsafe { get_window_state(this) };
+    let weak_window_state = Arc::downgrade(&window_state);
+    let mut lock = window_state.as_ref().lock();
+    let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
+
+    let window_height = lock.content_size().height;
+    let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
+
+    if let Some(mut event) = event {
+        let synthesized_second_event = match &mut event {
+            InputEvent::MouseDown(
+                event @ MouseDownEvent {
+                    button: MouseButton::Left,
+                    modifiers: Modifiers { control: true, .. },
+                    ..
+                },
+            ) => {
+                *event = MouseDownEvent {
+                    button: MouseButton::Right,
+                    modifiers: Modifiers {
+                        control: false,
+                        ..event.modifiers
+                    },
+                    click_count: 1,
+                    ..*event
+                };
+
+                Some(InputEvent::MouseDown(MouseDownEvent {
+                    button: MouseButton::Right,
+                    ..*event
+                }))
+            }
+
+            // Because we map a ctrl-left_down to a right_down -> right_up let's ignore
+            // the ctrl-left_up to avoid having a mismatch in button down/up events if the
+            // user is still holding ctrl when releasing the left mouse button
+            InputEvent::MouseUp(MouseUpEvent {
+                button: MouseButton::Left,
+                modifiers: Modifiers { control: true, .. },
+                ..
+            }) => {
+                lock.synthetic_drag_counter += 1;
+                return;
+            }
+
+            _ => None,
+        };
+
+        match &event {
+            InputEvent::MouseMove(
+                event @ MouseMoveEvent {
+                    pressed_button: Some(_),
+                    ..
+                },
+            ) => {
+                lock.synthetic_drag_counter += 1;
+                let executor = lock.executor.clone();
+                executor
+                    .spawn_on_main_local(synthetic_drag(
+                        weak_window_state,
+                        lock.synthetic_drag_counter,
+                        event.clone(),
+                    ))
+                    .detach();
+            }
+
+            InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
+
+            InputEvent::MouseUp(MouseUpEvent {
+                button: MouseButton::Left,
+                ..
+            }) => {
+                lock.synthetic_drag_counter += 1;
+            }
+
+            InputEvent::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
+                // Only raise modifiers changed event when they have actually changed
+                if let Some(InputEvent::ModifiersChanged(ModifiersChangedEvent {
+                    modifiers: prev_modifiers,
+                })) = &lock.previous_modifiers_changed_event
+                {
+                    if prev_modifiers == modifiers {
+                        return;
+                    }
+                }
+
+                lock.previous_modifiers_changed_event = Some(event.clone());
+            }
+
+            _ => {}
+        }
+
+        if let Some(mut callback) = lock.event_callback.take() {
+            drop(lock);
+            callback(event);
+            if let Some(event) = synthesized_second_event {
+                callback(event);
+            }
+            window_state.lock().event_callback = Some(callback);
+        }
+    }
+}
+
+// Allows us to receive `cmd-.` (the shortcut for closing a dialog)
+// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6
+extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.as_ref().lock();
+
+    let keystroke = Keystroke {
+        modifiers: Default::default(),
+        key: ".".into(),
+        ime_key: None,
+    };
+    let event = InputEvent::KeyDown(KeyDownEvent {
+        keystroke: keystroke.clone(),
+        is_held: false,
+    });
+
+    lock.last_fresh_keydown = Some(keystroke);
+    if let Some(mut callback) = lock.event_callback.take() {
+        drop(lock);
+        callback(event);
+        window_state.lock().event_callback = Some(callback);
+    }
+}
+
+extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
+    let window_state = unsafe { get_window_state(this) };
+    window_state.as_ref().lock().move_traffic_light();
+}
+
+extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
+    window_fullscreen_changed(this, true);
+}
+
+extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
+    window_fullscreen_changed(this, false);
+}
+
+fn window_fullscreen_changed(this: &Object, is_fullscreen: bool) {
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.as_ref().lock();
+    if let Some(mut callback) = lock.fullscreen_callback.take() {
+        drop(lock);
+        callback(is_fullscreen);
+        window_state.lock().fullscreen_callback = Some(callback);
+    }
+}
+
+extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.as_ref().lock();
+    if let Some(mut callback) = lock.moved_callback.take() {
+        drop(lock);
+        callback();
+        window_state.lock().moved_callback = Some(callback);
+    }
+}
+
+extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
+    let window_state = unsafe { get_window_state(this) };
+    let lock = window_state.lock();
+    let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
+
+    // When opening a pop-up while the application isn't active, Cocoa sends a spurious
+    // `windowDidBecomeKey` message to the previous key window even though that window
+    // isn't actually key. This causes a bug if the application is later activated while
+    // the pop-up is still open, making it impossible to activate the previous key window
+    // even if the pop-up gets closed. The only way to activate it again is to de-activate
+    // the app and re-activate it, which is a pretty bad UX.
+    // The following code detects the spurious event and invokes `resignKeyWindow`:
+    // in theory, we're not supposed to invoke this method manually but it balances out
+    // the spurious `becomeKeyWindow` event and helps us work around that bug.
+    if selector == sel!(windowDidBecomeKey:) {
+        if !is_active {
+            unsafe {
+                let _: () = msg_send![lock.native_window, resignKeyWindow];
+                return;
+            }
+        }
+    }
+
+    let executor = lock.executor.clone();
+    drop(lock);
+    executor
+        .spawn_on_main_local(async move {
+            let mut lock = window_state.as_ref().lock();
+            if let Some(mut callback) = lock.activate_callback.take() {
+                drop(lock);
+                callback(is_active);
+                window_state.lock().activate_callback = Some(callback);
+            };
+        })
+        .detach();
+}
+
+extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL {
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.as_ref().lock();
+    if let Some(mut callback) = lock.should_close_callback.take() {
+        drop(lock);
+        let should_close = callback();
+        window_state.lock().should_close_callback = Some(callback);
+        should_close as BOOL
+    } else {
+        YES
+    }
+}
+
+extern "C" fn close_window(this: &Object, _: Sel) {
+    unsafe {
+        let close_callback = {
+            let window_state = get_window_state(this);
+            window_state
+                .as_ref()
+                .try_lock()
+                .and_then(|mut window_state| window_state.close_callback.take())
+        };
+
+        if let Some(callback) = close_callback {
+            callback();
+        }
+
+        let _: () = msg_send![super(this, class!(NSWindow)), close];
+    }
+}
+
+extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
+    let window_state = unsafe { get_window_state(this) };
+    let window_state = window_state.as_ref().lock();
+    window_state.renderer.layer().as_ptr() as id
+}
+
+extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.as_ref().lock();
+
+    unsafe {
+        let scale_factor = lock.scale_factor() as f64;
+        let size = lock.content_size();
+        let drawable_size: NSSize = NSSize {
+            width: f64::from(size.width) * scale_factor,
+            height: f64::from(size.height) * scale_factor,
+        };
+
+        let _: () = msg_send![
+            lock.renderer.layer(),
+            setContentsScale: scale_factor
+        ];
+        let _: () = msg_send![
+            lock.renderer.layer(),
+            setDrawableSize: drawable_size
+        ];
+    }
+
+    if let Some(mut callback) = lock.resize_callback.take() {
+        let content_size = lock.content_size();
+        let scale_factor = lock.scale_factor();
+        drop(lock);
+        callback(content_size, scale_factor);
+        window_state.as_ref().lock().resize_callback = Some(callback);
+    };
+}
+
+extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
+    let window_state = unsafe { get_window_state(this) };
+    let lock = window_state.as_ref().lock();
+
+    if lock.content_size() == size.into() {
+        return;
+    }
+
+    unsafe {
+        let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size];
+    }
+
+    let scale_factor = lock.scale_factor() as f64;
+    let drawable_size: NSSize = NSSize {
+        width: size.width * scale_factor,
+        height: size.height * scale_factor,
+    };
+
+    unsafe {
+        let _: () = msg_send![
+            lock.renderer.layer(),
+            setDrawableSize: drawable_size
+        ];
+    }
+
+    drop(lock);
+    let mut lock = window_state.lock();
+    if let Some(mut callback) = lock.resize_callback.take() {
+        let content_size = lock.content_size();
+        let scale_factor = lock.scale_factor();
+        drop(lock);
+        callback(content_size, scale_factor);
+        window_state.lock().resize_callback = Some(callback);
+    };
+}
+
+extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
+    unsafe {
+        let window_state = get_window_state(this);
+        let mut window_state = window_state.as_ref().lock();
+        if let Some(scene) = window_state.scene_to_render.take() {
+            window_state.renderer.draw(&scene);
+        }
+    }
+}
+
+extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
+    unsafe { msg_send![class!(NSArray), array] }
+}
+
+extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
+    with_input_handler(this, |input_handler| input_handler.marked_text_range())
+        .flatten()
+        .is_some() as BOOL
+}
+
+extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
+    with_input_handler(this, |input_handler| input_handler.marked_text_range())
+        .flatten()
+        .map_or(NSRange::invalid(), |range| range.into())
+}
+
+extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
+    with_input_handler(this, |input_handler| input_handler.selected_text_range())
+        .flatten()
+        .map_or(NSRange::invalid(), |range| range.into())
+}
+
+extern "C" fn first_rect_for_character_range(
+    this: &Object,
+    _: Sel,
+    range: NSRange,
+    _: id,
+) -> NSRect {
+    let frame = unsafe {
+        let window = get_window_state(this).lock().native_window;
+        NSView::frame(window)
+    };
+    with_input_handler(this, |input_handler| {
+        input_handler.bounds_for_range(range.to_range()?)
+    })
+    .flatten()
+    .map_or(
+        NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)),
+        |bounds| {
+            NSRect::new(
+                NSPoint::new(
+                    frame.origin.x + bounds.origin.x as f64,
+                    frame.origin.y + frame.size.height - bounds.origin.y as f64,
+                ),
+                NSSize::new(bounds.size.width as f64, bounds.size.height as f64),
+            )
+        },
+    )
+}
+
+extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) {
+    unsafe {
+        let window_state = get_window_state(this);
+        let mut lock = window_state.lock();
+        let pending_key_down = lock.pending_key_down.take();
+        drop(lock);
+
+        let is_attributed_string: BOOL =
+            msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
+        let text: id = if is_attributed_string == YES {
+            msg_send![text, string]
+        } else {
+            text
+        };
+        let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
+            .to_str()
+            .unwrap();
+        let replacement_range = replacement_range.to_range();
+
+        window_state.lock().ime_text = Some(text.to_string());
+        window_state.lock().ime_state = ImeState::Acted;
+
+        let is_composing =
+            with_input_handler(this, |input_handler| input_handler.marked_text_range())
+                .flatten()
+                .is_some();
+
+        if is_composing || text.chars().count() > 1 || pending_key_down.is_none() {
+            with_input_handler(this, |input_handler| {
+                input_handler.replace_text_in_range(replacement_range, text)
+            });
+        } else {
+            let mut pending_key_down = pending_key_down.unwrap();
+            pending_key_down.1 = Some(InsertText {
+                replacement_range,
+                text: text.to_string(),
+            });
+            window_state.lock().pending_key_down = Some(pending_key_down);
+        }
+    }
+}
+
+extern "C" fn set_marked_text(
+    this: &Object,
+    _: Sel,
+    text: id,
+    selected_range: NSRange,
+    replacement_range: NSRange,
+) {
+    unsafe {
+        let window_state = get_window_state(this);
+        window_state.lock().pending_key_down.take();
+
+        let is_attributed_string: BOOL =
+            msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
+        let text: id = if is_attributed_string == YES {
+            msg_send![text, string]
+        } else {
+            text
+        };
+        let selected_range = selected_range.to_range();
+        let replacement_range = replacement_range.to_range();
+        let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
+            .to_str()
+            .unwrap();
+
+        window_state.lock().ime_state = ImeState::Acted;
+        window_state.lock().ime_text = Some(text.to_string());
+
+        with_input_handler(this, |input_handler| {
+            input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range);
+        });
+    }
+}
+
+extern "C" fn unmark_text(this: &Object, _: Sel) {
+    unsafe {
+        let state = get_window_state(this);
+        let mut borrow = state.lock();
+        borrow.ime_state = ImeState::Acted;
+        borrow.ime_text.take();
+    }
+
+    with_input_handler(this, |input_handler| input_handler.unmark_text());
+}
+
+extern "C" fn attributed_substring_for_proposed_range(
+    this: &Object,
+    _: Sel,
+    range: NSRange,
+    _actual_range: *mut c_void,
+) -> id {
+    with_input_handler(this, |input_handler| {
+        let range = range.to_range()?;
+        if range.is_empty() {
+            return None;
+        }
+
+        let selected_text = input_handler.text_for_range(range)?;
+        unsafe {
+            let string: id = msg_send![class!(NSAttributedString), alloc];
+            let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
+            Some(string)
+        }
+    })
+    .flatten()
+    .unwrap_or(nil)
+}
+
+extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
+    unsafe {
+        let state = get_window_state(this);
+        let mut borrow = state.lock();
+        borrow.ime_state = ImeState::Continue;
+        borrow.ime_text.take();
+    }
+}
+
+extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
+    unsafe {
+        let state = get_window_state(this);
+        let mut lock = state.as_ref().lock();
+        if let Some(mut callback) = lock.appearance_changed_callback.take() {
+            drop(lock);
+            callback();
+            state.lock().appearance_changed_callback = Some(callback);
+        }
+    }
+}
+
+extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL {
+    unsafe {
+        let state = get_window_state(this);
+        let lock = state.as_ref().lock();
+        return if lock.kind == WindowKind::PopUp {
+            YES
+        } else {
+            NO
+        };
+    }
+}
+
+extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
+    let window_state = unsafe { get_window_state(this) };
+    if send_new_event(&window_state, {
+        let position = drag_event_position(&window_state, dragging_info);
+        let paths = external_paths_from_event(dragging_info);
+        InputEvent::FileDrop(FileDropEvent::Entered {
+            position,
+            files: paths,
+        })
+    }) {
+        NSDragOperationCopy
+    } else {
+        NSDragOperationNone
+    }
+}
+
+extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
+    let window_state = unsafe { get_window_state(this) };
+    let position = drag_event_position(&window_state, dragging_info);
+    if send_new_event(
+        &window_state,
+        InputEvent::FileDrop(FileDropEvent::Pending { position }),
+    ) {
+        NSDragOperationCopy
+    } else {
+        NSDragOperationNone
+    }
+}
+
+extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
+    let window_state = unsafe { get_window_state(this) };
+    send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
+}
+
+extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
+    let window_state = unsafe { get_window_state(this) };
+    let position = drag_event_position(&window_state, dragging_info);
+    if send_new_event(
+        &window_state,
+        InputEvent::FileDrop(FileDropEvent::Submit { position }),
+    ) {
+        YES
+    } else {
+        NO
+    }
+}
+
+fn external_paths_from_event(dragging_info: *mut Object) -> ExternalPaths {
+    let mut paths = SmallVec::new();
+    let pasteboard: id = unsafe { msg_send![dragging_info, draggingPasteboard] };
+    let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, NSFilenamesPboardType) };
+    for file in unsafe { filenames.iter() } {
+        let path = unsafe {
+            let f = NSString::UTF8String(file);
+            CStr::from_ptr(f).to_string_lossy().into_owned()
+        };
+        paths.push(PathBuf::from(path))
+    }
+    ExternalPaths(paths)
+}
+
+extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) {
+    let window_state = unsafe { get_window_state(this) };
+    send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
+}
+
+async fn synthetic_drag(
+    window_state: Weak<Mutex<MacWindowState>>,
+    drag_id: usize,
+    event: MouseMoveEvent,
+) {
+    loop {
+        Timer::after(Duration::from_millis(16)).await;
+        if let Some(window_state) = window_state.upgrade() {
+            let mut lock = window_state.lock();
+            if lock.synthetic_drag_counter == drag_id {
+                if let Some(mut callback) = lock.event_callback.take() {
+                    drop(lock);
+                    callback(InputEvent::MouseMove(event.clone()));
+                    window_state.lock().event_callback = Some(callback);
+                }
+            } else {
+                break;
+            }
+        }
+    }
+}
+
+fn send_new_event(window_state_lock: &Mutex<MacWindowState>, e: InputEvent) -> bool {
+    let window_state = window_state_lock.lock().event_callback.take();
+    if let Some(mut callback) = window_state {
+        callback(e);
+        window_state_lock.lock().event_callback = Some(callback);
+        true
+    } else {
+        false
+    }
+}
+
+fn drag_event_position(window_state: &Mutex<MacWindowState>, dragging_info: id) -> Point<Pixels> {
+    let drag_location: NSPoint = unsafe { msg_send![dragging_info, draggingLocation] };
+    convert_mouse_position(drag_location, window_state.lock().content_size().height)
+}
+
+fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
+where
+    F: FnOnce(&mut dyn PlatformInputHandler) -> R,
+{
+    let window_state = unsafe { get_window_state(window) };
+    let mut lock = window_state.as_ref().lock();
+    if let Some(mut input_handler) = lock.input_handler.take() {
+        drop(lock);
+        let result = f(input_handler.as_mut());
+        window_state.lock().input_handler = Some(input_handler);
+        Some(result)
+    } else {
+        None
+    }
+}

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

@@ -0,0 +1,35 @@
+use crate::WindowAppearance;
+use cocoa::{
+    appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight},
+    base::id,
+    foundation::NSString,
+};
+use objc::{msg_send, sel, sel_impl};
+use std::ffi::CStr;
+
+impl WindowAppearance {
+    pub unsafe fn from_native(appearance: id) -> Self {
+        let name: id = msg_send![appearance, name];
+        if name == NSAppearanceNameVibrantLight {
+            Self::VibrantLight
+        } else if name == NSAppearanceNameVibrantDark {
+            Self::VibrantDark
+        } else if name == NSAppearanceNameAqua {
+            Self::Light
+        } else if name == NSAppearanceNameDarkAqua {
+            Self::Dark
+        } else {
+            println!(
+                "unknown appearance: {:?}",
+                CStr::from_ptr(name.UTF8String())
+            );
+            Self::Light
+        }
+    }
+}
+
+#[link(name = "AppKit", kind = "framework")]
+extern "C" {
+    pub static NSAppearanceNameAqua: id;
+    pub static NSAppearanceNameDarkAqua: id;
+}

crates/gpui2/src/platform/test/dispatcher.rs 🔗

@@ -0,0 +1,244 @@
+use crate::PlatformDispatcher;
+use async_task::Runnable;
+use collections::{HashMap, VecDeque};
+use parking_lot::Mutex;
+use rand::prelude::*;
+use std::{
+    future::Future,
+    pin::Pin,
+    sync::Arc,
+    task::{Context, Poll},
+    time::Duration,
+};
+use util::post_inc;
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash)]
+struct TestDispatcherId(usize);
+
+pub struct TestDispatcher {
+    id: TestDispatcherId,
+    state: Arc<Mutex<TestDispatcherState>>,
+}
+
+struct TestDispatcherState {
+    random: StdRng,
+    foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
+    background: Vec<Runnable>,
+    delayed: Vec<(Duration, Runnable)>,
+    time: Duration,
+    is_main_thread: bool,
+    next_id: TestDispatcherId,
+}
+
+impl TestDispatcher {
+    pub fn new(random: StdRng) -> Self {
+        let state = TestDispatcherState {
+            random,
+            foreground: HashMap::default(),
+            background: Vec::new(),
+            delayed: Vec::new(),
+            time: Duration::ZERO,
+            is_main_thread: true,
+            next_id: TestDispatcherId(1),
+        };
+
+        TestDispatcher {
+            id: TestDispatcherId(0),
+            state: Arc::new(Mutex::new(state)),
+        }
+    }
+
+    pub fn advance_clock(&self, by: Duration) {
+        let new_now = self.state.lock().time + by;
+        loop {
+            self.run_until_parked();
+            let state = self.state.lock();
+            let next_due_time = state.delayed.first().map(|(time, _)| *time);
+            drop(state);
+            if let Some(due_time) = next_due_time {
+                if due_time <= new_now {
+                    self.state.lock().time = due_time;
+                    continue;
+                }
+            }
+            break;
+        }
+        self.state.lock().time = new_now;
+    }
+
+    pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
+        pub struct YieldNow {
+            count: usize,
+        }
+
+        impl Future for YieldNow {
+            type Output = ();
+
+            fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
+                if self.count > 0 {
+                    self.count -= 1;
+                    cx.waker().wake_by_ref();
+                    Poll::Pending
+                } else {
+                    Poll::Ready(())
+                }
+            }
+        }
+
+        YieldNow {
+            count: self.state.lock().random.gen_range(0..10),
+        }
+    }
+
+    pub fn run_until_parked(&self) {
+        while self.poll() {}
+    }
+}
+
+impl Clone for TestDispatcher {
+    fn clone(&self) -> Self {
+        let id = post_inc(&mut self.state.lock().next_id.0);
+        Self {
+            id: TestDispatcherId(id),
+            state: self.state.clone(),
+        }
+    }
+}
+
+impl PlatformDispatcher for TestDispatcher {
+    fn is_main_thread(&self) -> bool {
+        self.state.lock().is_main_thread
+    }
+
+    fn dispatch(&self, runnable: Runnable) {
+        self.state.lock().background.push(runnable);
+    }
+
+    fn dispatch_on_main_thread(&self, runnable: Runnable) {
+        self.state
+            .lock()
+            .foreground
+            .entry(self.id)
+            .or_default()
+            .push_back(runnable);
+    }
+
+    fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) {
+        let mut state = self.state.lock();
+        let next_time = state.time + duration;
+        let ix = match state.delayed.binary_search_by_key(&next_time, |e| e.0) {
+            Ok(ix) | Err(ix) => ix,
+        };
+        state.delayed.insert(ix, (next_time, runnable));
+    }
+
+    fn poll(&self) -> bool {
+        let mut state = self.state.lock();
+
+        while let Some((deadline, _)) = state.delayed.first() {
+            if *deadline > state.time {
+                break;
+            }
+            let (_, runnable) = state.delayed.remove(0);
+            state.background.push(runnable);
+        }
+
+        let foreground_len: usize = state
+            .foreground
+            .values()
+            .map(|runnables| runnables.len())
+            .sum();
+        let background_len = state.background.len();
+
+        if foreground_len == 0 && background_len == 0 {
+            return false;
+        }
+
+        let main_thread = state.random.gen_ratio(
+            foreground_len as u32,
+            (foreground_len + background_len) as u32,
+        );
+        let was_main_thread = state.is_main_thread;
+        state.is_main_thread = main_thread;
+
+        let runnable = if main_thread {
+            let state = &mut *state;
+            let runnables = state
+                .foreground
+                .values_mut()
+                .filter(|runnables| !runnables.is_empty())
+                .choose(&mut state.random)
+                .unwrap();
+            runnables.pop_front().unwrap()
+        } else {
+            let ix = state.random.gen_range(0..background_len);
+            state.background.swap_remove(ix)
+        };
+
+        drop(state);
+        runnable.run();
+
+        self.state.lock().is_main_thread = was_main_thread;
+
+        true
+    }
+
+    fn as_test(&self) -> Option<&TestDispatcher> {
+        Some(self)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::Executor;
+    use std::sync::Arc;
+
+    #[test]
+    fn test_dispatch() {
+        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
+        let executor = Executor::new(Arc::new(dispatcher));
+
+        let result = executor.block(async { executor.run_on_main(|| 1).await });
+        assert_eq!(result, 1);
+
+        let result = executor.block({
+            let executor = executor.clone();
+            async move {
+                executor
+                    .spawn_on_main({
+                        let executor = executor.clone();
+                        assert!(executor.is_main_thread());
+                        || async move {
+                            assert!(executor.is_main_thread());
+                            let result = executor
+                                .spawn({
+                                    let executor = executor.clone();
+                                    async move {
+                                        assert!(!executor.is_main_thread());
+
+                                        let result = executor
+                                            .spawn_on_main({
+                                                let executor = executor.clone();
+                                                || async move {
+                                                    assert!(executor.is_main_thread());
+                                                    2
+                                                }
+                                            })
+                                            .await;
+
+                                        assert!(!executor.is_main_thread());
+                                        result
+                                    }
+                                })
+                                .await;
+                            assert!(executor.is_main_thread());
+                            result
+                        }
+                    })
+                    .await
+            }
+        });
+        assert_eq!(result, 2);
+    }
+}

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

@@ -0,0 +1,186 @@
+use crate::{DisplayId, Executor, Platform, PlatformTextSystem};
+use anyhow::{anyhow, Result};
+use std::sync::Arc;
+
+pub struct TestPlatform {
+    executor: Executor,
+}
+
+impl TestPlatform {
+    pub fn new(executor: Executor) -> Self {
+        TestPlatform { executor }
+    }
+}
+
+// todo!("implement out what our tests needed in GPUI 1")
+impl Platform for TestPlatform {
+    fn executor(&self) -> Executor {
+        self.executor.clone()
+    }
+
+    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
+        Arc::new(crate::platform::mac::MacTextSystem::new())
+    }
+
+    fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
+        unimplemented!()
+    }
+
+    fn quit(&self) {
+        unimplemented!()
+    }
+
+    fn restart(&self) {
+        unimplemented!()
+    }
+
+    fn activate(&self, _ignoring_other_apps: bool) {
+        unimplemented!()
+    }
+
+    fn hide(&self) {
+        unimplemented!()
+    }
+
+    fn hide_other_apps(&self) {
+        unimplemented!()
+    }
+
+    fn unhide_other_apps(&self) {
+        unimplemented!()
+    }
+
+    fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
+        unimplemented!()
+    }
+
+    fn display(&self, _id: DisplayId) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
+        unimplemented!()
+    }
+
+    fn main_window(&self) -> Option<crate::AnyWindowHandle> {
+        unimplemented!()
+    }
+
+    fn open_window(
+        &self,
+        _handle: crate::AnyWindowHandle,
+        _options: crate::WindowOptions,
+    ) -> Box<dyn crate::PlatformWindow> {
+        unimplemented!()
+    }
+
+    fn set_display_link_output_callback(
+        &self,
+        _display_id: DisplayId,
+        _callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp)>,
+    ) {
+        unimplemented!()
+    }
+
+    fn start_display_link(&self, _display_id: DisplayId) {
+        unimplemented!()
+    }
+
+    fn stop_display_link(&self, _display_id: DisplayId) {
+        unimplemented!()
+    }
+
+    fn open_url(&self, _url: &str) {
+        unimplemented!()
+    }
+
+    fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
+        unimplemented!()
+    }
+
+    fn prompt_for_paths(
+        &self,
+        _options: crate::PathPromptOptions,
+    ) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
+        unimplemented!()
+    }
+
+    fn prompt_for_new_path(
+        &self,
+        _directory: &std::path::Path,
+    ) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
+        unimplemented!()
+    }
+
+    fn reveal_path(&self, _path: &std::path::Path) {
+        unimplemented!()
+    }
+
+    fn on_become_active(&self, _callback: Box<dyn FnMut()>) {
+        unimplemented!()
+    }
+
+    fn on_resign_active(&self, _callback: Box<dyn FnMut()>) {
+        unimplemented!()
+    }
+
+    fn on_quit(&self, _callback: Box<dyn FnMut()>) {
+        unimplemented!()
+    }
+
+    fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
+        unimplemented!()
+    }
+
+    fn on_event(&self, _callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
+        unimplemented!()
+    }
+
+    fn os_name(&self) -> &'static str {
+        "test"
+    }
+
+    fn os_version(&self) -> Result<crate::SemanticVersion> {
+        Err(anyhow!("os_version called on TestPlatform"))
+    }
+
+    fn app_version(&self) -> Result<crate::SemanticVersion> {
+        Err(anyhow!("app_version called on TestPlatform"))
+    }
+
+    fn app_path(&self) -> Result<std::path::PathBuf> {
+        unimplemented!()
+    }
+
+    fn local_timezone(&self) -> time::UtcOffset {
+        unimplemented!()
+    }
+
+    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
+        unimplemented!()
+    }
+
+    fn set_cursor_style(&self, _style: crate::CursorStyle) {
+        unimplemented!()
+    }
+
+    fn should_auto_hide_scrollbars(&self) -> bool {
+        unimplemented!()
+    }
+
+    fn write_to_clipboard(&self, _item: crate::ClipboardItem) {
+        unimplemented!()
+    }
+
+    fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
+        unimplemented!()
+    }
+
+    fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Result<()> {
+        Ok(())
+    }
+
+    fn read_credentials(&self, _url: &str) -> Result<Option<(String, Vec<u8>)>> {
+        Ok(None)
+    }
+
+    fn delete_credentials(&self, _url: &str) -> Result<()> {
+        Ok(())
+    }
+}

crates/gpui2/src/scene.rs 🔗

@@ -0,0 +1,829 @@
+use crate::{
+    point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Point,
+    ScaledPixels, StackingOrder,
+};
+use collections::BTreeMap;
+use etagere::euclid::{Point3D, Vector3D};
+use plane_split::{BspSplitter, Polygon as BspPolygon};
+use std::{fmt::Debug, iter::Peekable, mem, slice};
+
+// Exported to metal
+pub(crate) type PointF = Point<f32>;
+#[allow(non_camel_case_types, unused)]
+pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
+
+pub type LayerId = u32;
+
+pub type DrawOrder = u32;
+
+pub(crate) struct SceneBuilder {
+    layers_by_order: BTreeMap<StackingOrder, LayerId>,
+    splitter: BspSplitter<(PrimitiveKind, usize)>,
+    shadows: Vec<Shadow>,
+    quads: Vec<Quad>,
+    paths: Vec<Path<ScaledPixels>>,
+    underlines: Vec<Underline>,
+    monochrome_sprites: Vec<MonochromeSprite>,
+    polychrome_sprites: Vec<PolychromeSprite>,
+}
+
+impl SceneBuilder {
+    pub fn new() -> SceneBuilder {
+        SceneBuilder {
+            layers_by_order: BTreeMap::new(),
+            splitter: BspSplitter::new(),
+            shadows: Vec::new(),
+            quads: Vec::new(),
+            paths: Vec::new(),
+            underlines: Vec::new(),
+            monochrome_sprites: Vec::new(),
+            polychrome_sprites: Vec::new(),
+        }
+    }
+
+    pub fn build(&mut self) -> Scene {
+        // Map each layer id to a float between 0. and 1., with 1. closer to the viewer.
+        let mut layer_z_values = vec![0.; self.layers_by_order.len()];
+        for (ix, layer_id) in self.layers_by_order.values().enumerate() {
+            layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32;
+        }
+        self.layers_by_order.clear();
+
+        // Add all primitives to the BSP splitter to determine draw order
+        self.splitter.reset();
+
+        for (ix, shadow) in self.shadows.iter().enumerate() {
+            let z = layer_z_values[shadow.order as LayerId as usize];
+            self.splitter
+                .add(shadow.bounds.to_bsp_polygon(z, (PrimitiveKind::Shadow, ix)));
+        }
+
+        for (ix, quad) in self.quads.iter().enumerate() {
+            let z = layer_z_values[quad.order as LayerId as usize];
+            self.splitter
+                .add(quad.bounds.to_bsp_polygon(z, (PrimitiveKind::Quad, ix)));
+        }
+
+        for (ix, path) in self.paths.iter().enumerate() {
+            let z = layer_z_values[path.order as LayerId as usize];
+            self.splitter
+                .add(path.bounds.to_bsp_polygon(z, (PrimitiveKind::Path, ix)));
+        }
+
+        for (ix, underline) in self.underlines.iter().enumerate() {
+            let z = layer_z_values[underline.order as LayerId as usize];
+            self.splitter.add(
+                underline
+                    .bounds
+                    .to_bsp_polygon(z, (PrimitiveKind::Underline, ix)),
+            );
+        }
+
+        for (ix, monochrome_sprite) in self.monochrome_sprites.iter().enumerate() {
+            let z = layer_z_values[monochrome_sprite.order as LayerId as usize];
+            self.splitter.add(
+                monochrome_sprite
+                    .bounds
+                    .to_bsp_polygon(z, (PrimitiveKind::MonochromeSprite, ix)),
+            );
+        }
+
+        for (ix, polychrome_sprite) in self.polychrome_sprites.iter().enumerate() {
+            let z = layer_z_values[polychrome_sprite.order as LayerId as usize];
+            self.splitter.add(
+                polychrome_sprite
+                    .bounds
+                    .to_bsp_polygon(z, (PrimitiveKind::PolychromeSprite, ix)),
+            );
+        }
+
+        // Sort all polygons, then reassign the order field of each primitive to `draw_order`
+        // We need primitives to be repr(C), hence the weird reuse of the order field for two different types.
+        for (draw_order, polygon) in self
+            .splitter
+            .sort(Vector3D::new(0., 0., 1.))
+            .iter()
+            .enumerate()
+        {
+            match polygon.anchor {
+                (PrimitiveKind::Shadow, ix) => self.shadows[ix].order = draw_order as DrawOrder,
+                (PrimitiveKind::Quad, ix) => self.quads[ix].order = draw_order as DrawOrder,
+                (PrimitiveKind::Path, ix) => self.paths[ix].order = draw_order as DrawOrder,
+                (PrimitiveKind::Underline, ix) => {
+                    self.underlines[ix].order = draw_order as DrawOrder
+                }
+                (PrimitiveKind::MonochromeSprite, ix) => {
+                    self.monochrome_sprites[ix].order = draw_order as DrawOrder
+                }
+                (PrimitiveKind::PolychromeSprite, ix) => {
+                    self.polychrome_sprites[ix].order = draw_order as DrawOrder
+                }
+            }
+        }
+
+        self.shadows.sort_unstable();
+        self.quads.sort_unstable();
+        self.paths.sort_unstable();
+        self.underlines.sort_unstable();
+        self.monochrome_sprites.sort_unstable();
+        self.polychrome_sprites.sort_unstable();
+
+        Scene {
+            shadows: mem::take(&mut self.shadows),
+            quads: mem::take(&mut self.quads),
+            paths: mem::take(&mut self.paths),
+            underlines: mem::take(&mut self.underlines),
+            monochrome_sprites: mem::take(&mut self.monochrome_sprites),
+            polychrome_sprites: mem::take(&mut self.polychrome_sprites),
+        }
+    }
+
+    pub fn insert(&mut self, order: &StackingOrder, primitive: impl Into<Primitive>) {
+        let primitive = primitive.into();
+        let clipped_bounds = primitive
+            .bounds()
+            .intersect(&primitive.content_mask().bounds);
+        if clipped_bounds.size.width <= ScaledPixels(0.)
+            || clipped_bounds.size.height <= ScaledPixels(0.)
+        {
+            return;
+        }
+
+        let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
+            *layer_id
+        } else {
+            let next_id = self.layers_by_order.len() as LayerId;
+            self.layers_by_order.insert(order.clone(), next_id);
+            next_id
+        };
+
+        match primitive {
+            Primitive::Shadow(mut shadow) => {
+                shadow.order = layer_id;
+                self.shadows.push(shadow);
+            }
+            Primitive::Quad(mut quad) => {
+                quad.order = layer_id;
+                self.quads.push(quad);
+            }
+            Primitive::Path(mut path) => {
+                path.order = layer_id;
+                path.id = PathId(self.paths.len());
+                self.paths.push(path);
+            }
+            Primitive::Underline(mut underline) => {
+                underline.order = layer_id;
+                self.underlines.push(underline);
+            }
+            Primitive::MonochromeSprite(mut sprite) => {
+                sprite.order = layer_id;
+                self.monochrome_sprites.push(sprite);
+            }
+            Primitive::PolychromeSprite(mut sprite) => {
+                sprite.order = layer_id;
+                self.polychrome_sprites.push(sprite);
+            }
+        }
+    }
+}
+
+pub(crate) struct Scene {
+    pub shadows: Vec<Shadow>,
+    pub quads: Vec<Quad>,
+    pub paths: Vec<Path<ScaledPixels>>,
+    pub underlines: Vec<Underline>,
+    pub monochrome_sprites: Vec<MonochromeSprite>,
+    pub polychrome_sprites: Vec<PolychromeSprite>,
+}
+
+impl Scene {
+    #[allow(dead_code)]
+    pub fn paths(&self) -> &[Path<ScaledPixels>] {
+        &self.paths
+    }
+
+    pub fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
+        BatchIterator {
+            shadows: &self.shadows,
+            shadows_start: 0,
+            shadows_iter: self.shadows.iter().peekable(),
+            quads: &self.quads,
+            quads_start: 0,
+            quads_iter: self.quads.iter().peekable(),
+            paths: &self.paths,
+            paths_start: 0,
+            paths_iter: self.paths.iter().peekable(),
+            underlines: &self.underlines,
+            underlines_start: 0,
+            underlines_iter: self.underlines.iter().peekable(),
+            monochrome_sprites: &self.monochrome_sprites,
+            monochrome_sprites_start: 0,
+            monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(),
+            polychrome_sprites: &self.polychrome_sprites,
+            polychrome_sprites_start: 0,
+            polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(),
+        }
+    }
+}
+
+struct BatchIterator<'a> {
+    shadows: &'a [Shadow],
+    shadows_start: usize,
+    shadows_iter: Peekable<slice::Iter<'a, Shadow>>,
+    quads: &'a [Quad],
+    quads_start: usize,
+    quads_iter: Peekable<slice::Iter<'a, Quad>>,
+    paths: &'a [Path<ScaledPixels>],
+    paths_start: usize,
+    paths_iter: Peekable<slice::Iter<'a, Path<ScaledPixels>>>,
+    underlines: &'a [Underline],
+    underlines_start: usize,
+    underlines_iter: Peekable<slice::Iter<'a, Underline>>,
+    monochrome_sprites: &'a [MonochromeSprite],
+    monochrome_sprites_start: usize,
+    monochrome_sprites_iter: Peekable<slice::Iter<'a, MonochromeSprite>>,
+    polychrome_sprites: &'a [PolychromeSprite],
+    polychrome_sprites_start: usize,
+    polychrome_sprites_iter: Peekable<slice::Iter<'a, PolychromeSprite>>,
+}
+
+impl<'a> Iterator for BatchIterator<'a> {
+    type Item = PrimitiveBatch<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let mut orders_and_kinds = [
+            (
+                self.shadows_iter.peek().map(|s| s.order),
+                PrimitiveKind::Shadow,
+            ),
+            (self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad),
+            (self.paths_iter.peek().map(|q| q.order), PrimitiveKind::Path),
+            (
+                self.underlines_iter.peek().map(|u| u.order),
+                PrimitiveKind::Underline,
+            ),
+            (
+                self.monochrome_sprites_iter.peek().map(|s| s.order),
+                PrimitiveKind::MonochromeSprite,
+            ),
+            (
+                self.polychrome_sprites_iter.peek().map(|s| s.order),
+                PrimitiveKind::PolychromeSprite,
+            ),
+        ];
+        orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind));
+
+        let first = orders_and_kinds[0];
+        let second = orders_and_kinds[1];
+        let (batch_kind, max_order) = if first.0.is_some() {
+            (first.1, second.0.unwrap_or(u32::MAX))
+        } else {
+            return None;
+        };
+
+        match batch_kind {
+            PrimitiveKind::Shadow => {
+                let shadows_start = self.shadows_start;
+                let mut shadows_end = shadows_start;
+                while self
+                    .shadows_iter
+                    .next_if(|shadow| shadow.order <= max_order)
+                    .is_some()
+                {
+                    shadows_end += 1;
+                }
+                self.shadows_start = shadows_end;
+                Some(PrimitiveBatch::Shadows(
+                    &self.shadows[shadows_start..shadows_end],
+                ))
+            }
+            PrimitiveKind::Quad => {
+                let quads_start = self.quads_start;
+                let mut quads_end = quads_start;
+                while self
+                    .quads_iter
+                    .next_if(|quad| quad.order <= max_order)
+                    .is_some()
+                {
+                    quads_end += 1;
+                }
+                self.quads_start = quads_end;
+                Some(PrimitiveBatch::Quads(&self.quads[quads_start..quads_end]))
+            }
+            PrimitiveKind::Path => {
+                let paths_start = self.paths_start;
+                let mut paths_end = paths_start;
+                while self
+                    .paths_iter
+                    .next_if(|path| path.order <= max_order)
+                    .is_some()
+                {
+                    paths_end += 1;
+                }
+                self.paths_start = paths_end;
+                Some(PrimitiveBatch::Paths(&self.paths[paths_start..paths_end]))
+            }
+            PrimitiveKind::Underline => {
+                let underlines_start = self.underlines_start;
+                let mut underlines_end = underlines_start;
+                while self
+                    .underlines_iter
+                    .next_if(|underline| underline.order <= max_order)
+                    .is_some()
+                {
+                    underlines_end += 1;
+                }
+                self.underlines_start = underlines_end;
+                Some(PrimitiveBatch::Underlines(
+                    &self.underlines[underlines_start..underlines_end],
+                ))
+            }
+            PrimitiveKind::MonochromeSprite => {
+                let texture_id = self.monochrome_sprites_iter.peek().unwrap().tile.texture_id;
+                let sprites_start = self.monochrome_sprites_start;
+                let mut sprites_end = sprites_start;
+                while self
+                    .monochrome_sprites_iter
+                    .next_if(|sprite| {
+                        sprite.order <= max_order && sprite.tile.texture_id == texture_id
+                    })
+                    .is_some()
+                {
+                    sprites_end += 1;
+                }
+                self.monochrome_sprites_start = sprites_end;
+                Some(PrimitiveBatch::MonochromeSprites {
+                    texture_id,
+                    sprites: &self.monochrome_sprites[sprites_start..sprites_end],
+                })
+            }
+            PrimitiveKind::PolychromeSprite => {
+                let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id;
+                let sprites_start = self.polychrome_sprites_start;
+                let mut sprites_end = self.polychrome_sprites_start;
+                while self
+                    .polychrome_sprites_iter
+                    .next_if(|sprite| {
+                        sprite.order <= max_order && sprite.tile.texture_id == texture_id
+                    })
+                    .is_some()
+                {
+                    sprites_end += 1;
+                }
+                self.polychrome_sprites_start = sprites_end;
+                Some(PrimitiveBatch::PolychromeSprites {
+                    texture_id,
+                    sprites: &self.polychrome_sprites[sprites_start..sprites_end],
+                })
+            }
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)]
+pub enum PrimitiveKind {
+    Shadow,
+    #[default]
+    Quad,
+    Path,
+    Underline,
+    MonochromeSprite,
+    PolychromeSprite,
+}
+
+pub enum Primitive {
+    Shadow(Shadow),
+    Quad(Quad),
+    Path(Path<ScaledPixels>),
+    Underline(Underline),
+    MonochromeSprite(MonochromeSprite),
+    PolychromeSprite(PolychromeSprite),
+}
+
+impl Primitive {
+    pub fn bounds(&self) -> &Bounds<ScaledPixels> {
+        match self {
+            Primitive::Shadow(shadow) => &shadow.bounds,
+            Primitive::Quad(quad) => &quad.bounds,
+            Primitive::Path(path) => &path.bounds,
+            Primitive::Underline(underline) => &underline.bounds,
+            Primitive::MonochromeSprite(sprite) => &sprite.bounds,
+            Primitive::PolychromeSprite(sprite) => &sprite.bounds,
+        }
+    }
+
+    pub fn content_mask(&self) -> &ContentMask<ScaledPixels> {
+        match self {
+            Primitive::Shadow(shadow) => &shadow.content_mask,
+            Primitive::Quad(quad) => &quad.content_mask,
+            Primitive::Path(path) => &path.content_mask,
+            Primitive::Underline(underline) => &underline.content_mask,
+            Primitive::MonochromeSprite(sprite) => &sprite.content_mask,
+            Primitive::PolychromeSprite(sprite) => &sprite.content_mask,
+        }
+    }
+}
+
+#[derive(Debug)]
+pub(crate) enum PrimitiveBatch<'a> {
+    Shadows(&'a [Shadow]),
+    Quads(&'a [Quad]),
+    Paths(&'a [Path<ScaledPixels>]),
+    Underlines(&'a [Underline]),
+    MonochromeSprites {
+        texture_id: AtlasTextureId,
+        sprites: &'a [MonochromeSprite],
+    },
+    PolychromeSprites {
+        texture_id: AtlasTextureId,
+        sprites: &'a [PolychromeSprite],
+    },
+}
+
+#[derive(Default, Debug, Clone, Eq, PartialEq)]
+#[repr(C)]
+pub struct Quad {
+    pub order: u32, // Initially a LayerId, then a DrawOrder.
+    pub bounds: Bounds<ScaledPixels>,
+    pub content_mask: ContentMask<ScaledPixels>,
+    pub background: Hsla,
+    pub border_color: Hsla,
+    pub corner_radii: Corners<ScaledPixels>,
+    pub border_widths: Edges<ScaledPixels>,
+}
+
+impl Ord for Quad {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.order.cmp(&other.order)
+    }
+}
+
+impl PartialOrd for Quad {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl From<Quad> for Primitive {
+    fn from(quad: Quad) -> Self {
+        Primitive::Quad(quad)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+#[repr(C)]
+pub struct Underline {
+    pub order: u32,
+    pub bounds: Bounds<ScaledPixels>,
+    pub content_mask: ContentMask<ScaledPixels>,
+    pub thickness: ScaledPixels,
+    pub color: Hsla,
+    pub wavy: bool,
+}
+
+impl Ord for Underline {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.order.cmp(&other.order)
+    }
+}
+
+impl PartialOrd for Underline {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl From<Underline> for Primitive {
+    fn from(underline: Underline) -> Self {
+        Primitive::Underline(underline)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+#[repr(C)]
+pub struct Shadow {
+    pub order: u32,
+    pub bounds: Bounds<ScaledPixels>,
+    pub corner_radii: Corners<ScaledPixels>,
+    pub content_mask: ContentMask<ScaledPixels>,
+    pub color: Hsla,
+    pub blur_radius: ScaledPixels,
+}
+
+impl Ord for Shadow {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.order.cmp(&other.order)
+    }
+}
+
+impl PartialOrd for Shadow {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl From<Shadow> for Primitive {
+    fn from(shadow: Shadow) -> Self {
+        Primitive::Shadow(shadow)
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+#[repr(C)]
+pub struct MonochromeSprite {
+    pub order: u32,
+    pub bounds: Bounds<ScaledPixels>,
+    pub content_mask: ContentMask<ScaledPixels>,
+    pub color: Hsla,
+    pub tile: AtlasTile,
+}
+
+impl Ord for MonochromeSprite {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        match self.order.cmp(&other.order) {
+            std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
+            order => order,
+        }
+    }
+}
+
+impl PartialOrd for MonochromeSprite {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl From<MonochromeSprite> for Primitive {
+    fn from(sprite: MonochromeSprite) -> Self {
+        Primitive::MonochromeSprite(sprite)
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+#[repr(C)]
+pub struct PolychromeSprite {
+    pub order: u32,
+    pub bounds: Bounds<ScaledPixels>,
+    pub content_mask: ContentMask<ScaledPixels>,
+    pub corner_radii: Corners<ScaledPixels>,
+    pub tile: AtlasTile,
+    pub grayscale: bool,
+}
+
+impl Ord for PolychromeSprite {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        match self.order.cmp(&other.order) {
+            std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
+            order => order,
+        }
+    }
+}
+
+impl PartialOrd for PolychromeSprite {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl From<PolychromeSprite> for Primitive {
+    fn from(sprite: PolychromeSprite) -> Self {
+        Primitive::PolychromeSprite(sprite)
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub(crate) struct PathId(pub(crate) usize);
+
+#[derive(Debug)]
+pub struct Path<P: Clone + Default + Debug> {
+    pub(crate) id: PathId,
+    order: u32,
+    pub(crate) bounds: Bounds<P>,
+    pub(crate) content_mask: ContentMask<P>,
+    pub(crate) vertices: Vec<PathVertex<P>>,
+    pub(crate) color: Hsla,
+    start: Point<P>,
+    current: Point<P>,
+    contour_count: usize,
+}
+
+impl Path<Pixels> {
+    pub fn new(start: Point<Pixels>) -> Self {
+        Self {
+            id: PathId(0),
+            order: 0,
+            vertices: Vec::new(),
+            start,
+            current: start,
+            bounds: Bounds {
+                origin: start,
+                size: Default::default(),
+            },
+            content_mask: Default::default(),
+            color: Default::default(),
+            contour_count: 0,
+        }
+    }
+
+    pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
+        Path {
+            id: self.id,
+            order: self.order,
+            bounds: self.bounds.scale(factor),
+            content_mask: self.content_mask.scale(factor),
+            vertices: self
+                .vertices
+                .iter()
+                .map(|vertex| vertex.scale(factor))
+                .collect(),
+            start: self.start.map(|start| start.scale(factor)),
+            current: self.current.scale(factor),
+            contour_count: self.contour_count,
+            color: self.color,
+        }
+    }
+
+    pub fn line_to(&mut self, to: Point<Pixels>) {
+        self.contour_count += 1;
+        if self.contour_count > 1 {
+            self.push_triangle(
+                (self.start, self.current, to),
+                (point(0., 1.), point(0., 1.), point(0., 1.)),
+            );
+        }
+        self.current = to;
+    }
+
+    pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
+        self.contour_count += 1;
+        if self.contour_count > 1 {
+            self.push_triangle(
+                (self.start, self.current, to),
+                (point(0., 1.), point(0., 1.), point(0., 1.)),
+            );
+        }
+
+        self.push_triangle(
+            (self.current, ctrl, to),
+            (point(0., 0.), point(0.5, 0.), point(1., 1.)),
+        );
+        self.current = to;
+    }
+
+    fn push_triangle(
+        &mut self,
+        xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>),
+        st: (Point<f32>, Point<f32>, Point<f32>),
+    ) {
+        self.bounds = self
+            .bounds
+            .union(&Bounds {
+                origin: xy.0,
+                size: Default::default(),
+            })
+            .union(&Bounds {
+                origin: xy.1,
+                size: Default::default(),
+            })
+            .union(&Bounds {
+                origin: xy.2,
+                size: Default::default(),
+            });
+
+        self.vertices.push(PathVertex {
+            xy_position: xy.0,
+            st_position: st.0,
+            content_mask: Default::default(),
+        });
+        self.vertices.push(PathVertex {
+            xy_position: xy.1,
+            st_position: st.1,
+            content_mask: Default::default(),
+        });
+        self.vertices.push(PathVertex {
+            xy_position: xy.2,
+            st_position: st.2,
+            content_mask: Default::default(),
+        });
+    }
+}
+
+impl Eq for Path<ScaledPixels> {}
+
+impl PartialEq for Path<ScaledPixels> {
+    fn eq(&self, other: &Self) -> bool {
+        self.order == other.order
+    }
+}
+
+impl Ord for Path<ScaledPixels> {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.order.cmp(&other.order)
+    }
+}
+
+impl PartialOrd for Path<ScaledPixels> {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl From<Path<ScaledPixels>> for Primitive {
+    fn from(path: Path<ScaledPixels>) -> Self {
+        Primitive::Path(path)
+    }
+}
+
+#[derive(Clone, Debug)]
+#[repr(C)]
+pub struct PathVertex<P: Clone + Default + Debug> {
+    pub(crate) xy_position: Point<P>,
+    pub(crate) st_position: Point<f32>,
+    pub(crate) content_mask: ContentMask<P>,
+}
+
+impl PathVertex<Pixels> {
+    pub fn scale(&self, factor: f32) -> PathVertex<ScaledPixels> {
+        PathVertex {
+            xy_position: self.xy_position.scale(factor),
+            st_position: self.st_position,
+            content_mask: self.content_mask.scale(factor),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct AtlasId(pub(crate) usize);
+
+impl Bounds<ScaledPixels> {
+    fn to_bsp_polygon<A: Copy>(&self, z: f32, anchor: A) -> BspPolygon<A> {
+        let upper_left = self.origin;
+        let upper_right = self.upper_right();
+        let lower_right = self.lower_right();
+        let lower_left = self.lower_left();
+
+        BspPolygon::from_points(
+            [
+                Point3D::new(upper_left.x.into(), upper_left.y.into(), z as f64),
+                Point3D::new(upper_right.x.into(), upper_right.y.into(), z as f64),
+                Point3D::new(lower_right.x.into(), lower_right.y.into(), z as f64),
+                Point3D::new(lower_left.x.into(), lower_left.y.into(), z as f64),
+            ],
+            anchor,
+        )
+        .expect("Polygon should not be empty")
+    }
+}
+
+// #[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,26 +1,17 @@
 use crate::{
-    color::Hsla,
-    elements::hoverable::{hoverable, Hoverable},
-    elements::pressable::{pressable, Pressable},
-    ViewContext,
+    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, Rgba,
+    SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext,
 };
-pub use fonts::Style as FontStyle;
-pub use fonts::Weight as FontWeight;
-pub use gpui::taffy::style::{
+use refineable::{Cascade, Refineable};
+use smallvec::SmallVec;
+pub use taffy::style::{
     AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
     Overflow, Position,
 };
-use gpui::{
-    fonts::{self, TextStyleRefinement},
-    geometry::{
-        rect::RectF, relative, vector::Vector2F, AbsoluteLength, DefiniteLength, Edges,
-        EdgesRefinement, Length, Point, PointRefinement, Size, SizeRefinement,
-    },
-    scene, taffy, WindowContext,
-};
-use gpui2_macros::styleable_helpers;
-use refineable::{Refineable, RefinementCascade};
-use std::sync::Arc;
+
+pub type StyleCascade = Cascade<Style>;
 
 #[derive(Clone, Refineable, Debug)]
 #[refineable(debug)]
@@ -92,111 +83,204 @@ pub struct Style {
     pub flex_shrink: f32,
 
     /// The fill color of this element
-    pub fill: Option<Fill>,
+    pub background: Option<Fill>,
 
     /// The border color of this element
     pub border_color: Option<Hsla>,
 
     /// The radius of the corners of this element
     #[refineable]
-    pub corner_radii: CornerRadii,
+    pub corner_radii: Corners<AbsoluteLength>,
 
-    /// The color of text within this element. Cascades to children unless overridden.
-    pub text_color: Option<Hsla>,
+    /// Box Shadow of the element
+    pub box_shadow: SmallVec<[BoxShadow; 2]>,
 
-    /// The font size in rems.
-    pub font_size: Option<f32>,
+    /// TEXT
+    pub text: TextStyleRefinement,
 
-    pub font_family: Option<Arc<str>>,
+    pub z_index: Option<u32>,
+}
 
-    pub font_weight: Option<FontWeight>,
+impl Styled for StyleRefinement {
+    fn style(&mut self) -> &mut StyleRefinement {
+        self
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct BoxShadow {
+    pub color: Hsla,
+    pub offset: Point<Pixels>,
+    pub blur_radius: Pixels,
+    pub spread_radius: Pixels,
+}
+
+#[derive(Refineable, Clone, Debug)]
+#[refineable(debug)]
+pub struct TextStyle {
+    pub color: Hsla,
+    pub font_family: SharedString,
+    pub font_features: FontFeatures,
+    pub font_size: Rems,
+    pub line_height: DefiniteLength,
+    pub font_weight: FontWeight,
+    pub font_style: FontStyle,
+    pub underline: Option<UnderlineStyle>,
+}
+
+impl Default for TextStyle {
+    fn default() -> Self {
+        TextStyle {
+            color: black(),
+            font_family: "Helvetica".into(), // todo!("Get a font we know exists on the system")
+            font_features: FontFeatures::default(),
+            font_size: rems(1.),
+            line_height: phi(),
+            font_weight: FontWeight::default(),
+            font_style: FontStyle::default(),
+            underline: None,
+        }
+    }
+}
+
+impl TextStyle {
+    pub fn highlight(mut self, style: HighlightStyle) -> Result<Self> {
+        if let Some(weight) = style.font_weight {
+            self.font_weight = weight;
+        }
+        if let Some(style) = style.font_style {
+            self.font_style = style;
+        }
 
+        if let Some(color) = style.color {
+            self.color = self.color.blend(color);
+        }
+
+        if let Some(factor) = style.fade_out {
+            self.color.fade_out(factor);
+        }
+
+        if let Some(underline) = style.underline {
+            self.underline = Some(underline);
+        }
+
+        Ok(self)
+    }
+
+    pub fn to_run(&self, len: usize) -> TextRun {
+        TextRun {
+            len,
+            font: Font {
+                family: self.font_family.clone(),
+                features: Default::default(),
+                weight: self.font_weight,
+                style: self.font_style,
+            },
+            color: self.color,
+            underline: self.underline.clone(),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq)]
+pub struct HighlightStyle {
+    pub color: Option<Hsla>,
+    pub font_weight: Option<FontWeight>,
     pub font_style: Option<FontStyle>,
+    pub underline: Option<UnderlineStyle>,
+    pub fade_out: Option<f32>,
 }
 
+impl Eq for HighlightStyle {}
+
 impl Style {
-    pub fn text_style(&self, cx: &WindowContext) -> Option<TextStyleRefinement> {
-        if self.text_color.is_none()
-            && self.font_size.is_none()
-            && self.font_family.is_none()
-            && self.font_weight.is_none()
-            && self.font_style.is_none()
-        {
-            return None;
+    pub fn text_style(&self, _cx: &WindowContext) -> Option<&TextStyleRefinement> {
+        if self.text.is_some() {
+            Some(&self.text)
+        } else {
+            None
         }
-
-        Some(TextStyleRefinement {
-            color: self.text_color.map(Into::into),
-            font_family: self.font_family.clone(),
-            font_size: self.font_size.map(|size| size * cx.rem_size()),
-            font_weight: self.font_weight,
-            font_style: self.font_style,
-            underline: None,
-        })
     }
 
-    pub fn to_taffy(&self, rem_size: f32) -> taffy::style::Style {
-        taffy::style::Style {
-            display: self.display,
-            overflow: self.overflow.clone().into(),
-            scrollbar_width: self.scrollbar_width,
-            position: self.position,
-            inset: self.inset.to_taffy(rem_size),
-            size: self.size.to_taffy(rem_size),
-            min_size: self.min_size.to_taffy(rem_size),
-            max_size: self.max_size.to_taffy(rem_size),
-            aspect_ratio: self.aspect_ratio,
-            margin: self.margin.to_taffy(rem_size),
-            padding: self.padding.to_taffy(rem_size),
-            border: self.border_widths.to_taffy(rem_size),
-            align_items: self.align_items,
-            align_self: self.align_self,
-            align_content: self.align_content,
-            justify_content: self.justify_content,
-            gap: self.gap.to_taffy(rem_size),
-            flex_direction: self.flex_direction,
-            flex_wrap: self.flex_wrap,
-            flex_basis: self.flex_basis.to_taffy(rem_size).into(),
-            flex_grow: self.flex_grow,
-            flex_shrink: self.flex_shrink,
-            ..Default::default() // Ignore grid properties for now
+    pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
+    where
+        C: BorrowAppContext,
+        F: FnOnce(&mut C) -> R,
+    {
+        if self.text.is_some() {
+            cx.with_text_style(self.text.clone(), f)
+        } else {
+            f(cx)
         }
     }
 
+    /// Apply overflow to content mask
+    pub fn apply_overflow<C, F, R>(&self, bounds: Bounds<Pixels>, cx: &mut C, f: F) -> R
+    where
+        C: BorrowWindow,
+        F: FnOnce(&mut C) -> R,
+    {
+        let current_mask = cx.content_mask();
+
+        let min = current_mask.bounds.origin;
+        let max = current_mask.bounds.lower_right();
+
+        let mask_bounds = match (
+            self.overflow.x == Overflow::Visible,
+            self.overflow.y == Overflow::Visible,
+        ) {
+            // x and y both visible
+            (true, true) => return f(cx),
+            // x visible, y hidden
+            (true, false) => Bounds::from_corners(
+                point(min.x, bounds.origin.y),
+                point(max.x, bounds.lower_right().y),
+            ),
+            // x hidden, y visible
+            (false, true) => Bounds::from_corners(
+                point(bounds.origin.x, min.y),
+                point(bounds.lower_right().x, max.y),
+            ),
+            // both hidden
+            (false, false) => bounds,
+        };
+        let mask = ContentMask {
+            bounds: mask_bounds,
+        };
+
+        cx.with_content_mask(mask, f)
+    }
+
     /// Paints the background of an element styled with this style.
-    pub fn paint_background<V: 'static>(&self, bounds: RectF, cx: &mut ViewContext<V>) {
+    pub fn paint<V: 'static>(&self, bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) {
         let rem_size = cx.rem_size();
-        if let Some(color) = self.fill.as_ref().and_then(Fill::color) {
-            cx.scene().push_quad(gpui::Quad {
+
+        cx.stack(0, |cx| {
+            cx.paint_shadows(
                 bounds,
-                background: Some(color.into()),
-                corner_radii: self.corner_radii.to_gpui(bounds.size(), rem_size),
-                border: Default::default(),
+                self.corner_radii.to_pixels(bounds.size, rem_size),
+                &self.box_shadow,
+            );
+        });
+
+        let background_color = self.background.as_ref().and_then(Fill::color);
+        if background_color.is_some() || self.is_border_visible() {
+            cx.stack(1, |cx| {
+                cx.paint_quad(
+                    bounds,
+                    self.corner_radii.to_pixels(bounds.size, rem_size),
+                    background_color.unwrap_or_default(),
+                    self.border_widths.to_pixels(rem_size),
+                    self.border_color.unwrap_or_default(),
+                );
             });
         }
     }
 
-    /// Paints the foreground of an element styled with this style.
-    pub fn paint_foreground<V: 'static>(&self, bounds: RectF, cx: &mut ViewContext<V>) {
-        let rem_size = cx.rem_size();
-
-        if let Some(color) = self.border_color {
-            let border = self.border_widths.to_pixels(rem_size);
-            if !border.is_empty() {
-                cx.scene().push_quad(gpui::Quad {
-                    bounds,
-                    background: None,
-                    corner_radii: self.corner_radii.to_gpui(bounds.size(), rem_size),
-                    border: scene::Border {
-                        color: color.into(),
-                        top: border.top,
-                        right: border.right,
-                        bottom: border.bottom,
-                        left: border.left,
-                    },
-                });
-            }
-        }
+    fn is_border_visible(&self) -> bool {
+        self.border_color
+            .map_or(false, |color| !color.is_transparent())
+            && self.border_widths.any(|length| !length.is_zero())
     }
 }
 
@@ -230,18 +314,24 @@ impl Default for Style {
             flex_grow: 0.0,
             flex_shrink: 1.0,
             flex_basis: Length::Auto,
-            fill: None,
+            background: None,
             border_color: None,
-            corner_radii: CornerRadii::default(),
-            text_color: None,
-            font_size: Some(1.),
-            font_family: None,
-            font_weight: None,
-            font_style: None,
+            corner_radii: Corners::default(),
+            box_shadow: Default::default(),
+            text: TextStyleRefinement::default(),
+            z_index: None,
         }
     }
 }
 
+#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Eq)]
+#[refineable(debug)]
+pub struct UnderlineStyle {
+    pub thickness: Pixels,
+    pub color: Option<Hsla>,
+    pub wavy: bool,
+}
+
 #[derive(Clone, Debug)]
 pub enum Fill {
     Color(Hsla),
@@ -267,232 +357,72 @@ impl From<Hsla> for Fill {
     }
 }
 
-#[derive(Clone, Refineable, Default, Debug)]
-#[refineable(debug)]
-pub struct CornerRadii {
-    top_left: AbsoluteLength,
-    top_right: AbsoluteLength,
-    bottom_left: AbsoluteLength,
-    bottom_right: AbsoluteLength,
+impl From<TextStyle> for HighlightStyle {
+    fn from(other: TextStyle) -> Self {
+        Self::from(&other)
+    }
 }
 
-impl CornerRadii {
-    pub fn to_gpui(&self, box_size: Vector2F, rem_size: f32) -> gpui::scene::CornerRadii {
-        let max_radius = box_size.x().min(box_size.y()) / 2.;
-
-        gpui::scene::CornerRadii {
-            top_left: self.top_left.to_pixels(rem_size).min(max_radius),
-            top_right: self.top_right.to_pixels(rem_size).min(max_radius),
-            bottom_left: self.bottom_left.to_pixels(rem_size).min(max_radius),
-            bottom_right: self.bottom_right.to_pixels(rem_size).min(max_radius),
+impl From<&TextStyle> for HighlightStyle {
+    fn from(other: &TextStyle) -> Self {
+        Self {
+            color: Some(other.color),
+            font_weight: Some(other.font_weight),
+            font_style: Some(other.font_style),
+            underline: other.underline.clone(),
+            fade_out: None,
         }
     }
 }
 
-pub trait Styleable {
-    type Style: Refineable + Default;
+impl HighlightStyle {
+    pub fn highlight(&mut self, other: HighlightStyle) {
+        match (self.color, other.color) {
+            (Some(self_color), Some(other_color)) => {
+                self.color = Some(Hsla::blend(other_color, self_color));
+            }
+            (None, Some(other_color)) => {
+                self.color = Some(other_color);
+            }
+            _ => {}
+        }
 
-    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style>;
-    fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement;
+        if other.font_weight.is_some() {
+            self.font_weight = other.font_weight;
+        }
 
-    fn computed_style(&mut self) -> Self::Style {
-        Self::Style::from_refinement(&self.style_cascade().merged())
-    }
+        if other.font_style.is_some() {
+            self.font_style = other.font_style;
+        }
 
-    fn hover(self) -> Hoverable<Self>
-    where
-        Self: Sized,
-    {
-        hoverable(self)
-    }
+        if other.underline.is_some() {
+            self.underline = other.underline;
+        }
 
-    fn active(self) -> Pressable<Self>
-    where
-        Self: Sized,
-    {
-        pressable(self)
+        match (other.fade_out, self.fade_out) {
+            (Some(source_fade), None) => self.fade_out = Some(source_fade),
+            (Some(source_fade), Some(dest_fade)) => {
+                self.fade_out = Some((dest_fade * (1. + source_fade)).clamp(0., 1.));
+            }
+            _ => {}
+        }
     }
 }
 
-use crate as gpui2;
-
-// Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
-//
-// Example:
-// // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
-// fn p_2(mut self) -> Self;
-pub trait StyleHelpers: Sized + Styleable<Style = Style> {
-    styleable_helpers!();
-
-    fn full(mut self) -> Self {
-        self.declared_style().size.width = Some(relative(1.).into());
-        self.declared_style().size.height = Some(relative(1.).into());
-        self
-    }
-
-    fn relative(mut self) -> Self {
-        self.declared_style().position = Some(Position::Relative);
-        self
-    }
-
-    fn absolute(mut self) -> Self {
-        self.declared_style().position = Some(Position::Absolute);
-        self
-    }
-
-    fn block(mut self) -> Self {
-        self.declared_style().display = Some(Display::Block);
-        self
-    }
-
-    fn flex(mut self) -> Self {
-        self.declared_style().display = Some(Display::Flex);
-        self
-    }
-
-    fn flex_col(mut self) -> Self {
-        self.declared_style().flex_direction = Some(FlexDirection::Column);
-        self
-    }
-
-    fn flex_row(mut self) -> Self {
-        self.declared_style().flex_direction = Some(FlexDirection::Row);
-        self
-    }
-
-    fn flex_1(mut self) -> Self {
-        self.declared_style().flex_grow = Some(1.);
-        self.declared_style().flex_shrink = Some(1.);
-        self.declared_style().flex_basis = Some(relative(0.).into());
-        self
-    }
-
-    fn flex_auto(mut self) -> Self {
-        self.declared_style().flex_grow = Some(1.);
-        self.declared_style().flex_shrink = Some(1.);
-        self.declared_style().flex_basis = Some(Length::Auto);
-        self
-    }
-
-    fn flex_initial(mut self) -> Self {
-        self.declared_style().flex_grow = Some(0.);
-        self.declared_style().flex_shrink = Some(1.);
-        self.declared_style().flex_basis = Some(Length::Auto);
-        self
-    }
-
-    fn flex_none(mut self) -> Self {
-        self.declared_style().flex_grow = Some(0.);
-        self.declared_style().flex_shrink = Some(0.);
-        self
-    }
-
-    fn grow(mut self) -> Self {
-        self.declared_style().flex_grow = Some(1.);
-        self
-    }
-
-    fn items_start(mut self) -> Self {
-        self.declared_style().align_items = Some(AlignItems::FlexStart);
-        self
-    }
-
-    fn items_end(mut self) -> Self {
-        self.declared_style().align_items = Some(AlignItems::FlexEnd);
-        self
-    }
-
-    fn items_center(mut self) -> Self {
-        self.declared_style().align_items = Some(AlignItems::Center);
-        self
-    }
-
-    fn justify_between(mut self) -> Self {
-        self.declared_style().justify_content = Some(JustifyContent::SpaceBetween);
-        self
-    }
-
-    fn justify_center(mut self) -> Self {
-        self.declared_style().justify_content = Some(JustifyContent::Center);
-        self
-    }
-
-    fn justify_start(mut self) -> Self {
-        self.declared_style().justify_content = Some(JustifyContent::Start);
-        self
-    }
-
-    fn justify_end(mut self) -> Self {
-        self.declared_style().justify_content = Some(JustifyContent::End);
-        self
-    }
-
-    fn justify_around(mut self) -> Self {
-        self.declared_style().justify_content = Some(JustifyContent::SpaceAround);
-        self
-    }
-
-    fn fill<F>(mut self, fill: F) -> Self
-    where
-        F: Into<Fill>,
-    {
-        self.declared_style().fill = Some(fill.into());
-        self
-    }
-
-    fn border_color<C>(mut self, border_color: C) -> Self
-    where
-        C: Into<Hsla>,
-    {
-        self.declared_style().border_color = Some(border_color.into());
-        self
-    }
-
-    fn text_color<C>(mut self, color: C) -> Self
-    where
-        C: Into<Hsla>,
-    {
-        self.declared_style().text_color = Some(color.into());
-        self
-    }
-
-    fn text_xs(mut self) -> Self {
-        self.declared_style().font_size = Some(0.75);
-        self
-    }
-
-    fn text_sm(mut self) -> Self {
-        self.declared_style().font_size = Some(0.875);
-        self
-    }
-
-    fn text_base(mut self) -> Self {
-        self.declared_style().font_size = Some(1.0);
-        self
-    }
-
-    fn text_lg(mut self) -> Self {
-        self.declared_style().font_size = Some(1.125);
-        self
-    }
-
-    fn text_xl(mut self) -> Self {
-        self.declared_style().font_size = Some(1.25);
-        self
-    }
-
-    fn text_2xl(mut self) -> Self {
-        self.declared_style().font_size = Some(1.5);
-        self
-    }
-
-    fn text_3xl(mut self) -> Self {
-        self.declared_style().font_size = Some(1.875);
-        self
+impl From<Hsla> for HighlightStyle {
+    fn from(color: Hsla) -> Self {
+        Self {
+            color: Some(color),
+            ..Default::default()
+        }
     }
+}
 
-    fn font(mut self, family_name: impl Into<Arc<str>>) -> Self {
-        self.declared_style().font_family = Some(family_name.into());
-        self
+impl From<Rgba> for HighlightStyle {
+    fn from(color: Rgba) -> Self {
+        Self {
+            color: Some(color.into()),
+            ..Default::default()
+        }
     }
 }

crates/gpui2/src/styled.rs 🔗

@@ -0,0 +1,576 @@
+use crate::{
+    self as gpui2, hsla, point, px, relative, rems, AlignItems, DefiniteLength, Display, Fill,
+    FlexDirection, Hsla, JustifyContent, Length, Position, Rems, SharedString, StyleRefinement,
+};
+use crate::{BoxShadow, TextStyleRefinement};
+use smallvec::smallvec;
+
+pub trait Styled {
+    fn style(&mut self) -> &mut StyleRefinement;
+
+    gpui2_macros::style_helpers!();
+
+    /// Sets the size of the element to the full width and height.
+    fn full(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().size.width = Some(relative(1.).into());
+        self.style().size.height = Some(relative(1.).into());
+        self
+    }
+
+    /// Sets the position of the element to `relative`.
+    /// [Docs](https://tailwindcss.com/docs/position)
+    fn relative(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().position = Some(Position::Relative);
+        self
+    }
+
+    /// Sets the position of the element to `absolute`.
+    /// [Docs](https://tailwindcss.com/docs/position)
+    fn absolute(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().position = Some(Position::Absolute);
+        self
+    }
+
+    /// Sets the display type of the element to `block`.
+    /// [Docs](https://tailwindcss.com/docs/display)
+    fn block(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().display = Some(Display::Block);
+        self
+    }
+
+    /// Sets the display type of the element to `flex`.
+    /// [Docs](https://tailwindcss.com/docs/display)
+    fn flex(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().display = Some(Display::Flex);
+        self
+    }
+
+    /// Sets the flex direction of the element to `column`.
+    /// [Docs](https://tailwindcss.com/docs/flex-direction#column)
+    fn flex_col(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().flex_direction = Some(FlexDirection::Column);
+        self
+    }
+
+    /// Sets the flex direction of the element to `row`.
+    /// [Docs](https://tailwindcss.com/docs/flex-direction#row)
+    fn flex_row(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().flex_direction = Some(FlexDirection::Row);
+        self
+    }
+
+    /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size.
+    /// [Docs](https://tailwindcss.com/docs/flex#flex-1)
+    fn flex_1(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().flex_grow = Some(1.);
+        self.style().flex_shrink = Some(1.);
+        self.style().flex_basis = Some(relative(0.).into());
+        self
+    }
+
+    /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size.
+    /// [Docs](https://tailwindcss.com/docs/flex#auto)
+    fn flex_auto(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().flex_grow = Some(1.);
+        self.style().flex_shrink = Some(1.);
+        self.style().flex_basis = Some(Length::Auto);
+        self
+    }
+
+    /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size.
+    /// [Docs](https://tailwindcss.com/docs/flex#initial)
+    fn flex_initial(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().flex_grow = Some(0.);
+        self.style().flex_shrink = Some(1.);
+        self.style().flex_basis = Some(Length::Auto);
+        self
+    }
+
+    /// Sets the element to prevent a flex item from growing or shrinking.
+    /// [Docs](https://tailwindcss.com/docs/flex#none)
+    fn flex_none(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().flex_grow = Some(0.);
+        self.style().flex_shrink = Some(0.);
+        self
+    }
+
+    /// Sets the element to allow a flex item to grow to fill any available space.
+    /// [Docs](https://tailwindcss.com/docs/flex-grow)
+    fn grow(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().flex_grow = Some(1.);
+        self
+    }
+
+    /// Sets the element to align flex items to the start of the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-items#start)
+    fn items_start(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().align_items = Some(AlignItems::FlexStart);
+        self
+    }
+
+    /// Sets the element to align flex items to the end of the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-items#end)
+    fn items_end(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().align_items = Some(AlignItems::FlexEnd);
+        self
+    }
+
+    /// Sets the element to align flex items along the center of the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-items#center)
+    fn items_center(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().align_items = Some(AlignItems::Center);
+        self
+    }
+
+    /// Sets the element to justify flex items along the container's main axis
+    /// such that there is an equal amount of space between each item.
+    /// [Docs](https://tailwindcss.com/docs/justify-content#space-between)
+    fn justify_between(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().justify_content = Some(JustifyContent::SpaceBetween);
+        self
+    }
+
+    /// Sets the element to justify flex items along the center of the container's main axis.
+    /// [Docs](https://tailwindcss.com/docs/justify-content#center)
+    fn justify_center(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().justify_content = Some(JustifyContent::Center);
+        self
+    }
+
+    /// Sets the element to justify flex items against the start of the container's main axis.
+    /// [Docs](https://tailwindcss.com/docs/justify-content#start)
+    fn justify_start(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().justify_content = Some(JustifyContent::Start);
+        self
+    }
+
+    /// Sets the element to justify flex items against the end of the container's main axis.
+    /// [Docs](https://tailwindcss.com/docs/justify-content#end)
+    fn justify_end(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().justify_content = Some(JustifyContent::End);
+        self
+    }
+
+    /// Sets the element to justify items along the container's main axis such
+    /// that there is an equal amount of space on each side of each item.
+    /// [Docs](https://tailwindcss.com/docs/justify-content#space-around)
+    fn justify_around(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().justify_content = Some(JustifyContent::SpaceAround);
+        self
+    }
+
+    /// Sets the background color of the element.
+    fn bg<F>(mut self, fill: F) -> Self
+    where
+        F: Into<Fill>,
+        Self: Sized,
+    {
+        self.style().background = Some(fill.into());
+        self
+    }
+
+    /// Sets the border color of the element.
+    fn border_color<C>(mut self, border_color: C) -> Self
+    where
+        C: Into<Hsla>,
+        Self: Sized,
+    {
+        self.style().border_color = Some(border_color.into());
+        self
+    }
+
+    /// Sets the box shadow of the element.
+    /// [Docs](https://tailwindcss.com/docs/box-shadow)
+    fn shadow(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().box_shadow = Some(smallvec![
+            BoxShadow {
+                color: hsla(0., 0., 0., 0.1),
+                offset: point(px(0.), px(1.)),
+                blur_radius: px(3.),
+                spread_radius: px(0.),
+            },
+            BoxShadow {
+                color: hsla(0., 0., 0., 0.1),
+                offset: point(px(0.), px(1.)),
+                blur_radius: px(2.),
+                spread_radius: px(-1.),
+            }
+        ]);
+        self
+    }
+
+    /// Clears the box shadow of the element.
+    /// [Docs](https://tailwindcss.com/docs/box-shadow)
+    fn shadow_none(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().box_shadow = Some(Default::default());
+        self
+    }
+
+    /// Sets the box shadow of the element.
+    /// [Docs](https://tailwindcss.com/docs/box-shadow)
+    fn shadow_sm(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().box_shadow = Some(smallvec::smallvec![BoxShadow {
+            color: hsla(0., 0., 0., 0.05),
+            offset: point(px(0.), px(1.)),
+            blur_radius: px(2.),
+            spread_radius: px(0.),
+        }]);
+        self
+    }
+
+    /// Sets the box shadow of the element.
+    /// [Docs](https://tailwindcss.com/docs/box-shadow)
+    fn shadow_md(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().box_shadow = Some(smallvec![
+            BoxShadow {
+                color: hsla(0.5, 0., 0., 0.1),
+                offset: point(px(0.), px(4.)),
+                blur_radius: px(6.),
+                spread_radius: px(-1.),
+            },
+            BoxShadow {
+                color: hsla(0., 0., 0., 0.1),
+                offset: point(px(0.), px(2.)),
+                blur_radius: px(4.),
+                spread_radius: px(-2.),
+            }
+        ]);
+        self
+    }
+
+    /// Sets the box shadow of the element.
+    /// [Docs](https://tailwindcss.com/docs/box-shadow)
+    fn shadow_lg(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().box_shadow = Some(smallvec![
+            BoxShadow {
+                color: hsla(0., 0., 0., 0.1),
+                offset: point(px(0.), px(10.)),
+                blur_radius: px(15.),
+                spread_radius: px(-3.),
+            },
+            BoxShadow {
+                color: hsla(0., 0., 0., 0.1),
+                offset: point(px(0.), px(4.)),
+                blur_radius: px(6.),
+                spread_radius: px(-4.),
+            }
+        ]);
+        self
+    }
+
+    /// Sets the box shadow of the element.
+    /// [Docs](https://tailwindcss.com/docs/box-shadow)
+    fn shadow_xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().box_shadow = Some(smallvec![
+            BoxShadow {
+                color: hsla(0., 0., 0., 0.1),
+                offset: point(px(0.), px(20.)),
+                blur_radius: px(25.),
+                spread_radius: px(-5.),
+            },
+            BoxShadow {
+                color: hsla(0., 0., 0., 0.1),
+                offset: point(px(0.), px(8.)),
+                blur_radius: px(10.),
+                spread_radius: px(-6.),
+            }
+        ]);
+        self
+    }
+
+    /// Sets the box shadow of the element.
+    /// [Docs](https://tailwindcss.com/docs/box-shadow)
+    fn shadow_2xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.style().box_shadow = Some(smallvec![BoxShadow {
+            color: hsla(0., 0., 0., 0.25),
+            offset: point(px(0.), px(25.)),
+            blur_radius: px(50.),
+            spread_radius: px(-12.),
+        }]);
+        self
+    }
+
+    fn text_style(&mut self) -> &mut Option<TextStyleRefinement> {
+        let style: &mut StyleRefinement = self.style();
+        &mut style.text
+    }
+
+    fn text_color(mut self, color: impl Into<Hsla>) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style().get_or_insert_with(Default::default).color = Some(color.into());
+        self
+    }
+
+    fn text_size(mut self, size: impl Into<Rems>) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_size = Some(size.into());
+        self
+    }
+
+    fn text_xs(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_size = Some(rems(0.75));
+        self
+    }
+
+    fn text_sm(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_size = Some(rems(0.875));
+        self
+    }
+
+    fn text_base(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_size = Some(rems(1.0));
+        self
+    }
+
+    fn text_lg(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_size = Some(rems(1.125));
+        self
+    }
+
+    fn text_xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_size = Some(rems(1.25));
+        self
+    }
+
+    fn text_2xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_size = Some(rems(1.5));
+        self
+    }
+
+    fn text_3xl(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_size = Some(rems(1.875));
+        self
+    }
+
+    fn text_decoration_none(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .underline = None;
+        self
+    }
+
+    fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.color = Some(color.into());
+        self
+    }
+
+    fn text_decoration_solid(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.wavy = false;
+        self
+    }
+
+    fn text_decoration_wavy(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.wavy = true;
+        self
+    }
+
+    fn text_decoration_0(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(0.);
+        self
+    }
+
+    fn text_decoration_1(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(1.);
+        self
+    }
+
+    fn text_decoration_2(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(2.);
+        self
+    }
+
+    fn text_decoration_4(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(4.);
+        self
+    }
+
+    fn text_decoration_8(mut self) -> Self
+    where
+        Self: Sized,
+    {
+        let style = self.text_style().get_or_insert_with(Default::default);
+        let underline = style.underline.get_or_insert_with(Default::default);
+        underline.thickness = px(8.);
+        self
+    }
+
+    fn font(mut self, family_name: impl Into<SharedString>) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_family = Some(family_name.into());
+        self
+    }
+
+    fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .line_height = Some(line_height.into());
+        self
+    }
+}

crates/gpui2/src/subscription.rs 🔗

@@ -0,0 +1,114 @@
+use collections::{BTreeMap, BTreeSet};
+use parking_lot::Mutex;
+use std::{fmt::Debug, mem, sync::Arc};
+use util::post_inc;
+
+pub(crate) struct SubscriberSet<EmitterKey, Callback>(
+    Arc<Mutex<SubscriberSetState<EmitterKey, Callback>>>,
+);
+
+impl<EmitterKey, Callback> Clone for SubscriberSet<EmitterKey, Callback> {
+    fn clone(&self) -> Self {
+        SubscriberSet(self.0.clone())
+    }
+}
+
+struct SubscriberSetState<EmitterKey, Callback> {
+    subscribers: BTreeMap<EmitterKey, BTreeMap<usize, Callback>>,
+    dropped_subscribers: BTreeSet<(EmitterKey, usize)>,
+    next_subscriber_id: usize,
+}
+
+impl<EmitterKey, Callback> SubscriberSet<EmitterKey, Callback>
+where
+    EmitterKey: 'static + Send + Ord + Clone + Debug,
+    Callback: 'static + Send,
+{
+    pub fn new() -> Self {
+        Self(Arc::new(Mutex::new(SubscriberSetState {
+            subscribers: Default::default(),
+            dropped_subscribers: Default::default(),
+            next_subscriber_id: 0,
+        })))
+    }
+
+    pub fn insert(&self, emitter_key: EmitterKey, callback: Callback) -> Subscription {
+        let mut lock = self.0.lock();
+        let subscriber_id = post_inc(&mut lock.next_subscriber_id);
+        lock.subscribers
+            .entry(emitter_key.clone())
+            .or_default()
+            .insert(subscriber_id, callback);
+        let this = self.0.clone();
+        Subscription {
+            unsubscribe: Some(Box::new(move || {
+                let mut lock = this.lock();
+                if let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) {
+                    subscribers.remove(&subscriber_id);
+                    if subscribers.is_empty() {
+                        lock.subscribers.remove(&emitter_key);
+                        return;
+                    }
+                }
+
+                // We didn't manage to remove the subscription, which means it was dropped
+                // while invoking the callback. Mark it as dropped so that we can remove it
+                // later.
+                lock.dropped_subscribers
+                    .insert((emitter_key, subscriber_id));
+            })),
+        }
+    }
+
+    pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator<Item = Callback> {
+        let subscribers = self.0.lock().subscribers.remove(&emitter);
+        subscribers.unwrap_or_default().into_values()
+    }
+
+    pub fn retain<F>(&self, emitter: &EmitterKey, mut f: F)
+    where
+        F: FnMut(&mut Callback) -> bool,
+    {
+        let entry = self.0.lock().subscribers.remove_entry(emitter);
+        if let Some((emitter, mut subscribers)) = entry {
+            subscribers.retain(|_, callback| f(callback));
+            let mut lock = self.0.lock();
+
+            // Add any new subscribers that were added while invoking the callback.
+            if let Some(new_subscribers) = lock.subscribers.remove(&emitter) {
+                subscribers.extend(new_subscribers);
+            }
+
+            // Remove any dropped subscriptions that were dropped while invoking the callback.
+            for (dropped_emitter, dropped_subscription_id) in
+                mem::take(&mut lock.dropped_subscribers)
+            {
+                debug_assert_eq!(emitter, dropped_emitter);
+                subscribers.remove(&dropped_subscription_id);
+            }
+
+            if !subscribers.is_empty() {
+                lock.subscribers.insert(emitter, subscribers);
+            }
+        }
+    }
+}
+
+#[must_use]
+pub struct Subscription {
+    unsubscribe: Option<Box<dyn FnOnce() + Send + 'static>>,
+}
+
+impl Subscription {
+    pub fn detach(mut self) {
+        self.unsubscribe.take();
+    }
+}
+
+impl Drop for Subscription {
+    fn drop(&mut self) {
+        if let Some(unsubscribe) = self.unsubscribe.take() {
+            unsubscribe();
+        }
+    }
+}

crates/gpui2/src/svg_renderer.rs 🔗

@@ -0,0 +1,46 @@
+use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size};
+use anyhow::anyhow;
+use std::{hash::Hash, sync::Arc};
+
+#[derive(Clone, PartialEq, Hash, Eq)]
+pub struct RenderSvgParams {
+    pub(crate) path: SharedString,
+    pub(crate) size: Size<DevicePixels>,
+}
+
+pub struct SvgRenderer {
+    asset_source: Arc<dyn AssetSource>,
+}
+
+impl SvgRenderer {
+    pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
+        Self { asset_source }
+    }
+
+    pub fn render(&self, params: &RenderSvgParams) -> Result<Vec<u8>> {
+        if params.size.is_zero() {
+            return Err(anyhow!("can't render at a zero size"));
+        }
+
+        // Load the tree.
+        let bytes = self.asset_source.load(&params.path)?;
+        let tree = usvg::Tree::from_data(&bytes, &usvg::Options::default())?;
+
+        // Render the SVG to a pixmap with the specified width and height.
+        let mut pixmap =
+            tiny_skia::Pixmap::new(params.size.width.into(), params.size.height.into()).unwrap();
+        resvg::render(
+            &tree,
+            usvg::FitTo::Width(params.size.width.into()),
+            pixmap.as_mut(),
+        );
+
+        // Convert the pixmap's pixels into an alpha mask.
+        let alpha_mask = pixmap
+            .pixels()
+            .iter()
+            .map(|p| p.alpha())
+            .collect::<Vec<_>>();
+        Ok(alpha_mask)
+    }
+}

crates/gpui2/src/taffy.rs 🔗

@@ -0,0 +1,435 @@
+use super::{AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style};
+use collections::HashMap;
+use std::fmt::Debug;
+use taffy::{
+    geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
+    style::AvailableSpace as TaffyAvailableSpace,
+    tree::{Measurable, MeasureFunc, NodeId},
+    Taffy,
+};
+
+pub struct TaffyLayoutEngine {
+    taffy: Taffy,
+    children_to_parents: HashMap<LayoutId, LayoutId>,
+    absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
+}
+
+static EXPECT_MESSAGE: &'static str =
+    "we should avoid taffy layout errors by construction if possible";
+
+impl TaffyLayoutEngine {
+    pub fn new() -> Self {
+        TaffyLayoutEngine {
+            taffy: Taffy::new(),
+            children_to_parents: HashMap::default(),
+            absolute_layout_bounds: HashMap::default(),
+        }
+    }
+
+    pub fn request_layout(
+        &mut self,
+        style: &Style,
+        rem_size: Pixels,
+        children: &[LayoutId],
+    ) -> LayoutId {
+        let style = style.to_taffy(rem_size);
+        if children.is_empty() {
+            self.taffy.new_leaf(style).expect(EXPECT_MESSAGE).into()
+        } else {
+            let parent_id = self
+                .taffy
+                // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
+                .new_with_children(style, unsafe { std::mem::transmute(children) })
+                .expect(EXPECT_MESSAGE)
+                .into();
+            for child_id in children {
+                self.children_to_parents.insert(*child_id, parent_id);
+            }
+            parent_id
+        }
+    }
+
+    pub fn request_measured_layout(
+        &mut self,
+        style: Style,
+        rem_size: Pixels,
+        measure: impl Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels>
+            + Send
+            + Sync
+            + 'static,
+    ) -> LayoutId {
+        let style = style.to_taffy(rem_size);
+
+        let measurable = Box::new(Measureable(measure)) as Box<dyn Measurable>;
+        self.taffy
+            .new_leaf_with_measure(style, MeasureFunc::Boxed(measurable))
+            .expect(EXPECT_MESSAGE)
+            .into()
+    }
+
+    // Used to understand performance
+    #[allow(dead_code)]
+    fn count_all_children(&self, parent: LayoutId) -> anyhow::Result<u32> {
+        let mut count = 0;
+
+        for child in self.taffy.children(parent.0)? {
+            // Count this child.
+            count += 1;
+
+            // Count all of this child's children.
+            count += self.count_all_children(LayoutId(child))?
+        }
+
+        Ok(count)
+    }
+
+    // Used to understand performance
+    #[allow(dead_code)]
+    fn max_depth(&self, depth: u32, parent: LayoutId) -> anyhow::Result<u32> {
+        println!(
+            "{parent:?} at depth {depth} has {} children",
+            self.taffy.child_count(parent.0)?
+        );
+
+        let mut max_child_depth = 0;
+
+        for child in self.taffy.children(parent.0)? {
+            max_child_depth = std::cmp::max(max_child_depth, self.max_depth(0, LayoutId(child))?);
+        }
+
+        Ok(depth + 1 + max_child_depth)
+    }
+
+    // Used to understand performance
+    #[allow(dead_code)]
+    fn get_edges(&self, parent: LayoutId) -> anyhow::Result<Vec<(LayoutId, LayoutId)>> {
+        let mut edges = Vec::new();
+
+        for child in self.taffy.children(parent.0)? {
+            edges.push((parent, LayoutId(child)));
+
+            edges.extend(self.get_edges(LayoutId(child))?);
+        }
+
+        Ok(edges)
+    }
+
+    pub fn compute_layout(&mut self, id: LayoutId, available_space: Size<AvailableSpace>) {
+        // println!("Laying out {} children", self.count_all_children(id)?);
+        // println!("Max layout depth: {}", self.max_depth(0, id)?);
+
+        // Output the edges (branches) of the tree in Mermaid format for visualization.
+        // println!("Edges:");
+        // for (a, b) in self.get_edges(id)? {
+        //     println!("N{} --> N{}", u64::from(a), u64::from(b));
+        // }
+        // println!("");
+
+        // let started_at = std::time::Instant::now();
+        self.taffy
+            .compute_layout(id.into(), available_space.into())
+            .expect(EXPECT_MESSAGE);
+        // println!("compute_layout took {:?}", started_at.elapsed());
+    }
+
+    pub fn layout_bounds(&mut self, id: LayoutId) -> Bounds<Pixels> {
+        if let Some(layout) = self.absolute_layout_bounds.get(&id).cloned() {
+            return layout;
+        }
+
+        let layout = self.taffy.layout(id.into()).expect(EXPECT_MESSAGE);
+        let mut bounds = Bounds {
+            origin: layout.location.into(),
+            size: layout.size.into(),
+        };
+
+        if let Some(parent_id) = self.children_to_parents.get(&id).copied() {
+            let parent_bounds = self.layout_bounds(parent_id);
+            bounds.origin += parent_bounds.origin;
+        }
+        self.absolute_layout_bounds.insert(id, bounds);
+
+        bounds
+    }
+}
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[repr(transparent)]
+pub struct LayoutId(NodeId);
+
+impl std::hash::Hash for LayoutId {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        u64::from(self.0).hash(state);
+    }
+}
+
+impl From<NodeId> for LayoutId {
+    fn from(node_id: NodeId) -> Self {
+        Self(node_id)
+    }
+}
+
+impl From<LayoutId> for NodeId {
+    fn from(layout_id: LayoutId) -> NodeId {
+        layout_id.0
+    }
+}
+
+struct Measureable<F>(F);
+
+impl<F> taffy::tree::Measurable for Measureable<F>
+where
+    F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync,
+{
+    fn measure(
+        &self,
+        known_dimensions: TaffySize<Option<f32>>,
+        available_space: TaffySize<TaffyAvailableSpace>,
+    ) -> TaffySize<f32> {
+        let known_dimensions: Size<Option<f32>> = known_dimensions.into();
+        let known_dimensions: Size<Option<Pixels>> = known_dimensions.map(|d| d.map(Into::into));
+        let available_space = available_space.into();
+        let size = (self.0)(known_dimensions, available_space);
+        size.into()
+    }
+}
+
+trait ToTaffy<Output> {
+    fn to_taffy(&self, rem_size: Pixels) -> Output;
+}
+
+impl ToTaffy<taffy::style::Style> for Style {
+    fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style {
+        taffy::style::Style {
+            display: self.display,
+            overflow: self.overflow.clone().into(),
+            scrollbar_width: self.scrollbar_width,
+            position: self.position,
+            inset: self.inset.to_taffy(rem_size),
+            size: self.size.to_taffy(rem_size),
+            min_size: self.min_size.to_taffy(rem_size),
+            max_size: self.max_size.to_taffy(rem_size),
+            aspect_ratio: self.aspect_ratio,
+            margin: self.margin.to_taffy(rem_size),
+            padding: self.padding.to_taffy(rem_size),
+            border: self.border_widths.to_taffy(rem_size),
+            align_items: self.align_items,
+            align_self: self.align_self,
+            align_content: self.align_content,
+            justify_content: self.justify_content,
+            gap: self.gap.to_taffy(rem_size),
+            flex_direction: self.flex_direction,
+            flex_wrap: self.flex_wrap,
+            flex_basis: self.flex_basis.to_taffy(rem_size),
+            flex_grow: self.flex_grow,
+            flex_shrink: self.flex_shrink,
+            ..Default::default() // Ignore grid properties for now
+        }
+    }
+}
+
+// impl ToTaffy for Bounds<Length> {
+//     type Output = taffy::prelude::Bounds<taffy::prelude::LengthPercentageAuto>;
+
+//     fn to_taffy(
+//         &self,
+//         rem_size: Pixels,
+//     ) -> taffy::prelude::Bounds<taffy::prelude::LengthPercentageAuto> {
+//         taffy::prelude::Bounds {
+//             origin: self.origin.to_taffy(rem_size),
+//             size: self.size.to_taffy(rem_size),
+//         }
+//     }
+// }
+
+impl ToTaffy<taffy::style::LengthPercentageAuto> for Length {
+    fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto {
+        match self {
+            Length::Definite(length) => length.to_taffy(rem_size),
+            Length::Auto => taffy::prelude::LengthPercentageAuto::Auto,
+        }
+    }
+}
+
+impl ToTaffy<taffy::style::Dimension> for Length {
+    fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::Dimension {
+        match self {
+            Length::Definite(length) => length.to_taffy(rem_size),
+            Length::Auto => taffy::prelude::Dimension::Auto,
+        }
+    }
+}
+
+impl ToTaffy<taffy::style::LengthPercentage> for DefiniteLength {
+    fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage {
+        match self {
+            DefiniteLength::Absolute(length) => match length {
+                AbsoluteLength::Pixels(pixels) => {
+                    taffy::style::LengthPercentage::Length(pixels.into())
+                }
+                AbsoluteLength::Rems(rems) => {
+                    taffy::style::LengthPercentage::Length((*rems * rem_size).into())
+                }
+            },
+            DefiniteLength::Fraction(fraction) => {
+                taffy::style::LengthPercentage::Percent(*fraction)
+            }
+        }
+    }
+}
+
+impl ToTaffy<taffy::style::LengthPercentageAuto> for DefiniteLength {
+    fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentageAuto {
+        match self {
+            DefiniteLength::Absolute(length) => match length {
+                AbsoluteLength::Pixels(pixels) => {
+                    taffy::style::LengthPercentageAuto::Length(pixels.into())
+                }
+                AbsoluteLength::Rems(rems) => {
+                    taffy::style::LengthPercentageAuto::Length((*rems * rem_size).into())
+                }
+            },
+            DefiniteLength::Fraction(fraction) => {
+                taffy::style::LengthPercentageAuto::Percent(*fraction)
+            }
+        }
+    }
+}
+
+impl ToTaffy<taffy::style::Dimension> for DefiniteLength {
+    fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Dimension {
+        match self {
+            DefiniteLength::Absolute(length) => match length {
+                AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::Length(pixels.into()),
+                AbsoluteLength::Rems(rems) => {
+                    taffy::style::Dimension::Length((*rems * rem_size).into())
+                }
+            },
+            DefiniteLength::Fraction(fraction) => taffy::style::Dimension::Percent(*fraction),
+        }
+    }
+}
+
+impl ToTaffy<taffy::style::LengthPercentage> for AbsoluteLength {
+    fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage {
+        match self {
+            AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(pixels.into()),
+            AbsoluteLength::Rems(rems) => {
+                taffy::style::LengthPercentage::Length((*rems * rem_size).into())
+            }
+        }
+    }
+}
+
+impl<T, T2> From<TaffyPoint<T>> for Point<T2>
+where
+    T: Into<T2>,
+    T2: Clone + Default + Debug,
+{
+    fn from(point: TaffyPoint<T>) -> Point<T2> {
+        Point {
+            x: point.x.into(),
+            y: point.y.into(),
+        }
+    }
+}
+
+impl<T, T2> Into<TaffyPoint<T2>> for Point<T>
+where
+    T: Into<T2> + Clone + Default + Debug,
+{
+    fn into(self) -> TaffyPoint<T2> {
+        TaffyPoint {
+            x: self.x.into(),
+            y: self.y.into(),
+        }
+    }
+}
+
+impl<T, U> ToTaffy<TaffySize<U>> for Size<T>
+where
+    T: ToTaffy<U> + Clone + Default + Debug,
+{
+    fn to_taffy(&self, rem_size: Pixels) -> TaffySize<U> {
+        TaffySize {
+            width: self.width.to_taffy(rem_size).into(),
+            height: self.height.to_taffy(rem_size).into(),
+        }
+    }
+}
+
+impl<T, U> ToTaffy<TaffyRect<U>> for Edges<T>
+where
+    T: ToTaffy<U> + Clone + Default + Debug,
+{
+    fn to_taffy(&self, rem_size: Pixels) -> TaffyRect<U> {
+        TaffyRect {
+            top: self.top.to_taffy(rem_size).into(),
+            right: self.right.to_taffy(rem_size).into(),
+            bottom: self.bottom.to_taffy(rem_size).into(),
+            left: self.left.to_taffy(rem_size).into(),
+        }
+    }
+}
+
+impl<T, U> From<TaffySize<T>> for Size<U>
+where
+    T: Into<U>,
+    U: Clone + Default + Debug,
+{
+    fn from(taffy_size: TaffySize<T>) -> Self {
+        Size {
+            width: taffy_size.width.into(),
+            height: taffy_size.height.into(),
+        }
+    }
+}
+
+impl<T, U> From<Size<T>> for TaffySize<U>
+where
+    T: Into<U> + Clone + Default + Debug,
+{
+    fn from(size: Size<T>) -> Self {
+        TaffySize {
+            width: size.width.into(),
+            height: size.height.into(),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Default, Debug)]
+pub enum AvailableSpace {
+    /// The amount of space available is the specified number of pixels
+    Definite(Pixels),
+    /// The amount of space available is indefinite and the node should be laid out under a min-content constraint
+    #[default]
+    MinContent,
+    /// The amount of space available is indefinite and the node should be laid out under a max-content constraint
+    MaxContent,
+}
+
+impl From<AvailableSpace> for TaffyAvailableSpace {
+    fn from(space: AvailableSpace) -> TaffyAvailableSpace {
+        match space {
+            AvailableSpace::Definite(Pixels(value)) => TaffyAvailableSpace::Definite(value),
+            AvailableSpace::MinContent => TaffyAvailableSpace::MinContent,
+            AvailableSpace::MaxContent => TaffyAvailableSpace::MaxContent,
+        }
+    }
+}
+
+impl From<TaffyAvailableSpace> for AvailableSpace {
+    fn from(space: TaffyAvailableSpace) -> AvailableSpace {
+        match space {
+            TaffyAvailableSpace::Definite(value) => AvailableSpace::Definite(Pixels(value)),
+            TaffyAvailableSpace::MinContent => AvailableSpace::MinContent,
+            TaffyAvailableSpace::MaxContent => AvailableSpace::MaxContent,
+        }
+    }
+}
+
+impl From<Pixels> for AvailableSpace {
+    fn from(pixels: Pixels) -> Self {
+        AvailableSpace::Definite(pixels)
+    }
+}

crates/gpui2/src/test.rs 🔗

@@ -0,0 +1,51 @@
+use crate::TestDispatcher;
+use rand::prelude::*;
+use std::{
+    env,
+    panic::{self, RefUnwindSafe},
+};
+
+pub fn run_test(
+    mut num_iterations: u64,
+    max_retries: usize,
+    test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)),
+    on_fail_fn: Option<fn()>,
+    _fn_name: String, // todo!("re-enable fn_name")
+) {
+    let starting_seed = env::var("SEED")
+        .map(|seed| seed.parse().expect("invalid SEED variable"))
+        .unwrap_or(0);
+    let is_randomized = num_iterations > 1;
+    if let Ok(iterations) = env::var("ITERATIONS") {
+        num_iterations = iterations.parse().expect("invalid ITERATIONS variable");
+    }
+
+    for seed in starting_seed..starting_seed + num_iterations {
+        let mut retry = 0;
+        loop {
+            if is_randomized {
+                eprintln!("seed = {seed}");
+            }
+            let result = panic::catch_unwind(|| {
+                let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed));
+                test_fn(dispatcher, seed);
+            });
+
+            match result {
+                Ok(_) => break,
+                Err(error) => {
+                    if retry < max_retries {
+                        println!("retrying: attempt {}", retry);
+                        retry += 1;
+                    } else {
+                        if is_randomized {
+                            eprintln!("failing seed: {}", seed);
+                        }
+                        on_fail_fn.map(|f| f());
+                        panic::resume_unwind(error);
+                    }
+                }
+            }
+        }
+    }
+}

crates/gpui2/src/text_system.rs 🔗

@@ -0,0 +1,537 @@
+mod font_features;
+mod line;
+mod line_layout;
+mod line_wrapper;
+
+use anyhow::anyhow;
+pub use font_features::*;
+pub use line::*;
+pub use line_layout::*;
+use line_wrapper::*;
+use smallvec::SmallVec;
+
+use crate::{
+    px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
+    UnderlineStyle,
+};
+use collections::HashMap;
+use core::fmt;
+use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
+use std::{
+    cmp,
+    fmt::{Debug, Display, Formatter},
+    hash::{Hash, Hasher},
+    ops::{Deref, DerefMut},
+    sync::Arc,
+};
+
+#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
+#[repr(C)]
+pub struct FontId(pub usize);
+
+#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
+pub struct FontFamilyId(pub usize);
+
+pub const SUBPIXEL_VARIANTS: u8 = 4;
+
+pub struct TextSystem {
+    line_layout_cache: Arc<LineLayoutCache>,
+    platform_text_system: Arc<dyn PlatformTextSystem>,
+    font_ids_by_font: RwLock<HashMap<Font, FontId>>,
+    font_metrics: RwLock<HashMap<FontId, FontMetrics>>,
+    wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>,
+    font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
+}
+
+impl TextSystem {
+    pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
+        TextSystem {
+            line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
+            platform_text_system,
+            font_metrics: RwLock::new(HashMap::default()),
+            font_ids_by_font: RwLock::new(HashMap::default()),
+            wrapper_pool: Mutex::new(HashMap::default()),
+            font_runs_pool: Default::default(),
+        }
+    }
+
+    pub fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
+        self.platform_text_system.add_fonts(fonts)
+    }
+
+    pub fn font_id(&self, font: &Font) -> Result<FontId> {
+        let font_id = self.font_ids_by_font.read().get(font).copied();
+        if let Some(font_id) = font_id {
+            Ok(font_id)
+        } else {
+            let font_id = self.platform_text_system.font_id(font)?;
+            self.font_ids_by_font.write().insert(font.clone(), font_id);
+            Ok(font_id)
+        }
+    }
+
+    pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Result<Bounds<Pixels>> {
+        self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size))
+    }
+
+    pub fn typographic_bounds(
+        &self,
+        font_id: FontId,
+        font_size: Pixels,
+        character: char,
+    ) -> Result<Bounds<Pixels>> {
+        let glyph_id = self
+            .platform_text_system
+            .glyph_for_char(font_id, character)
+            .ok_or_else(|| anyhow!("glyph not found for character '{}'", character))?;
+        let bounds = self
+            .platform_text_system
+            .typographic_bounds(font_id, glyph_id)?;
+        self.read_metrics(font_id, |metrics| {
+            (bounds / metrics.units_per_em as f32 * font_size.0).map(px)
+        })
+    }
+
+    pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
+        let glyph_id = self
+            .platform_text_system
+            .glyph_for_char(font_id, ch)
+            .ok_or_else(|| anyhow!("glyph not found for character '{}'", ch))?;
+        let result = self.platform_text_system.advance(font_id, glyph_id)?
+            / self.units_per_em(font_id)? as f32;
+
+        Ok(result * font_size)
+    }
+
+    pub fn units_per_em(&self, font_id: FontId) -> Result<u32> {
+        self.read_metrics(font_id, |metrics| metrics.units_per_em as u32)
+    }
+
+    pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+        self.read_metrics(font_id, |metrics| metrics.cap_height(font_size))
+    }
+
+    pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+        self.read_metrics(font_id, |metrics| metrics.x_height(font_size))
+    }
+
+    pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+        self.read_metrics(font_id, |metrics| metrics.ascent(font_size))
+    }
+
+    pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+        self.read_metrics(font_id, |metrics| metrics.descent(font_size))
+    }
+
+    pub fn baseline_offset(
+        &self,
+        font_id: FontId,
+        font_size: Pixels,
+        line_height: Pixels,
+    ) -> Result<Pixels> {
+        let ascent = self.ascent(font_id, font_size)?;
+        let descent = self.descent(font_id, font_size)?;
+        let padding_top = (line_height - ascent - descent) / 2.;
+        Ok(padding_top + ascent)
+    }
+
+    fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> Result<T> {
+        let lock = self.font_metrics.upgradable_read();
+
+        if let Some(metrics) = lock.get(&font_id) {
+            Ok(read(metrics))
+        } else {
+            let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
+            let metrics = lock
+                .entry(font_id)
+                .or_insert_with(|| self.platform_text_system.font_metrics(font_id));
+            Ok(read(metrics))
+        }
+    }
+
+    pub fn layout_text(
+        &self,
+        text: &SharedString,
+        font_size: Pixels,
+        runs: &[TextRun],
+        wrap_width: Option<Pixels>,
+    ) -> Result<SmallVec<[Line; 1]>> {
+        let mut runs = runs.iter().cloned().peekable();
+        let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
+
+        let mut lines = SmallVec::new();
+        let mut line_start = 0;
+        for line_text in text.split('\n') {
+            let line_text = SharedString::from(line_text.to_string());
+            let line_end = line_start + line_text.len();
+
+            let mut last_font: Option<Font> = None;
+            let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
+            let mut run_start = line_start;
+            while run_start < line_end {
+                let Some(run) = runs.peek_mut() else {
+                    break;
+                };
+
+                let run_len_within_line = cmp::min(line_end, run_start + run.len) - run_start;
+
+                if last_font == Some(run.font.clone()) {
+                    font_runs.last_mut().unwrap().len += run_len_within_line;
+                } else {
+                    last_font = Some(run.font.clone());
+                    font_runs.push(FontRun {
+                        len: run_len_within_line,
+                        font_id: self.platform_text_system.font_id(&run.font)?,
+                    });
+                }
+
+                if decoration_runs.last().map_or(false, |last_run| {
+                    last_run.color == run.color && last_run.underline == run.underline
+                }) {
+                    decoration_runs.last_mut().unwrap().len += run_len_within_line as u32;
+                } else {
+                    decoration_runs.push(DecorationRun {
+                        len: run_len_within_line as u32,
+                        color: run.color,
+                        underline: run.underline.clone(),
+                    });
+                }
+
+                if run_len_within_line == run.len {
+                    runs.next();
+                } else {
+                    // Preserve the remainder of the run for the next line
+                    run.len -= run_len_within_line;
+                }
+                run_start += run_len_within_line;
+            }
+
+            let layout = self
+                .line_layout_cache
+                .layout_line(&line_text, font_size, &font_runs, wrap_width);
+            lines.push(Line {
+                layout,
+                decorations: decoration_runs,
+            });
+
+            line_start = line_end + 1; // Skip `\n` character.
+            font_runs.clear();
+        }
+
+        self.font_runs_pool.lock().push(font_runs);
+
+        Ok(lines)
+    }
+
+    pub fn start_frame(&self) {
+        self.line_layout_cache.start_frame()
+    }
+
+    pub fn line_wrapper(
+        self: &Arc<Self>,
+        font: Font,
+        font_size: Pixels,
+    ) -> Result<LineWrapperHandle> {
+        let lock = &mut self.wrapper_pool.lock();
+        let font_id = self.font_id(&font)?;
+        let wrappers = lock
+            .entry(FontIdWithSize { font_id, font_size })
+            .or_default();
+        let wrapper = wrappers.pop().map(anyhow::Ok).unwrap_or_else(|| {
+            Ok(LineWrapper::new(
+                font_id,
+                font_size,
+                self.platform_text_system.clone(),
+            ))
+        })?;
+
+        Ok(LineWrapperHandle {
+            wrapper: Some(wrapper),
+            text_system: self.clone(),
+        })
+    }
+
+    pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
+        self.platform_text_system.glyph_raster_bounds(params)
+    }
+
+    pub fn rasterize_glyph(
+        &self,
+        glyph_id: &RenderGlyphParams,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
+        self.platform_text_system.rasterize_glyph(glyph_id)
+    }
+}
+
+#[derive(Hash, Eq, PartialEq)]
+struct FontIdWithSize {
+    font_id: FontId,
+    font_size: Pixels,
+}
+
+pub struct LineWrapperHandle {
+    wrapper: Option<LineWrapper>,
+    text_system: Arc<TextSystem>,
+}
+
+impl Drop for LineWrapperHandle {
+    fn drop(&mut self) {
+        let mut state = self.text_system.wrapper_pool.lock();
+        let wrapper = self.wrapper.take().unwrap();
+        state
+            .get_mut(&FontIdWithSize {
+                font_id: wrapper.font_id.clone(),
+                font_size: wrapper.font_size,
+            })
+            .unwrap()
+            .push(wrapper);
+    }
+}
+
+impl Deref for LineWrapperHandle {
+    type Target = LineWrapper;
+
+    fn deref(&self) -> &Self::Target {
+        self.wrapper.as_ref().unwrap()
+    }
+}
+
+impl DerefMut for LineWrapperHandle {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.wrapper.as_mut().unwrap()
+    }
+}
+
+/// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0,
+/// with 400.0 as normal.
+#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
+pub struct FontWeight(pub f32);
+
+impl Default for FontWeight {
+    #[inline]
+    fn default() -> FontWeight {
+        FontWeight::NORMAL
+    }
+}
+
+impl Hash for FontWeight {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        state.write_u32(u32::from_be_bytes(self.0.to_be_bytes()));
+    }
+}
+
+impl Eq for FontWeight {}
+
+impl FontWeight {
+    /// Thin weight (100), the thinnest value.
+    pub const THIN: FontWeight = FontWeight(100.0);
+    /// Extra light weight (200).
+    pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0);
+    /// Light weight (300).
+    pub const LIGHT: FontWeight = FontWeight(300.0);
+    /// Normal (400).
+    pub const NORMAL: FontWeight = FontWeight(400.0);
+    /// Medium weight (500, higher than normal).
+    pub const MEDIUM: FontWeight = FontWeight(500.0);
+    /// Semibold weight (600).
+    pub const SEMIBOLD: FontWeight = FontWeight(600.0);
+    /// Bold weight (700).
+    pub const BOLD: FontWeight = FontWeight(700.0);
+    /// Extra-bold weight (800).
+    pub const EXTRA_BOLD: FontWeight = FontWeight(800.0);
+    /// Black weight (900), the thickest value.
+    pub const BLACK: FontWeight = FontWeight(900.0);
+}
+
+/// Allows italic or oblique faces to be selected.
+#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
+pub enum FontStyle {
+    /// A face that is neither italic not obliqued.
+    Normal,
+    /// A form that is generally cursive in nature.
+    Italic,
+    /// A typically-sloped version of the regular face.
+    Oblique,
+}
+
+impl Default for FontStyle {
+    fn default() -> FontStyle {
+        FontStyle::Normal
+    }
+}
+
+impl Display for FontStyle {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        Debug::fmt(self, f)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct TextRun {
+    pub len: usize,
+    pub font: Font,
+    pub color: Hsla,
+    pub underline: Option<UnderlineStyle>,
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[repr(C)]
+pub struct GlyphId(u32);
+
+impl From<GlyphId> for u32 {
+    fn from(value: GlyphId) -> Self {
+        value.0
+    }
+}
+
+impl From<u16> for GlyphId {
+    fn from(num: u16) -> Self {
+        GlyphId(num as u32)
+    }
+}
+
+impl From<u32> for GlyphId {
+    fn from(num: u32) -> Self {
+        GlyphId(num)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct RenderGlyphParams {
+    pub(crate) font_id: FontId,
+    pub(crate) glyph_id: GlyphId,
+    pub(crate) font_size: Pixels,
+    pub(crate) subpixel_variant: Point<u8>,
+    pub(crate) scale_factor: f32,
+    pub(crate) is_emoji: bool,
+}
+
+impl Eq for RenderGlyphParams {}
+
+impl Hash for RenderGlyphParams {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.font_id.0.hash(state);
+        self.glyph_id.0.hash(state);
+        self.font_size.0.to_bits().hash(state);
+        self.subpixel_variant.hash(state);
+        self.scale_factor.to_bits().hash(state);
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct RenderEmojiParams {
+    pub(crate) font_id: FontId,
+    pub(crate) glyph_id: GlyphId,
+    pub(crate) font_size: Pixels,
+    pub(crate) scale_factor: f32,
+}
+
+impl Eq for RenderEmojiParams {}
+
+impl Hash for RenderEmojiParams {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.font_id.0.hash(state);
+        self.glyph_id.0.hash(state);
+        self.font_size.0.to_bits().hash(state);
+        self.scale_factor.to_bits().hash(state);
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub struct Font {
+    pub family: SharedString,
+    pub features: FontFeatures,
+    pub weight: FontWeight,
+    pub style: FontStyle,
+}
+
+pub fn font(family: impl Into<SharedString>) -> Font {
+    Font {
+        family: family.into(),
+        features: FontFeatures::default(),
+        weight: FontWeight::default(),
+        style: FontStyle::default(),
+    }
+}
+
+impl Font {
+    pub fn bold(mut self) -> Self {
+        self.weight = FontWeight::BOLD;
+        self
+    }
+}
+
+/// A struct for storing font metrics.
+/// It is used to define the measurements of a typeface.
+#[derive(Clone, Copy, Debug)]
+pub struct FontMetrics {
+    /// The number of font units that make up the "em square",
+    /// a scalable grid for determining the size of a typeface.
+    pub(crate) units_per_em: u32,
+
+    /// The vertical distance from the baseline of the font to the top of the glyph covers.
+    pub(crate) ascent: f32,
+
+    /// The vertical distance from the baseline of the font to the bottom of the glyph covers.
+    pub(crate) descent: f32,
+
+    /// The recommended additional space to add between lines of type.
+    pub(crate) line_gap: f32,
+
+    /// The suggested position of the underline.
+    pub(crate) underline_position: f32,
+
+    /// The suggested thickness of the underline.
+    pub(crate) underline_thickness: f32,
+
+    /// The height of a capital letter measured from the baseline of the font.
+    pub(crate) cap_height: f32,
+
+    /// The height of a lowercase x.
+    pub(crate) x_height: f32,
+
+    /// The outer limits of the area that the font covers.
+    pub(crate) bounding_box: Bounds<f32>,
+}
+
+impl FontMetrics {
+    /// Returns the vertical distance from the baseline of the font to the top of the glyph covers in pixels.
+    pub fn ascent(&self, font_size: Pixels) -> Pixels {
+        Pixels((self.ascent / self.units_per_em as f32) * font_size.0)
+    }
+
+    /// Returns the vertical distance from the baseline of the font to the bottom of the glyph covers in pixels.
+    pub fn descent(&self, font_size: Pixels) -> Pixels {
+        Pixels((self.descent / self.units_per_em as f32) * font_size.0)
+    }
+
+    /// Returns the recommended additional space to add between lines of type in pixels.
+    pub fn line_gap(&self, font_size: Pixels) -> Pixels {
+        Pixels((self.line_gap / self.units_per_em as f32) * font_size.0)
+    }
+
+    /// Returns the suggested position of the underline in pixels.
+    pub fn underline_position(&self, font_size: Pixels) -> Pixels {
+        Pixels((self.underline_position / self.units_per_em as f32) * font_size.0)
+    }
+
+    /// Returns the suggested thickness of the underline in pixels.
+    pub fn underline_thickness(&self, font_size: Pixels) -> Pixels {
+        Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0)
+    }
+
+    /// Returns the height of a capital letter measured from the baseline of the font in pixels.
+    pub fn cap_height(&self, font_size: Pixels) -> Pixels {
+        Pixels((self.cap_height / self.units_per_em as f32) * font_size.0)
+    }
+
+    /// Returns the height of a lowercase x in pixels.
+    pub fn x_height(&self, font_size: Pixels) -> Pixels {
+        Pixels((self.x_height / self.units_per_em as f32) * font_size.0)
+    }
+
+    /// Returns the outer limits of the area that the font covers in pixels.
+    pub fn bounding_box(&self, font_size: Pixels) -> Bounds<Pixels> {
+        (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
+    }
+}

crates/gpui2/src/text_system/font_features.rs 🔗

@@ -0,0 +1,162 @@
+use schemars::{
+    schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
+    JsonSchema,
+};
+
+macro_rules! create_definitions {
+    ($($(#[$meta:meta])* ($name:ident, $idx:expr)),* $(,)?) => {
+        #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+        pub struct FontFeatures {
+            enabled: u64,
+            disabled: u64,
+        }
+
+        impl FontFeatures {
+            $(
+                pub fn $name(&self) -> Option<bool> {
+                    if (self.enabled & (1 << $idx)) != 0 {
+                        Some(true)
+                    } else if (self.disabled & (1 << $idx)) != 0 {
+                        Some(false)
+                    } else {
+                        None
+                    }
+                }
+            )*
+        }
+
+        impl std::fmt::Debug for FontFeatures {
+            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                let mut debug = f.debug_struct("FontFeatures");
+                $(
+                    if let Some(value) = self.$name() {
+                        debug.field(stringify!($name), &value);
+                    };
+                )*
+                debug.finish()
+            }
+        }
+
+        impl<'de> serde::Deserialize<'de> for FontFeatures {
+            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+            where
+                D: serde::Deserializer<'de>,
+            {
+                use serde::de::{MapAccess, Visitor};
+                use std::fmt;
+
+                struct FontFeaturesVisitor;
+
+                impl<'de> Visitor<'de> for FontFeaturesVisitor {
+                    type Value = FontFeatures;
+
+                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                        formatter.write_str("a map of font features")
+                    }
+
+                    fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
+                    where
+                        M: MapAccess<'de>,
+                    {
+                        let mut enabled: u64 = 0;
+                        let mut disabled: u64 = 0;
+
+                        while let Some((key, value)) = access.next_entry::<String, Option<bool>>()? {
+                            let idx = match key.as_str() {
+                                $(stringify!($name) => $idx,)*
+                                _ => continue,
+                            };
+                            match value {
+                                Some(true) => enabled |= 1 << idx,
+                                Some(false) => disabled |= 1 << idx,
+                                None => {}
+                            };
+                        }
+                        Ok(FontFeatures { enabled, disabled })
+                    }
+                }
+
+                let features = deserializer.deserialize_map(FontFeaturesVisitor)?;
+                Ok(features)
+            }
+        }
+
+        impl serde::Serialize for FontFeatures {
+            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+            where
+                S: serde::Serializer,
+            {
+                use serde::ser::SerializeMap;
+
+                let mut map = serializer.serialize_map(None)?;
+
+                $(
+                    let feature = stringify!($name);
+                    if let Some(value) = self.$name() {
+                        map.serialize_entry(feature, &value)?;
+                    }
+                )*
+
+                map.end()
+            }
+        }
+
+        impl JsonSchema for FontFeatures {
+            fn schema_name() -> String {
+                "FontFeatures".into()
+            }
+
+            fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema {
+                let mut schema = SchemaObject::default();
+                let properties = &mut schema.object().properties;
+                let feature_schema = Schema::Object(SchemaObject {
+                    instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
+                    ..Default::default()
+                });
+
+                $(
+                    properties.insert(stringify!($name).to_owned(), feature_schema.clone());
+                )*
+
+                schema.into()
+            }
+        }
+    };
+}
+
+create_definitions!(
+    (calt, 0),
+    (case, 1),
+    (cpsp, 2),
+    (frac, 3),
+    (liga, 4),
+    (onum, 5),
+    (ordn, 6),
+    (pnum, 7),
+    (ss01, 8),
+    (ss02, 9),
+    (ss03, 10),
+    (ss04, 11),
+    (ss05, 12),
+    (ss06, 13),
+    (ss07, 14),
+    (ss08, 15),
+    (ss09, 16),
+    (ss10, 17),
+    (ss11, 18),
+    (ss12, 19),
+    (ss13, 20),
+    (ss14, 21),
+    (ss15, 22),
+    (ss16, 23),
+    (ss17, 24),
+    (ss18, 25),
+    (ss19, 26),
+    (ss20, 27),
+    (subs, 28),
+    (sups, 29),
+    (swsh, 30),
+    (titl, 31),
+    (tnum, 32),
+    (zero, 33)
+);

crates/gpui2/src/text_system/line.rs 🔗

@@ -0,0 +1,154 @@
+use crate::{
+    black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size,
+    UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
+};
+use smallvec::SmallVec;
+use std::sync::Arc;
+
+#[derive(Debug, Clone)]
+pub struct DecorationRun {
+    pub len: u32,
+    pub color: Hsla,
+    pub underline: Option<UnderlineStyle>,
+}
+
+#[derive(Clone, Default, Debug)]
+pub struct Line {
+    pub(crate) layout: Arc<WrappedLineLayout>,
+    pub(crate) decorations: SmallVec<[DecorationRun; 32]>,
+}
+
+impl Line {
+    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
+        size(
+            self.layout.width,
+            line_height * (self.layout.wrap_boundaries.len() + 1),
+        )
+    }
+
+    pub fn wrap_count(&self) -> usize {
+        self.layout.wrap_boundaries.len()
+    }
+
+    pub fn paint(
+        &self,
+        origin: Point<Pixels>,
+        line_height: Pixels,
+        cx: &mut WindowContext,
+    ) -> Result<()> {
+        let padding_top =
+            (line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.;
+        let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent);
+
+        let mut style_runs = self.decorations.iter();
+        let mut wraps = self.layout.wrap_boundaries.iter().peekable();
+        let mut run_end = 0;
+        let mut color = black();
+        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+        let text_system = cx.text_system().clone();
+
+        let mut glyph_origin = origin;
+        let mut prev_glyph_position = Point::default();
+        for (run_ix, run) in self.layout.layout.runs.iter().enumerate() {
+            let max_glyph_size = text_system
+                .bounding_box(run.font_id, self.layout.layout.font_size)?
+                .size;
+
+            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
+                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
+
+                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
+                    wraps.next();
+                    if let Some((underline_origin, underline_style)) = current_underline.take() {
+                        cx.paint_underline(
+                            underline_origin,
+                            glyph_origin.x - underline_origin.x,
+                            &underline_style,
+                        )?;
+                    }
+
+                    glyph_origin.x = origin.x;
+                    glyph_origin.y += line_height;
+                }
+                prev_glyph_position = glyph.position;
+                let glyph_origin = glyph_origin + baseline_offset;
+
+                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+                if glyph.index >= run_end {
+                    if let Some(style_run) = style_runs.next() {
+                        if let Some((_, underline_style)) = &mut current_underline {
+                            if style_run.underline.as_ref() != Some(underline_style) {
+                                finished_underline = current_underline.take();
+                            }
+                        }
+                        if let Some(run_underline) = style_run.underline.as_ref() {
+                            current_underline.get_or_insert((
+                                point(
+                                    glyph_origin.x,
+                                    origin.y
+                                        + baseline_offset.y
+                                        + (self.layout.layout.descent * 0.618),
+                                ),
+                                UnderlineStyle {
+                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
+                                    thickness: run_underline.thickness,
+                                    wavy: run_underline.wavy,
+                                },
+                            ));
+                        }
+
+                        run_end += style_run.len as usize;
+                        color = style_run.color;
+                    } else {
+                        run_end = self.layout.text.len();
+                        finished_underline = current_underline.take();
+                    }
+                }
+
+                if let Some((underline_origin, underline_style)) = finished_underline {
+                    cx.paint_underline(
+                        underline_origin,
+                        glyph_origin.x - underline_origin.x,
+                        &underline_style,
+                    )?;
+                }
+
+                let max_glyph_bounds = Bounds {
+                    origin: glyph_origin,
+                    size: max_glyph_size,
+                };
+
+                let content_mask = cx.content_mask();
+                if max_glyph_bounds.intersects(&content_mask.bounds) {
+                    if glyph.is_emoji {
+                        cx.paint_emoji(
+                            glyph_origin,
+                            run.font_id,
+                            glyph.id,
+                            self.layout.layout.font_size,
+                        )?;
+                    } else {
+                        cx.paint_glyph(
+                            glyph_origin,
+                            run.font_id,
+                            glyph.id,
+                            self.layout.layout.font_size,
+                            color,
+                        )?;
+                    }
+                }
+            }
+        }
+
+        if let Some((underline_start, underline_style)) = current_underline.take() {
+            let line_end_x = origin.x + self.layout.layout.width;
+            cx.paint_underline(
+                underline_start,
+                line_end_x - underline_start.x,
+                &underline_style,
+            )?;
+        }
+
+        Ok(())
+    }
+}

crates/gpui2/src/text_system/line_layout.rs 🔗

@@ -0,0 +1,295 @@
+use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString};
+use derive_more::{Deref, DerefMut};
+use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
+use smallvec::SmallVec;
+use std::{
+    borrow::Borrow,
+    collections::HashMap,
+    hash::{Hash, Hasher},
+    sync::Arc,
+};
+
+#[derive(Default, Debug)]
+pub struct LineLayout {
+    pub font_size: Pixels,
+    pub width: Pixels,
+    pub ascent: Pixels,
+    pub descent: Pixels,
+    pub runs: Vec<ShapedRun>,
+}
+
+#[derive(Debug)]
+pub struct ShapedRun {
+    pub font_id: FontId,
+    pub glyphs: SmallVec<[ShapedGlyph; 8]>,
+}
+
+#[derive(Clone, Debug)]
+pub struct ShapedGlyph {
+    pub id: GlyphId,
+    pub position: Point<Pixels>,
+    pub index: usize,
+    pub is_emoji: bool,
+}
+
+impl LineLayout {
+    pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
+        if x >= self.width {
+            None
+        } else {
+            for run in self.runs.iter().rev() {
+                for glyph in run.glyphs.iter().rev() {
+                    if glyph.position.x <= x {
+                        return Some(glyph.index);
+                    }
+                }
+            }
+            Some(0)
+        }
+    }
+
+    pub fn x_for_index(&self, index: usize) -> Pixels {
+        for run in &self.runs {
+            for glyph in &run.glyphs {
+                if glyph.index >= index {
+                    return glyph.position.x;
+                }
+            }
+        }
+        self.width
+    }
+
+    pub fn font_for_index(&self, index: usize) -> Option<FontId> {
+        for run in &self.runs {
+            for glyph in &run.glyphs {
+                if glyph.index >= index {
+                    return Some(run.font_id);
+                }
+            }
+        }
+
+        None
+    }
+
+    fn compute_wrap_boundaries(
+        &self,
+        text: &str,
+        wrap_width: Pixels,
+    ) -> SmallVec<[WrapBoundary; 1]> {
+        let mut boundaries = SmallVec::new();
+
+        let mut first_non_whitespace_ix = None;
+        let mut last_candidate_ix = None;
+        let mut last_candidate_x = px(0.);
+        let mut last_boundary = WrapBoundary {
+            run_ix: 0,
+            glyph_ix: 0,
+        };
+        let mut last_boundary_x = px(0.);
+        let mut prev_ch = '\0';
+        let mut glyphs = self
+            .runs
+            .iter()
+            .enumerate()
+            .flat_map(move |(run_ix, run)| {
+                run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| {
+                    let character = text[glyph.index..].chars().next().unwrap();
+                    (
+                        WrapBoundary { run_ix, glyph_ix },
+                        character,
+                        glyph.position.x,
+                    )
+                })
+            })
+            .peekable();
+
+        while let Some((boundary, ch, x)) = glyphs.next() {
+            if ch == '\n' {
+                continue;
+            }
+
+            if prev_ch == ' ' && ch != ' ' && first_non_whitespace_ix.is_some() {
+                last_candidate_ix = Some(boundary);
+                last_candidate_x = x;
+            }
+
+            if ch != ' ' && first_non_whitespace_ix.is_none() {
+                first_non_whitespace_ix = Some(boundary);
+            }
+
+            let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x);
+            let width = next_x - last_boundary_x;
+            if width > wrap_width && boundary > last_boundary {
+                if let Some(last_candidate_ix) = last_candidate_ix.take() {
+                    last_boundary = last_candidate_ix;
+                    last_boundary_x = last_candidate_x;
+                } else {
+                    last_boundary = boundary;
+                    last_boundary_x = x;
+                }
+
+                boundaries.push(last_boundary);
+            }
+            prev_ch = ch;
+        }
+
+        boundaries
+    }
+}
+
+#[derive(Deref, DerefMut, Default, Debug)]
+pub struct WrappedLineLayout {
+    #[deref]
+    #[deref_mut]
+    pub layout: LineLayout,
+    pub text: SharedString,
+    pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct WrapBoundary {
+    pub run_ix: usize,
+    pub glyph_ix: usize,
+}
+
+pub(crate) struct LineLayoutCache {
+    prev_frame: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    curr_frame: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    platform_text_system: Arc<dyn PlatformTextSystem>,
+}
+
+impl LineLayoutCache {
+    pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
+        Self {
+            prev_frame: Mutex::new(HashMap::new()),
+            curr_frame: RwLock::new(HashMap::new()),
+            platform_text_system,
+        }
+    }
+
+    pub fn start_frame(&self) {
+        let mut prev_frame = self.prev_frame.lock();
+        let mut curr_frame = self.curr_frame.write();
+        std::mem::swap(&mut *prev_frame, &mut *curr_frame);
+        curr_frame.clear();
+    }
+
+    pub fn layout_line(
+        &self,
+        text: &SharedString,
+        font_size: Pixels,
+        runs: &[FontRun],
+        wrap_width: Option<Pixels>,
+    ) -> Arc<WrappedLineLayout> {
+        let key = &CacheKeyRef {
+            text,
+            font_size,
+            runs,
+            wrap_width,
+        } as &dyn AsCacheKeyRef;
+        let curr_frame = self.curr_frame.upgradable_read();
+        if let Some(layout) = curr_frame.get(key) {
+            return layout.clone();
+        }
+
+        let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
+        if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
+            curr_frame.insert(key, layout.clone());
+            layout
+        } else {
+            let layout = self.platform_text_system.layout_line(text, font_size, runs);
+            let wrap_boundaries = wrap_width
+                .map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width))
+                .unwrap_or_default();
+            let wrapped_line = Arc::new(WrappedLineLayout {
+                layout,
+                text: text.clone(),
+                wrap_boundaries,
+            });
+
+            let key = CacheKey {
+                text: text.clone(),
+                font_size,
+                runs: SmallVec::from(runs),
+                wrap_width,
+            };
+            curr_frame.insert(key, wrapped_line.clone());
+            wrapped_line
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+pub struct FontRun {
+    pub(crate) len: usize,
+    pub(crate) font_id: FontId,
+}
+
+trait AsCacheKeyRef {
+    fn as_cache_key_ref(&self) -> CacheKeyRef;
+}
+
+#[derive(Eq)]
+struct CacheKey {
+    text: SharedString,
+    font_size: Pixels,
+    runs: SmallVec<[FontRun; 1]>,
+    wrap_width: Option<Pixels>,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash)]
+struct CacheKeyRef<'a> {
+    text: &'a str,
+    font_size: Pixels,
+    runs: &'a [FontRun],
+    wrap_width: Option<Pixels>,
+}
+
+impl<'a> PartialEq for (dyn AsCacheKeyRef + 'a) {
+    fn eq(&self, other: &dyn AsCacheKeyRef) -> bool {
+        self.as_cache_key_ref() == other.as_cache_key_ref()
+    }
+}
+
+impl<'a> Eq for (dyn AsCacheKeyRef + 'a) {}
+
+impl<'a> Hash for (dyn AsCacheKeyRef + 'a) {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.as_cache_key_ref().hash(state)
+    }
+}
+
+impl AsCacheKeyRef for CacheKey {
+    fn as_cache_key_ref(&self) -> CacheKeyRef {
+        CacheKeyRef {
+            text: &self.text,
+            font_size: self.font_size,
+            runs: self.runs.as_slice(),
+            wrap_width: self.wrap_width,
+        }
+    }
+}
+
+impl PartialEq for CacheKey {
+    fn eq(&self, other: &Self) -> bool {
+        self.as_cache_key_ref().eq(&other.as_cache_key_ref())
+    }
+}
+
+impl Hash for CacheKey {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.as_cache_key_ref().hash(state);
+    }
+}
+
+impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for CacheKey {
+    fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) {
+        self as &dyn AsCacheKeyRef
+    }
+}
+
+impl<'a> AsCacheKeyRef for CacheKeyRef<'a> {
+    fn as_cache_key_ref(&self) -> CacheKeyRef {
+        *self
+    }
+}

crates/gpui2/src/text_system/line_wrapper.rs 🔗

@@ -0,0 +1,282 @@
+use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem};
+use collections::HashMap;
+use std::{iter, sync::Arc};
+
+pub struct LineWrapper {
+    platform_text_system: Arc<dyn PlatformTextSystem>,
+    pub(crate) font_id: FontId,
+    pub(crate) font_size: Pixels,
+    cached_ascii_char_widths: [Option<Pixels>; 128],
+    cached_other_char_widths: HashMap<char, Pixels>,
+}
+
+impl LineWrapper {
+    pub const MAX_INDENT: u32 = 256;
+
+    pub fn new(
+        font_id: FontId,
+        font_size: Pixels,
+        text_system: Arc<dyn PlatformTextSystem>,
+    ) -> Self {
+        Self {
+            platform_text_system: text_system,
+            font_id,
+            font_size,
+            cached_ascii_char_widths: [None; 128],
+            cached_other_char_widths: HashMap::default(),
+        }
+    }
+
+    pub fn wrap_line<'a>(
+        &'a mut self,
+        line: &'a str,
+        wrap_width: Pixels,
+    ) -> impl Iterator<Item = Boundary> + 'a {
+        let mut width = px(0.);
+        let mut first_non_whitespace_ix = None;
+        let mut indent = None;
+        let mut last_candidate_ix = 0;
+        let mut last_candidate_width = px(0.);
+        let mut last_wrap_ix = 0;
+        let mut prev_c = '\0';
+        let mut char_indices = line.char_indices();
+        iter::from_fn(move || {
+            for (ix, c) in char_indices.by_ref() {
+                if c == '\n' {
+                    continue;
+                }
+
+                if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
+                    last_candidate_ix = ix;
+                    last_candidate_width = width;
+                }
+
+                if c != ' ' && first_non_whitespace_ix.is_none() {
+                    first_non_whitespace_ix = Some(ix);
+                }
+
+                let char_width = self.width_for_char(c);
+                width += char_width;
+                if width > wrap_width && ix > last_wrap_ix {
+                    if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
+                    {
+                        indent = Some(
+                            Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
+                        );
+                    }
+
+                    if last_candidate_ix > 0 {
+                        last_wrap_ix = last_candidate_ix;
+                        width -= last_candidate_width;
+                        last_candidate_ix = 0;
+                    } else {
+                        last_wrap_ix = ix;
+                        width = char_width;
+                    }
+
+                    if let Some(indent) = indent {
+                        width += self.width_for_char(' ') * indent as f32;
+                    }
+
+                    return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
+                }
+                prev_c = c;
+            }
+
+            None
+        })
+    }
+
+    #[inline(always)]
+    fn width_for_char(&mut self, c: char) -> Pixels {
+        if (c as u32) < 128 {
+            if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
+                cached_width
+            } else {
+                let width = self.compute_width_for_char(c);
+                self.cached_ascii_char_widths[c as usize] = Some(width);
+                width
+            }
+        } else {
+            if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
+                *cached_width
+            } else {
+                let width = self.compute_width_for_char(c);
+                self.cached_other_char_widths.insert(c, width);
+                width
+            }
+        }
+    }
+
+    fn compute_width_for_char(&self, c: char) -> Pixels {
+        let mut buffer = [0; 4];
+        let buffer = c.encode_utf8(&mut buffer);
+        self.platform_text_system
+            .layout_line(
+                buffer,
+                self.font_size,
+                &[FontRun {
+                    len: 1,
+                    font_id: self.font_id,
+                }],
+            )
+            .width
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct Boundary {
+    pub ix: usize,
+    pub next_indent: u32,
+}
+
+impl Boundary {
+    fn new(ix: usize, next_indent: u32) -> Self {
+        Self { ix, next_indent }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{font, TestAppContext, TestDispatcher};
+    use rand::prelude::*;
+
+    #[test]
+    fn test_wrap_line() {
+        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
+        let cx = TestAppContext::new(dispatcher);
+
+        cx.update(|cx| {
+            let text_system = cx.text_system().clone();
+            let mut wrapper = LineWrapper::new(
+                text_system.font_id(&font("Courier")).unwrap(),
+                px(16.),
+                text_system.platform_text_system.clone(),
+            );
+            assert_eq!(
+                wrapper
+                    .wrap_line("aa bbb cccc ddddd eeee", px(72.))
+                    .collect::<Vec<_>>(),
+                &[
+                    Boundary::new(7, 0),
+                    Boundary::new(12, 0),
+                    Boundary::new(18, 0)
+                ],
+            );
+            assert_eq!(
+                wrapper
+                    .wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0))
+                    .collect::<Vec<_>>(),
+                &[
+                    Boundary::new(4, 0),
+                    Boundary::new(11, 0),
+                    Boundary::new(18, 0)
+                ],
+            );
+            assert_eq!(
+                wrapper
+                    .wrap_line("     aaaaaaa", px(72.))
+                    .collect::<Vec<_>>(),
+                &[
+                    Boundary::new(7, 5),
+                    Boundary::new(9, 5),
+                    Boundary::new(11, 5),
+                ]
+            );
+            assert_eq!(
+                wrapper
+                    .wrap_line("                            ", px(72.))
+                    .collect::<Vec<_>>(),
+                &[
+                    Boundary::new(7, 0),
+                    Boundary::new(14, 0),
+                    Boundary::new(21, 0)
+                ]
+            );
+            assert_eq!(
+                wrapper
+                    .wrap_line("          aaaaaaaaaaaaaa", px(72.))
+                    .collect::<Vec<_>>(),
+                &[
+                    Boundary::new(7, 0),
+                    Boundary::new(14, 3),
+                    Boundary::new(18, 3),
+                    Boundary::new(22, 3),
+                ]
+            );
+        });
+    }
+
+    // todo!("move this to a test on TextSystem::layout_text")
+    // todo! repeat this test
+    // #[test]
+    // fn test_wrap_shaped_line() {
+    //     App::test().run(|cx| {
+    //         let text_system = cx.text_system().clone();
+
+    //         let normal = TextRun {
+    //             len: 0,
+    //             font: font("Helvetica"),
+    //             color: Default::default(),
+    //             underline: Default::default(),
+    //         };
+    //         let bold = TextRun {
+    //             len: 0,
+    //             font: font("Helvetica").bold(),
+    //             color: Default::default(),
+    //             underline: Default::default(),
+    //         };
+
+    //         impl TextRun {
+    //             fn with_len(&self, len: usize) -> Self {
+    //                 let mut this = self.clone();
+    //                 this.len = len;
+    //                 this
+    //             }
+    //         }
+
+    //         let text = "aa bbb cccc ddddd eeee".into();
+    //         let lines = text_system
+    //             .layout_text(
+    //                 &text,
+    //                 px(16.),
+    //                 &[
+    //                     normal.with_len(4),
+    //                     bold.with_len(5),
+    //                     normal.with_len(6),
+    //                     bold.with_len(1),
+    //                     normal.with_len(7),
+    //                 ],
+    //                 None,
+    //             )
+    //             .unwrap();
+    //         let line = &lines[0];
+
+    //         let mut wrapper = LineWrapper::new(
+    //             text_system.font_id(&normal.font).unwrap(),
+    //             px(16.),
+    //             text_system.platform_text_system.clone(),
+    //         );
+    //         assert_eq!(
+    //             wrapper
+    //                 .wrap_shaped_line(&text, &line, px(72.))
+    //                 .collect::<Vec<_>>(),
+    //             &[
+    //                 ShapedBoundary {
+    //                     run_ix: 1,
+    //                     glyph_ix: 3
+    //                 },
+    //                 ShapedBoundary {
+    //                     run_ix: 2,
+    //                     glyph_ix: 3
+    //                 },
+    //                 ShapedBoundary {
+    //                     run_ix: 4,
+    //                     glyph_ix: 2
+    //                 }
+    //             ],
+    //         );
+    //     });
+    // }
+}

crates/gpui2/src/util.rs 🔗

@@ -0,0 +1,41 @@
+pub use util::*;
+
+// pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
+// where
+//     F: Future<Output = T>,
+// {
+//     let timer = async {
+//         smol::Timer::after(timeout).await;
+//         Err(())
+//     };
+//     let future = async move { Ok(f.await) };
+//     timer.race(future).await
+// }
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace);
+
+#[cfg(any(test, feature = "test-support"))]
+impl<'a> std::fmt::Debug for CwdBacktrace<'a> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        use backtrace::{BacktraceFmt, BytesOrWideString};
+
+        let cwd = std::env::current_dir().unwrap();
+        let cwd = cwd.parent().unwrap();
+        let mut print_path = |fmt: &mut std::fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
+            std::fmt::Display::fmt(&path, fmt)
+        };
+        let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path);
+        for frame in self.0.frames() {
+            let mut formatted_frame = fmt.frame();
+            if frame
+                .symbols()
+                .iter()
+                .any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd)))
+            {
+                formatted_frame.backtrace_frame(frame)?;
+            }
+        }
+        fmt.finish()
+    }
+}

crates/gpui2/src/view.rs 🔗

@@ -1,26 +1,410 @@
 use crate::{
-    adapter::AdapterElement,
-    element::{AnyElement, Element},
+    private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
+    BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, LayoutId, Model, Pixels,
+    Size, ViewContext, VisualContext, WeakModel, WindowContext,
 };
-use gpui::ViewContext;
+use anyhow::{Context, Result};
+use std::{any::TypeId, marker::PhantomData};
 
-pub fn view<F, E>(mut render: F) -> ViewFn
+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> {
+    pub(crate) model: Model<V>,
+}
+
+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 {
+            model: self.model.clone(),
+        }
+    }
+}
+
+impl<V: Render, ParentViewState: 'static> Component<ParentViewState> for View<V> {
+    fn render(self) -> AnyElement<ParentViewState> {
+        AnyElement::new(EraseViewState {
+            view: self,
+            parent_view_state_type: PhantomData,
+        })
+    }
+}
+
+impl<V> Element<()> for View<V>
+where
+    V: Render,
+{
+    type ElementState = AnyElement<V>;
+
+    fn id(&self) -> Option<crate::ElementId> {
+        Some(ElementId::View(self.model.entity_id))
+    }
+
+    fn initialize(
+        &mut self,
+        _: &mut (),
+        _: Option<Self::ElementState>,
+        cx: &mut ViewContext<()>,
+    ) -> Self::ElementState {
+        self.update(cx, |state, cx| {
+            let mut any_element = AnyElement::new(state.render(cx));
+            any_element.initialize(state, cx);
+            any_element
+        })
+    }
+
+    fn layout(
+        &mut self,
+        _: &mut (),
+        element: &mut Self::ElementState,
+        cx: &mut ViewContext<()>,
+    ) -> LayoutId {
+        self.update(cx, |state, cx| element.layout(state, cx))
+    }
+
+    fn paint(
+        &mut self,
+        _: Bounds<Pixels>,
+        _: &mut (),
+        element: &mut Self::ElementState,
+        cx: &mut ViewContext<()>,
+    ) {
+        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(),
+        }
+    }
+}
+
+struct EraseViewState<V, ParentV> {
+    view: View<V>,
+    parent_view_state_type: PhantomData<ParentV>,
+}
+
+unsafe impl<V, ParentV> Send 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: Render, ParentV: 'static> Element<ParentV> for EraseViewState<V, ParentV> {
+    type ElementState = AnyBox;
+
+    fn id(&self) -> Option<crate::ElementId> {
+        Element::id(&self.view)
+    }
+
+    fn initialize(
+        &mut self,
+        _: &mut ParentV,
+        _: Option<Self::ElementState>,
+        cx: &mut ViewContext<ParentV>,
+    ) -> Self::ElementState {
+        ViewObject::initialize(&mut self.view, cx)
+    }
+
+    fn layout(
+        &mut self,
+        _: &mut ParentV,
+        element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentV>,
+    ) -> LayoutId {
+        ViewObject::layout(&mut self.view, element, cx)
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        _: &mut ParentV,
+        element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentV>,
+    ) {
+        ViewObject::paint(&mut self.view, bounds, element, cx)
+    }
+}
+
+trait ViewObject: Send + Sync {
+    fn entity_type(&self) -> TypeId;
+    fn entity_id(&self) -> EntityId;
+    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> ViewObject for View<V>
 where
-    F: 'static + FnMut(&mut ViewContext<ViewFn>) -> E,
-    E: Element<ViewFn>,
+    V: Render,
 {
-    ViewFn(Box::new(move |cx| (render)(cx).into_any()))
+    fn entity_type(&self) -> TypeId {
+        TypeId::of::<V>()
+    }
+
+    fn entity_id(&self) -> EntityId {
+        Entity::entity_id(self)
+    }
+
+    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
+            })
+        })
+    }
+
+    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(&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);
+            });
+        });
+    }
+
+    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()
+    }
+}
+
+#[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),
+}
+
+impl AnyView {
+    pub fn downgrade(&self) -> AnyWeakView {
+        AnyWeakView {
+            model: self.model.downgrade(),
+            initialize: self.initialize,
+            layout: self.layout,
+            paint: self.paint,
+        }
+    }
+
+    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,
+            }),
+        }
+    }
+
+    pub(crate) fn entity_type(&self) -> TypeId {
+        self.model.entity_type
+    }
+
+    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);
+    }
+}
+
+impl<V: 'static> Component<V> for AnyView {
+    fn render(self) -> AnyElement<V> {
+        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<ParentViewState: 'static> Element<ParentViewState> for AnyView {
+    type ElementState = AnyBox;
+
+    fn id(&self) -> Option<ElementId> {
+        Some(self.model.entity_id.into())
+    }
+
+    fn initialize(
+        &mut self,
+        _view_state: &mut ParentViewState,
+        _element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<ParentViewState>,
+    ) -> Self::ElementState {
+        (self.initialize)(self, cx)
+    }
+
+    fn layout(
+        &mut self,
+        _view_state: &mut ParentViewState,
+        rendered_element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentViewState>,
+    ) -> LayoutId {
+        (self.layout)(self, rendered_element, cx)
+    }
+
+    fn paint(
+        &mut self,
+        _bounds: Bounds<Pixels>,
+        _view_state: &mut ParentViewState,
+        rendered_element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentViewState>,
+    ) {
+        (self.paint)(self, rendered_element, cx)
+    }
 }
 
-pub struct ViewFn(Box<dyn FnMut(&mut ViewContext<ViewFn>) -> AnyElement<ViewFn>>);
+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 gpui::Entity for ViewFn {
-    type Event = ();
+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 gpui::View for ViewFn {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
-        use gpui::Element as _;
-        AdapterElement((self.0)(cx)).into_any()
+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/view_context.rs 🔗

@@ -1,79 +0,0 @@
-use std::{any::TypeId, rc::Rc};
-
-use crate::{element::LayoutId, style::Style};
-use anyhow::{anyhow, Result};
-use derive_more::{Deref, DerefMut};
-use gpui::{geometry::Size, scene::EventHandler, EventContext, Layout, MeasureParams};
-pub use gpui::{taffy::tree::NodeId, ViewContext as LegacyViewContext};
-
-#[derive(Deref, DerefMut)]
-pub struct ViewContext<'a, 'b, 'c, V> {
-    #[deref]
-    #[deref_mut]
-    pub(crate) legacy_cx: &'c mut LegacyViewContext<'a, 'b, V>,
-}
-
-impl<'a, 'b, 'c, V: 'static> ViewContext<'a, 'b, 'c, V> {
-    pub fn new(legacy_cx: &'c mut LegacyViewContext<'a, 'b, V>) -> Self {
-        Self { legacy_cx }
-    }
-
-    pub fn add_layout_node(
-        &mut self,
-        style: Style,
-        children: impl IntoIterator<Item = NodeId>,
-    ) -> Result<LayoutId> {
-        let rem_size = self.rem_size();
-        let style = style.to_taffy(rem_size);
-        let id = self
-            .legacy_cx
-            .layout_engine()
-            .ok_or_else(|| anyhow!("no layout engine"))?
-            .add_node(style, children)?;
-
-        Ok(id)
-    }
-
-    pub fn add_measured_layout_node<F>(&mut self, style: Style, measure: F) -> Result<LayoutId>
-    where
-        F: Fn(MeasureParams) -> Size<f32> + Sync + Send + 'static,
-    {
-        let rem_size = self.rem_size();
-        let layout_id = self
-            .layout_engine()
-            .ok_or_else(|| anyhow!("no layout engine"))?
-            .add_measured_node(style.to_taffy(rem_size), measure)?;
-
-        Ok(layout_id)
-    }
-
-    pub fn on_event<E: 'static>(
-        &mut self,
-        order: u32,
-        handler: impl Fn(&mut V, &E, &mut EventContext<V>) + 'static,
-    ) {
-        let view = self.weak_handle();
-
-        self.scene().event_handlers.push(EventHandler {
-            order,
-            handler: Rc::new(move |event, window_cx| {
-                if let Some(view) = view.upgrade(window_cx) {
-                    view.update(window_cx, |view, view_cx| {
-                        let mut event_cx = EventContext::new(view_cx);
-                        handler(view, event.downcast_ref().unwrap(), &mut event_cx);
-                        event_cx.bubble
-                    })
-                } else {
-                    true
-                }
-            }),
-            event_type: TypeId::of::<E>(),
-        })
-    }
-
-    pub(crate) fn computed_layout(&mut self, layout_id: LayoutId) -> Result<Layout> {
-        self.layout_engine()
-            .ok_or_else(|| anyhow!("no layout engine present"))?
-            .computed_layout(layout_id)
-    }
-}

crates/gpui2/src/window.rs 🔗

@@ -0,0 +1,2022 @@
+use crate::{
+    px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
+    Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect,
+    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, View, VisualContext, WeakModel, WeakView,
+    WindowOptions, SUBPIXEL_VARIANTS,
+};
+use anyhow::Result;
+use collections::HashMap;
+use derive_more::{Deref, DerefMut};
+use parking_lot::RwLock;
+use slotmap::SlotMap;
+use smallvec::SmallVec;
+use std::{
+    any::{Any, TypeId},
+    borrow::{Borrow, BorrowMut, Cow},
+    fmt::Debug,
+    future::Future,
+    marker::PhantomData,
+    mem,
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+};
+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(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 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, 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 = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + Send + 'static>;
+type AnyKeyListener = Box<
+    dyn Fn(
+            &dyn Any,
+            &[&DispatchContext],
+            DispatchPhase,
+            &mut WindowContext,
+        ) -> Option<Box<dyn Action>>
+        + Send
+        + '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>>>,
+}
+
+impl FocusHandle {
+    pub(crate) fn new(handles: &Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>) -> Self {
+        let id = handles.write().insert(AtomicUsize::new(1));
+        Self {
+            id,
+            handles: handles.clone(),
+        }
+    }
+
+    pub(crate) fn for_id(
+        id: FocusId,
+        handles: &Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
+    ) -> Option<Self> {
+        let lock = handles.read();
+        let ref_count = lock.get(id)?;
+        if ref_count.load(SeqCst) == 0 {
+            None
+        } else {
+            ref_count.fetch_add(1, SeqCst);
+            Some(Self {
+                id,
+                handles: handles.clone(),
+            })
+        }
+    }
+
+    /// 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 {
+            if self.id == ancestor_id {
+                return true;
+            } else {
+                ancestor = cx.window.focus_parents_by_child.get(&ancestor_id).copied();
+            }
+        }
+        false
+    }
+}
+
+impl Clone for FocusHandle {
+    fn clone(&self) -> Self {
+        Self::for_id(self.id, &self.handles).unwrap()
+    }
+}
+
+impl PartialEq for FocusHandle {
+    fn eq(&self, other: &Self) -> bool {
+        self.id == other.id
+    }
+}
+
+impl Eq for FocusHandle {}
+
+impl Drop for FocusHandle {
+    fn drop(&mut self) {
+        self.handles
+            .read()
+            .get(self.id)
+            .unwrap()
+            .fetch_sub(1, SeqCst);
+    }
+}
+
+// Holds the state for a specific window.
+pub struct Window {
+    handle: AnyWindowHandle,
+    platform_window: MainThreadOnly<Box<dyn PlatformWindow>>,
+    display_id: DisplayId,
+    sprite_atlas: Arc<dyn PlatformAtlas>,
+    rem_size: Pixels,
+    content_size: Size<Pixels>,
+    pub(crate) layout_engine: TaffyLayoutEngine,
+    pub(crate) root_view: Option<AnyView>,
+    pub(crate) element_id_stack: GlobalElementId,
+    prev_frame_element_states: HashMap<GlobalElementId, AnyBox>,
+    element_states: HashMap<GlobalElementId, AnyBox>,
+    prev_frame_key_matchers: HashMap<GlobalElementId, KeyMatcher>,
+    key_matchers: HashMap<GlobalElementId, KeyMatcher>,
+    z_index_stack: StackingOrder,
+    content_mask_stack: Vec<ContentMask<Pixels>>,
+    element_offset_stack: Vec<Point<Pixels>>,
+    mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>,
+    key_dispatch_stack: Vec<KeyDispatchStackFrame>,
+    freeze_key_dispatch_stack: bool,
+    focus_stack: Vec<FocusId>,
+    focus_parents_by_child: HashMap<FocusId, FocusId>,
+    pub(crate) focus_listeners: Vec<AnyFocusListener>,
+    pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
+    default_prevented: bool,
+    mouse_position: Point<Pixels>,
+    scale_factor: f32,
+    pub(crate) scene_builder: SceneBuilder,
+    pub(crate) dirty: bool,
+    pub(crate) last_blur: Option<Option<FocusId>>,
+    pub(crate) focus: Option<FocusId>,
+}
+
+impl Window {
+    pub(crate) fn new(
+        handle: AnyWindowHandle,
+        options: WindowOptions,
+        cx: &mut MainThread<AppContext>,
+    ) -> Self {
+        let platform_window = cx.platform().open_window(handle, options);
+        let display_id = platform_window.display().id();
+        let sprite_atlas = platform_window.sprite_atlas();
+        let mouse_position = platform_window.mouse_position();
+        let content_size = platform_window.content_size();
+        let scale_factor = platform_window.scale_factor();
+        platform_window.on_resize(Box::new({
+            let cx = cx.to_async();
+            move |content_size, scale_factor| {
+                cx.update_window(handle, |cx| {
+                    cx.window.scale_factor = scale_factor;
+                    cx.window.scene_builder = SceneBuilder::new();
+                    cx.window.content_size = content_size;
+                    cx.window.display_id = cx
+                        .window
+                        .platform_window
+                        .borrow_on_main_thread()
+                        .display()
+                        .id();
+                    cx.window.dirty = true;
+                })
+                .log_err();
+            }
+        }));
+
+        platform_window.on_input({
+            let cx = cx.to_async();
+            Box::new(move |event| {
+                cx.update_window(handle, |cx| cx.dispatch_event(event))
+                    .log_err()
+                    .unwrap_or(true)
+            })
+        });
+
+        let platform_window = MainThreadOnly::new(Arc::new(platform_window), cx.executor.clone());
+
+        Window {
+            handle,
+            platform_window,
+            display_id,
+            sprite_atlas,
+            rem_size: px(16.),
+            content_size,
+            layout_engine: TaffyLayoutEngine::new(),
+            root_view: None,
+            element_id_stack: GlobalElementId::default(),
+            prev_frame_element_states: HashMap::default(),
+            element_states: HashMap::default(),
+            prev_frame_key_matchers: HashMap::default(),
+            key_matchers: HashMap::default(),
+            z_index_stack: StackingOrder(SmallVec::new()),
+            content_mask_stack: Vec::new(),
+            element_offset_stack: Vec::new(),
+            mouse_listeners: HashMap::default(),
+            key_dispatch_stack: Vec::new(),
+            freeze_key_dispatch_stack: false,
+            focus_stack: Vec::new(),
+            focus_parents_by_child: HashMap::default(),
+            focus_listeners: Vec::new(),
+            focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
+            default_prevented: true,
+            mouse_position,
+            scale_factor,
+            scene_builder: SceneBuilder::new(),
+            dirty: true,
+            last_blur: None,
+            focus: None,
+        }
+    }
+}
+
+/// 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,
+        listener: AnyKeyListener,
+    },
+    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> {
+    pub bounds: Bounds<P>,
+}
+
+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> {
+    pub(crate) app: Reference<'a, AppContext>,
+    pub(crate) window: Reference<'w, Window>,
+}
+
+impl<'a, 'w> WindowContext<'a, 'w> {
+    pub(crate) fn immutable(app: &'a AppContext, window: &'w Window) -> Self {
+        Self {
+            app: Reference::Immutable(app),
+            window: Reference::Immutable(window),
+        }
+    }
+
+    pub(crate) fn mutable(app: &'a mut AppContext, window: &'w mut Window) -> Self {
+        Self {
+            app: Reference::Mutable(app),
+            window: Reference::Mutable(window),
+        }
+    }
+
+    /// 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);
+        }
+
+        let window_id = self.window.handle.id;
+        self.window.focus = Some(handle.id);
+        self.app.push_effect(Effect::FocusChanged {
+            window_id,
+            focused: Some(handle.id),
+        });
+        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);
+        }
+
+        let window_id = self.window.handle.id;
+        self.window.focus = None;
+        self.app.push_effect(Effect::FocusChanged {
+            window_id,
+            focused: None,
+        });
+        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,
+    ) -> Task<Result<R>>
+    where
+        R: Send + 'static,
+    {
+        if self.executor.is_main_thread() {
+            Task::ready(Ok(f(unsafe {
+                mem::transmute::<&mut Self, &mut MainThread<Self>>(self)
+            })))
+        } else {
+            let id = self.window.handle.id;
+            self.app.run_on_main(move |cx| cx.update_window(id, f))
+        }
+    }
+
+    /// 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;
+        self.run_on_main(move |cx| {
+            if let Some(callbacks) = cx.next_frame_callbacks.get_mut(&display_id) {
+                callbacks.push(f);
+                // If there was already a callback, it means that we already scheduled a frame.
+                if callbacks.len() > 1 {
+                    return;
+                }
+            } else {
+                let async_cx = cx.to_async();
+                cx.next_frame_callbacks.insert(display_id, vec![f]);
+                cx.platform().set_display_link_output_callback(
+                    display_id,
+                    Box::new(move |_current_time, _output_time| {
+                        let _ = async_cx.update(|cx| {
+                            let callbacks = cx
+                                .next_frame_callbacks
+                                .get_mut(&display_id)
+                                .unwrap()
+                                .drain(..)
+                                .collect::<Vec<_>>();
+                            for callback in callbacks {
+                                callback(cx);
+                            }
+
+                            cx.run_on_main(move |cx| {
+                                if cx.next_frame_callbacks.get(&display_id).unwrap().is_empty() {
+                                    cx.platform().stop_display_link(display_id);
+                                }
+                            })
+                            .detach();
+                        });
+                    }),
+                );
+            }
+
+            cx.platform().start_display_link(display_id);
+        })
+        .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,
+    ) -> Task<R>
+    where
+        R: Send + 'static,
+        Fut: Future<Output = R> + Send + 'static,
+    {
+        let window = self.window.handle;
+        self.app.spawn(move |app| {
+            let cx = AsyncWindowContext::new(app, window);
+            let future = f(window, cx);
+            async move { future.await }
+        })
+    }
+
+    /// 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,
+    {
+        let mut global = self.app.lease_global::<G>();
+        let result = f(&mut global, self);
+        self.app.end_global_lease(global);
+        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,
+        children: impl IntoIterator<Item = LayoutId>,
+    ) -> LayoutId {
+        self.app.layout_id_buffer.clear();
+        self.app.layout_id_buffer.extend(children.into_iter());
+        let rem_size = self.rem_size();
+
+        self.window
+            .layout_engine
+            .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,
+    >(
+        &mut self,
+        style: Style,
+        rem_size: Pixels,
+        measure: F,
+    ) -> LayoutId {
+        self.window
+            .layout_engine
+            .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
+            .layout_engine
+            .layout_bounds(layout_id)
+            .map(Into::into);
+        bounds.origin += self.element_offset();
+        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();
+        text_style
+            .line_height
+            .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 + 'static,
+    ) {
+        let order = self.window.z_index_stack.clone();
+        self.window
+            .mouse_listeners
+            .entry(TypeId::of::<Event>())
+            .or_default()
+            .push((
+                order,
+                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
+    }
+
+    /// 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>,
+        corner_radii: Corners<Pixels>,
+        shadows: &[BoxShadow],
+    ) {
+        let scale_factor = self.scale_factor();
+        let content_mask = self.content_mask();
+        let window = &mut *self.window;
+        for shadow in shadows {
+            let mut shadow_bounds = bounds;
+            shadow_bounds.origin += shadow.offset;
+            shadow_bounds.dilate(shadow.spread_radius);
+            window.scene_builder.insert(
+                &window.z_index_stack,
+                Shadow {
+                    order: 0,
+                    bounds: shadow_bounds.scale(scale_factor),
+                    content_mask: content_mask.scale(scale_factor),
+                    corner_radii: corner_radii.scale(scale_factor),
+                    color: shadow.color,
+                    blur_radius: shadow.blur_radius.scale(scale_factor),
+                },
+            );
+        }
+    }
+
+    /// 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>,
+        corner_radii: Corners<Pixels>,
+        background: impl Into<Hsla>,
+        border_widths: Edges<Pixels>,
+        border_color: impl Into<Hsla>,
+    ) {
+        let scale_factor = self.scale_factor();
+        let content_mask = self.content_mask();
+
+        let window = &mut *self.window;
+        window.scene_builder.insert(
+            &window.z_index_stack,
+            Quad {
+                order: 0,
+                bounds: bounds.scale(scale_factor),
+                content_mask: content_mask.scale(scale_factor),
+                background: background.into(),
+                border_color: border_color.into(),
+                corner_radii: corner_radii.scale(scale_factor),
+                border_widths: border_widths.scale(scale_factor),
+            },
+        );
+    }
+
+    /// 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();
+        path.content_mask = content_mask;
+        path.color = color.into();
+        let window = &mut *self.window;
+        window
+            .scene_builder
+            .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>,
+        width: Pixels,
+        style: &UnderlineStyle,
+    ) -> Result<()> {
+        let scale_factor = self.scale_factor();
+        let height = if style.wavy {
+            style.thickness * 3.
+        } else {
+            style.thickness
+        };
+        let bounds = Bounds {
+            origin,
+            size: size(width, height),
+        };
+        let content_mask = self.content_mask();
+        let window = &mut *self.window;
+        window.scene_builder.insert(
+            &window.z_index_stack,
+            Underline {
+                order: 0,
+                bounds: bounds.scale(scale_factor),
+                content_mask: content_mask.scale(scale_factor),
+                thickness: style.thickness.scale(scale_factor),
+                color: style.color.unwrap_or_default(),
+                wavy: style.wavy,
+            },
+        );
+        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>,
+        font_id: FontId,
+        glyph_id: GlyphId,
+        font_size: Pixels,
+        color: Hsla,
+    ) -> Result<()> {
+        let scale_factor = self.scale_factor();
+        let glyph_origin = origin.scale(scale_factor);
+        let subpixel_variant = Point {
+            x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
+            y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
+        };
+        let params = RenderGlyphParams {
+            font_id,
+            glyph_id,
+            font_size,
+            subpixel_variant,
+            scale_factor,
+            is_emoji: false,
+        };
+
+        let raster_bounds = self.text_system().raster_bounds(&params)?;
+        if !raster_bounds.is_zero() {
+            let tile =
+                self.window
+                    .sprite_atlas
+                    .get_or_insert_with(&params.clone().into(), &mut || {
+                        let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
+                        Ok((size, Cow::Owned(bytes)))
+                    })?;
+            let bounds = Bounds {
+                origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
+                size: tile.bounds.size.map(Into::into),
+            };
+            let content_mask = self.content_mask().scale(scale_factor);
+            let window = &mut *self.window;
+            window.scene_builder.insert(
+                &window.z_index_stack,
+                MonochromeSprite {
+                    order: 0,
+                    bounds,
+                    content_mask,
+                    color,
+                    tile,
+                },
+            );
+        }
+        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>,
+        font_id: FontId,
+        glyph_id: GlyphId,
+        font_size: Pixels,
+    ) -> Result<()> {
+        let scale_factor = self.scale_factor();
+        let glyph_origin = origin.scale(scale_factor);
+        let params = RenderGlyphParams {
+            font_id,
+            glyph_id,
+            font_size,
+            // We don't render emojis with subpixel variants.
+            subpixel_variant: Default::default(),
+            scale_factor,
+            is_emoji: true,
+        };
+
+        let raster_bounds = self.text_system().raster_bounds(&params)?;
+        if !raster_bounds.is_zero() {
+            let tile =
+                self.window
+                    .sprite_atlas
+                    .get_or_insert_with(&params.clone().into(), &mut || {
+                        let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
+                        Ok((size, Cow::Owned(bytes)))
+                    })?;
+            let bounds = Bounds {
+                origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
+                size: tile.bounds.size.map(Into::into),
+            };
+            let content_mask = self.content_mask().scale(scale_factor);
+            let window = &mut *self.window;
+
+            window.scene_builder.insert(
+                &window.z_index_stack,
+                PolychromeSprite {
+                    order: 0,
+                    bounds,
+                    corner_radii: Default::default(),
+                    content_mask,
+                    tile,
+                    grayscale: false,
+                },
+            );
+        }
+        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>,
+        path: SharedString,
+        color: Hsla,
+    ) -> Result<()> {
+        let scale_factor = self.scale_factor();
+        let bounds = bounds.scale(scale_factor);
+        // Render the SVG at twice the size to get a higher quality result.
+        let params = RenderSvgParams {
+            path,
+            size: bounds
+                .size
+                .map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)),
+        };
+
+        let tile =
+            self.window
+                .sprite_atlas
+                .get_or_insert_with(&params.clone().into(), &mut || {
+                    let bytes = self.svg_renderer.render(&params)?;
+                    Ok((params.size, Cow::Owned(bytes)))
+                })?;
+        let content_mask = self.content_mask().scale(scale_factor);
+
+        let window = &mut *self.window;
+        window.scene_builder.insert(
+            &window.z_index_stack,
+            MonochromeSprite {
+                order: 0,
+                bounds,
+                content_mask,
+                color,
+                tile,
+            },
+        );
+
+        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>,
+        corner_radii: Corners<Pixels>,
+        data: Arc<ImageData>,
+        grayscale: bool,
+    ) -> Result<()> {
+        let scale_factor = self.scale_factor();
+        let bounds = bounds.scale(scale_factor);
+        let params = RenderImageParams { image_id: data.id };
+
+        let tile = self
+            .window
+            .sprite_atlas
+            .get_or_insert_with(&params.clone().into(), &mut || {
+                Ok((data.size(), Cow::Borrowed(data.as_bytes())))
+            })?;
+        let content_mask = self.content_mask().scale(scale_factor);
+        let corner_radii = corner_radii.scale(scale_factor);
+
+        let window = &mut *self.window;
+        window.scene_builder.insert(
+            &window.z_index_stack,
+            PolychromeSprite {
+                order: 0,
+                bounds,
+                content_mask,
+                corner_radii,
+                tile,
+                grayscale,
+            },
+        );
+        Ok(())
+    }
+
+    /// Draw pixels to the display for this window based on the contents of its scene.
+    pub(crate) fn draw(&mut self) {
+        let root_view = self.window.root_view.take().unwrap();
+
+        self.start_frame();
+
+        self.stack(0, |cx| {
+            let available_space = cx.window.content_size.map(Into::into);
+            root_view.draw(available_space, cx);
+        });
+
+        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) {
+        self.text_system().start_frame();
+
+        let window = &mut *self.window;
+
+        // Move the current frame element states to the previous frame.
+        // The new empty element states map will be populated for any element states we
+        // reference during the upcoming frame.
+        mem::swap(
+            &mut window.element_states,
+            &mut window.prev_frame_element_states,
+        );
+        window.element_states.clear();
+
+        // Make the current key matchers the previous, and then clear the current.
+        // An empty key matcher map will be created for every identified element in the
+        // upcoming frame.
+        mem::swap(
+            &mut window.key_matchers,
+            &mut window.prev_frame_key_matchers,
+        );
+        window.key_matchers.clear();
+
+        // Clear mouse event listeners, because elements add new element listeners
+        // when the upcoming frame is painted.
+        window.mouse_listeners.values_mut().for_each(Vec::clear);
+
+        // Clear focus state, because we determine what is focused when the new elements
+        // in the upcoming frame are initialized.
+        window.focus_listeners.clear();
+        window.key_dispatch_stack.clear();
+        window.focus_parents_by_child.clear();
+        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;
+                    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,
+                        click_count: 1,
+                        modifiers: Modifiers::default(),
+                    })
+                }
+                FileDropEvent::Pending { position } => {
+                    self.window.mouse_position = position;
+                    InputEvent::MouseMove(MouseMoveEvent {
+                        position,
+                        pressed_button: Some(MouseButton::Left),
+                        modifiers: Modifiers::default(),
+                    })
+                }
+                FileDropEvent::Submit { position } => {
+                    self.window.mouse_position = position;
+                    InputEvent::MouseUp(MouseUpEvent {
+                        button: MouseButton::Left,
+                        position,
+                        modifiers: Modifiers::default(),
+                        click_count: 1,
+                    })
+                }
+                FileDropEvent::Exited => InputEvent::MouseUp(MouseUpEvent {
+                    button: MouseButton::Left,
+                    position: Point::default(),
+                    modifiers: Modifiers::default(),
+                    click_count: 1,
+                }),
+            },
+            _ => event,
+        };
+
+        if let Some(any_mouse_event) = event.mouse_event() {
+            // Handlers may set this to false by calling `stop_propagation`
+            self.app.propagate_event = true;
+            self.window.default_prevented = false;
+
+            if let Some(mut handlers) = self
+                .window
+                .mouse_listeners
+                .remove(&any_mouse_event.type_id())
+            {
+                // Because handlers may add other handlers, we sort every time.
+                handlers.sort_by(|(a, _), (b, _)| a.cmp(b));
+
+                // Capture phase, events bubble from back to front. Handlers for this phase are used for
+                // special purposes, such as detecting events outside of a given Bounds.
+                for (_, handler) in &handlers {
+                    handler(any_mouse_event, DispatchPhase::Capture, self);
+                    if !self.app.propagate_event {
+                        break;
+                    }
+                }
+
+                // Bubble phase, where most normal handlers do their work.
+                if self.app.propagate_event {
+                    for (_, handler) in handlers.iter().rev() {
+                        handler(any_mouse_event, DispatchPhase::Bubble, self);
+                        if !self.app.propagate_event {
+                            break;
+                        }
+                    }
+                }
+
+                if self.app.propagate_event
+                    && any_mouse_event.downcast_ref::<MouseUpEvent>().is_some()
+                {
+                    self.active_drag = None;
+                }
+
+                // Just in case any handlers added new handlers, which is weird, but possible.
+                handlers.extend(
+                    self.window
+                        .mouse_listeners
+                        .get_mut(&any_mouse_event.type_id())
+                        .into_iter()
+                        .flat_map(|handlers| handlers.drain(..)),
+                );
+                self.window
+                    .mouse_listeners
+                    .insert(any_mouse_event.type_id(), handlers);
+            }
+        } else if let Some(any_key_event) = event.keyboard_event() {
+            let key_dispatch_stack = mem::take(&mut self.window.key_dispatch_stack);
+            let key_event_type = any_key_event.type_id();
+            let mut context_stack = SmallVec::<[&DispatchContext; 16]>::new();
+
+            for (ix, frame) in key_dispatch_stack.iter().enumerate() {
+                match frame {
+                    KeyDispatchStackFrame::Listener {
+                        event_type,
+                        listener,
+                    } => {
+                        if key_event_type == *event_type {
+                            if let Some(action) = listener(
+                                any_key_event,
+                                &context_stack,
+                                DispatchPhase::Capture,
+                                self,
+                            ) {
+                                self.dispatch_action(action, &key_dispatch_stack[..ix]);
+                            }
+                            if !self.app.propagate_event {
+                                break;
+                            }
+                        }
+                    }
+                    KeyDispatchStackFrame::Context(context) => {
+                        context_stack.push(&context);
+                    }
+                }
+            }
+
+            if self.app.propagate_event {
+                for (ix, frame) in key_dispatch_stack.iter().enumerate().rev() {
+                    match frame {
+                        KeyDispatchStackFrame::Listener {
+                            event_type,
+                            listener,
+                        } => {
+                            if key_event_type == *event_type {
+                                if let Some(action) = listener(
+                                    any_key_event,
+                                    &context_stack,
+                                    DispatchPhase::Bubble,
+                                    self,
+                                ) {
+                                    self.dispatch_action(action, &key_dispatch_stack[..ix]);
+                                }
+
+                                if !self.app.propagate_event {
+                                    break;
+                                }
+                            }
+                        }
+                        KeyDispatchStackFrame::Context(_) => {
+                            context_stack.pop();
+                        }
+                    }
+                }
+            }
+
+            drop(context_stack);
+            self.window.key_dispatch_stack = key_dispatch_stack;
+        }
+
+        true
+    }
+
+    /// Attempt to map a keystroke to an action based on the keymap.
+    pub fn match_keystroke(
+        &mut self,
+        element_id: &GlobalElementId,
+        keystroke: &Keystroke,
+        context_stack: &[&DispatchContext],
+    ) -> KeyMatch {
+        let key_match = self
+            .window
+            .key_matchers
+            .get_mut(element_id)
+            .unwrap()
+            .match_keystroke(keystroke, context_stack);
+
+        if key_match.is_some() {
+            for matcher in self.window.key_matchers.values_mut() {
+                matcher.clear_pending();
+            }
+        }
+
+        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 + 'static,
+    ) -> Subscription {
+        let window_id = self.window.handle.id;
+        self.global_observers.insert(
+            TypeId::of::<G>(),
+            Box::new(move |cx| cx.update_window(window_id, |cx| f(cx)).is_ok()),
+        )
+    }
+
+    fn dispatch_action(
+        &mut self,
+        action: Box<dyn Action>,
+        dispatch_stack: &[KeyDispatchStackFrame],
+    ) {
+        let action_type = action.as_any().type_id();
+
+        if let Some(mut global_listeners) = self.app.global_action_listeners.remove(&action_type) {
+            for listener in &global_listeners {
+                listener(action.as_ref(), DispatchPhase::Capture, self);
+                if !self.app.propagate_event {
+                    break;
+                }
+            }
+            global_listeners.extend(
+                self.global_action_listeners
+                    .remove(&action_type)
+                    .unwrap_or_default(),
+            );
+            self.global_action_listeners
+                .insert(action_type, global_listeners);
+        }
+
+        if self.app.propagate_event {
+            for stack_frame in dispatch_stack {
+                if let KeyDispatchStackFrame::Listener {
+                    event_type,
+                    listener,
+                } = stack_frame
+                {
+                    if action_type == *event_type {
+                        listener(action.as_any(), &[], DispatchPhase::Capture, self);
+                        if !self.app.propagate_event {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        if self.app.propagate_event {
+            for stack_frame in dispatch_stack.iter().rev() {
+                if let KeyDispatchStackFrame::Listener {
+                    event_type,
+                    listener,
+                } = stack_frame
+                {
+                    if action_type == *event_type {
+                        listener(action.as_any(), &[], DispatchPhase::Bubble, self);
+                        if !self.app.propagate_event {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        if self.app.propagate_event {
+            if let Some(mut global_listeners) =
+                self.app.global_action_listeners.remove(&action_type)
+            {
+                for listener in global_listeners.iter().rev() {
+                    listener(action.as_ref(), DispatchPhase::Bubble, self);
+                    if !self.app.propagate_event {
+                        break;
+                    }
+                }
+                global_listeners.extend(
+                    self.global_action_listeners
+                        .remove(&action_type)
+                        .unwrap_or_default(),
+                );
+                self.global_action_listeners
+                    .insert(action_type, global_listeners);
+            }
+        }
+    }
+}
+
+impl Context for WindowContext<'_, '_> {
+    type ModelContext<'a, T> = ModelContext<'a, T>;
+    type Result<T> = T;
+
+    fn build_model<T>(
+        &mut self,
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Model<T>
+    where
+        T: 'static + Send,
+    {
+        let slot = self.app.entities.reserve();
+        let model = build_model(&mut ModelContext::mutable(&mut *self.app, slot.downgrade()));
+        self.entities.insert(slot, model)
+    }
+
+    fn update_model<T: 'static, R>(
+        &mut self,
+        model: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
+    ) -> R {
+        let mut entity = self.entities.lease(model);
+        let result = update(
+            &mut *entity,
+            &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;
+
+    fn deref(&self) -> &Self::Target {
+        &self.app
+    }
+}
+
+impl<'a, 'w> std::ops::DerefMut for WindowContext<'a, 'w> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.app
+    }
+}
+
+impl<'a, 'w> Borrow<AppContext> for WindowContext<'a, 'w> {
+    fn borrow(&self) -> &AppContext {
+        &self.app
+    }
+}
+
+impl<'a, 'w> BorrowMut<AppContext> for WindowContext<'a, 'w> {
+    fn borrow_mut(&mut self) -> &mut AppContext {
+        &mut self.app
+    }
+}
+
+pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
+    fn app_mut(&mut self) -> &mut AppContext {
+        self.borrow_mut()
+    }
+
+    fn window(&self) -> &Window {
+        self.borrow()
+    }
+
+    fn window_mut(&mut self) -> &mut Window {
+        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>,
+        f: impl FnOnce(GlobalElementId, &mut Self) -> R,
+    ) -> R {
+        let keymap = self.app_mut().keymap.clone();
+        let window = self.window_mut();
+        window.element_id_stack.push(id.into());
+        let global_id = window.element_id_stack.clone();
+
+        if window.key_matchers.get(&global_id).is_none() {
+            window.key_matchers.insert(
+                global_id.clone(),
+                window
+                    .prev_frame_key_matchers
+                    .remove(&global_id)
+                    .unwrap_or_else(|| KeyMatcher::new(keymap)),
+            );
+        }
+
+        let result = f(global_id, self);
+        let window: &mut Window = self.borrow_mut();
+        window.element_id_stack.pop();
+        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>,
+        f: impl FnOnce(&mut Self) -> R,
+    ) -> R {
+        let mask = mask.intersect(&self.content_mask());
+        self.window_mut().content_mask_stack.push(mask);
+        let result = f(self);
+        self.window_mut().content_mask_stack.pop();
+        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>>,
+        f: impl FnOnce(&mut Self) -> R,
+    ) -> R {
+        let Some(offset) = offset else {
+            return f(self);
+        };
+
+        let offset = self.element_offset() + offset;
+        self.window_mut().element_offset_stack.push(offset);
+        let result = f(self);
+        self.window_mut().element_offset_stack.pop();
+        result
+    }
+
+    /// Obtain the current element offset.
+    fn element_offset(&self) -> Point<Pixels> {
+        self.window()
+            .element_offset_stack
+            .last()
+            .copied()
+            .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: 'static + Send,
+    {
+        self.with_element_id(id, |global_id, cx| {
+            if let Some(any) = cx
+                .window_mut()
+                .element_states
+                .remove(&global_id)
+                .or_else(|| cx.window_mut().prev_frame_element_states.remove(&global_id))
+            {
+                // Using the extra inner option to avoid needing to reallocate a new box.
+                let mut state_box = any
+                    .downcast::<Option<S>>()
+                    .expect("invalid element state type for id");
+                let state = state_box
+                    .take()
+                    .expect("element state is already on the stack");
+                let (result, state) = f(Some(state), cx);
+                state_box.replace(state);
+                cx.window_mut().element_states.insert(global_id, state_box);
+                result
+            } else {
+                let (result, state) = f(None, cx);
+                cx.window_mut()
+                    .element_states
+                    .insert(global_id, Box::new(Some(state)));
+                result
+            }
+        })
+    }
+
+    /// 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: 'static + Send,
+    {
+        if let Some(element_id) = element_id {
+            self.with_element_state(element_id, f)
+        } else {
+            f(None, self).0
+        }
+    }
+
+    /// Obtain the current content mask.
+    fn content_mask(&self) -> ContentMask<Pixels> {
+        self.window()
+            .content_mask_stack
+            .last()
+            .cloned()
+            .unwrap_or_else(|| ContentMask {
+                bounds: Bounds {
+                    origin: Point::default(),
+                    size: self.window().content_size,
+                },
+            })
+    }
+
+    /// 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
+    }
+}
+
+impl Borrow<Window> for WindowContext<'_, '_> {
+    fn borrow(&self) -> &Window {
+        &self.window
+    }
+}
+
+impl BorrowMut<Window> for WindowContext<'_, '_> {
+    fn borrow_mut(&mut self) -> &mut Window {
+        &mut self.window
+    }
+}
+
+impl<T> BorrowWindow for T where T: BorrowMut<AppContext> + BorrowMut<Window> {}
+
+pub struct ViewContext<'a, 'w, V> {
+    window_cx: WindowContext<'a, 'w>,
+    view: WeakView<V>,
+}
+
+impl<V> Borrow<AppContext> for ViewContext<'_, '_, V> {
+    fn borrow(&self) -> &AppContext {
+        &*self.window_cx.app
+    }
+}
+
+impl<V> BorrowMut<AppContext> for ViewContext<'_, '_, V> {
+    fn borrow_mut(&mut self) -> &mut AppContext {
+        &mut *self.window_cx.app
+    }
+}
+
+impl<V> Borrow<Window> for ViewContext<'_, '_, V> {
+    fn borrow(&self) -> &Window {
+        &*self.window_cx.window
+    }
+}
+
+impl<V> BorrowMut<Window> for ViewContext<'_, '_, V> {
+    fn borrow_mut(&mut self) -> &mut Window {
+        &mut *self.window_cx.window
+    }
+}
+
+impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> {
+    pub(crate) fn mutable(
+        app: &'a mut AppContext,
+        window: &'w mut Window,
+        view: WeakView<V>,
+    ) -> Self {
+        Self {
+            window_cx: WindowContext::mutable(app, window),
+            view,
+        }
+    }
+
+    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 {
+        self.window.z_index_stack.push(order);
+        let result = f(self);
+        self.window.z_index_stack.pop();
+        result
+    }
+
+    pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext<V>) + Send + 'static)
+    where
+        V: Any + Send,
+    {
+        let view = self.view().upgrade().unwrap();
+        self.window_cx.on_next_frame(move |cx| view.update(cx, f));
+    }
+
+    pub fn observe<V2, E>(
+        &mut self,
+        entity: &E,
+        mut on_notify: impl FnMut(&mut V, E, &mut ViewContext<'_, '_, V>) + Send + 'static,
+    ) -> Subscription
+    where
+        V2: 'static,
+        V: Any + Send,
+        E: Entity<V2>,
+    {
+        let view = self.view();
+        let entity_id = entity.entity_id();
+        let entity = entity.downgrade();
+        let window_handle = self.window.handle;
+        self.app.observers.insert(
+            entity_id,
+            Box::new(move |cx| {
+                cx.update_window(window_handle.id, |cx| {
+                    if let Some(handle) = E::upgrade_from(&entity) {
+                        view.update(cx, |this, cx| on_notify(this, handle, cx))
+                            .is_ok()
+                    } else {
+                        false
+                    }
+                })
+                .unwrap_or(false)
+            }),
+        )
+    }
+
+    pub fn subscribe<V2, E>(
+        &mut self,
+        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(
+            entity_id,
+            Box::new(move |event, cx| {
+                cx.update_window(window_handle.id, |cx| {
+                    if let Some(handle) = E::upgrade_from(&handle) {
+                        let event = event.downcast_ref().expect("invalid event type");
+                        view.update(cx, |this, cx| on_event(this, handle, event, cx))
+                            .is_ok()
+                    } else {
+                        false
+                    }
+                })
+                .unwrap_or(false)
+            }),
+        )
+    }
+
+    pub fn on_release(
+        &mut self,
+        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.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?")
+                let _ = cx.update_window(window_handle.id, |cx| on_release(this, cx));
+            }),
+        )
+    }
+
+    pub fn observe_release<V2, E>(
+        &mut self,
+        entity: &E,
+        mut on_release: impl FnMut(&mut V, &mut V2, &mut ViewContext<'_, '_, V>) + Send + 'static,
+    ) -> Subscription
+    where
+        V: Any + Send,
+        V2: 'static,
+        E: Entity<V2>,
+    {
+        let view = self.view();
+        let entity_id = entity.entity_id();
+        let window_handle = self.window.handle;
+        self.app.release_listeners.insert(
+            entity_id,
+            Box::new(move |entity, cx| {
+                let entity = entity.downcast_mut().expect("invalid entity type");
+                let _ = cx.update_window(window_handle.id, |cx| {
+                    view.update(cx, |this, cx| on_release(this, entity, cx))
+                });
+            }),
+        )
+    }
+
+    pub fn notify(&mut self) {
+        self.window_cx.notify();
+        self.window_cx.app.push_effect(Effect::Notify {
+            emitter: self.view.model.entity_id,
+        });
+    }
+
+    pub fn on_focus_changed(
+        &mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + Send + 'static,
+    ) {
+        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();
+        }));
+    }
+
+    pub fn with_key_listeners<R>(
+        &mut self,
+        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 {
+                let handle = self.view();
+                let listener = Box::new(
+                    move |event: &dyn Any,
+                          context_stack: &[&DispatchContext],
+                          phase: DispatchPhase,
+                          cx: &mut WindowContext<'_, '_>| {
+                        handle
+                            .update(cx, |view, cx| {
+                                listener(view, event, context_stack, phase, cx)
+                            })
+                            .log_err()
+                            .flatten()
+                    },
+                );
+                self.window
+                    .key_dispatch_stack
+                    .push(KeyDispatchStackFrame::Listener {
+                        event_type,
+                        listener,
+                    });
+            }
+        }
+
+        let result = f(self);
+
+        if !self.window.freeze_key_dispatch_stack {
+            self.window.key_dispatch_stack.truncate(old_stack_len);
+        }
+
+        result
+    }
+
+    pub fn with_key_dispatch_context<R>(
+        &mut self,
+        context: DispatchContext,
+        f: impl FnOnce(&mut Self) -> R,
+    ) -> R {
+        if context.is_empty() {
+            return f(self);
+        }
+
+        if !self.window.freeze_key_dispatch_stack {
+            self.window
+                .key_dispatch_stack
+                .push(KeyDispatchStackFrame::Context(context));
+        }
+
+        let result = f(self);
+
+        if !self.window.freeze_key_dispatch_stack {
+            self.window.key_dispatch_stack.pop();
+        }
+
+        result
+    }
+
+    pub fn with_focus<R>(
+        &mut self,
+        focus_handle: FocusHandle,
+        f: impl FnOnce(&mut Self) -> R,
+    ) -> R {
+        if let Some(parent_focus_id) = self.window.focus_stack.last().copied() {
+            self.window
+                .focus_parents_by_child
+                .insert(focus_handle.id, parent_focus_id);
+        }
+        self.window.focus_stack.push(focus_handle.id);
+
+        if Some(focus_handle.id) == self.window.focus {
+            self.window.freeze_key_dispatch_stack = true;
+        }
+
+        let result = f(self);
+
+        self.window.focus_stack.pop();
+        result
+    }
+
+    pub fn run_on_main<R>(
+        &mut self,
+        view: &mut V,
+        f: impl FnOnce(&mut V, &mut MainThread<ViewContext<'_, '_, V>>) -> R + Send + 'static,
+    ) -> Task<Result<R>>
+    where
+        R: Send + 'static,
+    {
+        if self.executor.is_main_thread() {
+            let cx = unsafe { mem::transmute::<&mut Self, &mut MainThread<Self>>(self) };
+            Task::ready(Ok(f(view, cx)))
+        } else {
+            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(WeakView<V>, AsyncWindowContext) -> Fut + Send + 'static,
+    ) -> Task<R>
+    where
+        R: Send + 'static,
+        Fut: Future<Output = R> + Send + 'static,
+    {
+        let view = self.view();
+        self.window_cx.spawn(move |_, 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,
+    {
+        let mut global = self.app.lease_global::<G>();
+        let result = f(&mut global, self);
+        self.app.end_global_lease(global);
+        result
+    }
+
+    pub fn observe_global<G: 'static>(
+        &mut self,
+        f: impl Fn(&mut V, &mut ViewContext<'_, '_, V>) + Send + 'static,
+    ) -> Subscription {
+        let window_id = self.window.handle.id;
+        let handle = self.view();
+        self.global_observers.insert(
+            TypeId::of::<G>(),
+            Box::new(move |cx| {
+                cx.update_window(window_id, |cx| {
+                    handle.update(cx, |view, cx| f(view, cx)).is_ok()
+                })
+                .unwrap_or(false)
+            }),
+        )
+    }
+
+    pub fn on_mouse_event<Event: 'static>(
+        &mut self,
+        handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + Send + 'static,
+    ) {
+        let handle = self.view().upgrade().unwrap();
+        self.window_cx.on_mouse_event(move |event, phase, cx| {
+            handle.update(cx, |view, cx| {
+                handler(view, event, phase, cx);
+            })
+        });
+    }
+}
+
+impl<'a, 'w, V> ViewContext<'a, 'w, V>
+where
+    V: EventEmitter,
+    V::Event: Any + Send,
+{
+    pub fn emit(&mut self, event: V::Event) {
+        let emitter = self.view.model.entity_id;
+        self.app.push_effect(Effect::Emit {
+            emitter,
+            event: Box::new(event),
+        });
+    }
+}
+
+impl<'a, 'w, V> Context for ViewContext<'a, 'w, V> {
+    type ModelContext<'b, U> = ModelContext<'b, U>;
+    type Result<U> = U;
+
+    fn build_model<T>(
+        &mut self,
+        build_model: impl FnOnce(&mut Self::ModelContext<'_, T>) -> T,
+    ) -> Model<T>
+    where
+        T: 'static + Send,
+    {
+        self.window_cx.build_model(build_model)
+    }
+
+    fn update_model<T: 'static, R>(
+        &mut self,
+        model: &Model<T>,
+        update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R,
+    ) -> R {
+        self.window_cx.update_model(model, update)
+    }
+}
+
+impl<V: 'static> VisualContext for ViewContext<'_, '_, V> {
+    type ViewContext<'a, 'w, V2> = ViewContext<'a, 'w, V2>;
+
+    fn build_view<W: 'static + Send>(
+        &mut self,
+        build_view: impl FnOnce(&mut Self::ViewContext<'_, '_, W>) -> W,
+    ) -> Self::Result<View<W>> {
+        self.window_cx.build_view(build_view)
+    }
+
+    fn update_view<V2: 'static, R>(
+        &mut self,
+        view: &View<V2>,
+        update: impl FnOnce(&mut V2, &mut Self::ViewContext<'_, '_, V2>) -> R,
+    ) -> Self::Result<R> {
+        self.window_cx.update_view(view, update)
+    }
+}
+
+impl<'a, 'w, V> std::ops::Deref for ViewContext<'a, 'w, V> {
+    type Target = WindowContext<'a, 'w>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.window_cx
+    }
+}
+
+impl<'a, 'w, V> std::ops::DerefMut for ViewContext<'a, 'w, V> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.window_cx
+    }
+}
+
+// #[derive(Clone, Copy, Eq, PartialEq, Hash)]
+slotmap::new_key_type! { pub struct WindowId; }
+
+impl WindowId {
+    pub fn as_u64(&self) -> u64 {
+        self.0.as_ffi()
+    }
+}
+
+#[derive(PartialEq, Eq)]
+pub struct WindowHandle<V> {
+    id: WindowId,
+    state_type: PhantomData<V>,
+}
+
+impl<S> Copy for WindowHandle<S> {}
+
+impl<S> Clone for WindowHandle<S> {
+    fn clone(&self) -> Self {
+        WindowHandle {
+            id: self.id,
+            state_type: PhantomData,
+        }
+    }
+}
+
+impl<S> WindowHandle<S> {
+    pub fn new(id: WindowId) -> Self {
+        WindowHandle {
+            id,
+            state_type: PhantomData,
+        }
+    }
+}
+
+impl<S: 'static> Into<AnyWindowHandle> for WindowHandle<S> {
+    fn into(self) -> AnyWindowHandle {
+        AnyWindowHandle {
+            id: self.id,
+            state_type: TypeId::of::<S>(),
+        }
+    }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub struct AnyWindowHandle {
+    pub(crate) id: WindowId,
+    state_type: TypeId,
+}
+
+impl AnyWindowHandle {
+    pub fn window_id(&self) -> WindowId {
+        self.id
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl From<SmallVec<[u32; 16]>> for StackingOrder {
+    fn from(small_vec: SmallVec<[u32; 16]>) -> Self {
+        StackingOrder(small_vec)
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub enum ElementId {
+    View(EntityId),
+    Number(usize),
+    Name(SharedString),
+    FocusHandle(FocusId),
+}
+
+impl From<EntityId> for ElementId {
+    fn from(id: EntityId) -> Self {
+        ElementId::View(id)
+    }
+}
+
+impl From<usize> for ElementId {
+    fn from(id: usize) -> Self {
+        ElementId::Number(id)
+    }
+}
+
+impl From<i32> for ElementId {
+    fn from(id: i32) -> Self {
+        Self::Number(id as usize)
+    }
+}
+
+impl From<SharedString> for ElementId {
+    fn from(name: SharedString) -> Self {
+        ElementId::Name(name)
+    }
+}
+
+impl From<&'static str> for ElementId {
+    fn from(name: &'static str) -> Self {
+        ElementId::Name(name.into())
+    }
+}
+
+impl<'a> From<&'a FocusHandle> for ElementId {
+    fn from(handle: &'a FocusHandle) -> Self {
+        ElementId::FocusHandle(handle.id)
+    }
+}

crates/gpui2_macros/src/derive_component.rs 🔗

@@ -0,0 +1,66 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, parse_quote, DeriveInput};
+
+pub fn derive_component(input: TokenStream) -> TokenStream {
+    let ast = parse_macro_input!(input as DeriveInput);
+    let name = &ast.ident;
+
+    let mut trait_generics = ast.generics.clone();
+    let view_type = if let Some(view_type) = specified_view_type(&ast) {
+        quote! { #view_type }
+    } else {
+        if let Some(first_type_param) = ast.generics.params.iter().find_map(|param| {
+            if let syn::GenericParam::Type(type_param) = param {
+                Some(type_param.ident.clone())
+            } else {
+                None
+            }
+        }) {
+            quote! { #first_type_param }
+        } else {
+            trait_generics.params.push(parse_quote! { V: 'static });
+            quote! { V }
+        }
+    };
+
+    let (impl_generics, _, where_clause) = trait_generics.split_for_impl();
+    let (_, ty_generics, _) = ast.generics.split_for_impl();
+
+    let expanded = quote! {
+        impl #impl_generics gpui2::Component<#view_type> for #name #ty_generics #where_clause {
+            fn render(self) -> gpui2::AnyElement<#view_type> {
+                (move |view_state: &mut #view_type, cx: &mut gpui2::ViewContext<'_, '_, #view_type>| self.render(view_state, cx))
+                    .render()
+            }
+        }
+    };
+
+    TokenStream::from(expanded)
+}
+
+fn specified_view_type(ast: &DeriveInput) -> Option<proc_macro2::Ident> {
+    let component_attr = ast
+        .attrs
+        .iter()
+        .find(|attr| attr.path.is_ident("component"))?;
+
+    if let Ok(syn::Meta::List(meta_list)) = component_attr.parse_meta() {
+        meta_list.nested.iter().find_map(|nested| {
+            if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = nested {
+                if nv.path.is_ident("view_type") {
+                    if let syn::Lit::Str(lit_str) = &nv.lit {
+                        return Some(
+                            lit_str
+                                .parse::<syn::Ident>()
+                                .expect("Failed to parse view_type"),
+                        );
+                    }
+                }
+            }
+            None
+        })
+    } else {
+        None
+    }
+}

crates/gpui2_macros/src/derive_element.rs 🔗

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

crates/gpui2_macros/src/derive_into_element.rs 🔗

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

crates/gpui2_macros/src/gpui2_macros.rs 🔗

@@ -1,20 +1,20 @@
 use proc_macro::TokenStream;
 
-mod derive_element;
-mod derive_into_element;
-mod styleable_helpers;
+mod derive_component;
+mod style_helpers;
+mod test;
 
 #[proc_macro]
-pub fn styleable_helpers(args: TokenStream) -> TokenStream {
-    styleable_helpers::styleable_helpers(args)
+pub fn style_helpers(args: TokenStream) -> TokenStream {
+    style_helpers::style_helpers(args)
 }
 
-#[proc_macro_derive(Element, attributes(element_crate))]
-pub fn derive_element(input: TokenStream) -> TokenStream {
-    derive_element::derive_element(input)
+#[proc_macro_derive(Component, attributes(component))]
+pub fn derive_component(input: TokenStream) -> TokenStream {
+    derive_component::derive_component(input)
 }
 
-#[proc_macro_derive(IntoElement, attributes(element_crate))]
-pub fn derive_into_element(input: TokenStream) -> TokenStream {
-    derive_into_element::derive_into_element(input)
+#[proc_macro_attribute]
+pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
+    test::test(args, function)
 }

crates/gpui2_macros/src/style_helpers.rs 🔗

@@ -0,0 +1,561 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{
+    parse::{Parse, ParseStream, Result},
+    parse_macro_input,
+};
+
+struct StyleableMacroInput;
+
+impl Parse for StyleableMacroInput {
+    fn parse(_input: ParseStream) -> Result<Self> {
+        Ok(StyleableMacroInput)
+    }
+}
+
+pub fn style_helpers(input: TokenStream) -> TokenStream {
+    let _ = parse_macro_input!(input as StyleableMacroInput);
+    let methods = generate_methods();
+    let output = quote! {
+        #(#methods)*
+    };
+
+    output.into()
+}
+
+fn generate_methods() -> Vec<TokenStream2> {
+    let mut methods = Vec::new();
+
+    for (prefix, auto_allowed, fields, prefix_doc_string) in box_prefixes() {
+        methods.push(generate_custom_value_setter(
+            prefix,
+            if auto_allowed {
+                quote! { Length }
+            } else {
+                quote! { DefiniteLength }
+            },
+            &fields,
+            prefix_doc_string,
+        ));
+
+        for (suffix, length_tokens, suffix_doc_string) in box_suffixes() {
+            if suffix != "auto" || auto_allowed {
+                methods.push(generate_predefined_setter(
+                    prefix,
+                    suffix,
+                    &fields,
+                    &length_tokens,
+                    false,
+                    &format!("{prefix_doc_string}\n\n{suffix_doc_string}"),
+                ));
+            }
+
+            if suffix != "auto" {
+                methods.push(generate_predefined_setter(
+                    prefix,
+                    suffix,
+                    &fields,
+                    &length_tokens,
+                    true,
+                    &format!("{prefix_doc_string}\n\n{suffix_doc_string}"),
+                ));
+            }
+        }
+    }
+
+    for (prefix, fields, prefix_doc_string) in corner_prefixes() {
+        methods.push(generate_custom_value_setter(
+            prefix,
+            quote! { AbsoluteLength },
+            &fields,
+            prefix_doc_string,
+        ));
+
+        for (suffix, radius_tokens, suffix_doc_string) in corner_suffixes() {
+            methods.push(generate_predefined_setter(
+                prefix,
+                suffix,
+                &fields,
+                &radius_tokens,
+                false,
+                &format!("{prefix_doc_string}\n\n{suffix_doc_string}"),
+            ));
+        }
+    }
+
+    for (prefix, fields, prefix_doc_string) in border_prefixes() {
+        for (suffix, width_tokens, suffix_doc_string) in border_suffixes() {
+            methods.push(generate_predefined_setter(
+                prefix,
+                suffix,
+                &fields,
+                &width_tokens,
+                false,
+                &format!("{prefix_doc_string}\n\n{suffix_doc_string}"),
+            ));
+        }
+    }
+    methods
+}
+
+fn generate_predefined_setter(
+    name: &'static str,
+    length: &'static str,
+    fields: &Vec<TokenStream2>,
+    length_tokens: &TokenStream2,
+    negate: bool,
+    doc_string: &str,
+) -> TokenStream2 {
+    let (negation_prefix, negation_token) = if negate {
+        ("neg_", quote! { - })
+    } else {
+        ("", quote! {})
+    };
+
+    let method_name = if length.is_empty() {
+        format_ident!("{}{}", negation_prefix, name)
+    } else {
+        format_ident!("{}{}_{}", negation_prefix, name, length)
+    };
+
+    let field_assignments = fields
+        .iter()
+        .map(|field_tokens| {
+            quote! {
+                style.#field_tokens = Some((#negation_token gpui2::#length_tokens).into());
+            }
+        })
+        .collect::<Vec<_>>();
+
+    let method = quote! {
+        #[doc = #doc_string]
+        fn #method_name(mut self) -> Self where Self: std::marker::Sized {
+            let style = self.style();
+            #(#field_assignments)*
+            self
+        }
+    };
+
+    method
+}
+
+fn generate_custom_value_setter(
+    prefix: &'static str,
+    length_type: TokenStream2,
+    fields: &Vec<TokenStream2>,
+    doc_string: &str,
+) -> TokenStream2 {
+    let method_name = format_ident!("{}", prefix);
+
+    let mut iter = fields.into_iter();
+    let last = iter.next_back().unwrap();
+    let field_assignments = iter
+        .map(|field_tokens| {
+            quote! {
+                style.#field_tokens = Some(length.clone().into());
+            }
+        })
+        .chain(std::iter::once(quote! {
+            style.#last = Some(length.into());
+        }))
+        .collect::<Vec<_>>();
+
+    let method = quote! {
+        #[doc = #doc_string]
+        fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui2::#length_type>) -> Self where Self: std::marker::Sized {
+            let style = self.style();
+            #(#field_assignments)*
+            self
+        }
+    };
+
+    method
+}
+
+/// Returns a vec of (Property name, has 'auto' suffix, tokens for accessing the property, documentation)
+fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>, &'static str)> {
+    vec![
+        (
+            "w",
+            true,
+            vec![quote! { size.width }],
+            "Sets the width of the element. [Docs](https://tailwindcss.com/docs/width)",
+        ),
+        ("h", true, vec![quote! { size.height }], "Sets the height of the element. [Docs](https://tailwindcss.com/docs/height)"),
+        (
+            "size",
+            true,
+            vec![quote! {size.width}, quote! {size.height}],
+            "Sets the width and height of the element."
+        ),
+        // TODO: These don't use the same size ramp as the others
+        // see https://tailwindcss.com/docs/max-width
+        (
+            "min_w",
+            true,
+            vec![quote! { min_size.width }],
+            "Sets the minimum width of the element. [Docs](https://tailwindcss.com/docs/min-width)",
+        ),
+        // TODO: These don't use the same size ramp as the others
+        // see https://tailwindcss.com/docs/max-width
+        (
+            "min_h",
+            true,
+            vec![quote! { min_size.height }],
+            "Sets the minimum height of the element. [Docs](https://tailwindcss.com/docs/min-height)",
+        ),
+        // TODO: These don't use the same size ramp as the others
+        // see https://tailwindcss.com/docs/max-width
+        (
+            "max_w",
+            true,
+            vec![quote! { max_size.width }],
+            "Sets the maximum width of the element. [Docs](https://tailwindcss.com/docs/max-width)",
+        ),
+        // TODO: These don't use the same size ramp as the others
+        // see https://tailwindcss.com/docs/max-width
+        (
+            "max_h",
+            true,
+            vec![quote! { max_size.height }],
+            "Sets the maximum height of the element. [Docs](https://tailwindcss.com/docs/max-height)",
+        ),
+        (
+            "m",
+            true,
+            vec![
+                quote! { margin.top },
+                quote! { margin.bottom },
+                quote! { margin.left },
+                quote! { margin.right },
+            ],
+            "Sets the margin of the element. [Docs](https://tailwindcss.com/docs/margin)"
+        ),
+        ("mt", true, vec![quote! { margin.top }], "Sets the top margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"),
+        (
+            "mb",
+            true,
+            vec![quote! { margin.bottom }],
+            "Sets the bottom margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"
+        ),
+        (
+            "my",
+            true,
+            vec![quote! { margin.top }, quote! { margin.bottom }],
+            "Sets the vertical margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-vertical-margin)"
+        ),
+        (
+            "mx",
+            true,
+            vec![quote! { margin.left }, quote! { margin.right }],
+            "Sets the horizontal margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-horizontal-margin)"
+        ),
+        ("ml", true, vec![quote! { margin.left }], "Sets the left margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"),
+        (
+            "mr",
+            true,
+            vec![quote! { margin.right }],
+            "Sets the right margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)"
+        ),
+        (
+            "p",
+            false,
+            vec![
+                quote! { padding.top },
+                quote! { padding.bottom },
+                quote! { padding.left },
+                quote! { padding.right },
+            ],
+            "Sets the padding of the element. [Docs](https://tailwindcss.com/docs/padding)"
+        ),
+        (
+            "pt",
+            false,
+            vec![quote! { padding.top }],
+            "Sets the top padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)"
+        ),
+        (
+            "pb",
+            false,
+            vec![quote! { padding.bottom }],
+            "Sets the bottom padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)"
+        ),
+        (
+            "px",
+            false,
+            vec![quote! { padding.left }, quote! { padding.right }],
+            "Sets the horizontal padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-horizontal-padding)"
+        ),
+        (
+            "py",
+            false,
+            vec![quote! { padding.top }, quote! { padding.bottom }],
+            "Sets the vertical padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-vertical-padding)"
+        ),
+        (
+            "pl",
+            false,
+            vec![quote! { padding.left }],
+            "Sets the left padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)"
+        ),
+        (
+            "pr",
+            false,
+            vec![quote! { padding.right }],
+            "Sets the right padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)"
+        ),
+        (
+            "inset",
+            true,
+            vec![quote! { inset.top }, quote! { inset.right }, quote! { inset.bottom }, quote! { inset.left }],
+            "Sets the top, right, bottom, and left values of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
+        ),
+        (
+            "top",
+            true,
+            vec![quote! { inset.top }],
+            "Sets the top value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
+        ),
+        (
+            "bottom",
+            true,
+            vec![quote! { inset.bottom }],
+            "Sets the bottom value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
+        ),
+        (
+            "left",
+            true,
+            vec![quote! { inset.left }],
+            "Sets the left value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
+        ),
+        (
+            "right",
+            true,
+            vec![quote! { inset.right }],
+            "Sets the right value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
+        ),
+        (
+            "gap",
+            false,
+            vec![quote! { gap.width }, quote! { gap.height }],
+            "Sets the gap between rows and columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap)"
+        ),
+        (
+            "gap_x",
+            false,
+            vec![quote! { gap.width }],
+            "Sets the gap between columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)"
+        ),
+        (
+            "gap_y",
+            false,
+            vec![quote! { gap.height }],
+            "Sets the gap between rows in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)"
+        ),
+    ]
+}
+
+/// Returns a vec of (Suffix size, tokens that correspond to this size, documentation)
+fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
+    vec![
+        ("0", quote! { px(0.) }, "0px"),
+        ("0p5", quote! { rems(0.125) }, "2px (0.125rem)"),
+        ("1", quote! { rems(0.25) }, "4px (0.25rem)"),
+        ("1p5", quote! { rems(0.375) }, "6px (0.375rem)"),
+        ("2", quote! { rems(0.5) }, "8px (0.5rem)"),
+        ("2p5", quote! { rems(0.625) }, "10px (0.625rem)"),
+        ("3", quote! { rems(0.75) }, "12px (0.75rem)"),
+        ("3p5", quote! { rems(0.875) }, "14px (0.875rem)"),
+        ("4", quote! { rems(1.) }, "16px (1rem)"),
+        ("5", quote! { rems(1.25) }, "20px (1.25rem)"),
+        ("6", quote! { rems(1.5) }, "24px (1.5rem)"),
+        ("7", quote! { rems(1.75) }, "28px (1.75rem)"),
+        ("8", quote! { rems(2.0) }, "32px (2rem)"),
+        ("9", quote! { rems(2.25) }, "36px (2.25rem)"),
+        ("10", quote! { rems(2.5) }, "40px (2.5rem)"),
+        ("11", quote! { rems(2.75) }, "44px (2.75rem)"),
+        ("12", quote! { rems(3.) }, "48px (3rem)"),
+        ("16", quote! { rems(4.) }, "64px (4rem)"),
+        ("20", quote! { rems(5.) }, "80px (5rem)"),
+        ("24", quote! { rems(6.) }, "96px (6rem)"),
+        ("32", quote! { rems(8.) }, "128px (8rem)"),
+        ("40", quote! { rems(10.) }, "160px (10rem)"),
+        ("48", quote! { rems(12.) }, "192px (12rem)"),
+        ("56", quote! { rems(14.) }, "224px (14rem)"),
+        ("64", quote! { rems(16.) }, "256px (16rem)"),
+        ("72", quote! { rems(18.) }, "288px (18rem)"),
+        ("80", quote! { rems(20.) }, "320px (20rem)"),
+        ("96", quote! { rems(24.) }, "384px (24rem)"),
+        ("auto", quote! { auto() }, "Auto"),
+        ("px", quote! { px(1.) }, "1px"),
+        ("full", quote! { relative(1.) }, "100%"),
+        ("1_2", quote! { relative(0.5) }, "50% (1/2)"),
+        ("1_3", quote! { relative(1./3.) }, "33% (1/3)"),
+        ("2_3", quote! { relative(2./3.) }, "66% (2/3)"),
+        ("1_4", quote! { relative(0.25) }, "25% (1/4)"),
+        ("2_4", quote! { relative(0.5) }, "50% (2/4)"),
+        ("3_4", quote! { relative(0.75) }, "75% (3/4)"),
+        ("1_5", quote! { relative(0.2) }, "20% (1/5)"),
+        ("2_5", quote! { relative(0.4) }, "40% (2/5)"),
+        ("3_5", quote! { relative(0.6) }, "60% (3/5)"),
+        ("4_5", quote! { relative(0.8) }, "80% (4/5)"),
+        ("1_6", quote! { relative(1./6.) }, "16% (1/6)"),
+        ("5_6", quote! { relative(5./6.) }, "80% (5/6)"),
+        ("1_12", quote! { relative(1./12.) }, "8% (1/12)"),
+    ]
+}
+
+fn corner_prefixes() -> Vec<(&'static str, Vec<TokenStream2>, &'static str)> {
+    vec![
+        (
+            "rounded",
+            vec![
+                quote! { corner_radii.top_left },
+                quote! { corner_radii.top_right },
+                quote! { corner_radii.bottom_right },
+                quote! { corner_radii.bottom_left },
+            ],
+            "Sets the border radius of the element. [Docs](https://tailwindcss.com/docs/border-radius)"
+        ),
+        (
+            "rounded_t",
+            vec![
+                quote! { corner_radii.top_left },
+                quote! { corner_radii.top_right },
+            ],
+            "Sets the border radius of the top side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)"
+        ),
+        (
+            "rounded_b",
+            vec![
+                quote! { corner_radii.bottom_left },
+                quote! { corner_radii.bottom_right },
+            ],
+            "Sets the border radius of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)"
+        ),
+        (
+            "rounded_r",
+            vec![
+                quote! { corner_radii.top_right },
+                quote! { corner_radii.bottom_right },
+            ],
+            "Sets the border radius of the right side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)"
+        ),
+        (
+            "rounded_l",
+            vec![
+                quote! { corner_radii.top_left },
+                quote! { corner_radii.bottom_left },
+            ],
+            "Sets the border radius of the left side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)"
+        ),
+        (
+            "rounded_tl",
+            vec![quote! { corner_radii.top_left }],
+            "Sets the border radius of the top left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)"
+        ),
+        (
+            "rounded_tr",
+            vec![quote! { corner_radii.top_right }],
+            "Sets the border radius of the top right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)"
+        ),
+        (
+            "rounded_bl",
+            vec![quote! { corner_radii.bottom_left }],
+            "Sets the border radius of the bottom left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)"
+        ),
+        (
+            "rounded_br",
+            vec![quote! { corner_radii.bottom_right }],
+            "Sets the border radius of the bottom right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)"
+        ),
+    ]
+}
+
+fn corner_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
+    vec![
+        ("none", quote! { px(0.) }, "0px"),
+        ("sm", quote! { rems(0.125) }, "2px (0.125rem)"),
+        ("md", quote! { rems(0.25) }, "4px (0.25rem)"),
+        ("lg", quote! { rems(0.5) }, "8px (0.5rem)"),
+        ("xl", quote! { rems(0.75) }, "12px (0.75rem)"),
+        ("2xl", quote! { rems(1.) }, "16px (1rem)"),
+        ("3xl", quote! { rems(1.5) }, "24px (1.5rem)"),
+        ("full", quote! {  px(9999.) }, "9999px"),
+    ]
+}
+
+fn border_prefixes() -> Vec<(&'static str, Vec<TokenStream2>, &'static str)> {
+    vec![
+        (
+            "border",
+            vec![
+                quote! { border_widths.top },
+                quote! { border_widths.right },
+                quote! { border_widths.bottom },
+                quote! { border_widths.left },
+            ],
+            "Sets the border width of the element. [Docs](https://tailwindcss.com/docs/border-width)"
+        ),
+        (
+            "border_t",
+            vec![quote! { border_widths.top }],
+            "Sets the border width of the top side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)"
+        ),
+        (
+            "border_b",
+            vec![quote! { border_widths.bottom }],
+            "Sets the border width of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)"
+        ),
+        (
+            "border_r",
+            vec![quote! { border_widths.right }],
+            "Sets the border width of the right side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)"
+        ),
+        (
+            "border_l",
+            vec![quote! { border_widths.left }],
+            "Sets the border width of the left side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)"
+        ),
+        (
+            "border_x",
+            vec![
+                quote! { border_widths.left },
+                quote! { border_widths.right },
+            ],
+            "Sets the border width of the vertical sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)"
+        ),
+        (
+            "border_y",
+            vec![
+                quote! { border_widths.top },
+                quote! { border_widths.bottom },
+            ],
+            "Sets the border width of the horizontal sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)"
+        ),
+    ]
+}
+
+fn border_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
+    vec![
+        ("", quote! { px(1.)}, "1px"),
+        ("0", quote! { px(0.)}, "0px"),
+        ("1", quote! { px(1.) }, "1px"),
+        ("2", quote! { px(2.) }, "2px"),
+        ("3", quote! { px(3.) }, "3px"),
+        ("4", quote! { px(4.) }, "4px"),
+        ("5", quote! { px(5.) }, "5px"),
+        ("6", quote! { px(6.) }, "6px"),
+        ("7", quote! { px(7.) }, "7px"),
+        ("8", quote! { px(8.) }, "8px"),
+        ("9", quote! { px(9.) }, "9px"),
+        ("10", quote! { px(10.) }, "10px"),
+        ("11", quote! { px(11.) }, "11px"),
+        ("12", quote! { px(12.) }, "12px"),
+        ("16", quote! { px(16.) }, "16px"),
+        ("20", quote! { px(20.) }, "20px"),
+        ("24", quote! { px(24.) }, "24px"),
+        ("32", quote! { px(32.) }, "32px"),
+    ]
+}

crates/gpui2_macros/src/styleable_helpers.rs 🔗

@@ -1,408 +0,0 @@
-use proc_macro::TokenStream;
-use proc_macro2::TokenStream as TokenStream2;
-use quote::{format_ident, quote};
-use syn::{
-    parse::{Parse, ParseStream, Result},
-    parse_macro_input,
-};
-
-struct StyleableMacroInput;
-
-impl Parse for StyleableMacroInput {
-    fn parse(_input: ParseStream) -> Result<Self> {
-        Ok(StyleableMacroInput)
-    }
-}
-
-pub fn styleable_helpers(input: TokenStream) -> TokenStream {
-    let _ = parse_macro_input!(input as StyleableMacroInput);
-    let methods = generate_methods();
-    let output = quote! {
-        #(#methods)*
-    };
-
-    output.into()
-}
-
-fn generate_methods() -> Vec<TokenStream2> {
-    let mut methods = Vec::new();
-
-    for (prefix, auto_allowed, fields) in box_prefixes() {
-        methods.push(generate_custom_value_setter(
-            prefix,
-            if auto_allowed {
-                quote! { Length }
-            } else {
-                quote! { DefiniteLength }
-            },
-            &fields,
-        ));
-
-        for (suffix, length_tokens, doc_string) in box_suffixes() {
-            if suffix != "auto" || auto_allowed {
-                methods.push(generate_predefined_setter(
-                    prefix,
-                    suffix,
-                    &fields,
-                    &length_tokens,
-                    false,
-                    doc_string,
-                ));
-            }
-
-            if suffix != "auto" {
-                methods.push(generate_predefined_setter(
-                    prefix,
-                    suffix,
-                    &fields,
-                    &length_tokens,
-                    true,
-                    doc_string,
-                ));
-            }
-        }
-    }
-
-    for (prefix, fields) in corner_prefixes() {
-        methods.push(generate_custom_value_setter(
-            prefix,
-            quote! { AbsoluteLength },
-            &fields,
-        ));
-
-        for (suffix, radius_tokens, doc_string) in corner_suffixes() {
-            methods.push(generate_predefined_setter(
-                prefix,
-                suffix,
-                &fields,
-                &radius_tokens,
-                false,
-                doc_string,
-            ));
-        }
-    }
-
-    for (prefix, fields) in border_prefixes() {
-        for (suffix, width_tokens, doc_string) in border_suffixes() {
-            methods.push(generate_predefined_setter(
-                prefix,
-                suffix,
-                &fields,
-                &width_tokens,
-                false,
-                doc_string,
-            ));
-        }
-    }
-    methods
-}
-
-fn generate_predefined_setter(
-    name: &'static str,
-    length: &'static str,
-    fields: &Vec<TokenStream2>,
-    length_tokens: &TokenStream2,
-    negate: bool,
-    doc_string: &'static str,
-) -> TokenStream2 {
-    let (negation_prefix, negation_token) = if negate {
-        ("neg_", quote! { - })
-    } else {
-        ("", quote! {})
-    };
-
-    let method_name = if length.is_empty() {
-        format_ident!("{}{}", negation_prefix, name)
-    } else {
-        format_ident!("{}{}_{}", negation_prefix, name, length)
-    };
-
-    let field_assignments = fields
-        .iter()
-        .map(|field_tokens| {
-            quote! {
-                style.#field_tokens = Some((#negation_token gpui2::geometry::#length_tokens).into());
-            }
-        })
-        .collect::<Vec<_>>();
-
-    let method = quote! {
-        #[doc = #doc_string]
-        fn #method_name(mut self) -> Self where Self: std::marker::Sized {
-            let mut style = self.declared_style();
-            #(#field_assignments)*
-            self
-        }
-    };
-
-    method
-}
-
-fn generate_custom_value_setter(
-    prefix: &'static str,
-    length_type: TokenStream2,
-    fields: &Vec<TokenStream2>,
-) -> TokenStream2 {
-    let method_name = format_ident!("{}", prefix);
-
-    let mut iter = fields.into_iter();
-    let last = iter.next_back().unwrap();
-    let field_assignments = iter
-        .map(|field_tokens| {
-            quote! {
-                style.#field_tokens = Some(length.clone().into());
-            }
-        })
-        .chain(std::iter::once(quote! {
-            style.#last = Some(length.into());
-        }))
-        .collect::<Vec<_>>();
-
-    let method = quote! {
-        fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui2::geometry::#length_type>) -> Self where Self: std::marker::Sized {
-            let mut style = self.declared_style();
-            #(#field_assignments)*
-            self
-        }
-    };
-
-    method
-}
-
-fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
-    vec![
-        ("w", true, vec![quote! { size.width }]),
-        ("h", true, vec![quote! { size.height }]),
-        (
-            "size",
-            true,
-            vec![quote! {size.width}, quote! {size.height}],
-        ),
-        ("min_w", true, vec![quote! { min_size.width }]),
-        ("min_h", true, vec![quote! { min_size.height }]),
-        ("max_w", true, vec![quote! { max_size.width }]),
-        ("max_h", true, vec![quote! { max_size.height }]),
-        (
-            "m",
-            true,
-            vec![
-                quote! { margin.top },
-                quote! { margin.bottom },
-                quote! { margin.left },
-                quote! { margin.right },
-            ],
-        ),
-        ("mt", true, vec![quote! { margin.top }]),
-        ("mb", true, vec![quote! { margin.bottom }]),
-        (
-            "my",
-            true,
-            vec![quote! { margin.top }, quote! { margin.bottom }],
-        ),
-        (
-            "mx",
-            true,
-            vec![quote! { margin.left }, quote! { margin.right }],
-        ),
-        ("ml", true, vec![quote! { margin.left }]),
-        ("mr", true, vec![quote! { margin.right }]),
-        (
-            "p",
-            false,
-            vec![
-                quote! { padding.top },
-                quote! { padding.bottom },
-                quote! { padding.left },
-                quote! { padding.right },
-            ],
-        ),
-        ("pt", false, vec![quote! { padding.top }]),
-        ("pb", false, vec![quote! { padding.bottom }]),
-        (
-            "px",
-            false,
-            vec![quote! { padding.left }, quote! { padding.right }],
-        ),
-        (
-            "py",
-            false,
-            vec![quote! { padding.top }, quote! { padding.bottom }],
-        ),
-        ("pl", false, vec![quote! { padding.left }]),
-        ("pr", false, vec![quote! { padding.right }]),
-        ("top", true, vec![quote! { inset.top }]),
-        ("bottom", true, vec![quote! { inset.bottom }]),
-        ("left", true, vec![quote! { inset.left }]),
-        ("right", true, vec![quote! { inset.right }]),
-        (
-            "gap",
-            false,
-            vec![quote! { gap.width }, quote! { gap.height }],
-        ),
-        ("gap_x", false, vec![quote! { gap.width }]),
-        ("gap_y", false, vec![quote! { gap.height }]),
-    ]
-}
-
-fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
-    vec![
-        ("0", quote! { pixels(0.) }, "0px"),
-        ("0p5", quote! { rems(0.125) }, "2px (0.125rem)"),
-        ("1", quote! { rems(0.25) }, "4px (0.25rem)"),
-        ("1p5", quote! { rems(0.375) }, "6px (0.375rem)"),
-        ("2", quote! { rems(0.5) }, "8px (0.5rem)"),
-        ("2p5", quote! { rems(0.625) }, "10px (0.625rem)"),
-        ("3", quote! { rems(0.75) }, "12px (0.75rem)"),
-        ("3p5", quote! { rems(0.875) }, "14px (0.875rem)"),
-        ("4", quote! { rems(1.) }, "16px (1rem)"),
-        ("5", quote! { rems(1.25) }, "20px (1.25rem)"),
-        ("6", quote! { rems(1.5) }, "24px (1.5rem)"),
-        ("7", quote! { rems(1.75) }, "28px (1.75rem)"),
-        ("8", quote! { rems(2.0) }, "32px (2rem)"),
-        ("9", quote! { rems(2.25) }, "36px (2.25rem)"),
-        ("10", quote! { rems(2.5) }, "40px (2.5rem)"),
-        ("11", quote! { rems(2.75) }, "44px (2.75rem)"),
-        ("12", quote! { rems(3.) }, "48px (3rem)"),
-        ("16", quote! { rems(4.) }, "64px (4rem)"),
-        ("20", quote! { rems(5.) }, "80px (5rem)"),
-        ("24", quote! { rems(6.) }, "96px (6rem)"),
-        ("32", quote! { rems(8.) }, "128px (8rem)"),
-        ("40", quote! { rems(10.) }, "160px (10rem)"),
-        ("48", quote! { rems(12.) }, "192px (12rem)"),
-        ("56", quote! { rems(14.) }, "224px (14rem)"),
-        ("64", quote! { rems(16.) }, "256px (16rem)"),
-        ("72", quote! { rems(18.) }, "288px (18rem)"),
-        ("80", quote! { rems(20.) }, "320px (20rem)"),
-        ("96", quote! { rems(24.) }, "384px (24rem)"),
-        ("auto", quote! { auto() }, "Auto"),
-        ("px", quote! { pixels(1.) }, "1px"),
-        ("full", quote! { relative(1.) }, "100%"),
-        ("1_2", quote! { relative(0.5) }, "50% (1/2)"),
-        ("1_3", quote! { relative(1./3.) }, "33% (1/3)"),
-        ("2_3", quote! { relative(2./3.) }, "66% (2/3)"),
-        ("1_4", quote! { relative(0.25) }, "25% (1/4)"),
-        ("2_4", quote! { relative(0.5) }, "50% (2/4)"),
-        ("3_4", quote! { relative(0.75) }, "75% (3/4)"),
-        ("1_5", quote! { relative(0.2) }, "20% (1/5)"),
-        ("2_5", quote! { relative(0.4) }, "40% (2/5)"),
-        ("3_5", quote! { relative(0.6) }, "60% (3/5)"),
-        ("4_5", quote! { relative(0.8) }, "80% (4/5)"),
-        ("1_6", quote! { relative(1./6.) }, "16% (1/6)"),
-        ("5_6", quote! { relative(5./6.) }, "80% (5/6)"),
-        ("1_12", quote! { relative(1./12.) }, "8% (1/12)"),
-    ]
-}
-
-fn corner_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
-    vec![
-        (
-            "rounded",
-            vec![
-                quote! { corner_radii.top_left },
-                quote! { corner_radii.top_right },
-                quote! { corner_radii.bottom_right },
-                quote! { corner_radii.bottom_left },
-            ],
-        ),
-        (
-            "rounded_t",
-            vec![
-                quote! { corner_radii.top_left },
-                quote! { corner_radii.top_right },
-            ],
-        ),
-        (
-            "rounded_b",
-            vec![
-                quote! { corner_radii.bottom_left },
-                quote! { corner_radii.bottom_right },
-            ],
-        ),
-        (
-            "rounded_r",
-            vec![
-                quote! { corner_radii.top_right },
-                quote! { corner_radii.bottom_right },
-            ],
-        ),
-        (
-            "rounded_l",
-            vec![
-                quote! { corner_radii.top_left },
-                quote! { corner_radii.bottom_left },
-            ],
-        ),
-        ("rounded_tl", vec![quote! { corner_radii.top_left }]),
-        ("rounded_tr", vec![quote! { corner_radii.top_right }]),
-        ("rounded_bl", vec![quote! { corner_radii.bottom_left }]),
-        ("rounded_br", vec![quote! { corner_radii.bottom_right }]),
-    ]
-}
-
-fn corner_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
-    vec![
-        ("none", quote! { pixels(0.) }, "0px"),
-        ("sm", quote! { rems(0.125) }, "2px (0.125rem)"),
-        ("md", quote! { rems(0.25) }, "4px (0.25rem)"),
-        ("lg", quote! { rems(0.5) }, "8px (0.5rem)"),
-        ("xl", quote! { rems(0.75) }, "12px (0.75rem)"),
-        ("2xl", quote! { rems(1.) }, "16px (1rem)"),
-        ("3xl", quote! { rems(1.5) }, "24px (1.5rem)"),
-        ("full", quote! {  pixels(9999.) }, "9999px"),
-    ]
-}
-
-fn border_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
-    vec![
-        (
-            "border",
-            vec![
-                quote! { border_widths.top },
-                quote! { border_widths.right },
-                quote! { border_widths.bottom },
-                quote! { border_widths.left },
-            ],
-        ),
-        ("border_t", vec![quote! { border_widths.top }]),
-        ("border_b", vec![quote! { border_widths.bottom }]),
-        ("border_r", vec![quote! { border_widths.right }]),
-        ("border_l", vec![quote! { border_widths.left }]),
-        (
-            "border_x",
-            vec![
-                quote! { border_widths.left },
-                quote! { border_widths.right },
-            ],
-        ),
-        (
-            "border_y",
-            vec![
-                quote! { border_widths.top },
-                quote! { border_widths.bottom },
-            ],
-        ),
-    ]
-}
-
-fn border_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
-    vec![
-        ("", quote! { pixels(1.)}, "1px"),
-        ("0", quote! { pixels(0.)}, "0px"),
-        ("1", quote! { pixels(1.) }, "1px"),
-        ("2", quote! { pixels(2.) }, "2px"),
-        ("3", quote! { pixels(3.) }, "3px"),
-        ("4", quote! { pixels(4.) }, "4px"),
-        ("5", quote! { pixels(5.) }, "5px"),
-        ("6", quote! { pixels(6.) }, "6px"),
-        ("7", quote! { pixels(7.) }, "7px"),
-        ("8", quote! { pixels(8.) }, "8px"),
-        ("9", quote! { pixels(9.) }, "9px"),
-        ("10", quote! { pixels(10.) }, "10px"),
-        ("11", quote! { pixels(11.) }, "11px"),
-        ("12", quote! { pixels(12.) }, "12px"),
-        ("16", quote! { pixels(16.) }, "16px"),
-        ("20", quote! { pixels(20.) }, "20px"),
-        ("24", quote! { pixels(24.) }, "24px"),
-        ("32", quote! { pixels(32.) }, "32px"),
-    ]
-}

crates/gpui2_macros/src/test.rs 🔗

@@ -0,0 +1,239 @@
+use proc_macro::TokenStream;
+use proc_macro2::Ident;
+use quote::{format_ident, quote};
+use std::mem;
+use syn::{
+    parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, FnArg, ItemFn, Lit, Meta,
+    NestedMeta, Type,
+};
+
+pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
+    let args = syn::parse_macro_input!(args as AttributeArgs);
+    let mut max_retries = 0;
+    let mut num_iterations = 1;
+    let mut on_failure_fn_name = quote!(None);
+
+    for arg in args {
+        match arg {
+            NestedMeta::Meta(Meta::NameValue(meta)) => {
+                let key_name = meta.path.get_ident().map(|i| i.to_string());
+                let result = (|| {
+                    match key_name.as_deref() {
+                        Some("retries") => max_retries = parse_int(&meta.lit)?,
+                        Some("iterations") => num_iterations = parse_int(&meta.lit)?,
+                        Some("on_failure") => {
+                            if let Lit::Str(name) = meta.lit {
+                                let mut path = syn::Path {
+                                    leading_colon: None,
+                                    segments: Default::default(),
+                                };
+                                for part in name.value().split("::") {
+                                    path.segments.push(Ident::new(part, name.span()).into());
+                                }
+                                on_failure_fn_name = quote!(Some(#path));
+                            } else {
+                                return Err(TokenStream::from(
+                                    syn::Error::new(
+                                        meta.lit.span(),
+                                        "on_failure argument must be a string",
+                                    )
+                                    .into_compile_error(),
+                                ));
+                            }
+                        }
+                        _ => {
+                            return Err(TokenStream::from(
+                                syn::Error::new(meta.path.span(), "invalid argument")
+                                    .into_compile_error(),
+                            ))
+                        }
+                    }
+                    Ok(())
+                })();
+
+                if let Err(tokens) = result {
+                    return tokens;
+                }
+            }
+            other => {
+                return TokenStream::from(
+                    syn::Error::new_spanned(other, "invalid argument").into_compile_error(),
+                )
+            }
+        }
+    }
+
+    let mut inner_fn = parse_macro_input!(function as ItemFn);
+    if max_retries > 0 && num_iterations > 1 {
+        return TokenStream::from(
+            syn::Error::new_spanned(inner_fn, "retries and randomized iterations can't be mixed")
+                .into_compile_error(),
+        );
+    }
+    let inner_fn_attributes = mem::take(&mut inner_fn.attrs);
+    let inner_fn_name = format_ident!("_{}", inner_fn.sig.ident);
+    let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone());
+
+    let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() {
+        // Pass to the test function the number of app contexts that it needs,
+        // based on its parameter list.
+        let mut cx_vars = proc_macro2::TokenStream::new();
+        let mut cx_teardowns = proc_macro2::TokenStream::new();
+        let mut inner_fn_args = proc_macro2::TokenStream::new();
+        for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() {
+            if let FnArg::Typed(arg) = arg {
+                if let Type::Path(ty) = &*arg.ty {
+                    let last_segment = ty.path.segments.last();
+                    match last_segment.map(|s| s.ident.to_string()).as_deref() {
+                        Some("StdRng") => {
+                            inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
+                            continue;
+                        }
+                        Some("Executor") => {
+                            inner_fn_args.extend(quote!(gpui2::Executor::new(
+                                std::sync::Arc::new(dispatcher.clone())
+                            ),));
+                            continue;
+                        }
+                        _ => {}
+                    }
+                } else if let Type::Reference(ty) = &*arg.ty {
+                    if let Type::Path(ty) = &*ty.elem {
+                        let last_segment = ty.path.segments.last();
+                        if let Some("TestAppContext") =
+                            last_segment.map(|s| s.ident.to_string()).as_deref()
+                        {
+                            let cx_varname = format_ident!("cx_{}", ix);
+                            cx_vars.extend(quote!(
+                                let mut #cx_varname = gpui2::TestAppContext::new(
+                                    dispatcher.clone()
+                                );
+                            ));
+                            cx_teardowns.extend(quote!(
+                                #cx_varname.quit();
+                                dispatcher.run_until_parked();
+                            ));
+                            inner_fn_args.extend(quote!(&mut #cx_varname,));
+                            continue;
+                        }
+                    }
+                }
+            }
+
+            return TokenStream::from(
+                syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
+            );
+        }
+
+        parse_quote! {
+            #[test]
+            fn #outer_fn_name() {
+                #inner_fn
+
+                gpui2::run_test(
+                    #num_iterations as u64,
+                    #max_retries,
+                    &mut |dispatcher, _seed| {
+                        let executor = gpui2::Executor::new(std::sync::Arc::new(dispatcher.clone()));
+                        #cx_vars
+                        executor.block(#inner_fn_name(#inner_fn_args));
+                        #cx_teardowns
+                    },
+                    #on_failure_fn_name,
+                    stringify!(#outer_fn_name).to_string(),
+                );
+            }
+        }
+    } else {
+        // Pass to the test function the number of app contexts that it needs,
+        // based on its parameter list.
+        let mut cx_vars = proc_macro2::TokenStream::new();
+        let mut cx_teardowns = proc_macro2::TokenStream::new();
+        let mut inner_fn_args = proc_macro2::TokenStream::new();
+        for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() {
+            if let FnArg::Typed(arg) = arg {
+                if let Type::Path(ty) = &*arg.ty {
+                    let last_segment = ty.path.segments.last();
+
+                    if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() {
+                        inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
+                        continue;
+                    }
+                } else if let Type::Reference(ty) = &*arg.ty {
+                    if let Type::Path(ty) = &*ty.elem {
+                        let last_segment = ty.path.segments.last();
+                        match last_segment.map(|s| s.ident.to_string()).as_deref() {
+                            Some("AppContext") => {
+                                let cx_varname = format_ident!("cx_{}", ix);
+                                let cx_varname_lock = format_ident!("cx_{}_lock", ix);
+                                cx_vars.extend(quote!(
+                                    let mut #cx_varname = gpui2::TestAppContext::new(
+                                       dispatcher.clone()
+                                    );
+                                    let mut #cx_varname_lock = #cx_varname.app.lock();
+                                ));
+                                inner_fn_args.extend(quote!(&mut #cx_varname_lock,));
+                                cx_teardowns.extend(quote!(
+                                    #cx_varname_lock.quit();
+                                    dispatcher.run_until_parked();
+                                ));
+                                continue;
+                            }
+                            Some("TestAppContext") => {
+                                let cx_varname = format_ident!("cx_{}", ix);
+                                cx_vars.extend(quote!(
+                                    let mut #cx_varname = gpui2::TestAppContext::new(
+                                        dispatcher.clone()
+                                    );
+                                ));
+                                cx_teardowns.extend(quote!(
+                                    #cx_varname.quit();
+                                    dispatcher.run_until_parked();
+                                ));
+                                inner_fn_args.extend(quote!(&mut #cx_varname,));
+                                continue;
+                            }
+                            _ => {}
+                        }
+                    }
+                }
+            }
+
+            return TokenStream::from(
+                syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
+            );
+        }
+
+        parse_quote! {
+            #[test]
+            fn #outer_fn_name() {
+                #inner_fn
+
+                gpui2::run_test(
+                    #num_iterations as u64,
+                    #max_retries,
+                    &mut |dispatcher, _seed| {
+                        #cx_vars
+                        #inner_fn_name(#inner_fn_args);
+                        #cx_teardowns
+                    },
+                    #on_failure_fn_name,
+                    stringify!(#outer_fn_name).to_string(),
+                );
+            }
+        }
+    };
+    outer_fn.attrs.extend(inner_fn_attributes);
+
+    TokenStream::from(quote!(#outer_fn))
+}
+
+fn parse_int(literal: &Lit) -> Result<usize, TokenStream> {
+    let result = if let Lit::Int(int) = &literal {
+        int.base10_parse()
+    } else {
+        Err(syn::Error::new(literal.span(), "must be an integer"))
+    };
+
+    result.map_err(|err| TokenStream::from(err.into_compile_error()))
+}

crates/install_cli2/Cargo.toml 🔗

@@ -0,0 +1,18 @@
+[package]
+name = "install_cli2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/install_cli2.rs"
+
+[features]
+test-support = []
+
+[dependencies]
+smol.workspace = true
+anyhow.workspace = true
+log.workspace = true
+gpui2 = { path = "../gpui2" }
+util = { path = "../util" }

crates/install_cli2/src/install_cli2.rs 🔗

@@ -0,0 +1,57 @@
+use anyhow::{anyhow, Result};
+use gpui2::AsyncAppContext;
+use std::path::Path;
+use util::ResultExt;
+
+// todo!()
+// actions!(cli, [Install]);
+
+pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
+    let cli_path = cx
+        .run_on_main(|cx| cx.path_for_auxiliary_executable("cli"))?
+        .await?;
+    let link_path = Path::new("/usr/local/bin/zed");
+    let bin_dir_path = link_path.parent().unwrap();
+
+    // Don't re-create symlink if it points to the same CLI binary.
+    if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
+        return Ok(());
+    }
+
+    // If the symlink is not there or is outdated, first try replacing it
+    // without escalating.
+    smol::fs::remove_file(link_path).await.log_err();
+    if smol::fs::unix::symlink(&cli_path, link_path)
+        .await
+        .log_err()
+        .is_some()
+    {
+        return Ok(());
+    }
+
+    // The symlink could not be created, so use osascript with admin privileges
+    // to create it.
+    let status = smol::process::Command::new("/usr/bin/osascript")
+        .args([
+            "-e",
+            &format!(
+                "do shell script \" \
+                    mkdir -p \'{}\' && \
+                    ln -sf \'{}\' \'{}\' \
+                \" with administrator privileges",
+                bin_dir_path.to_string_lossy(),
+                cli_path.to_string_lossy(),
+                link_path.to_string_lossy(),
+            ),
+        ])
+        .stdout(smol::process::Stdio::inherit())
+        .stderr(smol::process::Stdio::inherit())
+        .output()
+        .await?
+        .status;
+    if status.success() {
+        Ok(())
+    } else {
+        Err(anyhow!("error running osascript"))
+    }
+}

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

@@ -0,0 +1,85 @@
+[package]
+name = "language2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/language2.rs"
+doctest = false
+
+[features]
+test-support = [
+    "rand",
+    "client2/test-support",
+    "collections/test-support",
+    "lsp2/test-support",
+    "text/test-support",
+    "tree-sitter-rust",
+    "tree-sitter-typescript",
+    "settings2/test-support",
+    "util/test-support",
+]
+
+[dependencies]
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+fuzzy2 = { path = "../fuzzy2" }
+git = { path = "../git" }
+gpui2 = { path = "../gpui2" }
+lsp2 = { path = "../lsp2" }
+rpc2 = { path = "../rpc2" }
+settings2 = { path = "../settings2" }
+sum_tree = { path = "../sum_tree" }
+text = { path = "../text" }
+theme2 = { path = "../theme2" }
+util = { path = "../util" }
+
+anyhow.workspace = true
+async-broadcast = "0.4"
+async-trait.workspace = true
+futures.workspace = true
+globset.workspace = true
+lazy_static.workspace = true
+log.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+regex.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+similar = "1.3"
+smallvec.workspace = true
+smol.workspace = true
+tree-sitter.workspace = true
+unicase = "2.6"
+
+rand = { workspace = true, optional = true }
+tree-sitter-rust = { workspace = true, optional = true }
+tree-sitter-typescript = { workspace = true, optional = true }
+
+[dev-dependencies]
+client2 = { path = "../client2", features = ["test-support"] }
+collections = { path = "../collections", features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+lsp2 = { path = "../lsp2", features = ["test-support"] }
+text = { path = "../text", features = ["test-support"] }
+settings2 = { path = "../settings2", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+ctor.workspace = true
+env_logger.workspace = true
+indoc.workspace = true
+rand.workspace = true
+unindent.workspace = true
+
+tree-sitter-embedded-template.workspace = true
+tree-sitter-html.workspace = true
+tree-sitter-json.workspace = true
+tree-sitter-markdown.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-python.workspace = true
+tree-sitter-typescript.workspace = true
+tree-sitter-ruby.workspace = true
+tree-sitter-elixir.workspace = true
+tree-sitter-heex.workspace = true

crates/language2/build.rs 🔗

@@ -0,0 +1,5 @@
+fn main() {
+    if let Ok(bundled) = std::env::var("ZED_BUNDLE") {
+        println!("cargo:rustc-env=ZED_BUNDLE={}", bundled);
+    }
+}

crates/language2/src/buffer.rs 🔗

@@ -0,0 +1,3098 @@
+pub use crate::{
+    diagnostic_set::DiagnosticSet,
+    highlight_map::{HighlightId, HighlightMap},
+    proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
+};
+use crate::{
+    diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
+    language_settings::{language_settings, LanguageSettings},
+    outline::OutlineItem,
+    syntax_map::{
+        SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
+        SyntaxSnapshot, ToTreeSitterPoint,
+    },
+    CodeLabel, LanguageScope, Outline,
+};
+use anyhow::{anyhow, Result};
+pub use clock::ReplicaId;
+use futures::FutureExt as _;
+use gpui2::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task};
+use lsp2::LanguageServerId;
+use parking_lot::Mutex;
+use similar::{ChangeTag, TextDiff};
+use smallvec::SmallVec;
+use smol::future::yield_now;
+use std::{
+    any::Any,
+    cmp::{self, Ordering},
+    collections::BTreeMap,
+    ffi::OsStr,
+    future::Future,
+    iter::{self, Iterator, Peekable},
+    mem,
+    ops::{Deref, Range},
+    path::{Path, PathBuf},
+    str,
+    sync::Arc,
+    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
+    vec,
+};
+use sum_tree::TreeMap;
+use text::operation_queue::OperationQueue;
+pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
+use theme2::SyntaxTheme;
+#[cfg(any(test, feature = "test-support"))]
+use util::RandomCharIter;
+use util::{RangeExt, TryFutureExt as _};
+
+#[cfg(any(test, feature = "test-support"))]
+pub use {tree_sitter_rust, tree_sitter_typescript};
+
+pub use lsp2::DiagnosticSeverity;
+
+pub struct Buffer {
+    text: TextBuffer,
+    diff_base: Option<String>,
+    git_diff: git::diff::BufferDiff,
+    file: Option<Arc<dyn File>>,
+    saved_version: clock::Global,
+    saved_version_fingerprint: RopeFingerprint,
+    saved_mtime: SystemTime,
+    transaction_depth: usize,
+    was_dirty_before_starting_transaction: Option<bool>,
+    language: Option<Arc<Language>>,
+    autoindent_requests: Vec<Arc<AutoindentRequest>>,
+    pending_autoindent: Option<Task<()>>,
+    sync_parse_timeout: Duration,
+    syntax_map: Mutex<SyntaxMap>,
+    parsing_in_background: bool,
+    parse_count: usize,
+    diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
+    remote_selections: TreeMap<ReplicaId, SelectionSet>,
+    selections_update_count: usize,
+    diagnostics_update_count: usize,
+    diagnostics_timestamp: clock::Lamport,
+    file_update_count: usize,
+    git_diff_update_count: usize,
+    completion_triggers: Vec<String>,
+    completion_triggers_timestamp: clock::Lamport,
+    deferred_ops: OperationQueue<Operation>,
+}
+
+pub struct BufferSnapshot {
+    text: text::BufferSnapshot,
+    pub git_diff: git::diff::BufferDiff,
+    pub(crate) syntax: SyntaxSnapshot,
+    file: Option<Arc<dyn File>>,
+    diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
+    diagnostics_update_count: usize,
+    file_update_count: usize,
+    git_diff_update_count: usize,
+    remote_selections: TreeMap<ReplicaId, SelectionSet>,
+    selections_update_count: usize,
+    language: Option<Arc<Language>>,
+    parse_count: usize,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
+pub struct IndentSize {
+    pub len: u32,
+    pub kind: IndentKind,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
+pub enum IndentKind {
+    #[default]
+    Space,
+    Tab,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
+pub enum CursorShape {
+    #[default]
+    Bar,
+    Block,
+    Underscore,
+    Hollow,
+}
+
+#[derive(Clone, Debug)]
+struct SelectionSet {
+    line_mode: bool,
+    cursor_shape: CursorShape,
+    selections: Arc<[Selection<Anchor>]>,
+    lamport_timestamp: clock::Lamport,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct GroupId {
+    source: Arc<str>,
+    id: usize,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Diagnostic {
+    pub source: Option<String>,
+    pub code: Option<String>,
+    pub severity: DiagnosticSeverity,
+    pub message: String,
+    pub group_id: usize,
+    pub is_valid: bool,
+    pub is_primary: bool,
+    pub is_disk_based: bool,
+    pub is_unnecessary: bool,
+}
+
+#[derive(Clone, Debug)]
+pub struct Completion {
+    pub old_range: Range<Anchor>,
+    pub new_text: String,
+    pub label: CodeLabel,
+    pub server_id: LanguageServerId,
+    pub lsp_completion: lsp2::CompletionItem,
+}
+
+#[derive(Clone, Debug)]
+pub struct CodeAction {
+    pub server_id: LanguageServerId,
+    pub range: Range<Anchor>,
+    pub lsp_action: lsp2::CodeAction,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Operation {
+    Buffer(text::Operation),
+
+    UpdateDiagnostics {
+        server_id: LanguageServerId,
+        diagnostics: Arc<[DiagnosticEntry<Anchor>]>,
+        lamport_timestamp: clock::Lamport,
+    },
+
+    UpdateSelections {
+        selections: Arc<[Selection<Anchor>]>,
+        lamport_timestamp: clock::Lamport,
+        line_mode: bool,
+        cursor_shape: CursorShape,
+    },
+
+    UpdateCompletionTriggers {
+        triggers: Vec<String>,
+        lamport_timestamp: clock::Lamport,
+    },
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Event {
+    Operation(Operation),
+    Edited,
+    DirtyChanged,
+    Saved,
+    FileHandleChanged,
+    Reloaded,
+    DiffBaseChanged,
+    LanguageChanged,
+    Reparsed,
+    DiagnosticsUpdated,
+    Closed,
+}
+
+pub trait File: Send + Sync {
+    fn as_local(&self) -> Option<&dyn LocalFile>;
+
+    fn is_local(&self) -> bool {
+        self.as_local().is_some()
+    }
+
+    fn mtime(&self) -> SystemTime;
+
+    /// Returns the path of this file relative to the worktree's root directory.
+    fn path(&self) -> &Arc<Path>;
+
+    /// Returns the path of this file relative to the worktree's parent directory (this means it
+    /// includes the name of the worktree's root folder).
+    fn full_path(&self, cx: &AppContext) -> PathBuf;
+
+    /// Returns the last component of this handle's absolute path. If this handle refers to the root
+    /// of its worktree, then this method will return the name of the worktree itself.
+    fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr;
+
+    /// Returns the id of the worktree to which this file belongs.
+    ///
+    /// This is needed for looking up project-specific settings.
+    fn worktree_id(&self) -> usize;
+
+    fn is_deleted(&self) -> bool;
+
+    fn as_any(&self) -> &dyn Any;
+
+    fn to_proto(&self) -> rpc2::proto::File;
+}
+
+pub trait LocalFile: File {
+    /// Returns the absolute path of this file.
+    fn abs_path(&self, cx: &AppContext) -> PathBuf;
+
+    fn load(&self, cx: &AppContext) -> Task<Result<String>>;
+
+    fn buffer_reloaded(
+        &self,
+        buffer_id: u64,
+        version: &clock::Global,
+        fingerprint: RopeFingerprint,
+        line_ending: LineEnding,
+        mtime: SystemTime,
+        cx: &mut AppContext,
+    );
+}
+
+#[derive(Clone, Debug)]
+pub enum AutoindentMode {
+    /// Indent each line of inserted text.
+    EachLine,
+    /// Apply the same indentation adjustment to all of the lines
+    /// in a given insertion.
+    Block {
+        /// The original indentation level of the first line of each
+        /// insertion, if it has been copied.
+        original_indent_columns: Vec<u32>,
+    },
+}
+
+#[derive(Clone)]
+struct AutoindentRequest {
+    before_edit: BufferSnapshot,
+    entries: Vec<AutoindentRequestEntry>,
+    is_block_mode: bool,
+}
+
+#[derive(Clone)]
+struct AutoindentRequestEntry {
+    /// A range of the buffer whose indentation should be adjusted.
+    range: Range<Anchor>,
+    /// Whether or not these lines should be considered brand new, for the
+    /// purpose of auto-indent. When text is not new, its indentation will
+    /// only be adjusted if the suggested indentation level has *changed*
+    /// since the edit was made.
+    first_line_is_new: bool,
+    indent_size: IndentSize,
+    original_indent_column: Option<u32>,
+}
+
+#[derive(Debug)]
+struct IndentSuggestion {
+    basis_row: u32,
+    delta: Ordering,
+    within_error: bool,
+}
+
+struct BufferChunkHighlights<'a> {
+    captures: SyntaxMapCaptures<'a>,
+    next_capture: Option<SyntaxMapCapture<'a>>,
+    stack: Vec<(usize, HighlightId)>,
+    highlight_maps: Vec<HighlightMap>,
+}
+
+pub struct BufferChunks<'a> {
+    range: Range<usize>,
+    chunks: text::Chunks<'a>,
+    diagnostic_endpoints: Peekable<vec::IntoIter<DiagnosticEndpoint>>,
+    error_depth: usize,
+    warning_depth: usize,
+    information_depth: usize,
+    hint_depth: usize,
+    unnecessary_depth: usize,
+    highlights: Option<BufferChunkHighlights<'a>>,
+}
+
+#[derive(Clone, Copy, Debug, Default)]
+pub struct Chunk<'a> {
+    pub text: &'a str,
+    pub syntax_highlight_id: Option<HighlightId>,
+    pub highlight_style: Option<HighlightStyle>,
+    pub diagnostic_severity: Option<DiagnosticSeverity>,
+    pub is_unnecessary: bool,
+    pub is_tab: bool,
+}
+
+pub struct Diff {
+    pub(crate) base_version: clock::Global,
+    line_ending: LineEnding,
+    edits: Vec<(Range<usize>, Arc<str>)>,
+}
+
+#[derive(Clone, Copy)]
+pub(crate) struct DiagnosticEndpoint {
+    offset: usize,
+    is_start: bool,
+    severity: DiagnosticSeverity,
+    is_unnecessary: bool,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
+pub enum CharKind {
+    Whitespace,
+    Punctuation,
+    Word,
+}
+
+impl CharKind {
+    pub fn coerce_punctuation(self, treat_punctuation_as_word: bool) -> Self {
+        if treat_punctuation_as_word && self == CharKind::Punctuation {
+            CharKind::Word
+        } else {
+            self
+        }
+    }
+}
+
+impl Buffer {
+    pub fn new<T: Into<String>>(replica_id: ReplicaId, id: u64, base_text: T) -> Self {
+        Self::build(
+            TextBuffer::new(replica_id, id, base_text.into()),
+            None,
+            None,
+        )
+    }
+
+    pub fn remote(remote_id: u64, replica_id: ReplicaId, base_text: String) -> Self {
+        Self::build(
+            TextBuffer::new(replica_id, remote_id, base_text),
+            None,
+            None,
+        )
+    }
+
+    pub fn from_proto(
+        replica_id: ReplicaId,
+        message: proto::BufferState,
+        file: Option<Arc<dyn File>>,
+    ) -> Result<Self> {
+        let buffer = TextBuffer::new(replica_id, message.id, message.base_text);
+        let mut this = Self::build(
+            buffer,
+            message.diff_base.map(|text| text.into_boxed_str().into()),
+            file,
+        );
+        this.text.set_line_ending(proto::deserialize_line_ending(
+            rpc2::proto::LineEnding::from_i32(message.line_ending)
+                .ok_or_else(|| anyhow!("missing line_ending"))?,
+        ));
+        this.saved_version = proto::deserialize_version(&message.saved_version);
+        this.saved_version_fingerprint =
+            proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
+        this.saved_mtime = message
+            .saved_mtime
+            .ok_or_else(|| anyhow!("invalid saved_mtime"))?
+            .into();
+        Ok(this)
+    }
+
+    pub fn to_proto(&self) -> proto::BufferState {
+        proto::BufferState {
+            id: self.remote_id(),
+            file: self.file.as_ref().map(|f| f.to_proto()),
+            base_text: self.base_text().to_string(),
+            diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
+            line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
+            saved_version: proto::serialize_version(&self.saved_version),
+            saved_version_fingerprint: proto::serialize_fingerprint(self.saved_version_fingerprint),
+            saved_mtime: Some(self.saved_mtime.into()),
+        }
+    }
+
+    pub fn serialize_ops(
+        &self,
+        since: Option<clock::Global>,
+        cx: &AppContext,
+    ) -> Task<Vec<proto::Operation>> {
+        let mut operations = Vec::new();
+        operations.extend(self.deferred_ops.iter().map(proto::serialize_operation));
+
+        operations.extend(self.remote_selections.iter().map(|(_, set)| {
+            proto::serialize_operation(&Operation::UpdateSelections {
+                selections: set.selections.clone(),
+                lamport_timestamp: set.lamport_timestamp,
+                line_mode: set.line_mode,
+                cursor_shape: set.cursor_shape,
+            })
+        }));
+
+        for (server_id, diagnostics) in &self.diagnostics {
+            operations.push(proto::serialize_operation(&Operation::UpdateDiagnostics {
+                lamport_timestamp: self.diagnostics_timestamp,
+                server_id: *server_id,
+                diagnostics: diagnostics.iter().cloned().collect(),
+            }));
+        }
+
+        operations.push(proto::serialize_operation(
+            &Operation::UpdateCompletionTriggers {
+                triggers: self.completion_triggers.clone(),
+                lamport_timestamp: self.completion_triggers_timestamp,
+            },
+        ));
+
+        let text_operations = self.text.operations().clone();
+        cx.spawn(|_| async move {
+            let since = since.unwrap_or_default();
+            operations.extend(
+                text_operations
+                    .iter()
+                    .filter(|(_, op)| !since.observed(op.timestamp()))
+                    .map(|(_, op)| proto::serialize_operation(&Operation::Buffer(op.clone()))),
+            );
+            operations.sort_unstable_by_key(proto::lamport_timestamp_for_operation);
+            operations
+        })
+    }
+
+    pub fn with_language(mut self, language: Arc<Language>, cx: &mut ModelContext<Self>) -> Self {
+        self.set_language(Some(language), cx);
+        self
+    }
+
+    pub fn build(
+        buffer: TextBuffer,
+        diff_base: Option<String>,
+        file: Option<Arc<dyn File>>,
+    ) -> Self {
+        let saved_mtime = if let Some(file) = file.as_ref() {
+            file.mtime()
+        } else {
+            UNIX_EPOCH
+        };
+
+        Self {
+            saved_mtime,
+            saved_version: buffer.version(),
+            saved_version_fingerprint: buffer.as_rope().fingerprint(),
+            transaction_depth: 0,
+            was_dirty_before_starting_transaction: None,
+            text: buffer,
+            diff_base,
+            git_diff: git::diff::BufferDiff::new(),
+            file,
+            syntax_map: Mutex::new(SyntaxMap::new()),
+            parsing_in_background: false,
+            parse_count: 0,
+            sync_parse_timeout: Duration::from_millis(1),
+            autoindent_requests: Default::default(),
+            pending_autoindent: Default::default(),
+            language: None,
+            remote_selections: Default::default(),
+            selections_update_count: 0,
+            diagnostics: Default::default(),
+            diagnostics_update_count: 0,
+            diagnostics_timestamp: Default::default(),
+            file_update_count: 0,
+            git_diff_update_count: 0,
+            completion_triggers: Default::default(),
+            completion_triggers_timestamp: Default::default(),
+            deferred_ops: OperationQueue::new(),
+        }
+    }
+
+    pub fn snapshot(&self) -> BufferSnapshot {
+        let text = self.text.snapshot();
+        let mut syntax_map = self.syntax_map.lock();
+        syntax_map.interpolate(&text);
+        let syntax = syntax_map.snapshot();
+
+        BufferSnapshot {
+            text,
+            syntax,
+            git_diff: self.git_diff.clone(),
+            file: self.file.clone(),
+            remote_selections: self.remote_selections.clone(),
+            diagnostics: self.diagnostics.clone(),
+            diagnostics_update_count: self.diagnostics_update_count,
+            file_update_count: self.file_update_count,
+            git_diff_update_count: self.git_diff_update_count,
+            language: self.language.clone(),
+            parse_count: self.parse_count,
+            selections_update_count: self.selections_update_count,
+        }
+    }
+
+    pub fn as_text_snapshot(&self) -> &text::BufferSnapshot {
+        &self.text
+    }
+
+    pub fn text_snapshot(&self) -> text::BufferSnapshot {
+        self.text.snapshot()
+    }
+
+    pub fn file(&self) -> Option<&Arc<dyn File>> {
+        self.file.as_ref()
+    }
+
+    pub fn saved_version(&self) -> &clock::Global {
+        &self.saved_version
+    }
+
+    pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
+        self.saved_version_fingerprint
+    }
+
+    pub fn saved_mtime(&self) -> SystemTime {
+        self.saved_mtime
+    }
+
+    pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut ModelContext<Self>) {
+        self.syntax_map.lock().clear();
+        self.language = language;
+        self.reparse(cx);
+        cx.emit(Event::LanguageChanged);
+    }
+
+    pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) {
+        self.syntax_map
+            .lock()
+            .set_language_registry(language_registry);
+    }
+
+    pub fn did_save(
+        &mut self,
+        version: clock::Global,
+        fingerprint: RopeFingerprint,
+        mtime: SystemTime,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.saved_version = version;
+        self.saved_version_fingerprint = fingerprint;
+        self.saved_mtime = mtime;
+        cx.emit(Event::Saved);
+        cx.notify();
+    }
+
+    pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
+        cx.spawn(|this, mut cx| async move {
+            if let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
+                let file = this.file.as_ref()?.as_local()?;
+                Some((file.mtime(), file.load(cx)))
+            })? {
+                let new_text = new_text.await?;
+                let diff = this
+                    .update(&mut cx, |this, cx| this.diff(new_text, cx))?
+                    .await;
+                this.update(&mut cx, |this, cx| {
+                    if this.version() == diff.base_version {
+                        this.finalize_last_transaction();
+                        this.apply_diff(diff, cx);
+                        if let Some(transaction) = this.finalize_last_transaction().cloned() {
+                            this.did_reload(
+                                this.version(),
+                                this.as_rope().fingerprint(),
+                                this.line_ending(),
+                                new_mtime,
+                                cx,
+                            );
+                            return Some(transaction);
+                        }
+                    }
+                    None
+                })
+            } else {
+                Ok(None)
+            }
+        })
+    }
+
+    pub fn did_reload(
+        &mut self,
+        version: clock::Global,
+        fingerprint: RopeFingerprint,
+        line_ending: LineEnding,
+        mtime: SystemTime,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.saved_version = version;
+        self.saved_version_fingerprint = fingerprint;
+        self.text.set_line_ending(line_ending);
+        self.saved_mtime = mtime;
+        if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
+            file.buffer_reloaded(
+                self.remote_id(),
+                &self.saved_version,
+                self.saved_version_fingerprint,
+                self.line_ending(),
+                self.saved_mtime,
+                cx,
+            );
+        }
+        cx.emit(Event::Reloaded);
+        cx.notify();
+    }
+
+    pub fn file_updated(
+        &mut self,
+        new_file: Arc<dyn File>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<()> {
+        let mut file_changed = false;
+        let mut task = Task::ready(());
+
+        if let Some(old_file) = self.file.as_ref() {
+            if new_file.path() != old_file.path() {
+                file_changed = true;
+            }
+
+            if new_file.is_deleted() {
+                if !old_file.is_deleted() {
+                    file_changed = true;
+                    if !self.is_dirty() {
+                        cx.emit(Event::DirtyChanged);
+                    }
+                }
+            } else {
+                let new_mtime = new_file.mtime();
+                if new_mtime != old_file.mtime() {
+                    file_changed = true;
+
+                    if !self.is_dirty() {
+                        let reload = self.reload(cx).log_err().map(drop);
+                        task = cx.executor().spawn(reload);
+                    }
+                }
+            }
+        } else {
+            file_changed = true;
+        };
+
+        self.file = Some(new_file);
+        if file_changed {
+            self.file_update_count += 1;
+            cx.emit(Event::FileHandleChanged);
+            cx.notify();
+        }
+        task
+    }
+
+    pub fn diff_base(&self) -> Option<&str> {
+        self.diff_base.as_deref()
+    }
+
+    pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
+        self.diff_base = diff_base;
+        self.git_diff_recalc(cx);
+        cx.emit(Event::DiffBaseChanged);
+    }
+
+    pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
+        let diff_base = self.diff_base.clone()?; // TODO: Make this an Arc
+        let snapshot = self.snapshot();
+
+        let mut diff = self.git_diff.clone();
+        let diff = cx.executor().spawn(async move {
+            diff.update(&diff_base, &snapshot).await;
+            diff
+        });
+
+        Some(cx.spawn(|this, mut cx| async move {
+            let buffer_diff = diff.await;
+            this.update(&mut cx, |this, _| {
+                this.git_diff = buffer_diff;
+                this.git_diff_update_count += 1;
+            })
+            .ok();
+        }))
+    }
+
+    pub fn close(&mut self, cx: &mut ModelContext<Self>) {
+        cx.emit(Event::Closed);
+    }
+
+    pub fn language(&self) -> Option<&Arc<Language>> {
+        self.language.as_ref()
+    }
+
+    pub fn language_at<D: ToOffset>(&self, position: D) -> Option<Arc<Language>> {
+        let offset = position.to_offset(self);
+        self.syntax_map
+            .lock()
+            .layers_for_range(offset..offset, &self.text)
+            .last()
+            .map(|info| info.language.clone())
+            .or_else(|| self.language.clone())
+    }
+
+    pub fn parse_count(&self) -> usize {
+        self.parse_count
+    }
+
+    pub fn selections_update_count(&self) -> usize {
+        self.selections_update_count
+    }
+
+    pub fn diagnostics_update_count(&self) -> usize {
+        self.diagnostics_update_count
+    }
+
+    pub fn file_update_count(&self) -> usize {
+        self.file_update_count
+    }
+
+    pub fn git_diff_update_count(&self) -> usize {
+        self.git_diff_update_count
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn is_parsing(&self) -> bool {
+        self.parsing_in_background
+    }
+
+    pub fn contains_unknown_injections(&self) -> bool {
+        self.syntax_map.lock().contains_unknown_injections()
+    }
+
+    #[cfg(test)]
+    pub fn set_sync_parse_timeout(&mut self, timeout: Duration) {
+        self.sync_parse_timeout = timeout;
+    }
+
+    /// Called after an edit to synchronize the buffer's main parse tree with
+    /// the buffer's new underlying state.
+    ///
+    /// Locks the syntax map and interpolates the edits since the last reparse
+    /// into the foreground syntax tree.
+    ///
+    /// Then takes a stable snapshot of the syntax map before unlocking it.
+    /// The snapshot with the interpolated edits is sent to a background thread,
+    /// where we ask Tree-sitter to perform an incremental parse.
+    ///
+    /// Meanwhile, in the foreground, we block the main thread for up to 1ms
+    /// waiting on the parse to complete. As soon as it completes, we proceed
+    /// synchronously, unless a 1ms timeout elapses.
+    ///
+    /// If we time out waiting on the parse, we spawn a second task waiting
+    /// until the parse does complete and return with the interpolated tree still
+    /// in the foreground. When the background parse completes, call back into
+    /// the main thread and assign the foreground parse state.
+    ///
+    /// If the buffer or grammar changed since the start of the background parse,
+    /// initiate an additional reparse recursively. To avoid concurrent parses
+    /// for the same buffer, we only initiate a new parse if we are not already
+    /// parsing in the background.
+    pub fn reparse(&mut self, cx: &mut ModelContext<Self>) {
+        if self.parsing_in_background {
+            return;
+        }
+        let language = if let Some(language) = self.language.clone() {
+            language
+        } else {
+            return;
+        };
+
+        let text = self.text_snapshot();
+        let parsed_version = self.version();
+
+        let mut syntax_map = self.syntax_map.lock();
+        syntax_map.interpolate(&text);
+        let language_registry = syntax_map.language_registry();
+        let mut syntax_snapshot = syntax_map.snapshot();
+        drop(syntax_map);
+
+        let parse_task = cx.executor().spawn({
+            let language = language.clone();
+            let language_registry = language_registry.clone();
+            async move {
+                syntax_snapshot.reparse(&text, language_registry, language);
+                syntax_snapshot
+            }
+        });
+
+        match cx
+            .executor()
+            .block_with_timeout(self.sync_parse_timeout, parse_task)
+        {
+            Ok(new_syntax_snapshot) => {
+                self.did_finish_parsing(new_syntax_snapshot, cx);
+                return;
+            }
+            Err(parse_task) => {
+                self.parsing_in_background = true;
+                cx.spawn(move |this, mut cx| async move {
+                    let new_syntax_map = parse_task.await;
+                    this.update(&mut cx, move |this, cx| {
+                        let grammar_changed =
+                            this.language.as_ref().map_or(true, |current_language| {
+                                !Arc::ptr_eq(&language, current_language)
+                            });
+                        let language_registry_changed = new_syntax_map
+                            .contains_unknown_injections()
+                            && language_registry.map_or(false, |registry| {
+                                registry.version() != new_syntax_map.language_registry_version()
+                            });
+                        let parse_again = language_registry_changed
+                            || grammar_changed
+                            || this.version.changed_since(&parsed_version);
+                        this.did_finish_parsing(new_syntax_map, cx);
+                        this.parsing_in_background = false;
+                        if parse_again {
+                            this.reparse(cx);
+                        }
+                    })
+                    .ok();
+                })
+                .detach();
+            }
+        }
+    }
+
+    fn did_finish_parsing(&mut self, syntax_snapshot: SyntaxSnapshot, cx: &mut ModelContext<Self>) {
+        self.parse_count += 1;
+        self.syntax_map.lock().did_parse(syntax_snapshot);
+        self.request_autoindent(cx);
+        cx.emit(Event::Reparsed);
+        cx.notify();
+    }
+
+    pub fn update_diagnostics(
+        &mut self,
+        server_id: LanguageServerId,
+        diagnostics: DiagnosticSet,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let lamport_timestamp = self.text.lamport_clock.tick();
+        let op = Operation::UpdateDiagnostics {
+            server_id,
+            diagnostics: diagnostics.iter().cloned().collect(),
+            lamport_timestamp,
+        };
+        self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx);
+        self.send_operation(op, cx);
+    }
+
+    fn request_autoindent(&mut self, cx: &mut ModelContext<Self>) {
+        if let Some(indent_sizes) = self.compute_autoindents() {
+            let indent_sizes = cx.executor().spawn(indent_sizes);
+            match cx
+                .executor()
+                .block_with_timeout(Duration::from_micros(500), indent_sizes)
+            {
+                Ok(indent_sizes) => self.apply_autoindents(indent_sizes, cx),
+                Err(indent_sizes) => {
+                    self.pending_autoindent = Some(cx.spawn(|this, mut cx| async move {
+                        let indent_sizes = indent_sizes.await;
+                        this.update(&mut cx, |this, cx| {
+                            this.apply_autoindents(indent_sizes, cx);
+                        })
+                        .ok();
+                    }));
+                }
+            }
+        } else {
+            self.autoindent_requests.clear();
+        }
+    }
+
+    fn compute_autoindents(&self) -> Option<impl Future<Output = BTreeMap<u32, IndentSize>>> {
+        let max_rows_between_yields = 100;
+        let snapshot = self.snapshot();
+        if snapshot.syntax.is_empty() || self.autoindent_requests.is_empty() {
+            return None;
+        }
+
+        let autoindent_requests = self.autoindent_requests.clone();
+        Some(async move {
+            let mut indent_sizes = BTreeMap::new();
+            for request in autoindent_requests {
+                // Resolve each edited range to its row in the current buffer and in the
+                // buffer before this batch of edits.
+                let mut row_ranges = Vec::new();
+                let mut old_to_new_rows = BTreeMap::new();
+                let mut language_indent_sizes_by_new_row = Vec::new();
+                for entry in &request.entries {
+                    let position = entry.range.start;
+                    let new_row = position.to_point(&snapshot).row;
+                    let new_end_row = entry.range.end.to_point(&snapshot).row + 1;
+                    language_indent_sizes_by_new_row.push((new_row, entry.indent_size));
+
+                    if !entry.first_line_is_new {
+                        let old_row = position.to_point(&request.before_edit).row;
+                        old_to_new_rows.insert(old_row, new_row);
+                    }
+                    row_ranges.push((new_row..new_end_row, entry.original_indent_column));
+                }
+
+                // Build a map containing the suggested indentation for each of the edited lines
+                // with respect to the state of the buffer before these edits. This map is keyed
+                // by the rows for these lines in the current state of the buffer.
+                let mut old_suggestions = BTreeMap::<u32, (IndentSize, bool)>::default();
+                let old_edited_ranges =
+                    contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields);
+                let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
+                let mut language_indent_size = IndentSize::default();
+                for old_edited_range in old_edited_ranges {
+                    let suggestions = request
+                        .before_edit
+                        .suggest_autoindents(old_edited_range.clone())
+                        .into_iter()
+                        .flatten();
+                    for (old_row, suggestion) in old_edited_range.zip(suggestions) {
+                        if let Some(suggestion) = suggestion {
+                            let new_row = *old_to_new_rows.get(&old_row).unwrap();
+
+                            // Find the indent size based on the language for this row.
+                            while let Some((row, size)) = language_indent_sizes.peek() {
+                                if *row > new_row {
+                                    break;
+                                }
+                                language_indent_size = *size;
+                                language_indent_sizes.next();
+                            }
+
+                            let suggested_indent = old_to_new_rows
+                                .get(&suggestion.basis_row)
+                                .and_then(|from_row| {
+                                    Some(old_suggestions.get(from_row).copied()?.0)
+                                })
+                                .unwrap_or_else(|| {
+                                    request
+                                        .before_edit
+                                        .indent_size_for_line(suggestion.basis_row)
+                                })
+                                .with_delta(suggestion.delta, language_indent_size);
+                            old_suggestions
+                                .insert(new_row, (suggested_indent, suggestion.within_error));
+                        }
+                    }
+                    yield_now().await;
+                }
+
+                // In block mode, only compute indentation suggestions for the first line
+                // of each insertion. Otherwise, compute suggestions for every inserted line.
+                let new_edited_row_ranges = contiguous_ranges(
+                    row_ranges.iter().flat_map(|(range, _)| {
+                        if request.is_block_mode {
+                            range.start..range.start + 1
+                        } else {
+                            range.clone()
+                        }
+                    }),
+                    max_rows_between_yields,
+                );
+
+                // Compute new suggestions for each line, but only include them in the result
+                // if they differ from the old suggestion for that line.
+                let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
+                let mut language_indent_size = IndentSize::default();
+                for new_edited_row_range in new_edited_row_ranges {
+                    let suggestions = snapshot
+                        .suggest_autoindents(new_edited_row_range.clone())
+                        .into_iter()
+                        .flatten();
+                    for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
+                        if let Some(suggestion) = suggestion {
+                            // Find the indent size based on the language for this row.
+                            while let Some((row, size)) = language_indent_sizes.peek() {
+                                if *row > new_row {
+                                    break;
+                                }
+                                language_indent_size = *size;
+                                language_indent_sizes.next();
+                            }
+
+                            let suggested_indent = indent_sizes
+                                .get(&suggestion.basis_row)
+                                .copied()
+                                .unwrap_or_else(|| {
+                                    snapshot.indent_size_for_line(suggestion.basis_row)
+                                })
+                                .with_delta(suggestion.delta, language_indent_size);
+                            if old_suggestions.get(&new_row).map_or(
+                                true,
+                                |(old_indentation, was_within_error)| {
+                                    suggested_indent != *old_indentation
+                                        && (!suggestion.within_error || *was_within_error)
+                                },
+                            ) {
+                                indent_sizes.insert(new_row, suggested_indent);
+                            }
+                        }
+                    }
+                    yield_now().await;
+                }
+
+                // For each block of inserted text, adjust the indentation of the remaining
+                // lines of the block by the same amount as the first line was adjusted.
+                if request.is_block_mode {
+                    for (row_range, original_indent_column) in
+                        row_ranges
+                            .into_iter()
+                            .filter_map(|(range, original_indent_column)| {
+                                if range.len() > 1 {
+                                    Some((range, original_indent_column?))
+                                } else {
+                                    None
+                                }
+                            })
+                    {
+                        let new_indent = indent_sizes
+                            .get(&row_range.start)
+                            .copied()
+                            .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start));
+                        let delta = new_indent.len as i64 - original_indent_column as i64;
+                        if delta != 0 {
+                            for row in row_range.skip(1) {
+                                indent_sizes.entry(row).or_insert_with(|| {
+                                    let mut size = snapshot.indent_size_for_line(row);
+                                    if size.kind == new_indent.kind {
+                                        match delta.cmp(&0) {
+                                            Ordering::Greater => size.len += delta as u32,
+                                            Ordering::Less => {
+                                                size.len = size.len.saturating_sub(-delta as u32)
+                                            }
+                                            Ordering::Equal => {}
+                                        }
+                                    }
+                                    size
+                                });
+                            }
+                        }
+                    }
+                }
+            }
+
+            indent_sizes
+        })
+    }
+
+    fn apply_autoindents(
+        &mut self,
+        indent_sizes: BTreeMap<u32, IndentSize>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.autoindent_requests.clear();
+
+        let edits: Vec<_> = indent_sizes
+            .into_iter()
+            .filter_map(|(row, indent_size)| {
+                let current_size = indent_size_for_line(self, row);
+                Self::edit_for_indent_size_adjustment(row, current_size, indent_size)
+            })
+            .collect();
+
+        self.edit(edits, None, cx);
+    }
+
+    // Create a minimal edit that will cause the the given row to be indented
+    // with the given size. After applying this edit, the length of the line
+    // will always be at least `new_size.len`.
+    pub fn edit_for_indent_size_adjustment(
+        row: u32,
+        current_size: IndentSize,
+        new_size: IndentSize,
+    ) -> Option<(Range<Point>, String)> {
+        if new_size.kind != current_size.kind {
+            Some((
+                Point::new(row, 0)..Point::new(row, current_size.len),
+                iter::repeat(new_size.char())
+                    .take(new_size.len as usize)
+                    .collect::<String>(),
+            ))
+        } else {
+            match new_size.len.cmp(&current_size.len) {
+                Ordering::Greater => {
+                    let point = Point::new(row, 0);
+                    Some((
+                        point..point,
+                        iter::repeat(new_size.char())
+                            .take((new_size.len - current_size.len) as usize)
+                            .collect::<String>(),
+                    ))
+                }
+
+                Ordering::Less => Some((
+                    Point::new(row, 0)..Point::new(row, current_size.len - new_size.len),
+                    String::new(),
+                )),
+
+                Ordering::Equal => None,
+            }
+        }
+    }
+
+    pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
+        let old_text = self.as_rope().clone();
+        let base_version = self.version();
+        cx.executor().spawn(async move {
+            let old_text = old_text.to_string();
+            let line_ending = LineEnding::detect(&new_text);
+            LineEnding::normalize(&mut new_text);
+            let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
+            let mut edits = Vec::new();
+            let mut offset = 0;
+            let empty: Arc<str> = "".into();
+            for change in diff.iter_all_changes() {
+                let value = change.value();
+                let end_offset = offset + value.len();
+                match change.tag() {
+                    ChangeTag::Equal => {
+                        offset = end_offset;
+                    }
+                    ChangeTag::Delete => {
+                        edits.push((offset..end_offset, empty.clone()));
+                        offset = end_offset;
+                    }
+                    ChangeTag::Insert => {
+                        edits.push((offset..offset, value.into()));
+                    }
+                }
+            }
+            Diff {
+                base_version,
+                line_ending,
+                edits,
+            }
+        })
+    }
+
+    /// Spawn a background task that searches the buffer for any whitespace
+    /// at the ends of a lines, and returns a `Diff` that removes that whitespace.
+    pub fn remove_trailing_whitespace(&self, cx: &AppContext) -> Task<Diff> {
+        let old_text = self.as_rope().clone();
+        let line_ending = self.line_ending();
+        let base_version = self.version();
+        cx.executor().spawn(async move {
+            let ranges = trailing_whitespace_ranges(&old_text);
+            let empty = Arc::<str>::from("");
+            Diff {
+                base_version,
+                line_ending,
+                edits: ranges
+                    .into_iter()
+                    .map(|range| (range, empty.clone()))
+                    .collect(),
+            }
+        })
+    }
+
+    /// Ensure that the buffer ends with a single newline character, and
+    /// no other whitespace.
+    pub fn ensure_final_newline(&mut self, cx: &mut ModelContext<Self>) {
+        let len = self.len();
+        let mut offset = len;
+        for chunk in self.as_rope().reversed_chunks_in_range(0..len) {
+            let non_whitespace_len = chunk
+                .trim_end_matches(|c: char| c.is_ascii_whitespace())
+                .len();
+            offset -= chunk.len();
+            offset += non_whitespace_len;
+            if non_whitespace_len != 0 {
+                if offset == len - 1 && chunk.get(non_whitespace_len..) == Some("\n") {
+                    return;
+                }
+                break;
+            }
+        }
+        self.edit([(offset..len, "\n")], None, cx);
+    }
+
+    /// Apply a diff to the buffer. If the buffer has changed since the given diff was
+    /// calculated, then adjust the diff to account for those changes, and discard any
+    /// parts of the diff that conflict with those changes.
+    pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
+        // Check for any edits to the buffer that have occurred since this diff
+        // was computed.
+        let snapshot = self.snapshot();
+        let mut edits_since = snapshot.edits_since::<usize>(&diff.base_version).peekable();
+        let mut delta = 0;
+        let adjusted_edits = diff.edits.into_iter().filter_map(|(range, new_text)| {
+            while let Some(edit_since) = edits_since.peek() {
+                // If the edit occurs after a diff hunk, then it does not
+                // affect that hunk.
+                if edit_since.old.start > range.end {
+                    break;
+                }
+                // If the edit precedes the diff hunk, then adjust the hunk
+                // to reflect the edit.
+                else if edit_since.old.end < range.start {
+                    delta += edit_since.new_len() as i64 - edit_since.old_len() as i64;
+                    edits_since.next();
+                }
+                // If the edit intersects a diff hunk, then discard that hunk.
+                else {
+                    return None;
+                }
+            }
+
+            let start = (range.start as i64 + delta) as usize;
+            let end = (range.end as i64 + delta) as usize;
+            Some((start..end, new_text))
+        });
+
+        self.start_transaction();
+        self.text.set_line_ending(diff.line_ending);
+        self.edit(adjusted_edits, None, cx);
+        self.end_transaction(cx)
+    }
+
+    pub fn is_dirty(&self) -> bool {
+        self.saved_version_fingerprint != self.as_rope().fingerprint()
+            || self.file.as_ref().map_or(false, |file| file.is_deleted())
+    }
+
+    pub fn has_conflict(&self) -> bool {
+        self.saved_version_fingerprint != self.as_rope().fingerprint()
+            && self
+                .file
+                .as_ref()
+                .map_or(false, |file| file.mtime() > self.saved_mtime)
+    }
+
+    pub fn subscribe(&mut self) -> Subscription {
+        self.text.subscribe()
+    }
+
+    pub fn start_transaction(&mut self) -> Option<TransactionId> {
+        self.start_transaction_at(Instant::now())
+    }
+
+    pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> {
+        self.transaction_depth += 1;
+        if self.was_dirty_before_starting_transaction.is_none() {
+            self.was_dirty_before_starting_transaction = Some(self.is_dirty());
+        }
+        self.text.start_transaction_at(now)
+    }
+
+    pub fn end_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
+        self.end_transaction_at(Instant::now(), cx)
+    }
+
+    pub fn end_transaction_at(
+        &mut self,
+        now: Instant,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<TransactionId> {
+        assert!(self.transaction_depth > 0);
+        self.transaction_depth -= 1;
+        let was_dirty = if self.transaction_depth == 0 {
+            self.was_dirty_before_starting_transaction.take().unwrap()
+        } else {
+            false
+        };
+        if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) {
+            self.did_edit(&start_version, was_dirty, cx);
+            Some(transaction_id)
+        } else {
+            None
+        }
+    }
+
+    pub fn push_transaction(&mut self, transaction: Transaction, now: Instant) {
+        self.text.push_transaction(transaction, now);
+    }
+
+    pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> {
+        self.text.finalize_last_transaction()
+    }
+
+    pub fn group_until_transaction(&mut self, transaction_id: TransactionId) {
+        self.text.group_until_transaction(transaction_id);
+    }
+
+    pub fn forget_transaction(&mut self, transaction_id: TransactionId) {
+        self.text.forget_transaction(transaction_id);
+    }
+
+    pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) {
+        self.text.merge_transactions(transaction, destination);
+    }
+
+    pub fn wait_for_edits(
+        &mut self,
+        edit_ids: impl IntoIterator<Item = clock::Lamport>,
+    ) -> impl Future<Output = Result<()>> {
+        self.text.wait_for_edits(edit_ids)
+    }
+
+    pub fn wait_for_anchors(
+        &mut self,
+        anchors: impl IntoIterator<Item = Anchor>,
+    ) -> impl 'static + Future<Output = Result<()>> {
+        self.text.wait_for_anchors(anchors)
+    }
+
+    pub fn wait_for_version(&mut self, version: clock::Global) -> impl Future<Output = Result<()>> {
+        self.text.wait_for_version(version)
+    }
+
+    pub fn give_up_waiting(&mut self) {
+        self.text.give_up_waiting();
+    }
+
+    pub fn set_active_selections(
+        &mut self,
+        selections: Arc<[Selection<Anchor>]>,
+        line_mode: bool,
+        cursor_shape: CursorShape,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let lamport_timestamp = self.text.lamport_clock.tick();
+        self.remote_selections.insert(
+            self.text.replica_id(),
+            SelectionSet {
+                selections: selections.clone(),
+                lamport_timestamp,
+                line_mode,
+                cursor_shape,
+            },
+        );
+        self.send_operation(
+            Operation::UpdateSelections {
+                selections,
+                line_mode,
+                lamport_timestamp,
+                cursor_shape,
+            },
+            cx,
+        );
+    }
+
+    pub fn remove_active_selections(&mut self, cx: &mut ModelContext<Self>) {
+        if self
+            .remote_selections
+            .get(&self.text.replica_id())
+            .map_or(true, |set| !set.selections.is_empty())
+        {
+            self.set_active_selections(Arc::from([]), false, Default::default(), cx);
+        }
+    }
+
+    pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Lamport>
+    where
+        T: Into<Arc<str>>,
+    {
+        self.autoindent_requests.clear();
+        self.edit([(0..self.len(), text)], None, cx)
+    }
+
+    pub fn edit<I, S, T>(
+        &mut self,
+        edits_iter: I,
+        autoindent_mode: Option<AutoindentMode>,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<clock::Lamport>
+    where
+        I: IntoIterator<Item = (Range<S>, T)>,
+        S: ToOffset,
+        T: Into<Arc<str>>,
+    {
+        // Skip invalid edits and coalesce contiguous ones.
+        let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new();
+        for (range, new_text) in edits_iter {
+            let mut range = range.start.to_offset(self)..range.end.to_offset(self);
+            if range.start > range.end {
+                mem::swap(&mut range.start, &mut range.end);
+            }
+            let new_text = new_text.into();
+            if !new_text.is_empty() || !range.is_empty() {
+                if let Some((prev_range, prev_text)) = edits.last_mut() {
+                    if prev_range.end >= range.start {
+                        prev_range.end = cmp::max(prev_range.end, range.end);
+                        *prev_text = format!("{prev_text}{new_text}").into();
+                    } else {
+                        edits.push((range, new_text));
+                    }
+                } else {
+                    edits.push((range, new_text));
+                }
+            }
+        }
+        if edits.is_empty() {
+            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();
+
+        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;
+                    }
+
+                    // 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: self.anchor_before(new_start + range_of_insertion_to_indent.start)
+                            ..self.anchor_after(new_start + range_of_insertion_to_indent.end),
+                    }
+                })
+                .collect();
+
+            self.autoindent_requests.push(Arc::new(AutoindentRequest {
+                before_edit,
+                entries,
+                is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
+            }));
+        }
+
+        self.end_transaction(cx);
+        self.send_operation(Operation::Buffer(edit_operation), cx);
+        Some(edit_id)
+    }
+
+    fn did_edit(
+        &mut self,
+        old_version: &clock::Global,
+        was_dirty: bool,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if self.edits_since::<usize>(old_version).next().is_none() {
+            return;
+        }
+
+        self.reparse(cx);
+
+        cx.emit(Event::Edited);
+        if was_dirty != self.is_dirty() {
+            cx.emit(Event::DirtyChanged);
+        }
+        cx.notify();
+    }
+
+    pub fn apply_ops<I: IntoIterator<Item = Operation>>(
+        &mut self,
+        ops: I,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        self.pending_autoindent.take();
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+        let mut deferred_ops = Vec::new();
+        let buffer_ops = ops
+            .into_iter()
+            .filter_map(|op| match op {
+                Operation::Buffer(op) => Some(op),
+                _ => {
+                    if self.can_apply_op(&op) {
+                        self.apply_op(op, cx);
+                    } else {
+                        deferred_ops.push(op);
+                    }
+                    None
+                }
+            })
+            .collect::<Vec<_>>();
+        self.text.apply_ops(buffer_ops)?;
+        self.deferred_ops.insert(deferred_ops);
+        self.flush_deferred_ops(cx);
+        self.did_edit(&old_version, was_dirty, cx);
+        // Notify independently of whether the buffer was edited as the operations could include a
+        // selection update.
+        cx.notify();
+        Ok(())
+    }
+
+    fn flush_deferred_ops(&mut self, cx: &mut ModelContext<Self>) {
+        let mut deferred_ops = Vec::new();
+        for op in self.deferred_ops.drain().iter().cloned() {
+            if self.can_apply_op(&op) {
+                self.apply_op(op, cx);
+            } else {
+                deferred_ops.push(op);
+            }
+        }
+        self.deferred_ops.insert(deferred_ops);
+    }
+
+    fn can_apply_op(&self, operation: &Operation) -> bool {
+        match operation {
+            Operation::Buffer(_) => {
+                unreachable!("buffer operations should never be applied at this layer")
+            }
+            Operation::UpdateDiagnostics {
+                diagnostics: diagnostic_set,
+                ..
+            } => diagnostic_set.iter().all(|diagnostic| {
+                self.text.can_resolve(&diagnostic.range.start)
+                    && self.text.can_resolve(&diagnostic.range.end)
+            }),
+            Operation::UpdateSelections { selections, .. } => selections
+                .iter()
+                .all(|s| self.can_resolve(&s.start) && self.can_resolve(&s.end)),
+            Operation::UpdateCompletionTriggers { .. } => true,
+        }
+    }
+
+    fn apply_op(&mut self, operation: Operation, cx: &mut ModelContext<Self>) {
+        match operation {
+            Operation::Buffer(_) => {
+                unreachable!("buffer operations should never be applied at this layer")
+            }
+            Operation::UpdateDiagnostics {
+                server_id,
+                diagnostics: diagnostic_set,
+                lamport_timestamp,
+            } => {
+                let snapshot = self.snapshot();
+                self.apply_diagnostic_update(
+                    server_id,
+                    DiagnosticSet::from_sorted_entries(diagnostic_set.iter().cloned(), &snapshot),
+                    lamport_timestamp,
+                    cx,
+                );
+            }
+            Operation::UpdateSelections {
+                selections,
+                lamport_timestamp,
+                line_mode,
+                cursor_shape,
+            } => {
+                if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) {
+                    if set.lamport_timestamp > lamport_timestamp {
+                        return;
+                    }
+                }
+
+                self.remote_selections.insert(
+                    lamport_timestamp.replica_id,
+                    SelectionSet {
+                        selections,
+                        lamport_timestamp,
+                        line_mode,
+                        cursor_shape,
+                    },
+                );
+                self.text.lamport_clock.observe(lamport_timestamp);
+                self.selections_update_count += 1;
+            }
+            Operation::UpdateCompletionTriggers {
+                triggers,
+                lamport_timestamp,
+            } => {
+                self.completion_triggers = triggers;
+                self.text.lamport_clock.observe(lamport_timestamp);
+            }
+        }
+    }
+
+    fn apply_diagnostic_update(
+        &mut self,
+        server_id: LanguageServerId,
+        diagnostics: DiagnosticSet,
+        lamport_timestamp: clock::Lamport,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if lamport_timestamp > self.diagnostics_timestamp {
+            let ix = self.diagnostics.binary_search_by_key(&server_id, |e| e.0);
+            if diagnostics.len() == 0 {
+                if let Ok(ix) = ix {
+                    self.diagnostics.remove(ix);
+                }
+            } else {
+                match ix {
+                    Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
+                    Ok(ix) => self.diagnostics[ix].1 = diagnostics,
+                };
+            }
+            self.diagnostics_timestamp = lamport_timestamp;
+            self.diagnostics_update_count += 1;
+            self.text.lamport_clock.observe(lamport_timestamp);
+            cx.notify();
+            cx.emit(Event::DiagnosticsUpdated);
+        }
+    }
+
+    fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext<Self>) {
+        cx.emit(Event::Operation(operation));
+    }
+
+    pub fn remove_peer(&mut self, replica_id: ReplicaId, cx: &mut ModelContext<Self>) {
+        self.remote_selections.remove(&replica_id);
+        cx.notify();
+    }
+
+    pub fn undo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+
+        if let Some((transaction_id, operation)) = self.text.undo() {
+            self.send_operation(Operation::Buffer(operation), cx);
+            self.did_edit(&old_version, was_dirty, cx);
+            Some(transaction_id)
+        } else {
+            None
+        }
+    }
+
+    pub fn undo_transaction(
+        &mut self,
+        transaction_id: TransactionId,
+        cx: &mut ModelContext<Self>,
+    ) -> bool {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+        if let Some(operation) = self.text.undo_transaction(transaction_id) {
+            self.send_operation(Operation::Buffer(operation), cx);
+            self.did_edit(&old_version, was_dirty, cx);
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn undo_to_transaction(
+        &mut self,
+        transaction_id: TransactionId,
+        cx: &mut ModelContext<Self>,
+    ) -> bool {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+
+        let operations = self.text.undo_to_transaction(transaction_id);
+        let undone = !operations.is_empty();
+        for operation in operations {
+            self.send_operation(Operation::Buffer(operation), cx);
+        }
+        if undone {
+            self.did_edit(&old_version, was_dirty, cx)
+        }
+        undone
+    }
+
+    pub fn redo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+
+        if let Some((transaction_id, operation)) = self.text.redo() {
+            self.send_operation(Operation::Buffer(operation), cx);
+            self.did_edit(&old_version, was_dirty, cx);
+            Some(transaction_id)
+        } else {
+            None
+        }
+    }
+
+    pub fn redo_to_transaction(
+        &mut self,
+        transaction_id: TransactionId,
+        cx: &mut ModelContext<Self>,
+    ) -> bool {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+
+        let operations = self.text.redo_to_transaction(transaction_id);
+        let redone = !operations.is_empty();
+        for operation in operations {
+            self.send_operation(Operation::Buffer(operation), cx);
+        }
+        if redone {
+            self.did_edit(&old_version, was_dirty, cx)
+        }
+        redone
+    }
+
+    pub fn set_completion_triggers(&mut self, triggers: Vec<String>, cx: &mut ModelContext<Self>) {
+        self.completion_triggers = triggers.clone();
+        self.completion_triggers_timestamp = self.text.lamport_clock.tick();
+        self.send_operation(
+            Operation::UpdateCompletionTriggers {
+                triggers,
+                lamport_timestamp: self.completion_triggers_timestamp,
+            },
+            cx,
+        );
+        cx.notify();
+    }
+
+    pub fn completion_triggers(&self) -> &[String] {
+        &self.completion_triggers
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl Buffer {
+    pub fn edit_via_marked_text(
+        &mut self,
+        marked_string: &str,
+        autoindent_mode: Option<AutoindentMode>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let edits = self.edits_for_marked_text(marked_string);
+        self.edit(edits, autoindent_mode, cx);
+    }
+
+    pub fn set_group_interval(&mut self, group_interval: Duration) {
+        self.text.set_group_interval(group_interval);
+    }
+
+    pub fn randomly_edit<T>(
+        &mut self,
+        rng: &mut T,
+        old_range_count: usize,
+        cx: &mut ModelContext<Self>,
+    ) where
+        T: rand::Rng,
+    {
+        let mut edits: Vec<(Range<usize>, String)> = Vec::new();
+        let mut last_end = None;
+        for _ in 0..old_range_count {
+            if last_end.map_or(false, |last_end| last_end >= self.len()) {
+                break;
+            }
+
+            let new_start = last_end.map_or(0, |last_end| last_end + 1);
+            let mut range = self.random_byte_range(new_start, rng);
+            if rng.gen_bool(0.2) {
+                mem::swap(&mut range.start, &mut range.end);
+            }
+            last_end = Some(range.end);
+
+            let new_text_len = rng.gen_range(0..10);
+            let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect();
+
+            edits.push((range, new_text));
+        }
+        log::info!("mutating buffer {} with {:?}", self.replica_id(), edits);
+        self.edit(edits, None, cx);
+    }
+
+    pub fn randomly_undo_redo(&mut self, rng: &mut impl rand::Rng, cx: &mut ModelContext<Self>) {
+        let was_dirty = self.is_dirty();
+        let old_version = self.version.clone();
+
+        let ops = self.text.randomly_undo_redo(rng);
+        if !ops.is_empty() {
+            for op in ops {
+                self.send_operation(Operation::Buffer(op), cx);
+                self.did_edit(&old_version, was_dirty, cx);
+            }
+        }
+    }
+}
+
+impl EventEmitter for Buffer {
+    type Event = Event;
+}
+
+impl Deref for Buffer {
+    type Target = TextBuffer;
+
+    fn deref(&self) -> &Self::Target {
+        &self.text
+    }
+}
+
+impl BufferSnapshot {
+    pub fn indent_size_for_line(&self, row: u32) -> IndentSize {
+        indent_size_for_line(self, row)
+    }
+
+    pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
+        let settings = language_settings(self.language_at(position), self.file(), cx);
+        if settings.hard_tabs {
+            IndentSize::tab()
+        } else {
+            IndentSize::spaces(settings.tab_size.get())
+        }
+    }
+
+    pub fn suggested_indents(
+        &self,
+        rows: impl Iterator<Item = u32>,
+        single_indent_size: IndentSize,
+    ) -> BTreeMap<u32, IndentSize> {
+        let mut result = BTreeMap::new();
+
+        for row_range in contiguous_ranges(rows, 10) {
+            let suggestions = match self.suggest_autoindents(row_range.clone()) {
+                Some(suggestions) => suggestions,
+                _ => break,
+            };
+
+            for (row, suggestion) in row_range.zip(suggestions) {
+                let indent_size = if let Some(suggestion) = suggestion {
+                    result
+                        .get(&suggestion.basis_row)
+                        .copied()
+                        .unwrap_or_else(|| self.indent_size_for_line(suggestion.basis_row))
+                        .with_delta(suggestion.delta, single_indent_size)
+                } else {
+                    self.indent_size_for_line(row)
+                };
+
+                result.insert(row, indent_size);
+            }
+        }
+
+        result
+    }
+
+    fn suggest_autoindents(
+        &self,
+        row_range: Range<u32>,
+    ) -> Option<impl Iterator<Item = Option<IndentSuggestion>> + '_> {
+        let config = &self.language.as_ref()?.config;
+        let prev_non_blank_row = self.prev_non_blank_row(row_range.start);
+
+        // Find the suggested indentation ranges based on the syntax tree.
+        let start = Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0);
+        let end = Point::new(row_range.end, 0);
+        let range = (start..end).to_offset(&self.text);
+        let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
+            Some(&grammar.indents_config.as_ref()?.query)
+        });
+        let indent_configs = matches
+            .grammars()
+            .iter()
+            .map(|grammar| grammar.indents_config.as_ref().unwrap())
+            .collect::<Vec<_>>();
+
+        let mut indent_ranges = Vec::<Range<Point>>::new();
+        let mut outdent_positions = Vec::<Point>::new();
+        while let Some(mat) = matches.peek() {
+            let mut start: Option<Point> = None;
+            let mut end: Option<Point> = None;
+
+            let config = &indent_configs[mat.grammar_index];
+            for capture in mat.captures {
+                if capture.index == config.indent_capture_ix {
+                    start.get_or_insert(Point::from_ts_point(capture.node.start_position()));
+                    end.get_or_insert(Point::from_ts_point(capture.node.end_position()));
+                } else if Some(capture.index) == config.start_capture_ix {
+                    start = Some(Point::from_ts_point(capture.node.end_position()));
+                } else if Some(capture.index) == config.end_capture_ix {
+                    end = Some(Point::from_ts_point(capture.node.start_position()));
+                } else if Some(capture.index) == config.outdent_capture_ix {
+                    outdent_positions.push(Point::from_ts_point(capture.node.start_position()));
+                }
+            }
+
+            matches.advance();
+            if let Some((start, end)) = start.zip(end) {
+                if start.row == end.row {
+                    continue;
+                }
+
+                let range = start..end;
+                match indent_ranges.binary_search_by_key(&range.start, |r| r.start) {
+                    Err(ix) => indent_ranges.insert(ix, range),
+                    Ok(ix) => {
+                        let prev_range = &mut indent_ranges[ix];
+                        prev_range.end = prev_range.end.max(range.end);
+                    }
+                }
+            }
+        }
+
+        let mut error_ranges = Vec::<Range<Point>>::new();
+        let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
+            Some(&grammar.error_query)
+        });
+        while let Some(mat) = matches.peek() {
+            let node = mat.captures[0].node;
+            let start = Point::from_ts_point(node.start_position());
+            let end = Point::from_ts_point(node.end_position());
+            let range = start..end;
+            let ix = match error_ranges.binary_search_by_key(&range.start, |r| r.start) {
+                Ok(ix) | Err(ix) => ix,
+            };
+            let mut end_ix = ix;
+            while let Some(existing_range) = error_ranges.get(end_ix) {
+                if existing_range.end < end {
+                    end_ix += 1;
+                } else {
+                    break;
+                }
+            }
+            error_ranges.splice(ix..end_ix, [range]);
+            matches.advance();
+        }
+
+        outdent_positions.sort();
+        for outdent_position in outdent_positions {
+            // find the innermost indent range containing this outdent_position
+            // set its end to the outdent position
+            if let Some(range_to_truncate) = indent_ranges
+                .iter_mut()
+                .filter(|indent_range| indent_range.contains(&outdent_position))
+                .last()
+            {
+                range_to_truncate.end = outdent_position;
+            }
+        }
+
+        // Find the suggested indentation increases and decreased based on regexes.
+        let mut indent_change_rows = Vec::<(u32, Ordering)>::new();
+        self.for_each_line(
+            Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0)
+                ..Point::new(row_range.end, 0),
+            |row, line| {
+                if config
+                    .decrease_indent_pattern
+                    .as_ref()
+                    .map_or(false, |regex| regex.is_match(line))
+                {
+                    indent_change_rows.push((row, Ordering::Less));
+                }
+                if config
+                    .increase_indent_pattern
+                    .as_ref()
+                    .map_or(false, |regex| regex.is_match(line))
+                {
+                    indent_change_rows.push((row + 1, Ordering::Greater));
+                }
+            },
+        );
+
+        let mut indent_changes = indent_change_rows.into_iter().peekable();
+        let mut prev_row = if config.auto_indent_using_last_non_empty_line {
+            prev_non_blank_row.unwrap_or(0)
+        } else {
+            row_range.start.saturating_sub(1)
+        };
+        let mut prev_row_start = Point::new(prev_row, self.indent_size_for_line(prev_row).len);
+        Some(row_range.map(move |row| {
+            let row_start = Point::new(row, self.indent_size_for_line(row).len);
+
+            let mut indent_from_prev_row = false;
+            let mut outdent_from_prev_row = false;
+            let mut outdent_to_row = u32::MAX;
+
+            while let Some((indent_row, delta)) = indent_changes.peek() {
+                match indent_row.cmp(&row) {
+                    Ordering::Equal => match delta {
+                        Ordering::Less => outdent_from_prev_row = true,
+                        Ordering::Greater => indent_from_prev_row = true,
+                        _ => {}
+                    },
+
+                    Ordering::Greater => break,
+                    Ordering::Less => {}
+                }
+
+                indent_changes.next();
+            }
+
+            for range in &indent_ranges {
+                if range.start.row >= row {
+                    break;
+                }
+                if range.start.row == prev_row && range.end > row_start {
+                    indent_from_prev_row = true;
+                }
+                if range.end > prev_row_start && range.end <= row_start {
+                    outdent_to_row = outdent_to_row.min(range.start.row);
+                }
+            }
+
+            let within_error = error_ranges
+                .iter()
+                .any(|e| e.start.row < row && e.end > row_start);
+
+            let suggestion = if outdent_to_row == prev_row
+                || (outdent_from_prev_row && indent_from_prev_row)
+            {
+                Some(IndentSuggestion {
+                    basis_row: prev_row,
+                    delta: Ordering::Equal,
+                    within_error,
+                })
+            } else if indent_from_prev_row {
+                Some(IndentSuggestion {
+                    basis_row: prev_row,
+                    delta: Ordering::Greater,
+                    within_error,
+                })
+            } else if outdent_to_row < prev_row {
+                Some(IndentSuggestion {
+                    basis_row: outdent_to_row,
+                    delta: Ordering::Equal,
+                    within_error,
+                })
+            } else if outdent_from_prev_row {
+                Some(IndentSuggestion {
+                    basis_row: prev_row,
+                    delta: Ordering::Less,
+                    within_error,
+                })
+            } else if config.auto_indent_using_last_non_empty_line || !self.is_line_blank(prev_row)
+            {
+                Some(IndentSuggestion {
+                    basis_row: prev_row,
+                    delta: Ordering::Equal,
+                    within_error,
+                })
+            } else {
+                None
+            };
+
+            prev_row = row;
+            prev_row_start = row_start;
+            suggestion
+        }))
+    }
+
+    fn prev_non_blank_row(&self, mut row: u32) -> Option<u32> {
+        while row > 0 {
+            row -= 1;
+            if !self.is_line_blank(row) {
+                return Some(row);
+            }
+        }
+        None
+    }
+
+    pub fn chunks<T: ToOffset>(&self, range: Range<T>, language_aware: bool) -> BufferChunks {
+        let range = range.start.to_offset(self)..range.end.to_offset(self);
+
+        let mut syntax = None;
+        let mut diagnostic_endpoints = Vec::new();
+        if language_aware {
+            let captures = self.syntax.captures(range.clone(), &self.text, |grammar| {
+                grammar.highlights_query.as_ref()
+            });
+            let highlight_maps = captures
+                .grammars()
+                .into_iter()
+                .map(|grammar| grammar.highlight_map())
+                .collect();
+            syntax = Some((captures, highlight_maps));
+            for entry in self.diagnostics_in_range::<_, usize>(range.clone(), false) {
+                diagnostic_endpoints.push(DiagnosticEndpoint {
+                    offset: entry.range.start,
+                    is_start: true,
+                    severity: entry.diagnostic.severity,
+                    is_unnecessary: entry.diagnostic.is_unnecessary,
+                });
+                diagnostic_endpoints.push(DiagnosticEndpoint {
+                    offset: entry.range.end,
+                    is_start: false,
+                    severity: entry.diagnostic.severity,
+                    is_unnecessary: entry.diagnostic.is_unnecessary,
+                });
+            }
+            diagnostic_endpoints
+                .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
+        }
+
+        BufferChunks::new(self.text.as_rope(), range, syntax, diagnostic_endpoints)
+    }
+
+    pub fn for_each_line(&self, range: Range<Point>, mut callback: impl FnMut(u32, &str)) {
+        let mut line = String::new();
+        let mut row = range.start.row;
+        for chunk in self
+            .as_rope()
+            .chunks_in_range(range.to_offset(self))
+            .chain(["\n"])
+        {
+            for (newline_ix, text) in chunk.split('\n').enumerate() {
+                if newline_ix > 0 {
+                    callback(row, &line);
+                    row += 1;
+                    line.clear();
+                }
+                line.push_str(text);
+            }
+        }
+    }
+
+    pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayerInfo> + '_ {
+        self.syntax.layers_for_range(0..self.len(), &self.text)
+    }
+
+    pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayerInfo> {
+        let offset = position.to_offset(self);
+        self.syntax
+            .layers_for_range(offset..offset, &self.text)
+            .filter(|l| l.node().end_byte() > offset)
+            .last()
+    }
+
+    pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
+        self.syntax_layer_at(position)
+            .map(|info| info.language)
+            .or(self.language.as_ref())
+    }
+
+    pub fn settings_at<'a, D: ToOffset>(
+        &self,
+        position: D,
+        cx: &'a AppContext,
+    ) -> &'a LanguageSettings {
+        language_settings(self.language_at(position), self.file.as_ref(), cx)
+    }
+
+    pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
+        let offset = position.to_offset(self);
+        let mut scope = None;
+        let mut smallest_range: Option<Range<usize>> = None;
+
+        // Use the layer that has the smallest node intersecting the given point.
+        for layer in self.syntax.layers_for_range(offset..offset, &self.text) {
+            let mut cursor = layer.node().walk();
+
+            let mut range = None;
+            loop {
+                let child_range = cursor.node().byte_range();
+                if !child_range.to_inclusive().contains(&offset) {
+                    break;
+                }
+
+                range = Some(child_range);
+                if cursor.goto_first_child_for_byte(offset).is_none() {
+                    break;
+                }
+            }
+
+            if let Some(range) = range {
+                if smallest_range
+                    .as_ref()
+                    .map_or(true, |smallest_range| range.len() < smallest_range.len())
+                {
+                    smallest_range = Some(range);
+                    scope = Some(LanguageScope {
+                        language: layer.language.clone(),
+                        override_id: layer.override_id(offset, &self.text),
+                    });
+                }
+            }
+        }
+
+        scope.or_else(|| {
+            self.language.clone().map(|language| LanguageScope {
+                language,
+                override_id: None,
+            })
+        })
+    }
+
+    pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
+        let mut start = start.to_offset(self);
+        let mut end = start;
+        let mut next_chars = self.chars_at(start).peekable();
+        let mut prev_chars = self.reversed_chars_at(start).peekable();
+
+        let scope = self.language_scope_at(start);
+        let kind = |c| char_kind(&scope, c);
+        let word_kind = cmp::max(
+            prev_chars.peek().copied().map(kind),
+            next_chars.peek().copied().map(kind),
+        );
+
+        for ch in prev_chars {
+            if Some(kind(ch)) == word_kind && ch != '\n' {
+                start -= ch.len_utf8();
+            } else {
+                break;
+            }
+        }
+
+        for ch in next_chars {
+            if Some(kind(ch)) == word_kind && ch != '\n' {
+                end += ch.len_utf8();
+            } else {
+                break;
+            }
+        }
+
+        (start..end, word_kind)
+    }
+
+    pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
+        let range = range.start.to_offset(self)..range.end.to_offset(self);
+        let mut result: Option<Range<usize>> = None;
+        'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
+            let mut cursor = layer.node().walk();
+
+            // Descend to the first leaf that touches the start of the range,
+            // and if the range is non-empty, extends beyond the start.
+            while cursor.goto_first_child_for_byte(range.start).is_some() {
+                if !range.is_empty() && cursor.node().end_byte() == range.start {
+                    cursor.goto_next_sibling();
+                }
+            }
+
+            // Ascend to the smallest ancestor that strictly contains the range.
+            loop {
+                let node_range = cursor.node().byte_range();
+                if node_range.start <= range.start
+                    && node_range.end >= range.end
+                    && node_range.len() > range.len()
+                {
+                    break;
+                }
+                if !cursor.goto_parent() {
+                    continue 'outer;
+                }
+            }
+
+            let left_node = cursor.node();
+            let mut layer_result = left_node.byte_range();
+
+            // For an empty range, try to find another node immediately to the right of the range.
+            if left_node.end_byte() == range.start {
+                let mut right_node = None;
+                while !cursor.goto_next_sibling() {
+                    if !cursor.goto_parent() {
+                        break;
+                    }
+                }
+
+                while cursor.node().start_byte() == range.start {
+                    right_node = Some(cursor.node());
+                    if !cursor.goto_first_child() {
+                        break;
+                    }
+                }
+
+                // If there is a candidate node on both sides of the (empty) range, then
+                // decide between the two by favoring a named node over an anonymous token.
+                // If both nodes are the same in that regard, favor the right one.
+                if let Some(right_node) = right_node {
+                    if right_node.is_named() || !left_node.is_named() {
+                        layer_result = right_node.byte_range();
+                    }
+                }
+            }
+
+            if let Some(previous_result) = &result {
+                if previous_result.len() < layer_result.len() {
+                    continue;
+                }
+            }
+            result = Some(layer_result);
+        }
+
+        result
+    }
+
+    pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
+        self.outline_items_containing(0..self.len(), true, theme)
+            .map(Outline::new)
+    }
+
+    pub fn symbols_containing<T: ToOffset>(
+        &self,
+        position: T,
+        theme: Option<&SyntaxTheme>,
+    ) -> Option<Vec<OutlineItem<Anchor>>> {
+        let position = position.to_offset(self);
+        let mut items = self.outline_items_containing(
+            position.saturating_sub(1)..self.len().min(position + 1),
+            false,
+            theme,
+        )?;
+        let mut prev_depth = None;
+        items.retain(|item| {
+            let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth);
+            prev_depth = Some(item.depth);
+            result
+        });
+        Some(items)
+    }
+
+    fn outline_items_containing(
+        &self,
+        range: Range<usize>,
+        include_extra_context: bool,
+        theme: Option<&SyntaxTheme>,
+    ) -> Option<Vec<OutlineItem<Anchor>>> {
+        let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
+            grammar.outline_config.as_ref().map(|c| &c.query)
+        });
+        let configs = matches
+            .grammars()
+            .iter()
+            .map(|g| g.outline_config.as_ref().unwrap())
+            .collect::<Vec<_>>();
+
+        let mut stack = Vec::<Range<usize>>::new();
+        let mut items = Vec::new();
+        while let Some(mat) = matches.peek() {
+            let config = &configs[mat.grammar_index];
+            let item_node = mat.captures.iter().find_map(|cap| {
+                if cap.index == config.item_capture_ix {
+                    Some(cap.node)
+                } else {
+                    None
+                }
+            })?;
+
+            let item_range = item_node.byte_range();
+            if item_range.end < range.start || item_range.start > range.end {
+                matches.advance();
+                continue;
+            }
+
+            let mut buffer_ranges = Vec::new();
+            for capture in mat.captures {
+                let node_is_name;
+                if capture.index == config.name_capture_ix {
+                    node_is_name = true;
+                } else if Some(capture.index) == config.context_capture_ix
+                    || (Some(capture.index) == config.extra_context_capture_ix
+                        && include_extra_context)
+                {
+                    node_is_name = false;
+                } else {
+                    continue;
+                }
+
+                let mut range = capture.node.start_byte()..capture.node.end_byte();
+                let start = capture.node.start_position();
+                if capture.node.end_position().row > start.row {
+                    range.end =
+                        range.start + self.line_len(start.row as u32) as usize - start.column;
+                }
+
+                buffer_ranges.push((range, node_is_name));
+            }
+
+            if buffer_ranges.is_empty() {
+                continue;
+            }
+
+            let mut text = String::new();
+            let mut highlight_ranges = Vec::new();
+            let mut name_ranges = Vec::new();
+            let mut chunks = self.chunks(
+                buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
+                true,
+            );
+            let mut last_buffer_range_end = 0;
+            for (buffer_range, is_name) in buffer_ranges {
+                if !text.is_empty() && buffer_range.start > last_buffer_range_end {
+                    text.push(' ');
+                }
+                last_buffer_range_end = buffer_range.end;
+                if is_name {
+                    let mut start = text.len();
+                    let end = start + buffer_range.len();
+
+                    // When multiple names are captured, then the matcheable text
+                    // includes the whitespace in between the names.
+                    if !name_ranges.is_empty() {
+                        start -= 1;
+                    }
+
+                    name_ranges.push(start..end);
+                }
+
+                let mut offset = buffer_range.start;
+                chunks.seek(offset);
+                for mut chunk in chunks.by_ref() {
+                    if chunk.text.len() > buffer_range.end - offset {
+                        chunk.text = &chunk.text[0..(buffer_range.end - offset)];
+                        offset = buffer_range.end;
+                    } else {
+                        offset += chunk.text.len();
+                    }
+                    let style = chunk
+                        .syntax_highlight_id
+                        .zip(theme)
+                        .and_then(|(highlight, theme)| highlight.style(theme));
+                    if let Some(style) = style {
+                        let start = text.len();
+                        let end = start + chunk.text.len();
+                        highlight_ranges.push((start..end, style));
+                    }
+                    text.push_str(chunk.text);
+                    if offset >= buffer_range.end {
+                        break;
+                    }
+                }
+            }
+
+            matches.advance();
+            while stack.last().map_or(false, |prev_range| {
+                prev_range.start > item_range.start || prev_range.end < item_range.end
+            }) {
+                stack.pop();
+            }
+            stack.push(item_range.clone());
+
+            items.push(OutlineItem {
+                depth: stack.len() - 1,
+                range: self.anchor_after(item_range.start)..self.anchor_before(item_range.end),
+                text,
+                highlight_ranges,
+                name_ranges,
+            })
+        }
+        Some(items)
+    }
+
+    pub fn matches(
+        &self,
+        range: Range<usize>,
+        query: fn(&Grammar) -> Option<&tree_sitter::Query>,
+    ) -> SyntaxMapMatches {
+        self.syntax.matches(range, self, query)
+    }
+
+    /// Returns bracket range pairs overlapping or adjacent to `range`
+    pub fn bracket_ranges<'a, T: ToOffset>(
+        &'a self,
+        range: Range<T>,
+    ) -> impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a {
+        // Find bracket pairs that *inclusively* contain the given range.
+        let range = range.start.to_offset(self).saturating_sub(1)
+            ..self.len().min(range.end.to_offset(self) + 1);
+
+        let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
+            grammar.brackets_config.as_ref().map(|c| &c.query)
+        });
+        let configs = matches
+            .grammars()
+            .iter()
+            .map(|grammar| grammar.brackets_config.as_ref().unwrap())
+            .collect::<Vec<_>>();
+
+        iter::from_fn(move || {
+            while let Some(mat) = matches.peek() {
+                let mut open = None;
+                let mut close = None;
+                let config = &configs[mat.grammar_index];
+                for capture in mat.captures {
+                    if capture.index == config.open_capture_ix {
+                        open = Some(capture.node.byte_range());
+                    } else if capture.index == config.close_capture_ix {
+                        close = Some(capture.node.byte_range());
+                    }
+                }
+
+                matches.advance();
+
+                let Some((open, close)) = open.zip(close) else {
+                    continue;
+                };
+
+                let bracket_range = open.start..=close.end;
+                if !bracket_range.overlaps(&range) {
+                    continue;
+                }
+
+                return Some((open, close));
+            }
+            None
+        })
+    }
+
+    #[allow(clippy::type_complexity)]
+    pub fn remote_selections_in_range(
+        &self,
+        range: Range<Anchor>,
+    ) -> impl Iterator<
+        Item = (
+            ReplicaId,
+            bool,
+            CursorShape,
+            impl Iterator<Item = &Selection<Anchor>> + '_,
+        ),
+    > + '_ {
+        self.remote_selections
+            .iter()
+            .filter(|(replica_id, set)| {
+                **replica_id != self.text.replica_id() && !set.selections.is_empty()
+            })
+            .map(move |(replica_id, set)| {
+                let start_ix = match set.selections.binary_search_by(|probe| {
+                    probe.end.cmp(&range.start, self).then(Ordering::Greater)
+                }) {
+                    Ok(ix) | Err(ix) => ix,
+                };
+                let end_ix = match set.selections.binary_search_by(|probe| {
+                    probe.start.cmp(&range.end, self).then(Ordering::Less)
+                }) {
+                    Ok(ix) | Err(ix) => ix,
+                };
+
+                (
+                    *replica_id,
+                    set.line_mode,
+                    set.cursor_shape,
+                    set.selections[start_ix..end_ix].iter(),
+                )
+            })
+    }
+
+    pub fn git_diff_hunks_in_row_range<'a>(
+        &'a self,
+        range: Range<u32>,
+    ) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
+        self.git_diff.hunks_in_row_range(range, self)
+    }
+
+    pub fn git_diff_hunks_intersecting_range<'a>(
+        &'a self,
+        range: Range<Anchor>,
+    ) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
+        self.git_diff.hunks_intersecting_range(range, self)
+    }
+
+    pub fn git_diff_hunks_intersecting_range_rev<'a>(
+        &'a self,
+        range: Range<Anchor>,
+    ) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
+        self.git_diff.hunks_intersecting_range_rev(range, self)
+    }
+
+    pub fn diagnostics_in_range<'a, T, O>(
+        &'a self,
+        search_range: Range<T>,
+        reversed: bool,
+    ) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
+    where
+        T: 'a + Clone + ToOffset,
+        O: 'a + FromAnchor + Ord,
+    {
+        let mut iterators: Vec<_> = self
+            .diagnostics
+            .iter()
+            .map(|(_, collection)| {
+                collection
+                    .range::<T, O>(search_range.clone(), self, true, reversed)
+                    .peekable()
+            })
+            .collect();
+
+        std::iter::from_fn(move || {
+            let (next_ix, _) = iterators
+                .iter_mut()
+                .enumerate()
+                .flat_map(|(ix, iter)| Some((ix, iter.peek()?)))
+                .min_by(|(_, a), (_, b)| a.range.start.cmp(&b.range.start))?;
+            iterators[next_ix].next()
+        })
+    }
+
+    pub fn diagnostic_groups(
+        &self,
+        language_server_id: Option<LanguageServerId>,
+    ) -> Vec<(LanguageServerId, DiagnosticGroup<Anchor>)> {
+        let mut groups = Vec::new();
+
+        if let Some(language_server_id) = language_server_id {
+            if let Ok(ix) = self
+                .diagnostics
+                .binary_search_by_key(&language_server_id, |e| e.0)
+            {
+                self.diagnostics[ix]
+                    .1
+                    .groups(language_server_id, &mut groups, self);
+            }
+        } else {
+            for (language_server_id, diagnostics) in self.diagnostics.iter() {
+                diagnostics.groups(*language_server_id, &mut groups, self);
+            }
+        }
+
+        groups.sort_by(|(id_a, group_a), (id_b, group_b)| {
+            let a_start = &group_a.entries[group_a.primary_ix].range.start;
+            let b_start = &group_b.entries[group_b.primary_ix].range.start;
+            a_start.cmp(b_start, self).then_with(|| id_a.cmp(&id_b))
+        });
+
+        groups
+    }
+
+    pub fn diagnostic_group<'a, O>(
+        &'a self,
+        group_id: usize,
+    ) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
+    where
+        O: 'a + FromAnchor,
+    {
+        self.diagnostics
+            .iter()
+            .flat_map(move |(_, set)| set.group(group_id, self))
+    }
+
+    pub fn diagnostics_update_count(&self) -> usize {
+        self.diagnostics_update_count
+    }
+
+    pub fn parse_count(&self) -> usize {
+        self.parse_count
+    }
+
+    pub fn selections_update_count(&self) -> usize {
+        self.selections_update_count
+    }
+
+    pub fn file(&self) -> Option<&Arc<dyn File>> {
+        self.file.as_ref()
+    }
+
+    pub fn resolve_file_path(&self, cx: &AppContext, include_root: bool) -> Option<PathBuf> {
+        if let Some(file) = self.file() {
+            if file.path().file_name().is_none() || include_root {
+                Some(file.full_path(cx))
+            } else {
+                Some(file.path().to_path_buf())
+            }
+        } else {
+            None
+        }
+    }
+
+    pub fn file_update_count(&self) -> usize {
+        self.file_update_count
+    }
+
+    pub fn git_diff_update_count(&self) -> usize {
+        self.git_diff_update_count
+    }
+}
+
+fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
+    indent_size_for_text(text.chars_at(Point::new(row, 0)))
+}
+
+pub fn indent_size_for_text(text: impl Iterator<Item = char>) -> IndentSize {
+    let mut result = IndentSize::spaces(0);
+    for c in text {
+        let kind = match c {
+            ' ' => IndentKind::Space,
+            '\t' => IndentKind::Tab,
+            _ => break,
+        };
+        if result.len == 0 {
+            result.kind = kind;
+        }
+        result.len += 1;
+    }
+    result
+}
+
+impl Clone for BufferSnapshot {
+    fn clone(&self) -> Self {
+        Self {
+            text: self.text.clone(),
+            git_diff: self.git_diff.clone(),
+            syntax: self.syntax.clone(),
+            file: self.file.clone(),
+            remote_selections: self.remote_selections.clone(),
+            diagnostics: self.diagnostics.clone(),
+            selections_update_count: self.selections_update_count,
+            diagnostics_update_count: self.diagnostics_update_count,
+            file_update_count: self.file_update_count,
+            git_diff_update_count: self.git_diff_update_count,
+            language: self.language.clone(),
+            parse_count: self.parse_count,
+        }
+    }
+}
+
+impl Deref for BufferSnapshot {
+    type Target = text::BufferSnapshot;
+
+    fn deref(&self) -> &Self::Target {
+        &self.text
+    }
+}
+
+unsafe impl<'a> Send for BufferChunks<'a> {}
+
+impl<'a> BufferChunks<'a> {
+    pub(crate) fn new(
+        text: &'a Rope,
+        range: Range<usize>,
+        syntax: Option<(SyntaxMapCaptures<'a>, Vec<HighlightMap>)>,
+        diagnostic_endpoints: Vec<DiagnosticEndpoint>,
+    ) -> Self {
+        let mut highlights = None;
+        if let Some((captures, highlight_maps)) = syntax {
+            highlights = Some(BufferChunkHighlights {
+                captures,
+                next_capture: None,
+                stack: Default::default(),
+                highlight_maps,
+            })
+        }
+
+        let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable();
+        let chunks = text.chunks_in_range(range.clone());
+
+        BufferChunks {
+            range,
+            chunks,
+            diagnostic_endpoints,
+            error_depth: 0,
+            warning_depth: 0,
+            information_depth: 0,
+            hint_depth: 0,
+            unnecessary_depth: 0,
+            highlights,
+        }
+    }
+
+    pub fn seek(&mut self, offset: usize) {
+        self.range.start = offset;
+        self.chunks.seek(self.range.start);
+        if let Some(highlights) = self.highlights.as_mut() {
+            highlights
+                .stack
+                .retain(|(end_offset, _)| *end_offset > offset);
+            if let Some(capture) = &highlights.next_capture {
+                if offset >= capture.node.start_byte() {
+                    let next_capture_end = capture.node.end_byte();
+                    if offset < next_capture_end {
+                        highlights.stack.push((
+                            next_capture_end,
+                            highlights.highlight_maps[capture.grammar_index].get(capture.index),
+                        ));
+                    }
+                    highlights.next_capture.take();
+                }
+            }
+            highlights.captures.set_byte_range(self.range.clone());
+        }
+    }
+
+    pub fn offset(&self) -> usize {
+        self.range.start
+    }
+
+    fn update_diagnostic_depths(&mut self, endpoint: DiagnosticEndpoint) {
+        let depth = match endpoint.severity {
+            DiagnosticSeverity::ERROR => &mut self.error_depth,
+            DiagnosticSeverity::WARNING => &mut self.warning_depth,
+            DiagnosticSeverity::INFORMATION => &mut self.information_depth,
+            DiagnosticSeverity::HINT => &mut self.hint_depth,
+            _ => return,
+        };
+        if endpoint.is_start {
+            *depth += 1;
+        } else {
+            *depth -= 1;
+        }
+
+        if endpoint.is_unnecessary {
+            if endpoint.is_start {
+                self.unnecessary_depth += 1;
+            } else {
+                self.unnecessary_depth -= 1;
+            }
+        }
+    }
+
+    fn current_diagnostic_severity(&self) -> Option<DiagnosticSeverity> {
+        if self.error_depth > 0 {
+            Some(DiagnosticSeverity::ERROR)
+        } else if self.warning_depth > 0 {
+            Some(DiagnosticSeverity::WARNING)
+        } else if self.information_depth > 0 {
+            Some(DiagnosticSeverity::INFORMATION)
+        } else if self.hint_depth > 0 {
+            Some(DiagnosticSeverity::HINT)
+        } else {
+            None
+        }
+    }
+
+    fn current_code_is_unnecessary(&self) -> bool {
+        self.unnecessary_depth > 0
+    }
+}
+
+impl<'a> Iterator for BufferChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let mut next_capture_start = usize::MAX;
+        let mut next_diagnostic_endpoint = usize::MAX;
+
+        if let Some(highlights) = self.highlights.as_mut() {
+            while let Some((parent_capture_end, _)) = highlights.stack.last() {
+                if *parent_capture_end <= self.range.start {
+                    highlights.stack.pop();
+                } else {
+                    break;
+                }
+            }
+
+            if highlights.next_capture.is_none() {
+                highlights.next_capture = highlights.captures.next();
+            }
+
+            while let Some(capture) = highlights.next_capture.as_ref() {
+                if self.range.start < capture.node.start_byte() {
+                    next_capture_start = capture.node.start_byte();
+                    break;
+                } else {
+                    let highlight_id =
+                        highlights.highlight_maps[capture.grammar_index].get(capture.index);
+                    highlights
+                        .stack
+                        .push((capture.node.end_byte(), highlight_id));
+                    highlights.next_capture = highlights.captures.next();
+                }
+            }
+        }
+
+        while let Some(endpoint) = self.diagnostic_endpoints.peek().copied() {
+            if endpoint.offset <= self.range.start {
+                self.update_diagnostic_depths(endpoint);
+                self.diagnostic_endpoints.next();
+            } else {
+                next_diagnostic_endpoint = endpoint.offset;
+                break;
+            }
+        }
+
+        if let Some(chunk) = self.chunks.peek() {
+            let chunk_start = self.range.start;
+            let mut chunk_end = (self.chunks.offset() + chunk.len())
+                .min(next_capture_start)
+                .min(next_diagnostic_endpoint);
+            let mut highlight_id = None;
+            if let Some(highlights) = self.highlights.as_ref() {
+                if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() {
+                    chunk_end = chunk_end.min(*parent_capture_end);
+                    highlight_id = Some(*parent_highlight_id);
+                }
+            }
+
+            let slice =
+                &chunk[chunk_start - self.chunks.offset()..chunk_end - self.chunks.offset()];
+            self.range.start = chunk_end;
+            if self.range.start == self.chunks.offset() + chunk.len() {
+                self.chunks.next().unwrap();
+            }
+
+            Some(Chunk {
+                text: slice,
+                syntax_highlight_id: highlight_id,
+                diagnostic_severity: self.current_diagnostic_severity(),
+                is_unnecessary: self.current_code_is_unnecessary(),
+                ..Default::default()
+            })
+        } else {
+            None
+        }
+    }
+}
+
+impl operation_queue::Operation for Operation {
+    fn lamport_timestamp(&self) -> clock::Lamport {
+        match self {
+            Operation::Buffer(_) => {
+                unreachable!("buffer operations should never be deferred at this layer")
+            }
+            Operation::UpdateDiagnostics {
+                lamport_timestamp, ..
+            }
+            | Operation::UpdateSelections {
+                lamport_timestamp, ..
+            }
+            | Operation::UpdateCompletionTriggers {
+                lamport_timestamp, ..
+            } => *lamport_timestamp,
+        }
+    }
+}
+
+impl Default for Diagnostic {
+    fn default() -> Self {
+        Self {
+            source: Default::default(),
+            code: None,
+            severity: DiagnosticSeverity::ERROR,
+            message: Default::default(),
+            group_id: 0,
+            is_primary: false,
+            is_valid: true,
+            is_disk_based: false,
+            is_unnecessary: false,
+        }
+    }
+}
+
+impl IndentSize {
+    pub fn spaces(len: u32) -> Self {
+        Self {
+            len,
+            kind: IndentKind::Space,
+        }
+    }
+
+    pub fn tab() -> Self {
+        Self {
+            len: 1,
+            kind: IndentKind::Tab,
+        }
+    }
+
+    pub fn chars(&self) -> impl Iterator<Item = char> {
+        iter::repeat(self.char()).take(self.len as usize)
+    }
+
+    pub fn char(&self) -> char {
+        match self.kind {
+            IndentKind::Space => ' ',
+            IndentKind::Tab => '\t',
+        }
+    }
+
+    pub fn with_delta(mut self, direction: Ordering, size: IndentSize) -> Self {
+        match direction {
+            Ordering::Less => {
+                if self.kind == size.kind && self.len >= size.len {
+                    self.len -= size.len;
+                }
+            }
+            Ordering::Equal => {}
+            Ordering::Greater => {
+                if self.len == 0 {
+                    self = size;
+                } else if self.kind == size.kind {
+                    self.len += size.len;
+                }
+            }
+        }
+        self
+    }
+}
+
+impl Completion {
+    pub fn sort_key(&self) -> (usize, &str) {
+        let kind_key = match self.lsp_completion.kind {
+            Some(lsp2::CompletionItemKind::VARIABLE) => 0,
+            _ => 1,
+        };
+        (kind_key, &self.label.text[self.label.filter_range.clone()])
+    }
+
+    pub fn is_snippet(&self) -> bool {
+        self.lsp_completion.insert_text_format == Some(lsp2::InsertTextFormat::SNIPPET)
+    }
+}
+
+pub fn contiguous_ranges(
+    values: impl Iterator<Item = u32>,
+    max_len: usize,
+) -> impl Iterator<Item = Range<u32>> {
+    let mut values = values;
+    let mut current_range: Option<Range<u32>> = None;
+    std::iter::from_fn(move || loop {
+        if let Some(value) = values.next() {
+            if let Some(range) = &mut current_range {
+                if value == range.end && range.len() < max_len {
+                    range.end += 1;
+                    continue;
+                }
+            }
+
+            let prev_range = current_range.clone();
+            current_range = Some(value..(value + 1));
+            if prev_range.is_some() {
+                return prev_range;
+            }
+        } else {
+            return current_range.take();
+        }
+    })
+}
+
+pub fn char_kind(scope: &Option<LanguageScope>, c: char) -> CharKind {
+    if c.is_whitespace() {
+        return CharKind::Whitespace;
+    } else if c.is_alphanumeric() || c == '_' {
+        return CharKind::Word;
+    }
+
+    if let Some(scope) = scope {
+        if let Some(characters) = scope.word_characters() {
+            if characters.contains(&c) {
+                return CharKind::Word;
+            }
+        }
+    }
+
+    CharKind::Punctuation
+}
+
+/// Find all of the ranges of whitespace that occur at the ends of lines
+/// in the given rope.
+///
+/// This could also be done with a regex search, but this implementation
+/// avoids copying text.
+pub fn trailing_whitespace_ranges(rope: &Rope) -> Vec<Range<usize>> {
+    let mut ranges = Vec::new();
+
+    let mut offset = 0;
+    let mut prev_chunk_trailing_whitespace_range = 0..0;
+    for chunk in rope.chunks() {
+        let mut prev_line_trailing_whitespace_range = 0..0;
+        for (i, line) in chunk.split('\n').enumerate() {
+            let line_end_offset = offset + line.len();
+            let trimmed_line_len = line.trim_end_matches(|c| matches!(c, ' ' | '\t')).len();
+            let mut trailing_whitespace_range = (offset + trimmed_line_len)..line_end_offset;
+
+            if i == 0 && trimmed_line_len == 0 {
+                trailing_whitespace_range.start = prev_chunk_trailing_whitespace_range.start;
+            }
+            if !prev_line_trailing_whitespace_range.is_empty() {
+                ranges.push(prev_line_trailing_whitespace_range);
+            }
+
+            offset = line_end_offset + 1;
+            prev_line_trailing_whitespace_range = trailing_whitespace_range;
+        }
+
+        offset -= 1;
+        prev_chunk_trailing_whitespace_range = prev_line_trailing_whitespace_range;
+    }
+
+    if !prev_chunk_trailing_whitespace_range.is_empty() {
+        ranges.push(prev_chunk_trailing_whitespace_range);
+    }
+
+    ranges
+}

crates/language2/src/buffer_tests.rs 🔗

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

crates/language2/src/diagnostic_set.rs 🔗

@@ -0,0 +1,236 @@
+use crate::Diagnostic;
+use collections::HashMap;
+use lsp2::LanguageServerId;
+use std::{
+    cmp::{Ordering, Reverse},
+    iter,
+    ops::Range,
+};
+use sum_tree::{self, Bias, SumTree};
+use text::{Anchor, FromAnchor, PointUtf16, ToOffset};
+
+#[derive(Clone, Debug, Default)]
+pub struct DiagnosticSet {
+    diagnostics: SumTree<DiagnosticEntry<Anchor>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct DiagnosticEntry<T> {
+    pub range: Range<T>,
+    pub diagnostic: Diagnostic,
+}
+
+#[derive(Debug)]
+pub struct DiagnosticGroup<T> {
+    pub entries: Vec<DiagnosticEntry<T>>,
+    pub primary_ix: usize,
+}
+
+#[derive(Clone, Debug)]
+pub struct Summary {
+    start: Anchor,
+    end: Anchor,
+    min_start: Anchor,
+    max_end: Anchor,
+    count: usize,
+}
+
+impl<T> DiagnosticEntry<T> {
+    // Used to provide diagnostic context to lsp codeAction request
+    pub fn to_lsp_diagnostic_stub(&self) -> lsp2::Diagnostic {
+        let code = self
+            .diagnostic
+            .code
+            .clone()
+            .map(lsp2::NumberOrString::String);
+
+        lsp2::Diagnostic {
+            code,
+            severity: Some(self.diagnostic.severity),
+            ..Default::default()
+        }
+    }
+}
+
+impl DiagnosticSet {
+    pub fn from_sorted_entries<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
+    where
+        I: IntoIterator<Item = DiagnosticEntry<Anchor>>,
+    {
+        Self {
+            diagnostics: SumTree::from_iter(iter, buffer),
+        }
+    }
+
+    pub fn new<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
+    where
+        I: IntoIterator<Item = DiagnosticEntry<PointUtf16>>,
+    {
+        let mut entries = iter.into_iter().collect::<Vec<_>>();
+        entries.sort_unstable_by_key(|entry| (entry.range.start, Reverse(entry.range.end)));
+        Self {
+            diagnostics: SumTree::from_iter(
+                entries.into_iter().map(|entry| DiagnosticEntry {
+                    range: buffer.anchor_before(entry.range.start)
+                        ..buffer.anchor_before(entry.range.end),
+                    diagnostic: entry.diagnostic,
+                }),
+                buffer,
+            ),
+        }
+    }
+
+    pub fn len(&self) -> usize {
+        self.diagnostics.summary().count
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry<Anchor>> {
+        self.diagnostics.iter()
+    }
+
+    pub fn range<'a, T, O>(
+        &'a self,
+        range: Range<T>,
+        buffer: &'a text::BufferSnapshot,
+        inclusive: bool,
+        reversed: bool,
+    ) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
+    where
+        T: 'a + ToOffset,
+        O: FromAnchor,
+    {
+        let end_bias = if inclusive { Bias::Right } else { Bias::Left };
+        let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias);
+        let mut cursor = self.diagnostics.filter::<_, ()>({
+            move |summary: &Summary| {
+                let start_cmp = range.start.cmp(&summary.max_end, buffer);
+                let end_cmp = range.end.cmp(&summary.min_start, buffer);
+                if inclusive {
+                    start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
+                } else {
+                    start_cmp == Ordering::Less && end_cmp == Ordering::Greater
+                }
+            }
+        });
+
+        if reversed {
+            cursor.prev(buffer);
+        } else {
+            cursor.next(buffer);
+        }
+        iter::from_fn({
+            move || {
+                if let Some(diagnostic) = cursor.item() {
+                    if reversed {
+                        cursor.prev(buffer);
+                    } else {
+                        cursor.next(buffer);
+                    }
+                    Some(diagnostic.resolve(buffer))
+                } else {
+                    None
+                }
+            }
+        })
+    }
+
+    pub fn groups(
+        &self,
+        language_server_id: LanguageServerId,
+        output: &mut Vec<(LanguageServerId, DiagnosticGroup<Anchor>)>,
+        buffer: &text::BufferSnapshot,
+    ) {
+        let mut groups = HashMap::default();
+        for entry in self.diagnostics.iter() {
+            groups
+                .entry(entry.diagnostic.group_id)
+                .or_insert(Vec::new())
+                .push(entry.clone());
+        }
+
+        let start_ix = output.len();
+        output.extend(groups.into_values().filter_map(|mut entries| {
+            entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer));
+            entries
+                .iter()
+                .position(|entry| entry.diagnostic.is_primary)
+                .map(|primary_ix| {
+                    (
+                        language_server_id,
+                        DiagnosticGroup {
+                            entries,
+                            primary_ix,
+                        },
+                    )
+                })
+        }));
+        output[start_ix..].sort_unstable_by(|(id_a, group_a), (id_b, group_b)| {
+            group_a.entries[group_a.primary_ix]
+                .range
+                .start
+                .cmp(&group_b.entries[group_b.primary_ix].range.start, buffer)
+                .then_with(|| id_a.cmp(&id_b))
+        });
+    }
+
+    pub fn group<'a, O: FromAnchor>(
+        &'a self,
+        group_id: usize,
+        buffer: &'a text::BufferSnapshot,
+    ) -> impl 'a + Iterator<Item = DiagnosticEntry<O>> {
+        self.iter()
+            .filter(move |entry| entry.diagnostic.group_id == group_id)
+            .map(|entry| entry.resolve(buffer))
+    }
+}
+impl sum_tree::Item for DiagnosticEntry<Anchor> {
+    type Summary = Summary;
+
+    fn summary(&self) -> Self::Summary {
+        Summary {
+            start: self.range.start,
+            end: self.range.end,
+            min_start: self.range.start,
+            max_end: self.range.end,
+            count: 1,
+        }
+    }
+}
+
+impl DiagnosticEntry<Anchor> {
+    pub fn resolve<O: FromAnchor>(&self, buffer: &text::BufferSnapshot) -> DiagnosticEntry<O> {
+        DiagnosticEntry {
+            range: O::from_anchor(&self.range.start, buffer)
+                ..O::from_anchor(&self.range.end, buffer),
+            diagnostic: self.diagnostic.clone(),
+        }
+    }
+}
+
+impl Default for Summary {
+    fn default() -> Self {
+        Self {
+            start: Anchor::MIN,
+            end: Anchor::MAX,
+            min_start: Anchor::MAX,
+            max_end: Anchor::MIN,
+            count: 0,
+        }
+    }
+}
+
+impl sum_tree::Summary for Summary {
+    type Context = text::BufferSnapshot;
+
+    fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
+        if other.min_start.cmp(&self.min_start, buffer).is_lt() {
+            self.min_start = other.min_start;
+        }
+        if other.max_end.cmp(&self.max_end, buffer).is_gt() {
+            self.max_end = other.max_end;
+        }
+        self.start = other.start;
+        self.end = other.end;
+        self.count += other.count;
+    }
+}

crates/language2/src/highlight_map.rs 🔗

@@ -0,0 +1,111 @@
+use gpui2::HighlightStyle;
+use std::sync::Arc;
+use theme2::SyntaxTheme;
+
+#[derive(Clone, Debug)]
+pub struct HighlightMap(Arc<[HighlightId]>);
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct HighlightId(pub u32);
+
+const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
+
+impl HighlightMap {
+    pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self {
+        // For each capture name in the highlight query, find the longest
+        // key in the theme's syntax styles that matches all of the
+        // dot-separated components of the capture name.
+        HighlightMap(
+            capture_names
+                .iter()
+                .map(|capture_name| {
+                    theme
+                        .highlights
+                        .iter()
+                        .enumerate()
+                        .filter_map(|(i, (key, _))| {
+                            let mut len = 0;
+                            let capture_parts = capture_name.split('.');
+                            for key_part in key.split('.') {
+                                if capture_parts.clone().any(|part| part == key_part) {
+                                    len += 1;
+                                } else {
+                                    return None;
+                                }
+                            }
+                            Some((i, len))
+                        })
+                        .max_by_key(|(_, len)| *len)
+                        .map_or(DEFAULT_SYNTAX_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32))
+                })
+                .collect(),
+        )
+    }
+
+    pub fn get(&self, capture_id: u32) -> HighlightId {
+        self.0
+            .get(capture_id as usize)
+            .copied()
+            .unwrap_or(DEFAULT_SYNTAX_HIGHLIGHT_ID)
+    }
+}
+
+impl HighlightId {
+    pub fn is_default(&self) -> bool {
+        *self == DEFAULT_SYNTAX_HIGHLIGHT_ID
+    }
+
+    pub fn style(&self, theme: &SyntaxTheme) -> Option<HighlightStyle> {
+        theme.highlights.get(self.0 as usize).map(|entry| entry.1)
+    }
+
+    pub fn name<'a>(&self, theme: &'a SyntaxTheme) -> Option<&'a str> {
+        theme.highlights.get(self.0 as usize).map(|e| e.0.as_str())
+    }
+}
+
+impl Default for HighlightMap {
+    fn default() -> Self {
+        Self(Arc::new([]))
+    }
+}
+
+impl Default for HighlightId {
+    fn default() -> Self {
+        DEFAULT_SYNTAX_HIGHLIGHT_ID
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui2::rgba;
+
+    #[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 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 🔗

@@ -0,0 +1,1959 @@
+mod buffer;
+mod diagnostic_set;
+mod highlight_map;
+pub mod language_settings;
+mod outline;
+pub mod proto;
+mod syntax_map;
+
+#[cfg(test)]
+mod buffer_tests;
+
+use anyhow::{anyhow, Context, Result};
+use async_trait::async_trait;
+use collections::{HashMap, HashSet};
+use futures::{
+    channel::{mpsc, oneshot},
+    future::{BoxFuture, Shared},
+    FutureExt, TryFutureExt as _,
+};
+use gpui2::{AppContext, AsyncAppContext, Executor, Task};
+pub use highlight_map::HighlightMap;
+use lazy_static::lazy_static;
+use lsp2::{CodeActionKind, LanguageServerBinary};
+use parking_lot::{Mutex, RwLock};
+use postage::watch;
+use regex::Regex;
+use serde::{de, Deserialize, Deserializer};
+use serde_json::Value;
+use std::{
+    any::Any,
+    borrow::Cow,
+    cell::RefCell,
+    fmt::Debug,
+    hash::Hash,
+    mem,
+    ops::{Not, Range},
+    path::{Path, PathBuf},
+    str,
+    sync::{
+        atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+};
+use syntax_map::SyntaxSnapshot;
+use theme2::{SyntaxTheme, Theme};
+use tree_sitter::{self, Query};
+use unicase::UniCase;
+use util::{http::HttpClient, paths::PathExt};
+use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
+
+pub use buffer::Operation;
+pub use buffer::*;
+pub use diagnostic_set::DiagnosticEntry;
+pub use lsp2::LanguageServerId;
+pub use outline::{Outline, OutlineItem};
+pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo};
+pub use text::LineEnding;
+pub use tree_sitter::{Parser, Tree};
+
+pub fn init(cx: &mut AppContext) {
+    language_settings::init(cx);
+}
+
+#[derive(Clone, Default)]
+struct LspBinaryStatusSender {
+    txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(Arc<Language>, LanguageServerBinaryStatus)>>>>,
+}
+
+impl LspBinaryStatusSender {
+    fn subscribe(&self) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+        let (tx, rx) = mpsc::unbounded();
+        self.txs.lock().push(tx);
+        rx
+    }
+
+    fn send(&self, language: Arc<Language>, status: LanguageServerBinaryStatus) {
+        let mut txs = self.txs.lock();
+        txs.retain(|tx| {
+            tx.unbounded_send((language.clone(), status.clone()))
+                .is_ok()
+        });
+    }
+}
+
+thread_local! {
+    static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
+}
+
+lazy_static! {
+    pub static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default();
+    pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
+        LanguageConfig {
+            name: "Plain Text".into(),
+            ..Default::default()
+        },
+        None,
+    ));
+}
+
+pub trait ToLspPosition {
+    fn to_lsp_position(self) -> lsp2::Position;
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct LanguageServerName(pub Arc<str>);
+
+/// Represents a Language Server, with certain cached sync properties.
+/// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
+/// once at startup, and caches the results.
+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;
+        let language_ids = adapter.language_ids().await;
+
+        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),
+        })
+    }
+
+    pub async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        self.adapter.fetch_latest_server_version(delegate).await
+    }
+
+    pub fn will_fetch_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        self.adapter.will_fetch_server(delegate, cx)
+    }
+
+    pub fn will_start_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        self.adapter.will_start_server(delegate, cx)
+    }
+
+    pub async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        self.adapter
+            .fetch_server_binary(version, container_dir, delegate)
+            .await
+    }
+
+    pub async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        self.adapter
+            .cached_server_binary(container_dir, delegate)
+            .await
+    }
+
+    pub fn can_be_reinstalled(&self) -> bool {
+        self.adapter.can_be_reinstalled()
+    }
+
+    pub async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        self.adapter.installation_test_binary(container_dir).await
+    }
+
+    pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        self.adapter.code_action_kinds()
+    }
+
+    pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
+        self.adapter.workspace_configuration(cx)
+    }
+
+    pub fn process_diagnostics(&self, params: &mut lsp2::PublishDiagnosticsParams) {
+        self.adapter.process_diagnostics(params)
+    }
+
+    pub async fn process_completion(&self, completion_item: &mut lsp2::CompletionItem) {
+        self.adapter.process_completion(completion_item).await
+    }
+
+    pub async fn label_for_completion(
+        &self,
+        completion_item: &lsp2::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        self.adapter
+            .label_for_completion(completion_item, language)
+            .await
+    }
+
+    pub async fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp2::SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        self.adapter.label_for_symbol(name, kind, language).await
+    }
+
+    pub fn prettier_plugins(&self) -> &[&'static str] {
+        self.adapter.prettier_plugins()
+    }
+}
+
+pub trait LspAdapterDelegate: Send + Sync {
+    fn show_notification(&self, message: &str, cx: &mut AppContext);
+    fn http_client(&self) -> Arc<dyn HttpClient>;
+}
+
+#[async_trait]
+pub trait LspAdapter: 'static + Send + Sync {
+    async fn name(&self) -> LanguageServerName;
+
+    fn short_name(&self) -> &'static str;
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>>;
+
+    fn will_fetch_server(
+        &self,
+        _: &Arc<dyn LspAdapterDelegate>,
+        _: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        None
+    }
+
+    fn will_start_server(
+        &self,
+        _: &Arc<dyn LspAdapterDelegate>,
+        _: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        None
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary>;
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary>;
+
+    fn can_be_reinstalled(&self) -> bool {
+        true
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary>;
+
+    fn process_diagnostics(&self, _: &mut lsp2::PublishDiagnosticsParams) {}
+
+    async fn process_completion(&self, _: &mut lsp2::CompletionItem) {}
+
+    async fn label_for_completion(
+        &self,
+        _: &lsp2::CompletionItem,
+        _: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        None
+    }
+
+    async fn label_for_symbol(
+        &self,
+        _: &str,
+        _: lsp2::SymbolKind,
+        _: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        None
+    }
+
+    async fn initialization_options(&self) -> Option<Value> {
+        None
+    }
+
+    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+        futures::future::ready(serde_json::json!({})).boxed()
+    }
+
+    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        Some(vec![
+            CodeActionKind::EMPTY,
+            CodeActionKind::QUICKFIX,
+            CodeActionKind::REFACTOR,
+            CodeActionKind::REFACTOR_EXTRACT,
+            CodeActionKind::SOURCE,
+        ])
+    }
+
+    async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
+        Default::default()
+    }
+
+    async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
+        None
+    }
+
+    async fn language_ids(&self) -> HashMap<String, String> {
+        Default::default()
+    }
+
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &[]
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct CodeLabel {
+    pub text: String,
+    pub runs: Vec<(Range<usize>, HighlightId)>,
+    pub filter_range: Range<usize>,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct LanguageConfig {
+    pub name: Arc<str>,
+    pub path_suffixes: Vec<String>,
+    pub brackets: BracketPairConfig,
+    #[serde(default, deserialize_with = "deserialize_regex")]
+    pub first_line_pattern: Option<Regex>,
+    #[serde(default = "auto_indent_using_last_non_empty_line_default")]
+    pub auto_indent_using_last_non_empty_line: bool,
+    #[serde(default, deserialize_with = "deserialize_regex")]
+    pub increase_indent_pattern: Option<Regex>,
+    #[serde(default, deserialize_with = "deserialize_regex")]
+    pub decrease_indent_pattern: Option<Regex>,
+    #[serde(default)]
+    pub autoclose_before: String,
+    #[serde(default)]
+    pub line_comment: Option<Arc<str>>,
+    #[serde(default)]
+    pub collapsed_placeholder: String,
+    #[serde(default)]
+    pub block_comment: Option<(Arc<str>, Arc<str>)>,
+    #[serde(default)]
+    pub scope_opt_in_language_servers: Vec<String>,
+    #[serde(default)]
+    pub overrides: HashMap<String, LanguageConfigOverride>,
+    #[serde(default)]
+    pub word_characters: HashSet<char>,
+    #[serde(default)]
+    pub prettier_parser_name: Option<String>,
+}
+
+#[derive(Debug, Default)]
+pub struct LanguageQueries {
+    pub highlights: Option<Cow<'static, str>>,
+    pub brackets: Option<Cow<'static, str>>,
+    pub indents: Option<Cow<'static, str>>,
+    pub outline: Option<Cow<'static, str>>,
+    pub embedding: Option<Cow<'static, str>>,
+    pub injections: Option<Cow<'static, str>>,
+    pub overrides: Option<Cow<'static, str>>,
+}
+
+#[derive(Clone, Debug)]
+pub struct LanguageScope {
+    language: Arc<Language>,
+    override_id: Option<u32>,
+}
+
+#[derive(Clone, Deserialize, Default, Debug)]
+pub struct LanguageConfigOverride {
+    #[serde(default)]
+    pub line_comment: Override<Arc<str>>,
+    #[serde(default)]
+    pub block_comment: Override<(Arc<str>, Arc<str>)>,
+    #[serde(skip_deserializing)]
+    pub disabled_bracket_ixs: Vec<u16>,
+    #[serde(default)]
+    pub word_characters: Override<HashSet<char>>,
+    #[serde(default)]
+    pub opt_into_language_servers: Vec<String>,
+}
+
+#[derive(Clone, Deserialize, Debug)]
+#[serde(untagged)]
+pub enum Override<T> {
+    Remove { remove: bool },
+    Set(T),
+}
+
+impl<T> Default for Override<T> {
+    fn default() -> Self {
+        Override::Remove { remove: false }
+    }
+}
+
+impl<T> Override<T> {
+    fn as_option<'a>(this: Option<&'a Self>, original: Option<&'a T>) -> Option<&'a T> {
+        match this {
+            Some(Self::Set(value)) => Some(value),
+            Some(Self::Remove { remove: true }) => None,
+            Some(Self::Remove { remove: false }) | None => original,
+        }
+    }
+}
+
+impl Default for LanguageConfig {
+    fn default() -> Self {
+        Self {
+            name: "".into(),
+            path_suffixes: Default::default(),
+            brackets: Default::default(),
+            auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
+            first_line_pattern: Default::default(),
+            increase_indent_pattern: Default::default(),
+            decrease_indent_pattern: Default::default(),
+            autoclose_before: Default::default(),
+            line_comment: Default::default(),
+            block_comment: Default::default(),
+            scope_opt_in_language_servers: Default::default(),
+            overrides: Default::default(),
+            collapsed_placeholder: Default::default(),
+            word_characters: Default::default(),
+            prettier_parser_name: None,
+        }
+    }
+}
+
+fn auto_indent_using_last_non_empty_line_default() -> bool {
+    true
+}
+
+fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D::Error> {
+    let source = Option::<String>::deserialize(d)?;
+    if let Some(source) = source {
+        Ok(Some(regex::Regex::new(&source).map_err(de::Error::custom)?))
+    } else {
+        Ok(None)
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct FakeLspAdapter {
+    pub name: &'static str,
+    pub initialization_options: Option<Value>,
+    pub capabilities: lsp2::ServerCapabilities,
+    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 prettier_plugins: Vec<&'static str>,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct BracketPairConfig {
+    pub pairs: Vec<BracketPair>,
+    pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
+}
+
+impl<'de> Deserialize<'de> for BracketPairConfig {
+    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        #[derive(Deserialize)]
+        pub struct Entry {
+            #[serde(flatten)]
+            pub bracket_pair: BracketPair,
+            #[serde(default)]
+            pub not_in: Vec<String>,
+        }
+
+        let result = Vec::<Entry>::deserialize(deserializer)?;
+        let mut brackets = Vec::with_capacity(result.len());
+        let mut disabled_scopes_by_bracket_ix = Vec::with_capacity(result.len());
+        for entry in result {
+            brackets.push(entry.bracket_pair);
+            disabled_scopes_by_bracket_ix.push(entry.not_in);
+        }
+
+        Ok(BracketPairConfig {
+            pairs: brackets,
+            disabled_scopes_by_bracket_ix,
+        })
+    }
+}
+
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+pub struct BracketPair {
+    pub start: String,
+    pub end: String,
+    pub close: bool,
+    pub newline: bool,
+}
+
+pub struct Language {
+    pub(crate) config: LanguageConfig,
+    pub(crate) grammar: Option<Arc<Grammar>>,
+    pub(crate) adapters: Vec<Arc<CachedLspAdapter>>,
+
+    #[cfg(any(test, feature = "test-support"))]
+    fake_adapter: Option<(
+        mpsc::UnboundedSender<lsp2::FakeLanguageServer>,
+        Arc<FakeLspAdapter>,
+    )>,
+}
+
+pub struct Grammar {
+    id: usize,
+    pub ts_language: tree_sitter::Language,
+    pub(crate) error_query: Query,
+    pub(crate) highlights_query: Option<Query>,
+    pub(crate) brackets_config: Option<BracketConfig>,
+    pub(crate) indents_config: Option<IndentConfig>,
+    pub outline_config: Option<OutlineConfig>,
+    pub embedding_config: Option<EmbeddingConfig>,
+    pub(crate) injection_config: Option<InjectionConfig>,
+    pub(crate) override_config: Option<OverrideConfig>,
+    pub(crate) highlight_map: Mutex<HighlightMap>,
+}
+
+struct IndentConfig {
+    query: Query,
+    indent_capture_ix: u32,
+    start_capture_ix: Option<u32>,
+    end_capture_ix: Option<u32>,
+    outdent_capture_ix: Option<u32>,
+}
+
+pub struct OutlineConfig {
+    pub query: Query,
+    pub item_capture_ix: u32,
+    pub name_capture_ix: u32,
+    pub context_capture_ix: Option<u32>,
+    pub extra_context_capture_ix: Option<u32>,
+}
+
+#[derive(Debug)]
+pub struct EmbeddingConfig {
+    pub query: Query,
+    pub item_capture_ix: u32,
+    pub name_capture_ix: Option<u32>,
+    pub context_capture_ix: Option<u32>,
+    pub collapse_capture_ix: Option<u32>,
+    pub keep_capture_ix: Option<u32>,
+}
+
+struct InjectionConfig {
+    query: Query,
+    content_capture_ix: u32,
+    language_capture_ix: Option<u32>,
+    patterns: Vec<InjectionPatternConfig>,
+}
+
+struct OverrideConfig {
+    query: Query,
+    values: HashMap<u32, (String, LanguageConfigOverride)>,
+}
+
+#[derive(Default, Clone)]
+struct InjectionPatternConfig {
+    language: Option<Box<str>>,
+    combined: bool,
+}
+
+struct BracketConfig {
+    query: Query,
+    open_capture_ix: u32,
+    close_capture_ix: u32,
+}
+
+#[derive(Clone)]
+pub enum LanguageServerBinaryStatus {
+    CheckingForUpdate,
+    Downloading,
+    Downloaded,
+    Cached,
+    Failed { error: String },
+}
+
+type AvailableLanguageId = usize;
+
+#[derive(Clone)]
+struct AvailableLanguage {
+    id: AvailableLanguageId,
+    path: &'static str,
+    config: LanguageConfig,
+    grammar: tree_sitter::Language,
+    lsp_adapters: Vec<Arc<dyn LspAdapter>>,
+    get_queries: fn(&str) -> LanguageQueries,
+    loaded: bool,
+}
+
+pub struct LanguageRegistry {
+    state: RwLock<LanguageRegistryState>,
+    language_server_download_dir: Option<Arc<Path>>,
+    login_shell_env_loaded: Shared<Task<()>>,
+    #[allow(clippy::type_complexity)]
+    lsp_binary_paths: Mutex<
+        HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
+    >,
+    executor: Option<Executor>,
+    lsp_binary_status_tx: LspBinaryStatusSender,
+}
+
+struct LanguageRegistryState {
+    next_language_server_id: usize,
+    languages: Vec<Arc<Language>>,
+    available_languages: Vec<AvailableLanguage>,
+    next_available_language_id: AvailableLanguageId,
+    loading_languages: HashMap<AvailableLanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
+    subscription: (watch::Sender<()>, watch::Receiver<()>),
+    theme: Option<Arc<Theme>>,
+    version: usize,
+    reload_count: usize,
+}
+
+pub struct PendingLanguageServer {
+    pub server_id: LanguageServerId,
+    pub task: Task<Result<lsp2::LanguageServer>>,
+    pub container_dir: Option<Arc<Path>>,
+}
+
+impl LanguageRegistry {
+    pub fn new(login_shell_env_loaded: Task<()>) -> Self {
+        Self {
+            state: RwLock::new(LanguageRegistryState {
+                next_language_server_id: 0,
+                languages: vec![PLAIN_TEXT.clone()],
+                available_languages: Default::default(),
+                next_available_language_id: 0,
+                loading_languages: Default::default(),
+                subscription: watch::channel(),
+                theme: Default::default(),
+                version: 0,
+                reload_count: 0,
+            }),
+            language_server_download_dir: None,
+            login_shell_env_loaded: login_shell_env_loaded.shared(),
+            lsp_binary_paths: Default::default(),
+            executor: None,
+            lsp_binary_status_tx: Default::default(),
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test() -> Self {
+        Self::new(Task::ready(()))
+    }
+
+    pub fn set_executor(&mut self, executor: Executor) {
+        self.executor = Some(executor);
+    }
+
+    /// Clear out all of the loaded languages and reload them from scratch.
+    ///
+    /// This is useful in development, when queries have changed.
+    #[cfg(debug_assertions)]
+    pub fn reload(&self) {
+        self.state.write().reload();
+    }
+
+    pub fn register(
+        &self,
+        path: &'static str,
+        config: LanguageConfig,
+        grammar: tree_sitter::Language,
+        lsp_adapters: Vec<Arc<dyn LspAdapter>>,
+        get_queries: fn(&str) -> LanguageQueries,
+    ) {
+        let state = &mut *self.state.write();
+        state.available_languages.push(AvailableLanguage {
+            id: post_inc(&mut state.next_available_language_id),
+            path,
+            config,
+            grammar,
+            lsp_adapters,
+            get_queries,
+            loaded: false,
+        });
+    }
+
+    pub fn language_names(&self) -> Vec<String> {
+        let state = self.state.read();
+        let mut result = state
+            .available_languages
+            .iter()
+            .filter_map(|l| l.loaded.not().then_some(l.config.name.to_string()))
+            .chain(state.languages.iter().map(|l| l.config.name.to_string()))
+            .collect::<Vec<_>>();
+        result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
+        result
+    }
+
+    pub fn add(&self, language: Arc<Language>) {
+        self.state.write().add(language);
+    }
+
+    pub fn subscribe(&self) -> watch::Receiver<()> {
+        self.state.read().subscription.1.clone()
+    }
+
+    /// The number of times that the registry has been changed,
+    /// by adding languages or reloading.
+    pub fn version(&self) -> usize {
+        self.state.read().version
+    }
+
+    /// The number of times that the registry has been reloaded.
+    pub fn reload_count(&self) -> usize {
+        self.state.read().reload_count
+    }
+
+    pub fn set_theme(&self, theme: Arc<Theme>) {
+        let mut state = self.state.write();
+        state.theme = Some(theme.clone());
+        for language in &state.languages {
+            language.set_theme(&theme.syntax);
+        }
+    }
+
+    pub fn set_language_server_download_dir(&mut self, path: impl Into<Arc<Path>>) {
+        self.language_server_download_dir = Some(path.into());
+    }
+
+    pub fn language_for_name(
+        self: &Arc<Self>,
+        name: &str,
+    ) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
+        let name = UniCase::new(name);
+        self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name)
+    }
+
+    pub fn language_for_name_or_extension(
+        self: &Arc<Self>,
+        string: &str,
+    ) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
+        let string = UniCase::new(string);
+        self.get_or_load_language(|config| {
+            UniCase::new(config.name.as_ref()) == string
+                || config
+                    .path_suffixes
+                    .iter()
+                    .any(|suffix| UniCase::new(suffix) == string)
+        })
+    }
+
+    pub fn language_for_file(
+        self: &Arc<Self>,
+        path: impl AsRef<Path>,
+        content: Option<&Rope>,
+    ) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
+        let path = path.as_ref();
+        let filename = path.file_name().and_then(|name| name.to_str());
+        let extension = path.extension_or_hidden_file_name();
+        let path_suffixes = [extension, filename];
+        self.get_or_load_language(|config| {
+            let path_matches = config
+                .path_suffixes
+                .iter()
+                .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
+            let content_matches = content.zip(config.first_line_pattern.as_ref()).map_or(
+                false,
+                |(content, pattern)| {
+                    let end = content.clip_point(Point::new(0, 256), Bias::Left);
+                    let end = content.point_to_offset(end);
+                    let text = content.chunks_in_range(0..end).collect::<String>();
+                    pattern.is_match(&text)
+                },
+            );
+            path_matches || content_matches
+        })
+    }
+
+    fn get_or_load_language(
+        self: &Arc<Self>,
+        callback: impl Fn(&LanguageConfig) -> bool,
+    ) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
+        let (tx, rx) = oneshot::channel();
+
+        let mut state = self.state.write();
+        if let Some(language) = state
+            .languages
+            .iter()
+            .find(|language| callback(&language.config))
+        {
+            let _ = tx.send(Ok(language.clone()));
+        } else if let Some(executor) = self.executor.clone() {
+            if let Some(language) = state
+                .available_languages
+                .iter()
+                .find(|l| !l.loaded && callback(&l.config))
+                .cloned()
+            {
+                let txs = state
+                    .loading_languages
+                    .entry(language.id)
+                    .or_insert_with(|| {
+                        let this = self.clone();
+                        executor
+                            .spawn(async move {
+                                let id = language.id;
+                                let queries = (language.get_queries)(&language.path);
+                                let language =
+                                    Language::new(language.config, Some(language.grammar))
+                                        .with_lsp_adapters(language.lsp_adapters)
+                                        .await;
+                                let name = language.name();
+                                match language.with_queries(queries) {
+                                    Ok(language) => {
+                                        let language = Arc::new(language);
+                                        let mut state = this.state.write();
+
+                                        state.add(language.clone());
+                                        state.mark_language_loaded(id);
+                                        if let Some(mut txs) = state.loading_languages.remove(&id) {
+                                            for tx in txs.drain(..) {
+                                                let _ = tx.send(Ok(language.clone()));
+                                            }
+                                        }
+                                    }
+                                    Err(e) => {
+                                        log::error!("failed to load language {name}:\n{:?}", e);
+                                        let mut state = this.state.write();
+                                        state.mark_language_loaded(id);
+                                        if let Some(mut txs) = state.loading_languages.remove(&id) {
+                                            for tx in txs.drain(..) {
+                                                let _ = tx.send(Err(anyhow!(
+                                                    "failed to load language {}: {}",
+                                                    name,
+                                                    e
+                                                )));
+                                            }
+                                        }
+                                    }
+                                };
+                            })
+                            .detach();
+
+                        Vec::new()
+                    });
+                txs.push(tx);
+            } else {
+                let _ = tx.send(Err(anyhow!("language not found")));
+            }
+        } else {
+            let _ = tx.send(Err(anyhow!("executor does not exist")));
+        }
+
+        rx.unwrap()
+    }
+
+    pub fn to_vec(&self) -> Vec<Arc<Language>> {
+        self.state.read().languages.iter().cloned().collect()
+    }
+
+    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>,
+        delegate: Arc<dyn LspAdapterDelegate>,
+        cx: &mut AppContext,
+    ) -> Option<PendingLanguageServer> {
+        let server_id = self.state.write().next_language_server_id();
+        log::info!(
+            "starting language server {:?}, path: {root_path:?}, id: {server_id}",
+            adapter.name.0
+        );
+
+        #[cfg(any(test, feature = "test-support"))]
+        if language.fake_adapter.is_some() {
+            let task = cx.spawn(|cx| async move {
+                let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap();
+                let (server, mut fake_server) = lsp2::LanguageServer::fake(
+                    fake_adapter.name.to_string(),
+                    fake_adapter.capabilities.clone(),
+                    cx.clone(),
+                );
+
+                if let Some(initializer) = &fake_adapter.initializer {
+                    initializer(&mut fake_server);
+                }
+
+                let servers_tx = servers_tx.clone();
+                cx.executor()
+                    .spawn(async move {
+                        if fake_server
+                            .try_receive_notification::<lsp2::notification::Initialized>()
+                            .await
+                            .is_some()
+                        {
+                            servers_tx.unbounded_send(fake_server).ok();
+                        }
+                    })
+                    .detach();
+
+                Ok(server)
+            });
+
+            return Some(PendingLanguageServer {
+                server_id,
+                task,
+                container_dir: None,
+            });
+        }
+
+        let download_dir = self
+            .language_server_download_dir
+            .clone()
+            .ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server"))
+            .log_err()?;
+        let this = self.clone();
+        let language = language.clone();
+        let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
+        let root_path = root_path.clone();
+        let adapter = adapter.clone();
+        let login_shell_env_loaded = self.login_shell_env_loaded.clone();
+        let lsp_binary_statuses = self.lsp_binary_status_tx.clone();
+
+        let task = {
+            let container_dir = container_dir.clone();
+            cx.spawn(move |mut cx| async move {
+                login_shell_env_loaded.await;
+
+                let entry = this
+                    .lsp_binary_paths
+                    .lock()
+                    .entry(adapter.name.clone())
+                    .or_insert_with(|| {
+                        let adapter = adapter.clone();
+                        let language = language.clone();
+                        let delegate = delegate.clone();
+                        cx.spawn(|cx| {
+                            get_binary(
+                                adapter,
+                                language,
+                                delegate,
+                                container_dir,
+                                lsp_binary_statuses,
+                                cx,
+                            )
+                            .map_err(Arc::new)
+                        })
+                        .shared()
+                    })
+                    .clone();
+
+                let binary = match entry.await {
+                    Ok(binary) => binary,
+                    Err(err) => anyhow::bail!("{err}"),
+                };
+
+                if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
+                    task.await?;
+                }
+
+                lsp2::LanguageServer::new(
+                    stderr_capture,
+                    server_id,
+                    binary,
+                    &root_path,
+                    adapter.code_action_kinds(),
+                    cx,
+                )
+            })
+        };
+
+        Some(PendingLanguageServer {
+            server_id,
+            task,
+            container_dir: Some(container_dir),
+        })
+    }
+
+    pub fn language_server_binary_statuses(
+        &self,
+    ) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+        self.lsp_binary_status_tx.subscribe()
+    }
+
+    pub fn delete_server_container(
+        &self,
+        adapter: Arc<CachedLspAdapter>,
+        cx: &mut AppContext,
+    ) -> Task<()> {
+        log::info!("deleting server container");
+
+        let mut lock = self.lsp_binary_paths.lock();
+        lock.remove(&adapter.name);
+
+        let download_dir = self
+            .language_server_download_dir
+            .clone()
+            .expect("language server download directory has not been assigned before deleting server container");
+
+        cx.spawn(|_| async move {
+            let container_dir = download_dir.join(adapter.name.0.as_ref());
+            smol::fs::remove_dir_all(container_dir)
+                .await
+                .context("server container removal")
+                .log_err();
+        })
+    }
+
+    pub fn next_language_server_id(&self) -> LanguageServerId {
+        self.state.write().next_language_server_id()
+    }
+}
+
+impl LanguageRegistryState {
+    fn next_language_server_id(&mut self) -> LanguageServerId {
+        LanguageServerId(post_inc(&mut self.next_language_server_id))
+    }
+
+    fn add(&mut self, language: Arc<Language>) {
+        if let Some(theme) = self.theme.as_ref() {
+            language.set_theme(&theme.syntax);
+        }
+        self.languages.push(language);
+        self.version += 1;
+        *self.subscription.0.borrow_mut() = ();
+    }
+
+    #[cfg(debug_assertions)]
+    fn reload(&mut self) {
+        self.languages.clear();
+        self.version += 1;
+        self.reload_count += 1;
+        for language in &mut self.available_languages {
+            language.loaded = false;
+        }
+        *self.subscription.0.borrow_mut() = ();
+    }
+
+    /// Mark the given language a having been loaded, so that the
+    /// language registry won't try to load it again.
+    fn mark_language_loaded(&mut self, id: AvailableLanguageId) {
+        for language in &mut self.available_languages {
+            if language.id == id {
+                language.loaded = true;
+                break;
+            }
+        }
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl Default for LanguageRegistry {
+    fn default() -> Self {
+        Self::test()
+    }
+}
+
+async fn get_binary(
+    adapter: Arc<CachedLspAdapter>,
+    language: Arc<Language>,
+    delegate: Arc<dyn LspAdapterDelegate>,
+    container_dir: Arc<Path>,
+    statuses: LspBinaryStatusSender,
+    mut cx: AsyncAppContext,
+) -> Result<LanguageServerBinary> {
+    if !container_dir.exists() {
+        smol::fs::create_dir_all(&container_dir)
+            .await
+            .context("failed to create container directory")?;
+    }
+
+    if let Some(task) = adapter.will_fetch_server(&delegate, &mut cx) {
+        task.await?;
+    }
+
+    let binary = fetch_latest_binary(
+        adapter.clone(),
+        language.clone(),
+        delegate.as_ref(),
+        &container_dir,
+        statuses.clone(),
+    )
+    .await;
+
+    if let Err(error) = binary.as_ref() {
+        if let Some(binary) = adapter
+            .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
+            .await
+        {
+            statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
+            return Ok(binary);
+        } else {
+            statuses.send(
+                language.clone(),
+                LanguageServerBinaryStatus::Failed {
+                    error: format!("{:?}", error),
+                },
+            );
+        }
+    }
+
+    binary
+}
+
+async fn fetch_latest_binary(
+    adapter: Arc<CachedLspAdapter>,
+    language: Arc<Language>,
+    delegate: &dyn LspAdapterDelegate,
+    container_dir: &Path,
+    lsp_binary_statuses_tx: LspBinaryStatusSender,
+) -> Result<LanguageServerBinary> {
+    let container_dir: Arc<Path> = container_dir.into();
+    lsp_binary_statuses_tx.send(
+        language.clone(),
+        LanguageServerBinaryStatus::CheckingForUpdate,
+    );
+
+    let version_info = adapter.fetch_latest_server_version(delegate).await?;
+    lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);
+
+    let binary = adapter
+        .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
+        .await?;
+    lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloaded);
+
+    Ok(binary)
+}
+
+impl Language {
+    pub fn new(config: LanguageConfig, ts_language: Option<tree_sitter::Language>) -> Self {
+        Self {
+            config,
+            grammar: ts_language.map(|ts_language| {
+                Arc::new(Grammar {
+                    id: NEXT_GRAMMAR_ID.fetch_add(1, SeqCst),
+                    highlights_query: None,
+                    brackets_config: None,
+                    outline_config: None,
+                    embedding_config: None,
+                    indents_config: None,
+                    injection_config: None,
+                    override_config: None,
+                    error_query: Query::new(ts_language, "(ERROR) @error").unwrap(),
+                    ts_language,
+                    highlight_map: Default::default(),
+                })
+            }),
+            adapters: Vec::new(),
+
+            #[cfg(any(test, feature = "test-support"))]
+            fake_adapter: None,
+        }
+    }
+
+    pub fn lsp_adapters(&self) -> &[Arc<CachedLspAdapter>] {
+        &self.adapters
+    }
+
+    pub fn id(&self) -> Option<usize> {
+        self.grammar.as_ref().map(|g| g.id)
+    }
+
+    pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
+        if let Some(query) = queries.highlights {
+            self = self
+                .with_highlights_query(query.as_ref())
+                .context("Error loading highlights query")?;
+        }
+        if let Some(query) = queries.brackets {
+            self = self
+                .with_brackets_query(query.as_ref())
+                .context("Error loading brackets query")?;
+        }
+        if let Some(query) = queries.indents {
+            self = self
+                .with_indents_query(query.as_ref())
+                .context("Error loading indents query")?;
+        }
+        if let Some(query) = queries.outline {
+            self = self
+                .with_outline_query(query.as_ref())
+                .context("Error loading outline query")?;
+        }
+        if let Some(query) = queries.embedding {
+            self = self
+                .with_embedding_query(query.as_ref())
+                .context("Error loading embedding query")?;
+        }
+        if let Some(query) = queries.injections {
+            self = self
+                .with_injection_query(query.as_ref())
+                .context("Error loading injection query")?;
+        }
+        if let Some(query) = queries.overrides {
+            self = self
+                .with_override_query(query.as_ref())
+                .context("Error loading override query")?;
+        }
+        Ok(self)
+    }
+
+    pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
+        let grammar = self.grammar_mut();
+        grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?);
+        Ok(self)
+    }
+
+    pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
+        let grammar = self.grammar_mut();
+        let query = Query::new(grammar.ts_language, source)?;
+        let mut item_capture_ix = None;
+        let mut name_capture_ix = None;
+        let mut context_capture_ix = None;
+        let mut extra_context_capture_ix = None;
+        get_capture_indices(
+            &query,
+            &mut [
+                ("item", &mut item_capture_ix),
+                ("name", &mut name_capture_ix),
+                ("context", &mut context_capture_ix),
+                ("context.extra", &mut extra_context_capture_ix),
+            ],
+        );
+        if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
+            grammar.outline_config = Some(OutlineConfig {
+                query,
+                item_capture_ix,
+                name_capture_ix,
+                context_capture_ix,
+                extra_context_capture_ix,
+            });
+        }
+        Ok(self)
+    }
+
+    pub fn with_embedding_query(mut self, source: &str) -> Result<Self> {
+        let grammar = self.grammar_mut();
+        let query = Query::new(grammar.ts_language, source)?;
+        let mut item_capture_ix = None;
+        let mut name_capture_ix = None;
+        let mut context_capture_ix = None;
+        let mut collapse_capture_ix = None;
+        let mut keep_capture_ix = None;
+        get_capture_indices(
+            &query,
+            &mut [
+                ("item", &mut item_capture_ix),
+                ("name", &mut name_capture_ix),
+                ("context", &mut context_capture_ix),
+                ("keep", &mut keep_capture_ix),
+                ("collapse", &mut collapse_capture_ix),
+            ],
+        );
+        if let Some(item_capture_ix) = item_capture_ix {
+            grammar.embedding_config = Some(EmbeddingConfig {
+                query,
+                item_capture_ix,
+                name_capture_ix,
+                context_capture_ix,
+                collapse_capture_ix,
+                keep_capture_ix,
+            });
+        }
+        Ok(self)
+    }
+
+    pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
+        let grammar = self.grammar_mut();
+        let query = Query::new(grammar.ts_language, source)?;
+        let mut open_capture_ix = None;
+        let mut close_capture_ix = None;
+        get_capture_indices(
+            &query,
+            &mut [
+                ("open", &mut open_capture_ix),
+                ("close", &mut close_capture_ix),
+            ],
+        );
+        if let Some((open_capture_ix, close_capture_ix)) = open_capture_ix.zip(close_capture_ix) {
+            grammar.brackets_config = Some(BracketConfig {
+                query,
+                open_capture_ix,
+                close_capture_ix,
+            });
+        }
+        Ok(self)
+    }
+
+    pub fn with_indents_query(mut self, source: &str) -> Result<Self> {
+        let grammar = self.grammar_mut();
+        let query = Query::new(grammar.ts_language, source)?;
+        let mut indent_capture_ix = None;
+        let mut start_capture_ix = None;
+        let mut end_capture_ix = None;
+        let mut outdent_capture_ix = None;
+        get_capture_indices(
+            &query,
+            &mut [
+                ("indent", &mut indent_capture_ix),
+                ("start", &mut start_capture_ix),
+                ("end", &mut end_capture_ix),
+                ("outdent", &mut outdent_capture_ix),
+            ],
+        );
+        if let Some(indent_capture_ix) = indent_capture_ix {
+            grammar.indents_config = Some(IndentConfig {
+                query,
+                indent_capture_ix,
+                start_capture_ix,
+                end_capture_ix,
+                outdent_capture_ix,
+            });
+        }
+        Ok(self)
+    }
+
+    pub fn with_injection_query(mut self, source: &str) -> Result<Self> {
+        let grammar = self.grammar_mut();
+        let query = Query::new(grammar.ts_language, source)?;
+        let mut language_capture_ix = None;
+        let mut content_capture_ix = None;
+        get_capture_indices(
+            &query,
+            &mut [
+                ("language", &mut language_capture_ix),
+                ("content", &mut content_capture_ix),
+            ],
+        );
+        let patterns = (0..query.pattern_count())
+            .map(|ix| {
+                let mut config = InjectionPatternConfig::default();
+                for setting in query.property_settings(ix) {
+                    match setting.key.as_ref() {
+                        "language" => {
+                            config.language = setting.value.clone();
+                        }
+                        "combined" => {
+                            config.combined = true;
+                        }
+                        _ => {}
+                    }
+                }
+                config
+            })
+            .collect();
+        if let Some(content_capture_ix) = content_capture_ix {
+            grammar.injection_config = Some(InjectionConfig {
+                query,
+                language_capture_ix,
+                content_capture_ix,
+                patterns,
+            });
+        }
+        Ok(self)
+    }
+
+    pub fn with_override_query(mut self, source: &str) -> anyhow::Result<Self> {
+        let query = Query::new(self.grammar_mut().ts_language, source)?;
+
+        let mut override_configs_by_id = HashMap::default();
+        for (ix, name) in query.capture_names().iter().enumerate() {
+            if !name.starts_with('_') {
+                let value = self.config.overrides.remove(name).unwrap_or_default();
+                for server_name in &value.opt_into_language_servers {
+                    if !self
+                        .config
+                        .scope_opt_in_language_servers
+                        .contains(server_name)
+                    {
+                        util::debug_panic!("Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server");
+                    }
+                }
+
+                override_configs_by_id.insert(ix as u32, (name.clone(), value));
+            }
+        }
+
+        if !self.config.overrides.is_empty() {
+            let keys = self.config.overrides.keys().collect::<Vec<_>>();
+            Err(anyhow!(
+                "language {:?} has overrides in config not in query: {keys:?}",
+                self.config.name
+            ))?;
+        }
+
+        for disabled_scope_name in self
+            .config
+            .brackets
+            .disabled_scopes_by_bracket_ix
+            .iter()
+            .flatten()
+        {
+            if !override_configs_by_id
+                .values()
+                .any(|(scope_name, _)| scope_name == disabled_scope_name)
+            {
+                Err(anyhow!(
+                    "language {:?} has overrides in config not in query: {disabled_scope_name:?}",
+                    self.config.name
+                ))?;
+            }
+        }
+
+        for (name, override_config) in override_configs_by_id.values_mut() {
+            override_config.disabled_bracket_ixs = self
+                .config
+                .brackets
+                .disabled_scopes_by_bracket_ix
+                .iter()
+                .enumerate()
+                .filter_map(|(ix, disabled_scope_names)| {
+                    if disabled_scope_names.contains(name) {
+                        Some(ix as u16)
+                    } else {
+                        None
+                    }
+                })
+                .collect();
+        }
+
+        self.config.brackets.disabled_scopes_by_bracket_ix.clear();
+        self.grammar_mut().override_config = Some(OverrideConfig {
+            query,
+            values: override_configs_by_id,
+        });
+        Ok(self)
+    }
+
+    fn grammar_mut(&mut self) -> &mut Grammar {
+        Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
+    }
+
+    pub async fn with_lsp_adapters(mut self, lsp_adapters: Vec<Arc<dyn LspAdapter>>) -> Self {
+        for adapter in lsp_adapters {
+            self.adapters.push(CachedLspAdapter::new(adapter).await);
+        }
+        self
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub async fn set_fake_lsp_adapter(
+        &mut self,
+        fake_lsp_adapter: Arc<FakeLspAdapter>,
+    ) -> mpsc::UnboundedReceiver<lsp2::FakeLanguageServer> {
+        let (servers_tx, servers_rx) = mpsc::unbounded();
+        self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone()));
+        let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await;
+        self.adapters = vec![adapter];
+        servers_rx
+    }
+
+    pub fn name(&self) -> Arc<str> {
+        self.config.name.clone()
+    }
+
+    pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
+        match self.adapters.first().as_ref() {
+            Some(adapter) => &adapter.disk_based_diagnostic_sources,
+            None => &[],
+        }
+    }
+
+    pub async fn disk_based_diagnostics_progress_token(&self) -> Option<&str> {
+        for adapter in &self.adapters {
+            let token = adapter.disk_based_diagnostics_progress_token.as_deref();
+            if token.is_some() {
+                return token;
+            }
+        }
+
+        None
+    }
+
+    pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp2::CompletionItem) {
+        for adapter in &self.adapters {
+            adapter.process_completion(completion).await;
+        }
+    }
+
+    pub async fn label_for_completion(
+        self: &Arc<Self>,
+        completion: &lsp2::CompletionItem,
+    ) -> Option<CodeLabel> {
+        self.adapters
+            .first()
+            .as_ref()?
+            .label_for_completion(completion, self)
+            .await
+    }
+
+    pub async fn label_for_symbol(
+        self: &Arc<Self>,
+        name: &str,
+        kind: lsp2::SymbolKind,
+    ) -> Option<CodeLabel> {
+        self.adapters
+            .first()
+            .as_ref()?
+            .label_for_symbol(name, kind, self)
+            .await
+    }
+
+    pub fn highlight_text<'a>(
+        self: &'a Arc<Self>,
+        text: &'a Rope,
+        range: Range<usize>,
+    ) -> Vec<(Range<usize>, HighlightId)> {
+        let mut result = Vec::new();
+        if let Some(grammar) = &self.grammar {
+            let tree = grammar.parse_text(text, None);
+            let captures =
+                SyntaxSnapshot::single_tree_captures(range.clone(), text, &tree, self, |grammar| {
+                    grammar.highlights_query.as_ref()
+                });
+            let highlight_maps = vec![grammar.highlight_map()];
+            let mut offset = 0;
+            for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), vec![]) {
+                let end_offset = offset + chunk.text.len();
+                if let Some(highlight_id) = chunk.syntax_highlight_id {
+                    if !highlight_id.is_default() {
+                        result.push((offset..end_offset, highlight_id));
+                    }
+                }
+                offset = end_offset;
+            }
+        }
+        result
+    }
+
+    pub fn path_suffixes(&self) -> &[String] {
+        &self.config.path_suffixes
+    }
+
+    pub fn should_autoclose_before(&self, c: char) -> bool {
+        c.is_whitespace() || self.config.autoclose_before.contains(c)
+    }
+
+    pub fn set_theme(&self, theme: &SyntaxTheme) {
+        if let Some(grammar) = self.grammar.as_ref() {
+            if let Some(highlights_query) = &grammar.highlights_query {
+                *grammar.highlight_map.lock() =
+                    HighlightMap::new(highlights_query.capture_names(), theme);
+            }
+        }
+    }
+
+    pub fn grammar(&self) -> Option<&Arc<Grammar>> {
+        self.grammar.as_ref()
+    }
+
+    pub fn default_scope(self: &Arc<Self>) -> LanguageScope {
+        LanguageScope {
+            language: self.clone(),
+            override_id: None,
+        }
+    }
+
+    pub fn prettier_parser_name(&self) -> Option<&str> {
+        self.config.prettier_parser_name.as_deref()
+    }
+}
+
+impl LanguageScope {
+    pub fn collapsed_placeholder(&self) -> &str {
+        self.language.config.collapsed_placeholder.as_ref()
+    }
+
+    pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
+        Override::as_option(
+            self.config_override().map(|o| &o.line_comment),
+            self.language.config.line_comment.as_ref(),
+        )
+    }
+
+    pub fn block_comment_delimiters(&self) -> Option<(&Arc<str>, &Arc<str>)> {
+        Override::as_option(
+            self.config_override().map(|o| &o.block_comment),
+            self.language.config.block_comment.as_ref(),
+        )
+        .map(|e| (&e.0, &e.1))
+    }
+
+    pub fn word_characters(&self) -> Option<&HashSet<char>> {
+        Override::as_option(
+            self.config_override().map(|o| &o.word_characters),
+            Some(&self.language.config.word_characters),
+        )
+    }
+
+    pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {
+        let mut disabled_ids = self
+            .config_override()
+            .map_or(&[] as _, |o| o.disabled_bracket_ixs.as_slice());
+        self.language
+            .config
+            .brackets
+            .pairs
+            .iter()
+            .enumerate()
+            .map(move |(ix, bracket)| {
+                let mut is_enabled = true;
+                if let Some(next_disabled_ix) = disabled_ids.first() {
+                    if ix == *next_disabled_ix as usize {
+                        disabled_ids = &disabled_ids[1..];
+                        is_enabled = false;
+                    }
+                }
+                (bracket, is_enabled)
+            })
+    }
+
+    pub fn should_autoclose_before(&self, c: char) -> bool {
+        c.is_whitespace() || self.language.config.autoclose_before.contains(c)
+    }
+
+    pub fn language_allowed(&self, name: &LanguageServerName) -> bool {
+        let config = &self.language.config;
+        let opt_in_servers = &config.scope_opt_in_language_servers;
+        if opt_in_servers.iter().any(|o| *o == *name.0) {
+            if let Some(over) = self.config_override() {
+                over.opt_into_language_servers.iter().any(|o| *o == *name.0)
+            } else {
+                false
+            }
+        } else {
+            true
+        }
+    }
+
+    fn config_override(&self) -> Option<&LanguageConfigOverride> {
+        let id = self.override_id?;
+        let grammar = self.language.grammar.as_ref()?;
+        let override_config = grammar.override_config.as_ref()?;
+        override_config.values.get(&id).map(|e| &e.1)
+    }
+}
+
+impl Hash for Language {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.id().hash(state)
+    }
+}
+
+impl PartialEq for Language {
+    fn eq(&self, other: &Self) -> bool {
+        self.id().eq(&other.id())
+    }
+}
+
+impl Eq for Language {}
+
+impl Debug for Language {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Language")
+            .field("name", &self.config.name)
+            .finish()
+    }
+}
+
+impl Grammar {
+    pub fn id(&self) -> usize {
+        self.id
+    }
+
+    fn parse_text(&self, text: &Rope, old_tree: Option<Tree>) -> Tree {
+        PARSER.with(|parser| {
+            let mut parser = parser.borrow_mut();
+            parser
+                .set_language(self.ts_language)
+                .expect("incompatible grammar");
+            let mut chunks = text.chunks_in_range(0..text.len());
+            parser
+                .parse_with(
+                    &mut move |offset, _| {
+                        chunks.seek(offset);
+                        chunks.next().unwrap_or("").as_bytes()
+                    },
+                    old_tree.as_ref(),
+                )
+                .unwrap()
+        })
+    }
+
+    pub fn highlight_map(&self) -> HighlightMap {
+        self.highlight_map.lock().clone()
+    }
+
+    pub fn highlight_id_for_name(&self, name: &str) -> Option<HighlightId> {
+        let capture_id = self
+            .highlights_query
+            .as_ref()?
+            .capture_index_for_name(name)?;
+        Some(self.highlight_map.lock().get(capture_id))
+    }
+}
+
+impl CodeLabel {
+    pub fn plain(text: String, filter_text: Option<&str>) -> Self {
+        let mut result = Self {
+            runs: Vec::new(),
+            filter_range: 0..text.len(),
+            text,
+        };
+        if let Some(filter_text) = filter_text {
+            if let Some(ix) = result.text.find(filter_text) {
+                result.filter_range = ix..ix + filter_text.len();
+            }
+        }
+        result
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl Default for FakeLspAdapter {
+    fn default() -> Self {
+        Self {
+            name: "the-fake-language-server",
+            capabilities: lsp2::LanguageServer::full_capabilities(),
+            initializer: None,
+            disk_based_diagnostics_progress_token: None,
+            initialization_options: None,
+            disk_based_diagnostics_sources: Vec::new(),
+            prettier_plugins: Vec::new(),
+        }
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+#[async_trait]
+impl LspAdapter for Arc<FakeLspAdapter> {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName(self.name.into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "FakeLspAdapter"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        unreachable!();
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        unreachable!();
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        unreachable!();
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        unreachable!();
+    }
+
+    fn process_diagnostics(&self, _: &mut lsp2::PublishDiagnosticsParams) {}
+
+    async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
+        self.disk_based_diagnostics_sources.clone()
+    }
+
+    async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
+        self.disk_based_diagnostics_progress_token.clone()
+    }
+
+    async fn initialization_options(&self) -> Option<Value> {
+        self.initialization_options.clone()
+    }
+
+    fn prettier_plugins(&self) -> &[&'static str] {
+        &self.prettier_plugins
+    }
+}
+
+fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
+    for (ix, name) in query.capture_names().iter().enumerate() {
+        for (capture_name, index) in captures.iter_mut() {
+            if capture_name == name {
+                **index = Some(ix as u32);
+                break;
+            }
+        }
+    }
+}
+
+pub fn point_to_lsp(point: PointUtf16) -> lsp2::Position {
+    lsp2::Position::new(point.row, point.column)
+}
+
+pub fn point_from_lsp(point: lsp2::Position) -> Unclipped<PointUtf16> {
+    Unclipped(PointUtf16::new(point.line, point.character))
+}
+
+pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp2::Range {
+    lsp2::Range {
+        start: point_to_lsp(range.start),
+        end: point_to_lsp(range.end),
+    }
+}
+
+pub fn range_from_lsp(range: lsp2::Range) -> Range<Unclipped<PointUtf16>> {
+    let mut start = point_from_lsp(range.start);
+    let mut end = point_from_lsp(range.end);
+    if start > end {
+        mem::swap(&mut start, &mut end);
+    }
+    start..end
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui2::TestAppContext;
+
+    #[gpui2::test(iterations = 10)]
+    async fn test_first_line_pattern(cx: &mut TestAppContext) {
+        let mut languages = LanguageRegistry::test();
+
+        languages.set_executor(cx.executor().clone());
+        let languages = Arc::new(languages);
+        languages.register(
+            "/javascript",
+            LanguageConfig {
+                name: "JavaScript".into(),
+                path_suffixes: vec!["js".into()],
+                first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
+                ..Default::default()
+            },
+            tree_sitter_typescript::language_tsx(),
+            vec![],
+            |_| Default::default(),
+        );
+
+        languages
+            .language_for_file("the/script", None)
+            .await
+            .unwrap_err();
+        languages
+            .language_for_file("the/script", Some(&"nothing".into()))
+            .await
+            .unwrap_err();
+        assert_eq!(
+            languages
+                .language_for_file("the/script", Some(&"#!/bin/env node".into()))
+                .await
+                .unwrap()
+                .name()
+                .as_ref(),
+            "JavaScript"
+        );
+    }
+
+    #[gpui2::test(iterations = 10)]
+    async fn test_language_loading(cx: &mut TestAppContext) {
+        let mut languages = LanguageRegistry::test();
+        languages.set_executor(cx.executor().clone());
+        let languages = Arc::new(languages);
+        languages.register(
+            "/JSON",
+            LanguageConfig {
+                name: "JSON".into(),
+                path_suffixes: vec!["json".into()],
+                ..Default::default()
+            },
+            tree_sitter_json::language(),
+            vec![],
+            |_| Default::default(),
+        );
+        languages.register(
+            "/rust",
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".into()],
+                ..Default::default()
+            },
+            tree_sitter_rust::language(),
+            vec![],
+            |_| Default::default(),
+        );
+        assert_eq!(
+            languages.language_names(),
+            &[
+                "JSON".to_string(),
+                "Plain Text".to_string(),
+                "Rust".to_string(),
+            ]
+        );
+
+        let rust1 = languages.language_for_name("Rust");
+        let rust2 = languages.language_for_name("Rust");
+
+        // Ensure language is still listed even if it's being loaded.
+        assert_eq!(
+            languages.language_names(),
+            &[
+                "JSON".to_string(),
+                "Plain Text".to_string(),
+                "Rust".to_string(),
+            ]
+        );
+
+        let (rust1, rust2) = futures::join!(rust1, rust2);
+        assert!(Arc::ptr_eq(&rust1.unwrap(), &rust2.unwrap()));
+
+        // Ensure language is still listed even after loading it.
+        assert_eq!(
+            languages.language_names(),
+            &[
+                "JSON".to_string(),
+                "Plain Text".to_string(),
+                "Rust".to_string(),
+            ]
+        );
+
+        // Loading an unknown language returns an error.
+        assert!(languages.language_for_name("Unknown").await.is_err());
+    }
+}

crates/language2/src/language_settings.rs 🔗

@@ -0,0 +1,431 @@
+use crate::{File, Language};
+use anyhow::Result;
+use collections::{HashMap, HashSet};
+use globset::GlobMatcher;
+use gpui2::AppContext;
+use schemars::{
+    schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
+    JsonSchema,
+};
+use serde::{Deserialize, Serialize};
+use settings2::Settings;
+use std::{num::NonZeroU32, path::Path, sync::Arc};
+
+pub fn init(cx: &mut AppContext) {
+    AllLanguageSettings::register(cx);
+}
+
+pub fn language_settings<'a>(
+    language: Option<&Arc<Language>>,
+    file: Option<&Arc<dyn File>>,
+    cx: &'a AppContext,
+) -> &'a LanguageSettings {
+    let language_name = language.map(|l| l.name());
+    all_language_settings(file, cx).language(language_name.as_deref())
+}
+
+pub fn all_language_settings<'a>(
+    file: Option<&Arc<dyn File>>,
+    cx: &'a AppContext,
+) -> &'a AllLanguageSettings {
+    let location = file.map(|f| (f.worktree_id(), f.path().as_ref()));
+    AllLanguageSettings::get(location, cx)
+}
+
+#[derive(Debug, Clone)]
+pub struct AllLanguageSettings {
+    pub copilot: CopilotSettings,
+    defaults: LanguageSettings,
+    languages: HashMap<Arc<str>, LanguageSettings>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct LanguageSettings {
+    pub tab_size: NonZeroU32,
+    pub hard_tabs: bool,
+    pub soft_wrap: SoftWrap,
+    pub preferred_line_length: u32,
+    pub show_wrap_guides: bool,
+    pub wrap_guides: Vec<usize>,
+    pub format_on_save: FormatOnSave,
+    pub remove_trailing_whitespace_on_save: bool,
+    pub ensure_final_newline_on_save: bool,
+    pub formatter: Formatter,
+    pub prettier: HashMap<String, serde_json::Value>,
+    pub enable_language_server: bool,
+    pub show_copilot_suggestions: bool,
+    pub show_whitespaces: ShowWhitespaceSetting,
+    pub extend_comment_on_newline: bool,
+    pub inlay_hints: InlayHintSettings,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct CopilotSettings {
+    pub feature_enabled: bool,
+    pub disabled_globs: Vec<GlobMatcher>,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct AllLanguageSettingsContent {
+    #[serde(default)]
+    pub features: Option<FeaturesContent>,
+    #[serde(default)]
+    pub copilot: Option<CopilotSettingsContent>,
+    #[serde(flatten)]
+    pub defaults: LanguageSettingsContent,
+    #[serde(default, alias = "language_overrides")]
+    pub languages: HashMap<Arc<str>, LanguageSettingsContent>,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct LanguageSettingsContent {
+    #[serde(default)]
+    pub tab_size: Option<NonZeroU32>,
+    #[serde(default)]
+    pub hard_tabs: Option<bool>,
+    #[serde(default)]
+    pub soft_wrap: Option<SoftWrap>,
+    #[serde(default)]
+    pub preferred_line_length: Option<u32>,
+    #[serde(default)]
+    pub show_wrap_guides: Option<bool>,
+    #[serde(default)]
+    pub wrap_guides: Option<Vec<usize>>,
+    #[serde(default)]
+    pub format_on_save: Option<FormatOnSave>,
+    #[serde(default)]
+    pub remove_trailing_whitespace_on_save: Option<bool>,
+    #[serde(default)]
+    pub ensure_final_newline_on_save: Option<bool>,
+    #[serde(default)]
+    pub formatter: Option<Formatter>,
+    #[serde(default)]
+    pub prettier: Option<HashMap<String, serde_json::Value>>,
+    #[serde(default)]
+    pub enable_language_server: Option<bool>,
+    #[serde(default)]
+    pub show_copilot_suggestions: Option<bool>,
+    #[serde(default)]
+    pub show_whitespaces: Option<ShowWhitespaceSetting>,
+    #[serde(default)]
+    pub extend_comment_on_newline: Option<bool>,
+    #[serde(default)]
+    pub inlay_hints: Option<InlayHintSettings>,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct CopilotSettingsContent {
+    #[serde(default)]
+    pub disabled_globs: Option<Vec<String>>,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct FeaturesContent {
+    pub copilot: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SoftWrap {
+    None,
+    EditorWidth,
+    PreferredLineLength,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum FormatOnSave {
+    On,
+    Off,
+    LanguageServer,
+    External {
+        command: Arc<str>,
+        arguments: Arc<[String]>,
+    },
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowWhitespaceSetting {
+    Selection,
+    None,
+    All,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Formatter {
+    #[default]
+    Auto,
+    LanguageServer,
+    Prettier,
+    External {
+        command: Arc<str>,
+        arguments: Arc<[String]>,
+    },
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct InlayHintSettings {
+    #[serde(default)]
+    pub enabled: bool,
+    #[serde(default = "default_true")]
+    pub show_type_hints: bool,
+    #[serde(default = "default_true")]
+    pub show_parameter_hints: bool,
+    #[serde(default = "default_true")]
+    pub show_other_hints: bool,
+}
+
+fn default_true() -> bool {
+    true
+}
+
+impl InlayHintSettings {
+    pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
+        let mut kinds = HashSet::default();
+        if self.show_type_hints {
+            kinds.insert(Some(InlayHintKind::Type));
+        }
+        if self.show_parameter_hints {
+            kinds.insert(Some(InlayHintKind::Parameter));
+        }
+        if self.show_other_hints {
+            kinds.insert(None);
+        }
+        kinds
+    }
+}
+
+impl AllLanguageSettings {
+    pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
+        if let Some(name) = language_name {
+            if let Some(overrides) = self.languages.get(name) {
+                return overrides;
+            }
+        }
+        &self.defaults
+    }
+
+    pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
+        !self
+            .copilot
+            .disabled_globs
+            .iter()
+            .any(|glob| glob.is_match(path))
+    }
+
+    pub fn copilot_enabled(&self, language: Option<&Arc<Language>>, path: Option<&Path>) -> bool {
+        if !self.copilot.feature_enabled {
+            return false;
+        }
+
+        if let Some(path) = path {
+            if !self.copilot_enabled_for_path(path) {
+                return false;
+            }
+        }
+
+        self.language(language.map(|l| l.name()).as_deref())
+            .show_copilot_suggestions
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum InlayHintKind {
+    Type,
+    Parameter,
+}
+
+impl InlayHintKind {
+    pub fn from_name(name: &str) -> Option<Self> {
+        match name {
+            "type" => Some(InlayHintKind::Type),
+            "parameter" => Some(InlayHintKind::Parameter),
+            _ => None,
+        }
+    }
+
+    pub fn name(&self) -> &'static str {
+        match self {
+            InlayHintKind::Type => "type",
+            InlayHintKind::Parameter => "parameter",
+        }
+    }
+}
+
+impl settings2::Settings for AllLanguageSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = AllLanguageSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_settings: &[&Self::FileContent],
+        _: &mut AppContext,
+    ) -> Result<Self> {
+        // A default is provided for all settings.
+        let mut defaults: LanguageSettings =
+            serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
+
+        let mut languages = HashMap::default();
+        for (language_name, settings) in &default_value.languages {
+            let mut language_settings = defaults.clone();
+            merge_settings(&mut language_settings, &settings);
+            languages.insert(language_name.clone(), language_settings);
+        }
+
+        let mut copilot_enabled = default_value
+            .features
+            .as_ref()
+            .and_then(|f| f.copilot)
+            .ok_or_else(Self::missing_default)?;
+        let mut copilot_globs = default_value
+            .copilot
+            .as_ref()
+            .and_then(|c| c.disabled_globs.as_ref())
+            .ok_or_else(Self::missing_default)?;
+
+        for user_settings in user_settings {
+            if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
+                copilot_enabled = copilot;
+            }
+            if let Some(globs) = user_settings
+                .copilot
+                .as_ref()
+                .and_then(|f| f.disabled_globs.as_ref())
+            {
+                copilot_globs = globs;
+            }
+
+            // A user's global settings override the default global settings and
+            // all default language-specific settings.
+            merge_settings(&mut defaults, &user_settings.defaults);
+            for language_settings in languages.values_mut() {
+                merge_settings(language_settings, &user_settings.defaults);
+            }
+
+            // A user's language-specific settings override default language-specific settings.
+            for (language_name, user_language_settings) in &user_settings.languages {
+                merge_settings(
+                    languages
+                        .entry(language_name.clone())
+                        .or_insert_with(|| defaults.clone()),
+                    &user_language_settings,
+                );
+            }
+        }
+
+        Ok(Self {
+            copilot: CopilotSettings {
+                feature_enabled: copilot_enabled,
+                disabled_globs: copilot_globs
+                    .iter()
+                    .filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
+                    .collect(),
+            },
+            defaults,
+            languages,
+        })
+    }
+
+    fn json_schema(
+        generator: &mut schemars::gen::SchemaGenerator,
+        params: &settings2::SettingsJsonSchemaParams,
+        _: &AppContext,
+    ) -> schemars::schema::RootSchema {
+        let mut root_schema = generator.root_schema_for::<Self::FileContent>();
+
+        // Create a schema for a 'languages overrides' object, associating editor
+        // settings with specific languages.
+        assert!(root_schema
+            .definitions
+            .contains_key("LanguageSettingsContent"));
+
+        let languages_object_schema = SchemaObject {
+            instance_type: Some(InstanceType::Object.into()),
+            object: Some(Box::new(ObjectValidation {
+                properties: params
+                    .language_names
+                    .iter()
+                    .map(|name| {
+                        (
+                            name.clone(),
+                            Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
+                        )
+                    })
+                    .collect(),
+                ..Default::default()
+            })),
+            ..Default::default()
+        };
+
+        root_schema
+            .definitions
+            .extend([("Languages".into(), languages_object_schema.into())]);
+
+        root_schema
+            .schema
+            .object
+            .as_mut()
+            .unwrap()
+            .properties
+            .extend([
+                (
+                    "languages".to_owned(),
+                    Schema::new_ref("#/definitions/Languages".into()),
+                ),
+                // For backward compatibility
+                (
+                    "language_overrides".to_owned(),
+                    Schema::new_ref("#/definitions/Languages".into()),
+                ),
+            ]);
+
+        root_schema
+    }
+}
+
+fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
+    merge(&mut settings.tab_size, src.tab_size);
+    merge(&mut settings.hard_tabs, src.hard_tabs);
+    merge(&mut settings.soft_wrap, src.soft_wrap);
+    merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
+    merge(&mut settings.wrap_guides, src.wrap_guides.clone());
+
+    merge(
+        &mut settings.preferred_line_length,
+        src.preferred_line_length,
+    );
+    merge(&mut settings.formatter, src.formatter.clone());
+    merge(&mut settings.prettier, src.prettier.clone());
+    merge(&mut settings.format_on_save, src.format_on_save.clone());
+    merge(
+        &mut settings.remove_trailing_whitespace_on_save,
+        src.remove_trailing_whitespace_on_save,
+    );
+    merge(
+        &mut settings.ensure_final_newline_on_save,
+        src.ensure_final_newline_on_save,
+    );
+    merge(
+        &mut settings.enable_language_server,
+        src.enable_language_server,
+    );
+    merge(
+        &mut settings.show_copilot_suggestions,
+        src.show_copilot_suggestions,
+    );
+    merge(&mut settings.show_whitespaces, src.show_whitespaces);
+    merge(
+        &mut settings.extend_comment_on_newline,
+        src.extend_comment_on_newline,
+    );
+    merge(&mut settings.inlay_hints, src.inlay_hints);
+    fn merge<T>(target: &mut T, value: Option<T>) {
+        if let Some(value) = value {
+            *target = value;
+        }
+    }
+}

crates/language2/src/outline.rs 🔗

@@ -0,0 +1,138 @@
+use fuzzy2::{StringMatch, StringMatchCandidate};
+use gpui2::{Executor, HighlightStyle};
+use std::ops::Range;
+
+#[derive(Debug)]
+pub struct Outline<T> {
+    pub items: Vec<OutlineItem<T>>,
+    candidates: Vec<StringMatchCandidate>,
+    path_candidates: Vec<StringMatchCandidate>,
+    path_candidate_prefixes: Vec<usize>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct OutlineItem<T> {
+    pub depth: usize,
+    pub range: Range<T>,
+    pub text: String,
+    pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
+    pub name_ranges: Vec<Range<usize>>,
+}
+
+impl<T> Outline<T> {
+    pub fn new(items: Vec<OutlineItem<T>>) -> Self {
+        let mut candidates = Vec::new();
+        let mut path_candidates = Vec::new();
+        let mut path_candidate_prefixes = Vec::new();
+        let mut path_text = String::new();
+        let mut path_stack = Vec::new();
+
+        for (id, item) in items.iter().enumerate() {
+            if item.depth < path_stack.len() {
+                path_stack.truncate(item.depth);
+                path_text.truncate(path_stack.last().copied().unwrap_or(0));
+            }
+            if !path_text.is_empty() {
+                path_text.push(' ');
+            }
+            path_candidate_prefixes.push(path_text.len());
+            path_text.push_str(&item.text);
+            path_stack.push(path_text.len());
+
+            let candidate_text = item
+                .name_ranges
+                .iter()
+                .map(|range| &item.text[range.start as usize..range.end as usize])
+                .collect::<String>();
+
+            path_candidates.push(StringMatchCandidate::new(id, path_text.clone()));
+            candidates.push(StringMatchCandidate::new(id, candidate_text));
+        }
+
+        Self {
+            candidates,
+            path_candidates,
+            path_candidate_prefixes,
+            items,
+        }
+    }
+
+    pub async fn search(&self, query: &str, executor: Executor) -> Vec<StringMatch> {
+        let query = query.trim_start();
+        let is_path_query = query.contains(' ');
+        let smart_case = query.chars().any(|c| c.is_uppercase());
+        let mut matches = fuzzy2::match_strings(
+            if is_path_query {
+                &self.path_candidates
+            } else {
+                &self.candidates
+            },
+            query,
+            smart_case,
+            100,
+            &Default::default(),
+            executor.clone(),
+        )
+        .await;
+        matches.sort_unstable_by_key(|m| m.candidate_id);
+
+        let mut tree_matches = Vec::new();
+
+        let mut prev_item_ix = 0;
+        for mut string_match in matches {
+            let outline_match = &self.items[string_match.candidate_id];
+
+            if is_path_query {
+                let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
+                string_match
+                    .positions
+                    .retain(|position| *position >= prefix_len);
+                for position in &mut string_match.positions {
+                    *position -= prefix_len;
+                }
+            } else {
+                let mut name_ranges = outline_match.name_ranges.iter();
+                let mut name_range = name_ranges.next().unwrap();
+                let mut preceding_ranges_len = 0;
+                for position in &mut string_match.positions {
+                    while *position >= preceding_ranges_len + name_range.len() as usize {
+                        preceding_ranges_len += name_range.len();
+                        name_range = name_ranges.next().unwrap();
+                    }
+                    *position = name_range.start as usize + (*position - preceding_ranges_len);
+                }
+            }
+
+            let insertion_ix = tree_matches.len();
+            let mut cur_depth = outline_match.depth;
+            for (ix, item) in self.items[prev_item_ix..string_match.candidate_id]
+                .iter()
+                .enumerate()
+                .rev()
+            {
+                if cur_depth == 0 {
+                    break;
+                }
+
+                let candidate_index = ix + prev_item_ix;
+                if item.depth == cur_depth - 1 {
+                    tree_matches.insert(
+                        insertion_ix,
+                        StringMatch {
+                            candidate_id: candidate_index,
+                            score: Default::default(),
+                            positions: Default::default(),
+                            string: Default::default(),
+                        },
+                    );
+                    cur_depth -= 1;
+                }
+            }
+
+            prev_item_ix = string_match.candidate_id + 1;
+            tree_matches.push(string_match);
+        }
+
+        tree_matches
+    }
+}

crates/language2/src/proto.rs 🔗

@@ -0,0 +1,589 @@
+use crate::{
+    diagnostic_set::DiagnosticEntry, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic,
+    Language,
+};
+use anyhow::{anyhow, Result};
+use clock::ReplicaId;
+use lsp2::{DiagnosticSeverity, LanguageServerId};
+use rpc2::proto;
+use std::{ops::Range, sync::Arc};
+use text::*;
+
+pub use proto::{BufferState, Operation};
+
+pub fn serialize_fingerprint(fingerprint: RopeFingerprint) -> String {
+    fingerprint.to_hex()
+}
+
+pub fn deserialize_fingerprint(fingerprint: &str) -> Result<RopeFingerprint> {
+    RopeFingerprint::from_hex(fingerprint)
+        .map_err(|error| anyhow!("invalid fingerprint: {}", error))
+}
+
+pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
+    match message {
+        proto::LineEnding::Unix => text::LineEnding::Unix,
+        proto::LineEnding::Windows => text::LineEnding::Windows,
+    }
+}
+
+pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding {
+    match message {
+        text::LineEnding::Unix => proto::LineEnding::Unix,
+        text::LineEnding::Windows => proto::LineEnding::Windows,
+    }
+}
+
+pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
+    proto::Operation {
+        variant: Some(match operation {
+            crate::Operation::Buffer(text::Operation::Edit(edit)) => {
+                proto::operation::Variant::Edit(serialize_edit_operation(edit))
+            }
+
+            crate::Operation::Buffer(text::Operation::Undo(undo)) => {
+                proto::operation::Variant::Undo(proto::operation::Undo {
+                    replica_id: undo.timestamp.replica_id as u32,
+                    lamport_timestamp: undo.timestamp.value,
+                    version: serialize_version(&undo.version),
+                    counts: undo
+                        .counts
+                        .iter()
+                        .map(|(edit_id, count)| proto::UndoCount {
+                            replica_id: edit_id.replica_id as u32,
+                            lamport_timestamp: edit_id.value,
+                            count: *count,
+                        })
+                        .collect(),
+                })
+            }
+
+            crate::Operation::UpdateSelections {
+                selections,
+                line_mode,
+                lamport_timestamp,
+                cursor_shape,
+            } => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
+                replica_id: lamport_timestamp.replica_id as u32,
+                lamport_timestamp: lamport_timestamp.value,
+                selections: serialize_selections(selections),
+                line_mode: *line_mode,
+                cursor_shape: serialize_cursor_shape(cursor_shape) as i32,
+            }),
+
+            crate::Operation::UpdateDiagnostics {
+                lamport_timestamp,
+                server_id,
+                diagnostics,
+            } => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics {
+                replica_id: lamport_timestamp.replica_id as u32,
+                lamport_timestamp: lamport_timestamp.value,
+                server_id: server_id.0 as u64,
+                diagnostics: serialize_diagnostics(diagnostics.iter()),
+            }),
+
+            crate::Operation::UpdateCompletionTriggers {
+                triggers,
+                lamport_timestamp,
+            } => proto::operation::Variant::UpdateCompletionTriggers(
+                proto::operation::UpdateCompletionTriggers {
+                    replica_id: lamport_timestamp.replica_id as u32,
+                    lamport_timestamp: lamport_timestamp.value,
+                    triggers: triggers.clone(),
+                },
+            ),
+        }),
+    }
+}
+
+pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
+    proto::operation::Edit {
+        replica_id: operation.timestamp.replica_id as u32,
+        lamport_timestamp: operation.timestamp.value,
+        version: serialize_version(&operation.version),
+        ranges: operation.ranges.iter().map(serialize_range).collect(),
+        new_text: operation
+            .new_text
+            .iter()
+            .map(|text| text.to_string())
+            .collect(),
+    }
+}
+
+pub fn serialize_undo_map_entry(
+    (edit_id, counts): (&clock::Lamport, &[(clock::Lamport, u32)]),
+) -> proto::UndoMapEntry {
+    proto::UndoMapEntry {
+        replica_id: edit_id.replica_id as u32,
+        local_timestamp: edit_id.value,
+        counts: counts
+            .iter()
+            .map(|(undo_id, count)| proto::UndoCount {
+                replica_id: undo_id.replica_id as u32,
+                lamport_timestamp: undo_id.value,
+                count: *count,
+            })
+            .collect(),
+    }
+}
+
+pub fn split_operations(
+    mut operations: Vec<proto::Operation>,
+) -> impl Iterator<Item = Vec<proto::Operation>> {
+    #[cfg(any(test, feature = "test-support"))]
+    const CHUNK_SIZE: usize = 5;
+
+    #[cfg(not(any(test, feature = "test-support")))]
+    const CHUNK_SIZE: usize = 100;
+
+    let mut done = false;
+    std::iter::from_fn(move || {
+        if done {
+            return None;
+        }
+
+        let operations = operations
+            .drain(..std::cmp::min(CHUNK_SIZE, operations.len()))
+            .collect::<Vec<_>>();
+        if operations.is_empty() {
+            done = true;
+        }
+        Some(operations)
+    })
+}
+
+pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
+    selections.iter().map(serialize_selection).collect()
+}
+
+pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+    proto::Selection {
+        id: selection.id as u64,
+        start: Some(proto::EditorAnchor {
+            anchor: Some(serialize_anchor(&selection.start)),
+            excerpt_id: 0,
+        }),
+        end: Some(proto::EditorAnchor {
+            anchor: Some(serialize_anchor(&selection.end)),
+            excerpt_id: 0,
+        }),
+        reversed: selection.reversed,
+    }
+}
+
+pub fn serialize_cursor_shape(cursor_shape: &CursorShape) -> proto::CursorShape {
+    match cursor_shape {
+        CursorShape::Bar => proto::CursorShape::CursorBar,
+        CursorShape::Block => proto::CursorShape::CursorBlock,
+        CursorShape::Underscore => proto::CursorShape::CursorUnderscore,
+        CursorShape::Hollow => proto::CursorShape::CursorHollow,
+    }
+}
+
+pub fn deserialize_cursor_shape(cursor_shape: proto::CursorShape) -> CursorShape {
+    match cursor_shape {
+        proto::CursorShape::CursorBar => CursorShape::Bar,
+        proto::CursorShape::CursorBlock => CursorShape::Block,
+        proto::CursorShape::CursorUnderscore => CursorShape::Underscore,
+        proto::CursorShape::CursorHollow => CursorShape::Hollow,
+    }
+}
+
+pub fn serialize_diagnostics<'a>(
+    diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<Anchor>>,
+) -> Vec<proto::Diagnostic> {
+    diagnostics
+        .into_iter()
+        .map(|entry| proto::Diagnostic {
+            source: entry.diagnostic.source.clone(),
+            start: Some(serialize_anchor(&entry.range.start)),
+            end: Some(serialize_anchor(&entry.range.end)),
+            message: entry.diagnostic.message.clone(),
+            severity: match entry.diagnostic.severity {
+                DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
+                DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
+                DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
+                DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
+                _ => proto::diagnostic::Severity::None,
+            } as i32,
+            group_id: entry.diagnostic.group_id as u64,
+            is_primary: entry.diagnostic.is_primary,
+            is_valid: entry.diagnostic.is_valid,
+            code: entry.diagnostic.code.clone(),
+            is_disk_based: entry.diagnostic.is_disk_based,
+            is_unnecessary: entry.diagnostic.is_unnecessary,
+        })
+        .collect()
+}
+
+pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
+    proto::Anchor {
+        replica_id: anchor.timestamp.replica_id as u32,
+        timestamp: anchor.timestamp.value,
+        offset: anchor.offset as u64,
+        bias: match anchor.bias {
+            Bias::Left => proto::Bias::Left as i32,
+            Bias::Right => proto::Bias::Right as i32,
+        },
+        buffer_id: anchor.buffer_id,
+    }
+}
+
+// This behavior is currently copied in the collab database, for snapshotting channel notes
+pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operation> {
+    Ok(
+        match message
+            .variant
+            .ok_or_else(|| anyhow!("missing operation variant"))?
+        {
+            proto::operation::Variant::Edit(edit) => {
+                crate::Operation::Buffer(text::Operation::Edit(deserialize_edit_operation(edit)))
+            }
+            proto::operation::Variant::Undo(undo) => {
+                crate::Operation::Buffer(text::Operation::Undo(UndoOperation {
+                    timestamp: clock::Lamport {
+                        replica_id: undo.replica_id as ReplicaId,
+                        value: undo.lamport_timestamp,
+                    },
+                    version: deserialize_version(&undo.version),
+                    counts: undo
+                        .counts
+                        .into_iter()
+                        .map(|c| {
+                            (
+                                clock::Lamport {
+                                    replica_id: c.replica_id as ReplicaId,
+                                    value: c.lamport_timestamp,
+                                },
+                                c.count,
+                            )
+                        })
+                        .collect(),
+                }))
+            }
+            proto::operation::Variant::UpdateSelections(message) => {
+                let selections = message
+                    .selections
+                    .into_iter()
+                    .filter_map(|selection| {
+                        Some(Selection {
+                            id: selection.id as usize,
+                            start: deserialize_anchor(selection.start?.anchor?)?,
+                            end: deserialize_anchor(selection.end?.anchor?)?,
+                            reversed: selection.reversed,
+                            goal: SelectionGoal::None,
+                        })
+                    })
+                    .collect::<Vec<_>>();
+
+                crate::Operation::UpdateSelections {
+                    lamport_timestamp: clock::Lamport {
+                        replica_id: message.replica_id as ReplicaId,
+                        value: message.lamport_timestamp,
+                    },
+                    selections: Arc::from(selections),
+                    line_mode: message.line_mode,
+                    cursor_shape: deserialize_cursor_shape(
+                        proto::CursorShape::from_i32(message.cursor_shape)
+                            .ok_or_else(|| anyhow!("Missing cursor shape"))?,
+                    ),
+                }
+            }
+            proto::operation::Variant::UpdateDiagnostics(message) => {
+                crate::Operation::UpdateDiagnostics {
+                    lamport_timestamp: clock::Lamport {
+                        replica_id: message.replica_id as ReplicaId,
+                        value: message.lamport_timestamp,
+                    },
+                    server_id: LanguageServerId(message.server_id as usize),
+                    diagnostics: deserialize_diagnostics(message.diagnostics),
+                }
+            }
+            proto::operation::Variant::UpdateCompletionTriggers(message) => {
+                crate::Operation::UpdateCompletionTriggers {
+                    triggers: message.triggers,
+                    lamport_timestamp: clock::Lamport {
+                        replica_id: message.replica_id as ReplicaId,
+                        value: message.lamport_timestamp,
+                    },
+                }
+            }
+        },
+    )
+}
+
+pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation {
+    EditOperation {
+        timestamp: clock::Lamport {
+            replica_id: edit.replica_id as ReplicaId,
+            value: edit.lamport_timestamp,
+        },
+        version: deserialize_version(&edit.version),
+        ranges: edit.ranges.into_iter().map(deserialize_range).collect(),
+        new_text: edit.new_text.into_iter().map(Arc::from).collect(),
+    }
+}
+
+pub fn deserialize_undo_map_entry(
+    entry: proto::UndoMapEntry,
+) -> (clock::Lamport, Vec<(clock::Lamport, u32)>) {
+    (
+        clock::Lamport {
+            replica_id: entry.replica_id as u16,
+            value: entry.local_timestamp,
+        },
+        entry
+            .counts
+            .into_iter()
+            .map(|undo_count| {
+                (
+                    clock::Lamport {
+                        replica_id: undo_count.replica_id as u16,
+                        value: undo_count.lamport_timestamp,
+                    },
+                    undo_count.count,
+                )
+            })
+            .collect(),
+    )
+}
+
+pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selection<Anchor>]> {
+    Arc::from(
+        selections
+            .into_iter()
+            .filter_map(deserialize_selection)
+            .collect::<Vec<_>>(),
+    )
+}
+
+pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
+    Some(Selection {
+        id: selection.id as usize,
+        start: deserialize_anchor(selection.start?.anchor?)?,
+        end: deserialize_anchor(selection.end?.anchor?)?,
+        reversed: selection.reversed,
+        goal: SelectionGoal::None,
+    })
+}
+
+pub fn deserialize_diagnostics(
+    diagnostics: Vec<proto::Diagnostic>,
+) -> Arc<[DiagnosticEntry<Anchor>]> {
+    diagnostics
+        .into_iter()
+        .filter_map(|diagnostic| {
+            Some(DiagnosticEntry {
+                range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?,
+                diagnostic: Diagnostic {
+                    source: diagnostic.source,
+                    severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? {
+                        proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR,
+                        proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING,
+                        proto::diagnostic::Severity::Information => DiagnosticSeverity::INFORMATION,
+                        proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT,
+                        proto::diagnostic::Severity::None => return None,
+                    },
+                    message: diagnostic.message,
+                    group_id: diagnostic.group_id as usize,
+                    code: diagnostic.code,
+                    is_valid: diagnostic.is_valid,
+                    is_primary: diagnostic.is_primary,
+                    is_disk_based: diagnostic.is_disk_based,
+                    is_unnecessary: diagnostic.is_unnecessary,
+                },
+            })
+        })
+        .collect()
+}
+
+pub fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
+    Some(Anchor {
+        timestamp: clock::Lamport {
+            replica_id: anchor.replica_id as ReplicaId,
+            value: anchor.timestamp,
+        },
+        offset: anchor.offset as usize,
+        bias: match proto::Bias::from_i32(anchor.bias)? {
+            proto::Bias::Left => Bias::Left,
+            proto::Bias::Right => Bias::Right,
+        },
+        buffer_id: anchor.buffer_id,
+    })
+}
+
+pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option<clock::Lamport> {
+    let replica_id;
+    let value;
+    match operation.variant.as_ref()? {
+        proto::operation::Variant::Edit(op) => {
+            replica_id = op.replica_id;
+            value = op.lamport_timestamp;
+        }
+        proto::operation::Variant::Undo(op) => {
+            replica_id = op.replica_id;
+            value = op.lamport_timestamp;
+        }
+        proto::operation::Variant::UpdateDiagnostics(op) => {
+            replica_id = op.replica_id;
+            value = op.lamport_timestamp;
+        }
+        proto::operation::Variant::UpdateSelections(op) => {
+            replica_id = op.replica_id;
+            value = op.lamport_timestamp;
+        }
+        proto::operation::Variant::UpdateCompletionTriggers(op) => {
+            replica_id = op.replica_id;
+            value = op.lamport_timestamp;
+        }
+    }
+
+    Some(clock::Lamport {
+        replica_id: replica_id as ReplicaId,
+        value,
+    })
+}
+
+pub fn serialize_completion(completion: &Completion) -> proto::Completion {
+    proto::Completion {
+        old_start: Some(serialize_anchor(&completion.old_range.start)),
+        old_end: Some(serialize_anchor(&completion.old_range.end)),
+        new_text: completion.new_text.clone(),
+        server_id: completion.server_id.0 as u64,
+        lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(),
+    }
+}
+
+pub async fn deserialize_completion(
+    completion: proto::Completion,
+    language: Option<Arc<Language>>,
+) -> Result<Completion> {
+    let old_start = completion
+        .old_start
+        .and_then(deserialize_anchor)
+        .ok_or_else(|| anyhow!("invalid old start"))?;
+    let old_end = completion
+        .old_end
+        .and_then(deserialize_anchor)
+        .ok_or_else(|| anyhow!("invalid old end"))?;
+    let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?;
+
+    let mut label = None;
+    if let Some(language) = language {
+        label = language.label_for_completion(&lsp_completion).await;
+    }
+
+    Ok(Completion {
+        old_range: old_start..old_end,
+        new_text: completion.new_text,
+        label: label.unwrap_or_else(|| {
+            CodeLabel::plain(
+                lsp_completion.label.clone(),
+                lsp_completion.filter_text.as_deref(),
+            )
+        }),
+        server_id: LanguageServerId(completion.server_id as usize),
+        lsp_completion,
+    })
+}
+
+pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction {
+    proto::CodeAction {
+        server_id: action.server_id.0 as u64,
+        start: Some(serialize_anchor(&action.range.start)),
+        end: Some(serialize_anchor(&action.range.end)),
+        lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(),
+    }
+}
+
+pub fn deserialize_code_action(action: proto::CodeAction) -> Result<CodeAction> {
+    let start = action
+        .start
+        .and_then(deserialize_anchor)
+        .ok_or_else(|| anyhow!("invalid start"))?;
+    let end = action
+        .end
+        .and_then(deserialize_anchor)
+        .ok_or_else(|| anyhow!("invalid end"))?;
+    let lsp_action = serde_json::from_slice(&action.lsp_action)?;
+    Ok(CodeAction {
+        server_id: LanguageServerId(action.server_id as usize),
+        range: start..end,
+        lsp_action,
+    })
+}
+
+pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction {
+    proto::Transaction {
+        id: Some(serialize_timestamp(transaction.id)),
+        edit_ids: transaction
+            .edit_ids
+            .iter()
+            .copied()
+            .map(serialize_timestamp)
+            .collect(),
+        start: serialize_version(&transaction.start),
+    }
+}
+
+pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transaction> {
+    Ok(Transaction {
+        id: deserialize_timestamp(
+            transaction
+                .id
+                .ok_or_else(|| anyhow!("missing transaction id"))?,
+        ),
+        edit_ids: transaction
+            .edit_ids
+            .into_iter()
+            .map(deserialize_timestamp)
+            .collect(),
+        start: deserialize_version(&transaction.start),
+    })
+}
+
+pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp {
+    proto::LamportTimestamp {
+        replica_id: timestamp.replica_id as u32,
+        value: timestamp.value,
+    }
+}
+
+pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lamport {
+    clock::Lamport {
+        replica_id: timestamp.replica_id as ReplicaId,
+        value: timestamp.value,
+    }
+}
+
+pub fn serialize_range(range: &Range<FullOffset>) -> proto::Range {
+    proto::Range {
+        start: range.start.0 as u64,
+        end: range.end.0 as u64,
+    }
+}
+
+pub fn deserialize_range(range: proto::Range) -> Range<FullOffset> {
+    FullOffset(range.start as usize)..FullOffset(range.end as usize)
+}
+
+pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global {
+    let mut version = clock::Global::new();
+    for entry in message {
+        version.observe(clock::Lamport {
+            replica_id: entry.replica_id as ReplicaId,
+            value: entry.timestamp,
+        });
+    }
+    version
+}
+
+pub fn serialize_version(version: &clock::Global) -> Vec<proto::VectorClockEntry> {
+    version
+        .iter()
+        .map(|entry| proto::VectorClockEntry {
+            replica_id: entry.replica_id as u32,
+            timestamp: entry.value,
+        })
+        .collect()
+}

crates/language2/src/syntax_map.rs 🔗

@@ -0,0 +1,1813 @@
+#[cfg(test)]
+mod syntax_map_tests;
+
+use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
+use collections::HashMap;
+use futures::FutureExt;
+use parking_lot::Mutex;
+use std::{
+    borrow::Cow,
+    cell::RefCell,
+    cmp::{self, Ordering, Reverse},
+    collections::BinaryHeap,
+    fmt, iter,
+    ops::{Deref, DerefMut, Range},
+    sync::Arc,
+};
+use sum_tree::{Bias, SeekTarget, SumTree};
+use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint};
+use tree_sitter::{
+    Node, Parser, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree,
+};
+
+thread_local! {
+    static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
+}
+
+static QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Mutex::new(vec![]);
+
+#[derive(Default)]
+pub struct SyntaxMap {
+    snapshot: SyntaxSnapshot,
+    language_registry: Option<Arc<LanguageRegistry>>,
+}
+
+#[derive(Clone, Default)]
+pub struct SyntaxSnapshot {
+    layers: SumTree<SyntaxLayer>,
+    parsed_version: clock::Global,
+    interpolated_version: clock::Global,
+    language_registry_version: usize,
+}
+
+#[derive(Default)]
+pub struct SyntaxMapCaptures<'a> {
+    layers: Vec<SyntaxMapCapturesLayer<'a>>,
+    active_layer_count: usize,
+    grammars: Vec<&'a Grammar>,
+}
+
+#[derive(Default)]
+pub struct SyntaxMapMatches<'a> {
+    layers: Vec<SyntaxMapMatchesLayer<'a>>,
+    active_layer_count: usize,
+    grammars: Vec<&'a Grammar>,
+}
+
+#[derive(Debug)]
+pub struct SyntaxMapCapture<'a> {
+    pub depth: usize,
+    pub node: Node<'a>,
+    pub index: u32,
+    pub grammar_index: usize,
+}
+
+#[derive(Debug)]
+pub struct SyntaxMapMatch<'a> {
+    pub depth: usize,
+    pub pattern_index: usize,
+    pub captures: &'a [QueryCapture<'a>],
+    pub grammar_index: usize,
+}
+
+struct SyntaxMapCapturesLayer<'a> {
+    depth: usize,
+    captures: QueryCaptures<'a, 'a, TextProvider<'a>, &'a [u8]>,
+    next_capture: Option<QueryCapture<'a>>,
+    grammar_index: usize,
+    _query_cursor: QueryCursorHandle,
+}
+
+struct SyntaxMapMatchesLayer<'a> {
+    depth: usize,
+    next_pattern_index: usize,
+    next_captures: Vec<QueryCapture<'a>>,
+    has_next: bool,
+    matches: QueryMatches<'a, 'a, TextProvider<'a>, &'a [u8]>,
+    grammar_index: usize,
+    _query_cursor: QueryCursorHandle,
+}
+
+#[derive(Clone)]
+struct SyntaxLayer {
+    depth: usize,
+    range: Range<Anchor>,
+    content: SyntaxLayerContent,
+}
+
+#[derive(Clone)]
+enum SyntaxLayerContent {
+    Parsed {
+        tree: tree_sitter::Tree,
+        language: Arc<Language>,
+    },
+    Pending {
+        language_name: Arc<str>,
+    },
+}
+
+impl SyntaxLayerContent {
+    fn language_id(&self) -> Option<usize> {
+        match self {
+            SyntaxLayerContent::Parsed { language, .. } => language.id(),
+            SyntaxLayerContent::Pending { .. } => None,
+        }
+    }
+
+    fn tree(&self) -> Option<&Tree> {
+        match self {
+            SyntaxLayerContent::Parsed { tree, .. } => Some(tree),
+            SyntaxLayerContent::Pending { .. } => None,
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct SyntaxLayerInfo<'a> {
+    pub depth: usize,
+    pub language: &'a Arc<Language>,
+    tree: &'a Tree,
+    offset: (usize, tree_sitter::Point),
+}
+
+#[derive(Clone)]
+pub struct OwnedSyntaxLayerInfo {
+    pub depth: usize,
+    pub language: Arc<Language>,
+    tree: tree_sitter::Tree,
+    offset: (usize, tree_sitter::Point),
+}
+
+#[derive(Debug, Clone)]
+struct SyntaxLayerSummary {
+    min_depth: usize,
+    max_depth: usize,
+    range: Range<Anchor>,
+    last_layer_range: Range<Anchor>,
+    last_layer_language: Option<usize>,
+    contains_unknown_injections: bool,
+}
+
+#[derive(Clone, Debug)]
+struct SyntaxLayerPosition {
+    depth: usize,
+    range: Range<Anchor>,
+    language: Option<usize>,
+}
+
+#[derive(Clone, Debug)]
+struct ChangeStartPosition {
+    depth: usize,
+    position: Anchor,
+}
+
+#[derive(Clone, Debug)]
+struct SyntaxLayerPositionBeforeChange {
+    position: SyntaxLayerPosition,
+    change: ChangeStartPosition,
+}
+
+struct ParseStep {
+    depth: usize,
+    language: ParseStepLanguage,
+    range: Range<Anchor>,
+    included_ranges: Vec<tree_sitter::Range>,
+    mode: ParseMode,
+}
+
+#[derive(Debug)]
+enum ParseStepLanguage {
+    Loaded { language: Arc<Language> },
+    Pending { name: Arc<str> },
+}
+
+impl ParseStepLanguage {
+    fn id(&self) -> Option<usize> {
+        match self {
+            ParseStepLanguage::Loaded { language } => language.id(),
+            ParseStepLanguage::Pending { .. } => None,
+        }
+    }
+}
+
+enum ParseMode {
+    Single,
+    Combined {
+        parent_layer_range: Range<usize>,
+        parent_layer_changed_ranges: Vec<Range<usize>>,
+    },
+}
+
+#[derive(Debug, PartialEq, Eq)]
+struct ChangedRegion {
+    depth: usize,
+    range: Range<Anchor>,
+}
+
+#[derive(Default)]
+struct ChangeRegionSet(Vec<ChangedRegion>);
+
+struct TextProvider<'a>(&'a Rope);
+
+struct ByteChunks<'a>(text::Chunks<'a>);
+
+struct QueryCursorHandle(Option<QueryCursor>);
+
+impl SyntaxMap {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn set_language_registry(&mut self, registry: Arc<LanguageRegistry>) {
+        self.language_registry = Some(registry);
+    }
+
+    pub fn snapshot(&self) -> SyntaxSnapshot {
+        self.snapshot.clone()
+    }
+
+    pub fn language_registry(&self) -> Option<Arc<LanguageRegistry>> {
+        self.language_registry.clone()
+    }
+
+    pub fn interpolate(&mut self, text: &BufferSnapshot) {
+        self.snapshot.interpolate(text);
+    }
+
+    #[allow(dead_code)] // todo!()
+    #[cfg(test)]
+    pub fn reparse(&mut self, language: Arc<Language>, text: &BufferSnapshot) {
+        self.snapshot
+            .reparse(text, self.language_registry.clone(), language);
+    }
+
+    pub fn did_parse(&mut self, snapshot: SyntaxSnapshot) {
+        self.snapshot = snapshot;
+    }
+
+    pub fn clear(&mut self) {
+        self.snapshot = SyntaxSnapshot::default();
+    }
+}
+
+impl SyntaxSnapshot {
+    pub fn is_empty(&self) -> bool {
+        self.layers.is_empty()
+    }
+
+    fn interpolate(&mut self, text: &BufferSnapshot) {
+        let edits = text
+            .anchored_edits_since::<(usize, Point)>(&self.interpolated_version)
+            .collect::<Vec<_>>();
+        self.interpolated_version = text.version().clone();
+
+        if edits.is_empty() {
+            return;
+        }
+
+        let mut layers = SumTree::new();
+        let mut first_edit_ix_for_depth = 0;
+        let mut prev_depth = 0;
+        let mut cursor = self.layers.cursor::<SyntaxLayerSummary>();
+        cursor.next(text);
+
+        'outer: loop {
+            let depth = cursor.end(text).max_depth;
+            if depth > prev_depth {
+                first_edit_ix_for_depth = 0;
+                prev_depth = depth;
+            }
+
+            // Preserve any layers at this depth that precede the first edit.
+            if let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) {
+                let target = ChangeStartPosition {
+                    depth,
+                    position: edit_range.start,
+                };
+                if target.cmp(&cursor.start(), text).is_gt() {
+                    let slice = cursor.slice(&target, Bias::Left, text);
+                    layers.append(slice, text);
+                }
+            }
+            // If this layer follows all of the edits, then preserve it and any
+            // subsequent layers at this same depth.
+            else if cursor.item().is_some() {
+                let slice = cursor.slice(
+                    &SyntaxLayerPosition {
+                        depth: depth + 1,
+                        range: Anchor::MIN..Anchor::MAX,
+                        language: None,
+                    },
+                    Bias::Left,
+                    text,
+                );
+                layers.append(slice, text);
+                continue;
+            };
+
+            let Some(layer) = cursor.item() else { break };
+            let (start_byte, start_point) = layer.range.start.summary::<(usize, Point)>(text);
+
+            // Ignore edits that end before the start of this layer, and don't consider them
+            // for any subsequent layers at this same depth.
+            loop {
+                let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else {
+                    continue 'outer;
+                };
+                if edit_range.end.cmp(&layer.range.start, text).is_le() {
+                    first_edit_ix_for_depth += 1;
+                } else {
+                    break;
+                }
+            }
+
+            let mut layer = layer.clone();
+            if let SyntaxLayerContent::Parsed { tree, .. } = &mut layer.content {
+                for (edit, edit_range) in &edits[first_edit_ix_for_depth..] {
+                    // Ignore any edits that follow this layer.
+                    if edit_range.start.cmp(&layer.range.end, text).is_ge() {
+                        break;
+                    }
+
+                    // Apply any edits that intersect this layer to the layer's syntax tree.
+                    let tree_edit = if edit_range.start.cmp(&layer.range.start, text).is_ge() {
+                        tree_sitter::InputEdit {
+                            start_byte: edit.new.start.0 - start_byte,
+                            old_end_byte: edit.new.start.0 - start_byte
+                                + (edit.old.end.0 - edit.old.start.0),
+                            new_end_byte: edit.new.end.0 - start_byte,
+                            start_position: (edit.new.start.1 - start_point).to_ts_point(),
+                            old_end_position: (edit.new.start.1 - start_point
+                                + (edit.old.end.1 - edit.old.start.1))
+                                .to_ts_point(),
+                            new_end_position: (edit.new.end.1 - start_point).to_ts_point(),
+                        }
+                    } else {
+                        let node = tree.root_node();
+                        tree_sitter::InputEdit {
+                            start_byte: 0,
+                            old_end_byte: node.end_byte(),
+                            new_end_byte: 0,
+                            start_position: Default::default(),
+                            old_end_position: node.end_position(),
+                            new_end_position: Default::default(),
+                        }
+                    };
+
+                    tree.edit(&tree_edit);
+                }
+
+                debug_assert!(
+                    tree.root_node().end_byte() <= text.len(),
+                    "tree's size {}, is larger than text size {}",
+                    tree.root_node().end_byte(),
+                    text.len(),
+                );
+            }
+
+            layers.push(layer, text);
+            cursor.next(text);
+        }
+
+        layers.append(cursor.suffix(&text), &text);
+        drop(cursor);
+        self.layers = layers;
+    }
+
+    pub fn reparse(
+        &mut self,
+        text: &BufferSnapshot,
+        registry: Option<Arc<LanguageRegistry>>,
+        root_language: Arc<Language>,
+    ) {
+        let edit_ranges = text
+            .edits_since::<usize>(&self.parsed_version)
+            .map(|edit| edit.new)
+            .collect::<Vec<_>>();
+        self.reparse_with_ranges(text, root_language.clone(), edit_ranges, registry.as_ref());
+
+        if let Some(registry) = registry {
+            if registry.version() != self.language_registry_version {
+                let mut resolved_injection_ranges = Vec::new();
+                let mut cursor = self
+                    .layers
+                    .filter::<_, ()>(|summary| summary.contains_unknown_injections);
+                cursor.next(text);
+                while let Some(layer) = cursor.item() {
+                    let SyntaxLayerContent::Pending { language_name } = &layer.content else {
+                        unreachable!()
+                    };
+                    if registry
+                        .language_for_name_or_extension(language_name)
+                        .now_or_never()
+                        .and_then(|language| language.ok())
+                        .is_some()
+                    {
+                        resolved_injection_ranges.push(layer.range.to_offset(text));
+                    }
+
+                    cursor.next(text);
+                }
+                drop(cursor);
+
+                if !resolved_injection_ranges.is_empty() {
+                    self.reparse_with_ranges(
+                        text,
+                        root_language,
+                        resolved_injection_ranges,
+                        Some(&registry),
+                    );
+                }
+                self.language_registry_version = registry.version();
+            }
+        }
+    }
+
+    fn reparse_with_ranges(
+        &mut self,
+        text: &BufferSnapshot,
+        root_language: Arc<Language>,
+        invalidated_ranges: Vec<Range<usize>>,
+        registry: Option<&Arc<LanguageRegistry>>,
+    ) {
+        log::trace!("reparse. invalidated ranges:{:?}", invalidated_ranges);
+
+        let max_depth = self.layers.summary().max_depth;
+        let mut cursor = self.layers.cursor::<SyntaxLayerSummary>();
+        cursor.next(&text);
+        let mut layers = SumTree::new();
+
+        let mut changed_regions = ChangeRegionSet::default();
+        let mut queue = BinaryHeap::new();
+        let mut combined_injection_ranges = HashMap::default();
+        queue.push(ParseStep {
+            depth: 0,
+            language: ParseStepLanguage::Loaded {
+                language: root_language,
+            },
+            included_ranges: vec![tree_sitter::Range {
+                start_byte: 0,
+                end_byte: text.len(),
+                start_point: Point::zero().to_ts_point(),
+                end_point: text.max_point().to_ts_point(),
+            }],
+            range: Anchor::MIN..Anchor::MAX,
+            mode: ParseMode::Single,
+        });
+
+        loop {
+            let step = queue.pop();
+            let position = if let Some(step) = &step {
+                SyntaxLayerPosition {
+                    depth: step.depth,
+                    range: step.range.clone(),
+                    language: step.language.id(),
+                }
+            } else {
+                SyntaxLayerPosition {
+                    depth: max_depth + 1,
+                    range: Anchor::MAX..Anchor::MAX,
+                    language: None,
+                }
+            };
+
+            let mut done = cursor.item().is_none();
+            while !done && position.cmp(&cursor.end(text), &text).is_gt() {
+                done = true;
+
+                let bounded_position = SyntaxLayerPositionBeforeChange {
+                    position: position.clone(),
+                    change: changed_regions.start_position(),
+                };
+                if bounded_position.cmp(&cursor.start(), &text).is_gt() {
+                    let slice = cursor.slice(&bounded_position, Bias::Left, text);
+                    if !slice.is_empty() {
+                        layers.append(slice, &text);
+                        if changed_regions.prune(cursor.end(text), text) {
+                            done = false;
+                        }
+                    }
+                }
+
+                while position.cmp(&cursor.end(text), text).is_gt() {
+                    let Some(layer) = cursor.item() else { break };
+
+                    if changed_regions.intersects(&layer, text) {
+                        if let SyntaxLayerContent::Parsed { language, .. } = &layer.content {
+                            log::trace!(
+                                "discard layer. language:{}, range:{:?}. changed_regions:{:?}",
+                                language.name(),
+                                LogAnchorRange(&layer.range, text),
+                                LogChangedRegions(&changed_regions, text),
+                            );
+                        }
+
+                        changed_regions.insert(
+                            ChangedRegion {
+                                depth: layer.depth + 1,
+                                range: layer.range.clone(),
+                            },
+                            text,
+                        );
+                    } else {
+                        layers.push(layer.clone(), text);
+                    }
+
+                    cursor.next(text);
+                    if changed_regions.prune(cursor.end(text), text) {
+                        done = false;
+                    }
+                }
+            }
+
+            let Some(step) = step else { break };
+            let (step_start_byte, step_start_point) =
+                step.range.start.summary::<(usize, Point)>(text);
+            let step_end_byte = step.range.end.to_offset(text);
+
+            let mut old_layer = cursor.item();
+            if let Some(layer) = old_layer {
+                if layer.range.to_offset(text) == (step_start_byte..step_end_byte)
+                    && layer.content.language_id() == step.language.id()
+                {
+                    cursor.next(&text);
+                } else {
+                    old_layer = None;
+                }
+            }
+
+            let content = match step.language {
+                ParseStepLanguage::Loaded { language } => {
+                    let Some(grammar) = language.grammar() else {
+                        continue;
+                    };
+                    let tree;
+                    let changed_ranges;
+
+                    let mut included_ranges = step.included_ranges;
+                    for range in &mut included_ranges {
+                        range.start_byte -= step_start_byte;
+                        range.end_byte -= step_start_byte;
+                        range.start_point = (Point::from_ts_point(range.start_point)
+                            - step_start_point)
+                            .to_ts_point();
+                        range.end_point = (Point::from_ts_point(range.end_point)
+                            - step_start_point)
+                            .to_ts_point();
+                    }
+
+                    if let Some((SyntaxLayerContent::Parsed { tree: old_tree, .. }, layer_start)) =
+                        old_layer.map(|layer| (&layer.content, layer.range.start))
+                    {
+                        log::trace!(
+                            "existing layer. language:{}, start:{:?}, ranges:{:?}",
+                            language.name(),
+                            LogPoint(layer_start.to_point(&text)),
+                            LogIncludedRanges(&old_tree.included_ranges())
+                        );
+
+                        if let ParseMode::Combined {
+                            mut parent_layer_changed_ranges,
+                            ..
+                        } = step.mode
+                        {
+                            for range in &mut parent_layer_changed_ranges {
+                                range.start = range.start.saturating_sub(step_start_byte);
+                                range.end = range.end.saturating_sub(step_start_byte);
+                            }
+
+                            let changed_indices;
+                            (included_ranges, changed_indices) = splice_included_ranges(
+                                old_tree.included_ranges(),
+                                &parent_layer_changed_ranges,
+                                &included_ranges,
+                            );
+                            insert_newlines_between_ranges(
+                                changed_indices,
+                                &mut included_ranges,
+                                &text,
+                                step_start_byte,
+                                step_start_point,
+                            );
+                        }
+
+                        if included_ranges.is_empty() {
+                            included_ranges.push(tree_sitter::Range {
+                                start_byte: 0,
+                                end_byte: 0,
+                                start_point: Default::default(),
+                                end_point: Default::default(),
+                            });
+                        }
+
+                        log::trace!(
+                            "update layer. language:{}, start:{:?}, included_ranges:{:?}",
+                            language.name(),
+                            LogAnchorRange(&step.range, text),
+                            LogIncludedRanges(&included_ranges),
+                        );
+
+                        tree = parse_text(
+                            grammar,
+                            text.as_rope(),
+                            step_start_byte,
+                            included_ranges,
+                            Some(old_tree.clone()),
+                        );
+                        changed_ranges = join_ranges(
+                            invalidated_ranges.iter().cloned().filter(|range| {
+                                range.start <= step_end_byte && range.end >= step_start_byte
+                            }),
+                            old_tree.changed_ranges(&tree).map(|r| {
+                                step_start_byte + r.start_byte..step_start_byte + r.end_byte
+                            }),
+                        );
+                    } else {
+                        if matches!(step.mode, ParseMode::Combined { .. }) {
+                            insert_newlines_between_ranges(
+                                0..included_ranges.len(),
+                                &mut included_ranges,
+                                text,
+                                step_start_byte,
+                                step_start_point,
+                            );
+                        }
+
+                        if included_ranges.is_empty() {
+                            included_ranges.push(tree_sitter::Range {
+                                start_byte: 0,
+                                end_byte: 0,
+                                start_point: Default::default(),
+                                end_point: Default::default(),
+                            });
+                        }
+
+                        log::trace!(
+                            "create layer. language:{}, range:{:?}, included_ranges:{:?}",
+                            language.name(),
+                            LogAnchorRange(&step.range, text),
+                            LogIncludedRanges(&included_ranges),
+                        );
+
+                        tree = parse_text(
+                            grammar,
+                            text.as_rope(),
+                            step_start_byte,
+                            included_ranges,
+                            None,
+                        );
+                        changed_ranges = vec![step_start_byte..step_end_byte];
+                    }
+
+                    if let (Some((config, registry)), false) = (
+                        grammar.injection_config.as_ref().zip(registry.as_ref()),
+                        changed_ranges.is_empty(),
+                    ) {
+                        for range in &changed_ranges {
+                            changed_regions.insert(
+                                ChangedRegion {
+                                    depth: step.depth + 1,
+                                    range: text.anchor_before(range.start)
+                                        ..text.anchor_after(range.end),
+                                },
+                                text,
+                            );
+                        }
+                        get_injections(
+                            config,
+                            text,
+                            step.range.clone(),
+                            tree.root_node_with_offset(
+                                step_start_byte,
+                                step_start_point.to_ts_point(),
+                            ),
+                            registry,
+                            step.depth + 1,
+                            &changed_ranges,
+                            &mut combined_injection_ranges,
+                            &mut queue,
+                        );
+                    }
+
+                    SyntaxLayerContent::Parsed { tree, language }
+                }
+                ParseStepLanguage::Pending { name } => SyntaxLayerContent::Pending {
+                    language_name: name,
+                },
+            };
+
+            layers.push(
+                SyntaxLayer {
+                    depth: step.depth,
+                    range: step.range,
+                    content,
+                },
+                &text,
+            );
+        }
+
+        drop(cursor);
+        self.layers = layers;
+        self.interpolated_version = text.version.clone();
+        self.parsed_version = text.version.clone();
+        #[cfg(debug_assertions)]
+        self.check_invariants(text);
+    }
+
+    #[cfg(debug_assertions)]
+    fn check_invariants(&self, text: &BufferSnapshot) {
+        let mut max_depth = 0;
+        let mut prev_range: Option<Range<Anchor>> = None;
+        for layer in self.layers.iter() {
+            if layer.depth == max_depth {
+                if let Some(prev_range) = prev_range {
+                    match layer.range.start.cmp(&prev_range.start, text) {
+                        Ordering::Less => panic!("layers out of order"),
+                        Ordering::Equal => {
+                            assert!(layer.range.end.cmp(&prev_range.end, text).is_ge())
+                        }
+                        Ordering::Greater => {}
+                    }
+                }
+            } else if layer.depth < max_depth {
+                panic!("layers out of order")
+            }
+            max_depth = layer.depth;
+            prev_range = Some(layer.range.clone());
+        }
+    }
+
+    pub fn single_tree_captures<'a>(
+        range: Range<usize>,
+        text: &'a Rope,
+        tree: &'a Tree,
+        language: &'a Arc<Language>,
+        query: fn(&Grammar) -> Option<&Query>,
+    ) -> SyntaxMapCaptures<'a> {
+        SyntaxMapCaptures::new(
+            range.clone(),
+            text,
+            [SyntaxLayerInfo {
+                language,
+                tree,
+                depth: 0,
+                offset: (0, tree_sitter::Point::new(0, 0)),
+            }]
+            .into_iter(),
+            query,
+        )
+    }
+
+    pub fn captures<'a>(
+        &'a self,
+        range: Range<usize>,
+        buffer: &'a BufferSnapshot,
+        query: fn(&Grammar) -> Option<&Query>,
+    ) -> SyntaxMapCaptures {
+        SyntaxMapCaptures::new(
+            range.clone(),
+            buffer.as_rope(),
+            self.layers_for_range(range, buffer).into_iter(),
+            query,
+        )
+    }
+
+    pub fn matches<'a>(
+        &'a self,
+        range: Range<usize>,
+        buffer: &'a BufferSnapshot,
+        query: fn(&Grammar) -> Option<&Query>,
+    ) -> SyntaxMapMatches {
+        SyntaxMapMatches::new(
+            range.clone(),
+            buffer.as_rope(),
+            self.layers_for_range(range, buffer).into_iter(),
+            query,
+        )
+    }
+
+    #[allow(dead_code)] // todo!()
+    #[cfg(test)]
+    pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec<SyntaxLayerInfo> {
+        self.layers_for_range(0..buffer.len(), buffer).collect()
+    }
+
+    pub fn layers_for_range<'a, T: ToOffset>(
+        &'a self,
+        range: Range<T>,
+        buffer: &'a BufferSnapshot,
+    ) -> impl 'a + Iterator<Item = SyntaxLayerInfo> {
+        let start_offset = range.start.to_offset(buffer);
+        let end_offset = range.end.to_offset(buffer);
+        let start = buffer.anchor_before(start_offset);
+        let end = buffer.anchor_after(end_offset);
+
+        let mut cursor = self.layers.filter::<_, ()>(move |summary| {
+            if summary.max_depth > summary.min_depth {
+                true
+            } else {
+                let is_before_start = summary.range.end.cmp(&start, buffer).is_lt();
+                let is_after_end = summary.range.start.cmp(&end, buffer).is_gt();
+                !is_before_start && !is_after_end
+            }
+        });
+
+        cursor.next(buffer);
+        iter::from_fn(move || {
+            while let Some(layer) = cursor.item() {
+                let mut info = None;
+                if let SyntaxLayerContent::Parsed { tree, language } = &layer.content {
+                    let layer_start_offset = layer.range.start.to_offset(buffer);
+                    let layer_start_point = layer.range.start.to_point(buffer).to_ts_point();
+
+                    info = Some(SyntaxLayerInfo {
+                        tree,
+                        language,
+                        depth: layer.depth,
+                        offset: (layer_start_offset, layer_start_point),
+                    });
+                }
+                cursor.next(buffer);
+                if info.is_some() {
+                    return info;
+                }
+            }
+            None
+        })
+    }
+
+    pub fn contains_unknown_injections(&self) -> bool {
+        self.layers.summary().contains_unknown_injections
+    }
+
+    pub fn language_registry_version(&self) -> usize {
+        self.language_registry_version
+    }
+}
+
+impl<'a> SyntaxMapCaptures<'a> {
+    fn new(
+        range: Range<usize>,
+        text: &'a Rope,
+        layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
+        query: fn(&Grammar) -> Option<&Query>,
+    ) -> Self {
+        let mut result = Self {
+            layers: Vec::new(),
+            grammars: Vec::new(),
+            active_layer_count: 0,
+        };
+        for layer in layers {
+            let grammar = match &layer.language.grammar {
+                Some(grammar) => grammar,
+                None => continue,
+            };
+            let query = match query(&grammar) {
+                Some(query) => query,
+                None => continue,
+            };
+
+            let mut query_cursor = QueryCursorHandle::new();
+
+            // TODO - add a Tree-sitter API to remove the need for this.
+            let cursor = unsafe {
+                std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut())
+            };
+
+            cursor.set_byte_range(range.clone());
+            let captures = cursor.captures(query, layer.node(), TextProvider(text));
+            let grammar_index = result
+                .grammars
+                .iter()
+                .position(|g| g.id == grammar.id())
+                .unwrap_or_else(|| {
+                    result.grammars.push(grammar);
+                    result.grammars.len() - 1
+                });
+            let mut layer = SyntaxMapCapturesLayer {
+                depth: layer.depth,
+                grammar_index,
+                next_capture: None,
+                captures,
+                _query_cursor: query_cursor,
+            };
+
+            layer.advance();
+            if layer.next_capture.is_some() {
+                let key = layer.sort_key();
+                let ix = match result.layers[..result.active_layer_count]
+                    .binary_search_by_key(&key, |layer| layer.sort_key())
+                {
+                    Ok(ix) | Err(ix) => ix,
+                };
+                result.layers.insert(ix, layer);
+                result.active_layer_count += 1;
+            } else {
+                result.layers.push(layer);
+            }
+        }
+
+        result
+    }
+
+    pub fn grammars(&self) -> &[&'a Grammar] {
+        &self.grammars
+    }
+
+    pub fn peek(&self) -> Option<SyntaxMapCapture<'a>> {
+        let layer = self.layers[..self.active_layer_count].first()?;
+        let capture = layer.next_capture?;
+        Some(SyntaxMapCapture {
+            depth: layer.depth,
+            grammar_index: layer.grammar_index,
+            index: capture.index,
+            node: capture.node,
+        })
+    }
+
+    pub fn advance(&mut self) -> bool {
+        let layer = if let Some(layer) = self.layers[..self.active_layer_count].first_mut() {
+            layer
+        } else {
+            return false;
+        };
+
+        layer.advance();
+        if layer.next_capture.is_some() {
+            let key = layer.sort_key();
+            let i = 1 + self.layers[1..self.active_layer_count]
+                .iter()
+                .position(|later_layer| key < later_layer.sort_key())
+                .unwrap_or(self.active_layer_count - 1);
+            self.layers[0..i].rotate_left(1);
+        } else {
+            self.layers[0..self.active_layer_count].rotate_left(1);
+            self.active_layer_count -= 1;
+        }
+
+        true
+    }
+
+    pub fn set_byte_range(&mut self, range: Range<usize>) {
+        for layer in &mut self.layers {
+            layer.captures.set_byte_range(range.clone());
+            if let Some(capture) = &layer.next_capture {
+                if capture.node.end_byte() > range.start {
+                    continue;
+                }
+            }
+            layer.advance();
+        }
+        self.layers.sort_unstable_by_key(|layer| layer.sort_key());
+        self.active_layer_count = self
+            .layers
+            .iter()
+            .position(|layer| layer.next_capture.is_none())
+            .unwrap_or(self.layers.len());
+    }
+}
+
+impl<'a> SyntaxMapMatches<'a> {
+    fn new(
+        range: Range<usize>,
+        text: &'a Rope,
+        layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
+        query: fn(&Grammar) -> Option<&Query>,
+    ) -> Self {
+        let mut result = Self::default();
+        for layer in layers {
+            let grammar = match &layer.language.grammar {
+                Some(grammar) => grammar,
+                None => continue,
+            };
+            let query = match query(&grammar) {
+                Some(query) => query,
+                None => continue,
+            };
+
+            let mut query_cursor = QueryCursorHandle::new();
+
+            // TODO - add a Tree-sitter API to remove the need for this.
+            let cursor = unsafe {
+                std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut())
+            };
+
+            cursor.set_byte_range(range.clone());
+            let matches = cursor.matches(query, layer.node(), TextProvider(text));
+            let grammar_index = result
+                .grammars
+                .iter()
+                .position(|g| g.id == grammar.id())
+                .unwrap_or_else(|| {
+                    result.grammars.push(grammar);
+                    result.grammars.len() - 1
+                });
+            let mut layer = SyntaxMapMatchesLayer {
+                depth: layer.depth,
+                grammar_index,
+                matches,
+                next_pattern_index: 0,
+                next_captures: Vec::new(),
+                has_next: false,
+                _query_cursor: query_cursor,
+            };
+
+            layer.advance();
+            if layer.has_next {
+                let key = layer.sort_key();
+                let ix = match result.layers[..result.active_layer_count]
+                    .binary_search_by_key(&key, |layer| layer.sort_key())
+                {
+                    Ok(ix) | Err(ix) => ix,
+                };
+                result.layers.insert(ix, layer);
+                result.active_layer_count += 1;
+            } else {
+                result.layers.push(layer);
+            }
+        }
+        result
+    }
+
+    pub fn grammars(&self) -> &[&'a Grammar] {
+        &self.grammars
+    }
+
+    pub fn peek(&self) -> Option<SyntaxMapMatch> {
+        let layer = self.layers.first()?;
+        if !layer.has_next {
+            return None;
+        }
+        Some(SyntaxMapMatch {
+            depth: layer.depth,
+            grammar_index: layer.grammar_index,
+            pattern_index: layer.next_pattern_index,
+            captures: &layer.next_captures,
+        })
+    }
+
+    pub fn advance(&mut self) -> bool {
+        let layer = if let Some(layer) = self.layers.first_mut() {
+            layer
+        } else {
+            return false;
+        };
+
+        layer.advance();
+        if layer.has_next {
+            let key = layer.sort_key();
+            let i = 1 + self.layers[1..self.active_layer_count]
+                .iter()
+                .position(|later_layer| key < later_layer.sort_key())
+                .unwrap_or(self.active_layer_count - 1);
+            self.layers[0..i].rotate_left(1);
+        } else {
+            self.layers[0..self.active_layer_count].rotate_left(1);
+            self.active_layer_count -= 1;
+        }
+
+        true
+    }
+}
+
+impl<'a> SyntaxMapCapturesLayer<'a> {
+    fn advance(&mut self) {
+        self.next_capture = self.captures.next().map(|(mat, ix)| mat.captures[ix]);
+    }
+
+    fn sort_key(&self) -> (usize, Reverse<usize>, usize) {
+        if let Some(capture) = &self.next_capture {
+            let range = capture.node.byte_range();
+            (range.start, Reverse(range.end), self.depth)
+        } else {
+            (usize::MAX, Reverse(0), usize::MAX)
+        }
+    }
+}
+
+impl<'a> SyntaxMapMatchesLayer<'a> {
+    fn advance(&mut self) {
+        if let Some(mat) = self.matches.next() {
+            self.next_captures.clear();
+            self.next_captures.extend_from_slice(&mat.captures);
+            self.next_pattern_index = mat.pattern_index;
+            self.has_next = true;
+        } else {
+            self.has_next = false;
+        }
+    }
+
+    fn sort_key(&self) -> (usize, Reverse<usize>, usize) {
+        if self.has_next {
+            let captures = &self.next_captures;
+            if let Some((first, last)) = captures.first().zip(captures.last()) {
+                return (
+                    first.node.start_byte(),
+                    Reverse(last.node.end_byte()),
+                    self.depth,
+                );
+            }
+        }
+        (usize::MAX, Reverse(0), usize::MAX)
+    }
+}
+
+impl<'a> Iterator for SyntaxMapCaptures<'a> {
+    type Item = SyntaxMapCapture<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let result = self.peek();
+        self.advance();
+        result
+    }
+}
+
+fn join_ranges(
+    a: impl Iterator<Item = Range<usize>>,
+    b: impl Iterator<Item = Range<usize>>,
+) -> Vec<Range<usize>> {
+    let mut result = Vec::<Range<usize>>::new();
+    let mut a = a.peekable();
+    let mut b = b.peekable();
+    loop {
+        let range = match (a.peek(), b.peek()) {
+            (Some(range_a), Some(range_b)) => {
+                if range_a.start < range_b.start {
+                    a.next().unwrap()
+                } else {
+                    b.next().unwrap()
+                }
+            }
+            (None, Some(_)) => b.next().unwrap(),
+            (Some(_), None) => a.next().unwrap(),
+            (None, None) => break,
+        };
+
+        if let Some(last) = result.last_mut() {
+            if range.start <= last.end {
+                last.end = last.end.max(range.end);
+                continue;
+            }
+        }
+        result.push(range);
+    }
+    result
+}
+
+fn parse_text(
+    grammar: &Grammar,
+    text: &Rope,
+    start_byte: usize,
+    ranges: Vec<tree_sitter::Range>,
+    old_tree: Option<Tree>,
+) -> Tree {
+    PARSER.with(|parser| {
+        let mut parser = parser.borrow_mut();
+        let mut chunks = text.chunks_in_range(start_byte..text.len());
+        parser
+            .set_included_ranges(&ranges)
+            .expect("overlapping ranges");
+        parser
+            .set_language(grammar.ts_language)
+            .expect("incompatible grammar");
+        parser
+            .parse_with(
+                &mut move |offset, _| {
+                    chunks.seek(start_byte + offset);
+                    chunks.next().unwrap_or("").as_bytes()
+                },
+                old_tree.as_ref(),
+            )
+            .expect("invalid language")
+    })
+}
+
+fn get_injections(
+    config: &InjectionConfig,
+    text: &BufferSnapshot,
+    outer_range: Range<Anchor>,
+    node: Node,
+    language_registry: &Arc<LanguageRegistry>,
+    depth: usize,
+    changed_ranges: &[Range<usize>],
+    combined_injection_ranges: &mut HashMap<Arc<Language>, Vec<tree_sitter::Range>>,
+    queue: &mut BinaryHeap<ParseStep>,
+) {
+    let mut query_cursor = QueryCursorHandle::new();
+    let mut prev_match = None;
+
+    // Ensure that a `ParseStep` is created for every combined injection language, even
+    // if there currently no matches for that injection.
+    combined_injection_ranges.clear();
+    for pattern in &config.patterns {
+        if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
+            if let Some(language) = language_registry
+                .language_for_name_or_extension(language_name)
+                .now_or_never()
+                .and_then(|language| language.ok())
+            {
+                combined_injection_ranges.insert(language, Vec::new());
+            }
+        }
+    }
+
+    for query_range in changed_ranges {
+        query_cursor.set_byte_range(query_range.start.saturating_sub(1)..query_range.end + 1);
+        for mat in query_cursor.matches(&config.query, node, TextProvider(text.as_rope())) {
+            let content_ranges = mat
+                .nodes_for_capture_index(config.content_capture_ix)
+                .map(|node| node.range())
+                .collect::<Vec<_>>();
+            if content_ranges.is_empty() {
+                continue;
+            }
+
+            let content_range =
+                content_ranges.first().unwrap().start_byte..content_ranges.last().unwrap().end_byte;
+
+            // Avoid duplicate matches if two changed ranges intersect the same injection.
+            if let Some((prev_pattern_ix, prev_range)) = &prev_match {
+                if mat.pattern_index == *prev_pattern_ix && content_range == *prev_range {
+                    continue;
+                }
+            }
+
+            prev_match = Some((mat.pattern_index, content_range.clone()));
+            let combined = config.patterns[mat.pattern_index].combined;
+
+            let mut language_name = None;
+            let mut step_range = content_range.clone();
+            if let Some(name) = config.patterns[mat.pattern_index].language.as_ref() {
+                language_name = Some(Cow::Borrowed(name.as_ref()))
+            } else if let Some(language_node) = config
+                .language_capture_ix
+                .and_then(|ix| mat.nodes_for_capture_index(ix).next())
+            {
+                step_range.start = cmp::min(content_range.start, language_node.start_byte());
+                step_range.end = cmp::max(content_range.end, language_node.end_byte());
+                language_name = Some(Cow::Owned(
+                    text.text_for_range(language_node.byte_range()).collect(),
+                ))
+            };
+
+            if let Some(language_name) = language_name {
+                let language = language_registry
+                    .language_for_name_or_extension(&language_name)
+                    .now_or_never()
+                    .and_then(|language| language.ok());
+                let range = text.anchor_before(step_range.start)..text.anchor_after(step_range.end);
+                if let Some(language) = language {
+                    if combined {
+                        combined_injection_ranges
+                            .entry(language.clone())
+                            .or_default()
+                            .extend(content_ranges);
+                    } else {
+                        queue.push(ParseStep {
+                            depth,
+                            language: ParseStepLanguage::Loaded { language },
+                            included_ranges: content_ranges,
+                            range,
+                            mode: ParseMode::Single,
+                        });
+                    }
+                } else {
+                    queue.push(ParseStep {
+                        depth,
+                        language: ParseStepLanguage::Pending {
+                            name: language_name.into(),
+                        },
+                        included_ranges: content_ranges,
+                        range,
+                        mode: ParseMode::Single,
+                    });
+                }
+            }
+        }
+    }
+
+    for (language, mut included_ranges) in combined_injection_ranges.drain() {
+        included_ranges.sort_unstable_by(|a, b| {
+            Ord::cmp(&a.start_byte, &b.start_byte).then_with(|| Ord::cmp(&a.end_byte, &b.end_byte))
+        });
+        queue.push(ParseStep {
+            depth,
+            language: ParseStepLanguage::Loaded { language },
+            range: outer_range.clone(),
+            included_ranges,
+            mode: ParseMode::Combined {
+                parent_layer_range: node.start_byte()..node.end_byte(),
+                parent_layer_changed_ranges: changed_ranges.to_vec(),
+            },
+        })
+    }
+}
+
+/// Update the given list of included `ranges`, removing any ranges that intersect
+/// `removed_ranges`, and inserting the given `new_ranges`.
+///
+/// Returns a new vector of ranges, and the range of the vector that was changed,
+/// from the previous `ranges` vector.
+pub(crate) fn splice_included_ranges(
+    mut ranges: Vec<tree_sitter::Range>,
+    removed_ranges: &[Range<usize>],
+    new_ranges: &[tree_sitter::Range],
+) -> (Vec<tree_sitter::Range>, Range<usize>) {
+    let mut removed_ranges = removed_ranges.iter().cloned().peekable();
+    let mut new_ranges = new_ranges.into_iter().cloned().peekable();
+    let mut ranges_ix = 0;
+    let mut changed_portion = usize::MAX..0;
+    loop {
+        let next_new_range = new_ranges.peek();
+        let next_removed_range = removed_ranges.peek();
+
+        let (remove, insert) = match (next_removed_range, next_new_range) {
+            (None, None) => break,
+            (Some(_), None) => (removed_ranges.next().unwrap(), None),
+            (Some(next_removed_range), Some(next_new_range)) => {
+                if next_removed_range.end < next_new_range.start_byte {
+                    (removed_ranges.next().unwrap(), None)
+                } else {
+                    let mut start = next_new_range.start_byte;
+                    let mut end = next_new_range.end_byte;
+
+                    while let Some(next_removed_range) = removed_ranges.peek() {
+                        if next_removed_range.start > next_new_range.end_byte {
+                            break;
+                        }
+                        let next_removed_range = removed_ranges.next().unwrap();
+                        start = cmp::min(start, next_removed_range.start);
+                        end = cmp::max(end, next_removed_range.end);
+                    }
+
+                    (start..end, Some(new_ranges.next().unwrap()))
+                }
+            }
+            (None, Some(next_new_range)) => (
+                next_new_range.start_byte..next_new_range.end_byte,
+                Some(new_ranges.next().unwrap()),
+            ),
+        };
+
+        let mut start_ix = ranges_ix
+            + match ranges[ranges_ix..].binary_search_by_key(&remove.start, |r| r.end_byte) {
+                Ok(ix) => ix,
+                Err(ix) => ix,
+            };
+        let mut end_ix = ranges_ix
+            + match ranges[ranges_ix..].binary_search_by_key(&remove.end, |r| r.start_byte) {
+                Ok(ix) => ix + 1,
+                Err(ix) => ix,
+            };
+
+        // If there are empty ranges, then there may be multiple ranges with the same
+        // start or end. Expand the splice to include any adjacent ranges that touch
+        // the changed range.
+        while start_ix > 0 {
+            if ranges[start_ix - 1].end_byte == remove.start {
+                start_ix -= 1;
+            } else {
+                break;
+            }
+        }
+        while let Some(range) = ranges.get(end_ix) {
+            if range.start_byte == remove.end {
+                end_ix += 1;
+            } else {
+                break;
+            }
+        }
+
+        changed_portion.start = changed_portion.start.min(start_ix);
+        changed_portion.end = changed_portion.end.max(if insert.is_some() {
+            start_ix + 1
+        } else {
+            start_ix
+        });
+
+        ranges.splice(start_ix..end_ix, insert);
+        ranges_ix = start_ix;
+    }
+
+    if changed_portion.end < changed_portion.start {
+        changed_portion = 0..0;
+    }
+
+    (ranges, changed_portion)
+}
+
+/// Ensure there are newline ranges in between content range that appear on
+/// different lines. For performance, only iterate through the given range of
+/// indices. All of the ranges in the array are relative to a given start byte
+/// and point.
+fn insert_newlines_between_ranges(
+    indices: Range<usize>,
+    ranges: &mut Vec<tree_sitter::Range>,
+    text: &text::BufferSnapshot,
+    start_byte: usize,
+    start_point: Point,
+) {
+    let mut ix = indices.end + 1;
+    while ix > indices.start {
+        ix -= 1;
+        if 0 == ix || ix == ranges.len() {
+            continue;
+        }
+
+        let range_b = ranges[ix].clone();
+        let range_a = &mut ranges[ix - 1];
+        if range_a.end_point.column == 0 {
+            continue;
+        }
+
+        if range_a.end_point.row < range_b.start_point.row {
+            let end_point = start_point + Point::from_ts_point(range_a.end_point);
+            let line_end = Point::new(end_point.row, text.line_len(end_point.row));
+            if end_point.column as u32 >= line_end.column {
+                range_a.end_byte += 1;
+                range_a.end_point.row += 1;
+                range_a.end_point.column = 0;
+            } else {
+                let newline_offset = text.point_to_offset(line_end);
+                ranges.insert(
+                    ix,
+                    tree_sitter::Range {
+                        start_byte: newline_offset - start_byte,
+                        end_byte: newline_offset - start_byte + 1,
+                        start_point: (line_end - start_point).to_ts_point(),
+                        end_point: ((line_end - start_point) + Point::new(1, 0)).to_ts_point(),
+                    },
+                )
+            }
+        }
+    }
+}
+
+impl OwnedSyntaxLayerInfo {
+    pub fn node(&self) -> Node {
+        self.tree
+            .root_node_with_offset(self.offset.0, self.offset.1)
+    }
+}
+
+impl<'a> SyntaxLayerInfo<'a> {
+    pub fn to_owned(&self) -> OwnedSyntaxLayerInfo {
+        OwnedSyntaxLayerInfo {
+            tree: self.tree.clone(),
+            offset: self.offset,
+            depth: self.depth,
+            language: self.language.clone(),
+        }
+    }
+
+    pub fn node(&self) -> Node<'a> {
+        self.tree
+            .root_node_with_offset(self.offset.0, self.offset.1)
+    }
+
+    pub(crate) fn override_id(&self, offset: usize, text: &text::BufferSnapshot) -> Option<u32> {
+        let text = TextProvider(text.as_rope());
+        let config = self.language.grammar.as_ref()?.override_config.as_ref()?;
+
+        let mut query_cursor = QueryCursorHandle::new();
+        query_cursor.set_byte_range(offset..offset);
+
+        let mut smallest_match: Option<(u32, Range<usize>)> = None;
+        for mat in query_cursor.matches(&config.query, self.node(), text) {
+            for capture in mat.captures {
+                if !config.values.contains_key(&capture.index) {
+                    continue;
+                }
+
+                let range = capture.node.byte_range();
+                if offset <= range.start || offset >= range.end {
+                    continue;
+                }
+
+                if let Some((_, smallest_range)) = &smallest_match {
+                    if range.len() < smallest_range.len() {
+                        smallest_match = Some((capture.index, range))
+                    }
+                    continue;
+                }
+
+                smallest_match = Some((capture.index, range));
+            }
+        }
+
+        smallest_match.map(|(index, _)| index)
+    }
+}
+
+impl std::ops::Deref for SyntaxMap {
+    type Target = SyntaxSnapshot;
+
+    fn deref(&self) -> &Self::Target {
+        &self.snapshot
+    }
+}
+
+impl PartialEq for ParseStep {
+    fn eq(&self, _: &Self) -> bool {
+        false
+    }
+}
+
+impl Eq for ParseStep {}
+
+impl PartialOrd for ParseStep {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(&other))
+    }
+}
+
+impl Ord for ParseStep {
+    fn cmp(&self, other: &Self) -> Ordering {
+        let range_a = self.range();
+        let range_b = other.range();
+        Ord::cmp(&other.depth, &self.depth)
+            .then_with(|| Ord::cmp(&range_b.start, &range_a.start))
+            .then_with(|| Ord::cmp(&range_a.end, &range_b.end))
+            .then_with(|| self.language.id().cmp(&other.language.id()))
+    }
+}
+
+impl ParseStep {
+    fn range(&self) -> Range<usize> {
+        if let ParseMode::Combined {
+            parent_layer_range, ..
+        } = &self.mode
+        {
+            parent_layer_range.clone()
+        } else {
+            let start = self.included_ranges.first().map_or(0, |r| r.start_byte);
+            let end = self.included_ranges.last().map_or(0, |r| r.end_byte);
+            start..end
+        }
+    }
+}
+
+impl ChangedRegion {
+    fn cmp(&self, other: &Self, buffer: &BufferSnapshot) -> Ordering {
+        let range_a = &self.range;
+        let range_b = &other.range;
+        Ord::cmp(&self.depth, &other.depth)
+            .then_with(|| range_a.start.cmp(&range_b.start, buffer))
+            .then_with(|| range_b.end.cmp(&range_a.end, buffer))
+    }
+}
+
+impl ChangeRegionSet {
+    fn start_position(&self) -> ChangeStartPosition {
+        self.0.first().map_or(
+            ChangeStartPosition {
+                depth: usize::MAX,
+                position: Anchor::MAX,
+            },
+            |region| ChangeStartPosition {
+                depth: region.depth,
+                position: region.range.start,
+            },
+        )
+    }
+
+    fn intersects(&self, layer: &SyntaxLayer, text: &BufferSnapshot) -> bool {
+        for region in &self.0 {
+            if region.depth < layer.depth {
+                continue;
+            }
+            if region.depth > layer.depth {
+                break;
+            }
+            if region.range.end.cmp(&layer.range.start, text).is_le() {
+                continue;
+            }
+            if region.range.start.cmp(&layer.range.end, text).is_ge() {
+                break;
+            }
+            return true;
+        }
+        false
+    }
+
+    fn insert(&mut self, region: ChangedRegion, text: &BufferSnapshot) {
+        if let Err(ix) = self.0.binary_search_by(|probe| probe.cmp(&region, text)) {
+            self.0.insert(ix, region);
+        }
+    }
+
+    fn prune(&mut self, summary: SyntaxLayerSummary, text: &BufferSnapshot) -> bool {
+        let prev_len = self.0.len();
+        self.0.retain(|region| {
+            region.depth > summary.max_depth
+                || (region.depth == summary.max_depth
+                    && region
+                        .range
+                        .end
+                        .cmp(&summary.last_layer_range.start, text)
+                        .is_gt())
+        });
+        self.0.len() < prev_len
+    }
+}
+
+impl Default for SyntaxLayerSummary {
+    fn default() -> Self {
+        Self {
+            max_depth: 0,
+            min_depth: 0,
+            range: Anchor::MAX..Anchor::MIN,
+            last_layer_range: Anchor::MIN..Anchor::MAX,
+            last_layer_language: None,
+            contains_unknown_injections: false,
+        }
+    }
+}
+
+impl sum_tree::Summary for SyntaxLayerSummary {
+    type Context = BufferSnapshot;
+
+    fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
+        if other.max_depth > self.max_depth {
+            self.max_depth = other.max_depth;
+            self.range = other.range.clone();
+        } else {
+            if self.range == (Anchor::MAX..Anchor::MAX) {
+                self.range.start = other.range.start;
+            }
+            if other.range.end.cmp(&self.range.end, buffer).is_gt() {
+                self.range.end = other.range.end;
+            }
+        }
+        self.last_layer_range = other.last_layer_range.clone();
+        self.last_layer_language = other.last_layer_language;
+        self.contains_unknown_injections |= other.contains_unknown_injections;
+    }
+}
+
+impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for SyntaxLayerPosition {
+    fn cmp(&self, cursor_location: &SyntaxLayerSummary, buffer: &BufferSnapshot) -> Ordering {
+        Ord::cmp(&self.depth, &cursor_location.max_depth)
+            .then_with(|| {
+                self.range
+                    .start
+                    .cmp(&cursor_location.last_layer_range.start, buffer)
+            })
+            .then_with(|| {
+                cursor_location
+                    .last_layer_range
+                    .end
+                    .cmp(&self.range.end, buffer)
+            })
+            .then_with(|| self.language.cmp(&cursor_location.last_layer_language))
+    }
+}
+
+impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for ChangeStartPosition {
+    fn cmp(&self, cursor_location: &SyntaxLayerSummary, text: &BufferSnapshot) -> Ordering {
+        Ord::cmp(&self.depth, &cursor_location.max_depth)
+            .then_with(|| self.position.cmp(&cursor_location.range.end, text))
+    }
+}
+
+impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary>
+    for SyntaxLayerPositionBeforeChange
+{
+    fn cmp(&self, cursor_location: &SyntaxLayerSummary, buffer: &BufferSnapshot) -> Ordering {
+        if self.change.cmp(cursor_location, buffer).is_le() {
+            return Ordering::Less;
+        } else {
+            self.position.cmp(cursor_location, buffer)
+        }
+    }
+}
+
+impl sum_tree::Item for SyntaxLayer {
+    type Summary = SyntaxLayerSummary;
+
+    fn summary(&self) -> Self::Summary {
+        SyntaxLayerSummary {
+            min_depth: self.depth,
+            max_depth: self.depth,
+            range: self.range.clone(),
+            last_layer_range: self.range.clone(),
+            last_layer_language: self.content.language_id(),
+            contains_unknown_injections: matches!(self.content, SyntaxLayerContent::Pending { .. }),
+        }
+    }
+}
+
+impl std::fmt::Debug for SyntaxLayer {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("SyntaxLayer")
+            .field("depth", &self.depth)
+            .field("range", &self.range)
+            .field("tree", &self.content.tree())
+            .finish()
+    }
+}
+
+impl<'a> tree_sitter::TextProvider<&'a [u8]> for TextProvider<'a> {
+    type I = ByteChunks<'a>;
+
+    fn text(&mut self, node: tree_sitter::Node) -> Self::I {
+        ByteChunks(self.0.chunks_in_range(node.byte_range()))
+    }
+}
+
+impl<'a> Iterator for ByteChunks<'a> {
+    type Item = &'a [u8];
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.0.next().map(str::as_bytes)
+    }
+}
+
+impl QueryCursorHandle {
+    pub(crate) fn new() -> Self {
+        let mut cursor = QUERY_CURSORS.lock().pop().unwrap_or_else(QueryCursor::new);
+        cursor.set_match_limit(64);
+        QueryCursorHandle(Some(cursor))
+    }
+}
+
+impl Deref for QueryCursorHandle {
+    type Target = QueryCursor;
+
+    fn deref(&self) -> &Self::Target {
+        self.0.as_ref().unwrap()
+    }
+}
+
+impl DerefMut for QueryCursorHandle {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.0.as_mut().unwrap()
+    }
+}
+
+impl Drop for QueryCursorHandle {
+    fn drop(&mut self) {
+        let mut cursor = self.0.take().unwrap();
+        cursor.set_byte_range(0..usize::MAX);
+        cursor.set_point_range(Point::zero().to_ts_point()..Point::MAX.to_ts_point());
+        QUERY_CURSORS.lock().push(cursor)
+    }
+}
+
+pub(crate) trait ToTreeSitterPoint {
+    fn to_ts_point(self) -> tree_sitter::Point;
+    fn from_ts_point(point: tree_sitter::Point) -> Self;
+}
+
+impl ToTreeSitterPoint for Point {
+    fn to_ts_point(self) -> tree_sitter::Point {
+        tree_sitter::Point::new(self.row as usize, self.column as usize)
+    }
+
+    fn from_ts_point(point: tree_sitter::Point) -> Self {
+        Point::new(point.row as u32, point.column as u32)
+    }
+}
+
+struct LogIncludedRanges<'a>(&'a [tree_sitter::Range]);
+struct LogPoint(Point);
+struct LogAnchorRange<'a>(&'a Range<Anchor>, &'a text::BufferSnapshot);
+struct LogChangedRegions<'a>(&'a ChangeRegionSet, &'a text::BufferSnapshot);
+
+impl<'a> fmt::Debug for LogIncludedRanges<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_list()
+            .entries(self.0.iter().map(|range| {
+                let start = range.start_point;
+                let end = range.end_point;
+                (start.row, start.column)..(end.row, end.column)
+            }))
+            .finish()
+    }
+}
+
+impl<'a> fmt::Debug for LogAnchorRange<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let range = self.0.to_point(self.1);
+        (LogPoint(range.start)..LogPoint(range.end)).fmt(f)
+    }
+}
+
+impl<'a> fmt::Debug for LogChangedRegions<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_list()
+            .entries(
+                self.0
+                     .0
+                    .iter()
+                    .map(|region| LogAnchorRange(&region.range, self.1)),
+            )
+            .finish()
+    }
+}
+
+impl fmt::Debug for LogPoint {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        (self.0.row, self.0.column).fmt(f)
+    }
+}

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

@@ -0,0 +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,
+            },
+        }
+    }
+}
+
+#[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/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 🔗

@@ -150,6 +150,10 @@ pub struct Room {
     _delegate: RoomDelegate,
 }
 
+// SAFETY: LiveKit objects are thread-safe: https://github.com/livekit/client-sdk-swift#thread-safety
+unsafe impl Send for Room {}
+unsafe impl Sync for Room {}
+
 impl Room {
     pub fn new() -> Arc<Self> {
         Arc::new_cyclic(|weak_room| {
@@ -225,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) {
@@ -251,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) {
@@ -617,6 +621,7 @@ impl Drop for RoomDelegate {
 }
 
 pub struct LocalAudioTrack(*const c_void);
+unsafe impl Send for LocalAudioTrack {}
 
 impl LocalAudioTrack {
     pub fn create() -> Self {
@@ -631,6 +636,7 @@ impl Drop for LocalAudioTrack {
 }
 
 pub struct LocalVideoTrack(*const c_void);
+unsafe impl Send for LocalVideoTrack {}
 
 impl LocalVideoTrack {
     pub fn screen_share_for_display(display: &MacOSDisplay) -> Self {
@@ -645,6 +651,7 @@ impl Drop for LocalVideoTrack {
 }
 
 pub struct LocalTrackPublication(*const c_void);
+unsafe impl Send for LocalTrackPublication {}
 
 impl LocalTrackPublication {
     pub fn new(native_track_publication: *const c_void) -> Self {
@@ -688,6 +695,8 @@ impl Drop for LocalTrackPublication {
 
 pub struct RemoteTrackPublication(*const c_void);
 
+unsafe impl Send for RemoteTrackPublication {}
+
 impl RemoteTrackPublication {
     pub fn new(native_track_publication: *const c_void) -> Self {
         unsafe {
@@ -743,6 +752,8 @@ pub struct RemoteAudioTrack {
     publisher_id: String,
 }
 
+unsafe impl Send for RemoteAudioTrack {}
+
 impl RemoteAudioTrack {
     fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
         unsafe {
@@ -779,6 +790,8 @@ pub struct RemoteVideoTrack {
     publisher_id: String,
 }
 
+unsafe impl Send for RemoteVideoTrack {}
+
 impl RemoteVideoTrack {
     fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
         unsafe {
@@ -860,6 +873,8 @@ pub enum RemoteAudioTrackUpdate {
 
 pub struct MacOSDisplay(*const c_void);
 
+unsafe impl Send for MacOSDisplay {}
+
 impl MacOSDisplay {
     fn new(ptr: *const c_void) -> Self {
         unsafe {

crates/live_kit_client/src/test.rs 🔗

@@ -381,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();
@@ -394,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/lsp2/Cargo.toml 🔗

@@ -0,0 +1,38 @@
+[package]
+name = "lsp2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/lsp2.rs"
+doctest = false
+
+[features]
+test-support = ["async-pipe"]
+
+[dependencies]
+collections = { path = "../collections" }
+gpui2 = { path = "../gpui2" }
+util = { path = "../util" }
+
+anyhow.workspace = true
+async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
+futures.workspace = true
+log.workspace = true
+lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "updated-completion-list-item-defaults" }
+parking_lot.workspace = true
+postage.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+smol.workspace = true
+
+[dev-dependencies]
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+
+async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
+ctor.workspace = true
+env_logger.workspace = true
+unindent.workspace = true

crates/lsp2/src/lsp2.rs 🔗

@@ -0,0 +1,1179 @@
+use log::warn;
+pub use lsp_types::request::*;
+pub use lsp_types::*;
+
+use anyhow::{anyhow, Context, Result};
+use collections::HashMap;
+use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt};
+use gpui2::{AsyncAppContext, Executor, Task};
+use parking_lot::Mutex;
+use postage::{barrier, prelude::Stream};
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use serde_json::{json, value::RawValue, Value};
+use smol::{
+    channel,
+    io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
+    process::{self, Child},
+};
+use std::{
+    ffi::OsString,
+    fmt,
+    future::Future,
+    io::Write,
+    path::PathBuf,
+    str::{self, FromStr as _},
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc, Weak,
+    },
+    time::{Duration, Instant},
+};
+use std::{path::Path, process::Stdio};
+use util::{ResultExt, TryFutureExt};
+
+const JSON_RPC_VERSION: &str = "2.0";
+const CONTENT_LEN_HEADER: &str = "Content-Length: ";
+const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
+
+type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
+type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
+type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
+
+#[derive(Debug, Clone, Copy)]
+pub enum IoKind {
+    StdOut,
+    StdIn,
+    StdErr,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct LanguageServerBinary {
+    pub path: PathBuf,
+    pub arguments: Vec<OsString>,
+}
+
+pub struct LanguageServer {
+    server_id: LanguageServerId,
+    next_id: AtomicUsize,
+    outbound_tx: channel::Sender<String>,
+    name: String,
+    capabilities: ServerCapabilities,
+    code_action_kinds: Option<Vec<CodeActionKind>>,
+    notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
+    response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
+    io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
+    executor: Executor,
+    #[allow(clippy::type_complexity)]
+    io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
+    output_done_rx: Mutex<Option<barrier::Receiver>>,
+    root_path: PathBuf,
+    _server: Option<Mutex<Child>>,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[repr(transparent)]
+pub struct LanguageServerId(pub usize);
+
+pub enum Subscription {
+    Notification {
+        method: &'static str,
+        notification_handlers: Option<Arc<Mutex<HashMap<&'static str, NotificationHandler>>>>,
+    },
+    Io {
+        id: usize,
+        io_handlers: Option<Weak<Mutex<HashMap<usize, IoHandler>>>>,
+    },
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Request<'a, T> {
+    jsonrpc: &'static str,
+    id: usize,
+    method: &'a str,
+    params: T,
+}
+
+#[derive(Serialize, Deserialize)]
+struct AnyResponse<'a> {
+    jsonrpc: &'a str,
+    id: usize,
+    #[serde(default)]
+    error: Option<Error>,
+    #[serde(borrow)]
+    result: Option<&'a RawValue>,
+}
+
+#[derive(Serialize)]
+struct Response<T> {
+    jsonrpc: &'static str,
+    id: usize,
+    result: Option<T>,
+    error: Option<Error>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct Notification<'a, T> {
+    jsonrpc: &'static str,
+    #[serde(borrow)]
+    method: &'a str,
+    params: T,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct AnyNotification<'a> {
+    #[serde(default)]
+    id: Option<usize>,
+    #[serde(borrow)]
+    method: &'a str,
+    #[serde(borrow, default)]
+    params: Option<&'a RawValue>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct Error {
+    message: String,
+}
+
+impl LanguageServer {
+    pub fn new(
+        stderr_capture: Arc<Mutex<Option<String>>>,
+        server_id: LanguageServerId,
+        binary: LanguageServerBinary,
+        root_path: &Path,
+        code_action_kinds: Option<Vec<CodeActionKind>>,
+        cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let working_dir = if root_path.is_dir() {
+            root_path
+        } else {
+            root_path.parent().unwrap_or_else(|| Path::new("/"))
+        };
+
+        let mut server = process::Command::new(&binary.path)
+            .current_dir(working_dir)
+            .args(binary.arguments)
+            .stdin(Stdio::piped())
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
+            .kill_on_drop(true)
+            .spawn()?;
+
+        let stdin = server.stdin.take().unwrap();
+        let stdout = server.stdout.take().unwrap();
+        let stderr = server.stderr.take().unwrap();
+        let mut server = Self::new_internal(
+            server_id.clone(),
+            stdin,
+            stdout,
+            Some(stderr),
+            stderr_capture,
+            Some(server),
+            root_path,
+            code_action_kinds,
+            cx,
+            move |notification| {
+                log::info!(
+                    "{} unhandled notification {}:\n{}",
+                    server_id,
+                    notification.method,
+                    serde_json::to_string_pretty(
+                        &notification
+                            .params
+                            .and_then(|params| Value::from_str(params.get()).ok())
+                            .unwrap_or(Value::Null)
+                    )
+                    .unwrap(),
+                );
+            },
+        );
+
+        if let Some(name) = binary.path.file_name() {
+            server.name = name.to_string_lossy().to_string();
+        }
+
+        Ok(server)
+    }
+
+    fn new_internal<Stdin, Stdout, Stderr, F>(
+        server_id: LanguageServerId,
+        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>>,
+        cx: AsyncAppContext,
+        on_unhandled_notification: F,
+    ) -> Self
+    where
+        Stdin: AsyncWrite + Unpin + Send + 'static,
+        Stdout: AsyncRead + Unpin + Send + 'static,
+        Stderr: AsyncRead + Unpin + Send + 'static,
+        F: FnMut(AnyNotification) + 'static + Send + Sync + Clone,
+    {
+        let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
+        let (output_done_tx, output_done_rx) = barrier::channel();
+        let notification_handlers =
+            Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
+        let response_handlers =
+            Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
+        let io_handlers = Arc::new(Mutex::new(HashMap::default()));
+
+        let stdout_input_task = cx.spawn({
+            let on_unhandled_notification = on_unhandled_notification.clone();
+            let notification_handlers = notification_handlers.clone();
+            let response_handlers = response_handlers.clone();
+            let io_handlers = io_handlers.clone();
+            move |cx| {
+                Self::handle_input(
+                    stdout,
+                    on_unhandled_notification,
+                    notification_handlers,
+                    response_handlers,
+                    io_handlers,
+                    cx,
+                )
+                .log_err()
+            }
+        });
+        let stderr_input_task = stderr
+            .map(|stderr| {
+                let io_handlers = io_handlers.clone();
+                let stderr_captures = stderr_capture.clone();
+                cx.spawn(|_| Self::handle_stderr(stderr, io_handlers, stderr_captures).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);
+            stdout.or(stderr)
+        });
+        let output_task = cx.executor().spawn({
+            Self::handle_output(
+                stdin,
+                outbound_rx,
+                output_done_tx,
+                response_handlers.clone(),
+                io_handlers.clone(),
+            )
+            .log_err()
+        });
+
+        Self {
+            server_id,
+            notification_handlers,
+            response_handlers,
+            io_handlers,
+            name: Default::default(),
+            capabilities: Default::default(),
+            code_action_kinds,
+            next_id: Default::default(),
+            outbound_tx,
+            executor: cx.executor().clone(),
+            io_tasks: Mutex::new(Some((input_task, output_task))),
+            output_done_rx: Mutex::new(Some(output_done_rx)),
+            root_path: root_path.to_path_buf(),
+            _server: server.map(|server| Mutex::new(server)),
+        }
+    }
+
+    pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        self.code_action_kinds.clone()
+    }
+
+    async fn handle_input<Stdout, F>(
+        stdout: Stdout,
+        mut on_unhandled_notification: F,
+        notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
+        response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
+        io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
+        cx: AsyncAppContext,
+    ) -> anyhow::Result<()>
+    where
+        Stdout: AsyncRead + Unpin + Send + 'static,
+        F: FnMut(AnyNotification) + 'static + Send,
+    {
+        let mut stdout = BufReader::new(stdout);
+        let _clear_response_handlers = util::defer({
+            let response_handlers = response_handlers.clone();
+            move || {
+                response_handlers.lock().take();
+            }
+        });
+        let mut buffer = Vec::new();
+        loop {
+            buffer.clear();
+            stdout.read_until(b'\n', &mut buffer).await?;
+            stdout.read_until(b'\n', &mut buffer).await?;
+            let header = std::str::from_utf8(&buffer)?;
+            let message_len: usize = header
+                .strip_prefix(CONTENT_LEN_HEADER)
+                .ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))?
+                .trim_end()
+                .parse()?;
+
+            buffer.resize(message_len, 0);
+            stdout.read_exact(&mut buffer).await?;
+
+            if let Ok(message) = str::from_utf8(&buffer) {
+                log::trace!("incoming message: {}", message);
+                for handler in io_handlers.lock().values_mut() {
+                    handler(IoKind::StdOut, message);
+                }
+            }
+
+            if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
+                if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
+                    handler(
+                        msg.id,
+                        &msg.params.map(|params| params.get()).unwrap_or("null"),
+                        cx.clone(),
+                    );
+                } else {
+                    on_unhandled_notification(msg);
+                }
+            } else if let Ok(AnyResponse {
+                id, error, result, ..
+            }) = serde_json::from_slice(&buffer)
+            {
+                if let Some(handler) = response_handlers
+                    .lock()
+                    .as_mut()
+                    .and_then(|handlers| handlers.remove(&id))
+                {
+                    if let Some(error) = error {
+                        handler(Err(error));
+                    } else if let Some(result) = result {
+                        handler(Ok(result.get().into()));
+                    } else {
+                        handler(Ok("null".into()));
+                    }
+                }
+            } else {
+                warn!(
+                    "failed to deserialize LSP message:\n{}",
+                    std::str::from_utf8(&buffer)?
+                );
+            }
+
+            // Don't starve the main thread when receiving lots of messages at once.
+            smol::future::yield_now().await;
+        }
+    }
+
+    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?;
+            if let Ok(message) = str::from_utf8(&buffer) {
+                log::trace!("incoming stderr message:{message}");
+                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.
+            smol::future::yield_now().await;
+        }
+    }
+
+    async fn handle_output<Stdin>(
+        stdin: Stdin,
+        outbound_rx: channel::Receiver<String>,
+        output_done_tx: barrier::Sender,
+        response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
+        io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
+    ) -> anyhow::Result<()>
+    where
+        Stdin: AsyncWrite + Unpin + Send + 'static,
+    {
+        let mut stdin = BufWriter::new(stdin);
+        let _clear_response_handlers = util::defer({
+            let response_handlers = response_handlers.clone();
+            move || {
+                response_handlers.lock().take();
+            }
+        });
+        let mut content_len_buffer = Vec::new();
+        while let Ok(message) = outbound_rx.recv().await {
+            log::trace!("outgoing message:{}", message);
+            for handler in io_handlers.lock().values_mut() {
+                handler(IoKind::StdIn, &message);
+            }
+
+            content_len_buffer.clear();
+            write!(content_len_buffer, "{}", message.len()).unwrap();
+            stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
+            stdin.write_all(&content_len_buffer).await?;
+            stdin.write_all("\r\n\r\n".as_bytes()).await?;
+            stdin.write_all(message.as_bytes()).await?;
+            stdin.flush().await?;
+        }
+        drop(output_done_tx);
+        Ok(())
+    }
+
+    /// Initializes a language server.
+    /// Note that `options` is used directly to construct [`InitializeParams`],
+    /// which is why it is owned.
+    pub async fn initialize(mut self, options: Option<Value>) -> Result<Arc<Self>> {
+        let root_uri = Url::from_file_path(&self.root_path).unwrap();
+        #[allow(deprecated)]
+        let params = InitializeParams {
+            process_id: Default::default(),
+            root_path: Default::default(),
+            root_uri: Some(root_uri.clone()),
+            initialization_options: options,
+            capabilities: ClientCapabilities {
+                workspace: Some(WorkspaceClientCapabilities {
+                    configuration: Some(true),
+                    did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
+                        dynamic_registration: Some(true),
+                        relative_pattern_support: Some(true),
+                    }),
+                    did_change_configuration: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: Some(true),
+                    }),
+                    workspace_folders: Some(true),
+                    symbol: Some(WorkspaceSymbolClientCapabilities {
+                        resolve_support: None,
+                        ..WorkspaceSymbolClientCapabilities::default()
+                    }),
+                    inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
+                        refresh_support: Some(true),
+                    }),
+                    ..Default::default()
+                }),
+                text_document: Some(TextDocumentClientCapabilities {
+                    definition: Some(GotoCapability {
+                        link_support: Some(true),
+                        ..Default::default()
+                    }),
+                    code_action: Some(CodeActionClientCapabilities {
+                        code_action_literal_support: Some(CodeActionLiteralSupport {
+                            code_action_kind: CodeActionKindLiteralSupport {
+                                value_set: vec![
+                                    CodeActionKind::REFACTOR.as_str().into(),
+                                    CodeActionKind::QUICKFIX.as_str().into(),
+                                    CodeActionKind::SOURCE.as_str().into(),
+                                ],
+                            },
+                        }),
+                        data_support: Some(true),
+                        resolve_support: Some(CodeActionCapabilityResolveSupport {
+                            properties: vec!["edit".to_string(), "command".to_string()],
+                        }),
+                        ..Default::default()
+                    }),
+                    completion: Some(CompletionClientCapabilities {
+                        completion_item: Some(CompletionItemCapability {
+                            snippet_support: Some(true),
+                            resolve_support: Some(CompletionItemCapabilityResolveSupport {
+                                properties: vec!["additionalTextEdits".to_string()],
+                            }),
+                            ..Default::default()
+                        }),
+                        completion_list: Some(CompletionListCapability {
+                            item_defaults: Some(vec![
+                                "commitCharacters".to_owned(),
+                                "editRange".to_owned(),
+                                "insertTextMode".to_owned(),
+                                "data".to_owned(),
+                            ]),
+                        }),
+                        ..Default::default()
+                    }),
+                    rename: Some(RenameClientCapabilities {
+                        prepare_support: Some(true),
+                        ..Default::default()
+                    }),
+                    hover: Some(HoverClientCapabilities {
+                        content_format: Some(vec![MarkupKind::Markdown]),
+                        ..Default::default()
+                    }),
+                    inlay_hint: Some(InlayHintClientCapabilities {
+                        resolve_support: Some(InlayHintResolveClientCapabilities {
+                            properties: vec![
+                                "textEdits".to_string(),
+                                "tooltip".to_string(),
+                                "label.tooltip".to_string(),
+                                "label.location".to_string(),
+                                "label.command".to_string(),
+                            ],
+                        }),
+                        dynamic_registration: Some(false),
+                    }),
+                    ..Default::default()
+                }),
+                experimental: Some(json!({
+                    "serverStatusNotification": true,
+                })),
+                window: Some(WindowClientCapabilities {
+                    work_done_progress: Some(true),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            trace: Default::default(),
+            workspace_folders: Some(vec![WorkspaceFolder {
+                uri: root_uri,
+                name: Default::default(),
+            }]),
+            client_info: Default::default(),
+            locale: Default::default(),
+        };
+
+        let response = self.request::<request::Initialize>(params).await?;
+        if let Some(info) = response.server_info {
+            self.name = info.name;
+        }
+        self.capabilities = response.capabilities;
+
+        self.notify::<notification::Initialized>(InitializedParams {})?;
+        Ok(Arc::new(self))
+    }
+
+    pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Option<()>>> {
+        if let Some(tasks) = self.io_tasks.lock().take() {
+            let response_handlers = self.response_handlers.clone();
+            let next_id = AtomicUsize::new(self.next_id.load(SeqCst));
+            let outbound_tx = self.outbound_tx.clone();
+            let executor = self.executor.clone();
+            let mut output_done = self.output_done_rx.lock().take().unwrap();
+            let shutdown_request = Self::request_internal::<request::Shutdown>(
+                &next_id,
+                &response_handlers,
+                &outbound_tx,
+                &executor,
+                (),
+            );
+            let exit = Self::notify_internal::<notification::Exit>(&outbound_tx, ());
+            outbound_tx.close();
+            Some(
+                async move {
+                    log::debug!("language server shutdown started");
+                    shutdown_request.await?;
+                    response_handlers.lock().take();
+                    exit?;
+                    output_done.recv().await;
+                    log::debug!("language server shutdown finished");
+                    drop(tasks);
+                    anyhow::Ok(())
+                }
+                .log_err(),
+            )
+        } else {
+            None
+        }
+    }
+
+    #[must_use]
+    pub fn on_notification<T, F>(&self, f: F) -> Subscription
+    where
+        T: notification::Notification,
+        F: 'static + Send + FnMut(T::Params, AsyncAppContext),
+    {
+        self.on_custom_notification(T::METHOD, f)
+    }
+
+    #[must_use]
+    pub fn on_request<T, F, Fut>(&self, f: F) -> Subscription
+    where
+        T: request::Request,
+        T::Params: 'static + Send,
+        F: 'static + Send + FnMut(T::Params, AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = Result<T::Result>> + Send,
+    {
+        self.on_custom_request(T::METHOD, f)
+    }
+
+    #[must_use]
+    pub fn on_io<F>(&self, f: F) -> Subscription
+    where
+        F: 'static + Send + FnMut(IoKind, &str),
+    {
+        let id = self.next_id.fetch_add(1, SeqCst);
+        self.io_handlers.lock().insert(id, Box::new(f));
+        Subscription::Io {
+            id,
+            io_handlers: Some(Arc::downgrade(&self.io_handlers)),
+        }
+    }
+
+    pub fn remove_request_handler<T: request::Request>(&self) {
+        self.notification_handlers.lock().remove(T::METHOD);
+    }
+
+    pub fn remove_notification_handler<T: notification::Notification>(&self) {
+        self.notification_handlers.lock().remove(T::METHOD);
+    }
+
+    pub fn has_notification_handler<T: notification::Notification>(&self) -> bool {
+        self.notification_handlers.lock().contains_key(T::METHOD)
+    }
+
+    #[must_use]
+    pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
+    where
+        F: 'static + Send + FnMut(Params, AsyncAppContext),
+        Params: DeserializeOwned,
+    {
+        let prev_handler = self.notification_handlers.lock().insert(
+            method,
+            Box::new(move |_, params, cx| {
+                if let Some(params) = serde_json::from_str(params).log_err() {
+                    f(params, cx);
+                }
+            }),
+        );
+        assert!(
+            prev_handler.is_none(),
+            "registered multiple handlers for the same LSP method"
+        );
+        Subscription::Notification {
+            method,
+            notification_handlers: Some(self.notification_handlers.clone()),
+        }
+    }
+
+    #[must_use]
+    pub fn on_custom_request<Params, Res, Fut, F>(
+        &self,
+        method: &'static str,
+        mut f: F,
+    ) -> Subscription
+    where
+        F: 'static + Send + FnMut(Params, AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = Result<Res>> + Send,
+        Params: DeserializeOwned + Send + 'static,
+        Res: Serialize,
+    {
+        let outbound_tx = self.outbound_tx.clone();
+        let prev_handler = self.notification_handlers.lock().insert(
+            method,
+            Box::new(move |id, params, cx| {
+                if let Some(id) = id {
+                    match serde_json::from_str(params) {
+                        Ok(params) => {
+                            let response = f(params, cx.clone());
+                            cx.executor()
+                                .spawn_on_main({
+                                    let outbound_tx = outbound_tx.clone();
+                                    move || async move {
+                                        let response = match response.await {
+                                            Ok(result) => Response {
+                                                jsonrpc: JSON_RPC_VERSION,
+                                                id,
+                                                result: Some(result),
+                                                error: None,
+                                            },
+                                            Err(error) => Response {
+                                                jsonrpc: JSON_RPC_VERSION,
+                                                id,
+                                                result: None,
+                                                error: Some(Error {
+                                                    message: error.to_string(),
+                                                }),
+                                            },
+                                        };
+                                        if let Some(response) =
+                                            serde_json::to_string(&response).log_err()
+                                        {
+                                            outbound_tx.try_send(response).ok();
+                                        }
+                                    }
+                                })
+                                .detach();
+                        }
+
+                        Err(error) => {
+                            log::error!(
+                                "error deserializing {} request: {:?}, message: {:?}",
+                                method,
+                                error,
+                                params
+                            );
+                            let response = AnyResponse {
+                                jsonrpc: JSON_RPC_VERSION,
+                                id,
+                                result: None,
+                                error: Some(Error {
+                                    message: error.to_string(),
+                                }),
+                            };
+                            if let Some(response) = serde_json::to_string(&response).log_err() {
+                                outbound_tx.try_send(response).ok();
+                            }
+                        }
+                    }
+                }
+            }),
+        );
+        assert!(
+            prev_handler.is_none(),
+            "registered multiple handlers for the same LSP method"
+        );
+        Subscription::Notification {
+            method,
+            notification_handlers: Some(self.notification_handlers.clone()),
+        }
+    }
+
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    pub fn capabilities(&self) -> &ServerCapabilities {
+        &self.capabilities
+    }
+
+    pub fn server_id(&self) -> LanguageServerId {
+        self.server_id
+    }
+
+    pub fn root_path(&self) -> &PathBuf {
+        &self.root_path
+    }
+
+    pub fn request<T: request::Request>(
+        &self,
+        params: T::Params,
+    ) -> impl Future<Output = Result<T::Result>>
+    where
+        T::Result: 'static + Send,
+    {
+        Self::request_internal::<T>(
+            &self.next_id,
+            &self.response_handlers,
+            &self.outbound_tx,
+            &self.executor,
+            params,
+        )
+    }
+
+    fn request_internal<T: request::Request>(
+        next_id: &AtomicUsize,
+        response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
+        outbound_tx: &channel::Sender<String>,
+        executor: &Executor,
+        params: T::Params,
+    ) -> impl 'static + Future<Output = anyhow::Result<T::Result>>
+    where
+        T::Result: 'static + Send,
+    {
+        let id = next_id.fetch_add(1, SeqCst);
+        let message = serde_json::to_string(&Request {
+            jsonrpc: JSON_RPC_VERSION,
+            id,
+            method: T::METHOD,
+            params,
+        })
+        .unwrap();
+
+        let (tx, rx) = oneshot::channel();
+        let handle_response = response_handlers
+            .lock()
+            .as_mut()
+            .ok_or_else(|| anyhow!("server shut down"))
+            .map(|handlers| {
+                let executor = executor.clone();
+                handlers.insert(
+                    id,
+                    Box::new(move |result| {
+                        executor
+                            .spawn(async move {
+                                let response = match result {
+                                    Ok(response) => serde_json::from_str(&response)
+                                        .context("failed to deserialize response"),
+                                    Err(error) => Err(anyhow!("{}", error.message)),
+                                };
+                                _ = tx.send(response);
+                            })
+                            .detach();
+                    }),
+                );
+            });
+
+        let send = outbound_tx
+            .try_send(message)
+            .context("failed to write to language server's stdin");
+
+        let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse();
+        let started = Instant::now();
+        async move {
+            handle_response?;
+            send?;
+
+            let method = T::METHOD;
+            futures::select! {
+                response = rx.fuse() => {
+                    let elapsed = started.elapsed();
+                    log::trace!("Took {elapsed:?} to recieve response to {method:?} id {id}");
+                    response?
+                }
+
+                _ = timeout => {
+                    log::error!("Cancelled LSP request task for {method:?} id {id} which took over {LSP_REQUEST_TIMEOUT:?}");
+                    anyhow::bail!("LSP request timeout");
+                }
+            }
+        }
+    }
+
+    pub fn notify<T: notification::Notification>(&self, params: T::Params) -> Result<()> {
+        Self::notify_internal::<T>(&self.outbound_tx, params)
+    }
+
+    fn notify_internal<T: notification::Notification>(
+        outbound_tx: &channel::Sender<String>,
+        params: T::Params,
+    ) -> Result<()> {
+        let message = serde_json::to_string(&Notification {
+            jsonrpc: JSON_RPC_VERSION,
+            method: T::METHOD,
+            params,
+        })
+        .unwrap();
+        outbound_tx.try_send(message)?;
+        Ok(())
+    }
+}
+
+impl Drop for LanguageServer {
+    fn drop(&mut self) {
+        if let Some(shutdown) = self.shutdown() {
+            self.executor.spawn(shutdown).detach();
+        }
+    }
+}
+
+impl Subscription {
+    pub fn detach(&mut self) {
+        match self {
+            Subscription::Notification {
+                notification_handlers,
+                ..
+            } => *notification_handlers = None,
+            Subscription::Io { io_handlers, .. } => *io_handlers = None,
+        }
+    }
+}
+
+impl fmt::Display for LanguageServerId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl fmt::Debug for LanguageServer {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("LanguageServer")
+            .field("id", &self.server_id.0)
+            .field("name", &self.name)
+            .finish_non_exhaustive()
+    }
+}
+
+impl Drop for Subscription {
+    fn drop(&mut self) {
+        match self {
+            Subscription::Notification {
+                method,
+                notification_handlers,
+            } => {
+                if let Some(handlers) = notification_handlers {
+                    handlers.lock().remove(method);
+                }
+            }
+            Subscription::Io { id, io_handlers } => {
+                if let Some(io_handlers) = io_handlers.as_ref().and_then(|h| h.upgrade()) {
+                    io_handlers.lock().remove(id);
+                }
+            }
+        }
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+#[derive(Clone)]
+pub struct FakeLanguageServer {
+    pub server: Arc<LanguageServer>,
+    notifications_rx: channel::Receiver<(String, String)>,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl LanguageServer {
+    pub fn full_capabilities() -> ServerCapabilities {
+        ServerCapabilities {
+            document_highlight_provider: Some(OneOf::Left(true)),
+            code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
+            document_formatting_provider: Some(OneOf::Left(true)),
+            document_range_formatting_provider: Some(OneOf::Left(true)),
+            definition_provider: Some(OneOf::Left(true)),
+            type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
+            ..Default::default()
+        }
+    }
+
+    pub fn fake(
+        name: String,
+        capabilities: ServerCapabilities,
+        cx: AsyncAppContext,
+    ) -> (Self, FakeLanguageServer) {
+        let (stdin_writer, stdin_reader) = async_pipe::pipe();
+        let (stdout_writer, stdout_reader) = async_pipe::pipe();
+        let (notifications_tx, notifications_rx) = channel::unbounded();
+
+        let server = Self::new_internal(
+            LanguageServerId(0),
+            stdin_writer,
+            stdout_reader,
+            None::<async_pipe::PipeReader>,
+            Arc::new(Mutex::new(None)),
+            None,
+            Path::new("/"),
+            None,
+            cx.clone(),
+            |_| {},
+        );
+        let fake = FakeLanguageServer {
+            server: Arc::new(Self::new_internal(
+                LanguageServerId(0),
+                stdout_writer,
+                stdin_reader,
+                None::<async_pipe::PipeReader>,
+                Arc::new(Mutex::new(None)),
+                None,
+                Path::new("/"),
+                None,
+                cx,
+                move |msg| {
+                    notifications_tx
+                        .try_send((
+                            msg.method.to_string(),
+                            msg.params
+                                .map(|raw_value| raw_value.get())
+                                .unwrap_or("null")
+                                .to_string(),
+                        ))
+                        .ok();
+                },
+            )),
+            notifications_rx,
+        };
+        fake.handle_request::<request::Initialize, _, _>({
+            let capabilities = capabilities;
+            move |_, _| {
+                let capabilities = capabilities.clone();
+                let name = name.clone();
+                async move {
+                    Ok(InitializeResult {
+                        capabilities,
+                        server_info: Some(ServerInfo {
+                            name,
+                            ..Default::default()
+                        }),
+                    })
+                }
+            }
+        });
+
+        (server, fake)
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeLanguageServer {
+    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
+        self.server.notify::<T>(params).ok();
+    }
+
+    pub async fn request<T>(&self, params: T::Params) -> Result<T::Result>
+    where
+        T: request::Request,
+        T::Result: 'static + Send,
+    {
+        self.server.executor.start_waiting();
+        self.server.request::<T>(params).await
+    }
+
+    pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
+        self.server.executor.start_waiting();
+        self.try_receive_notification::<T>().await.unwrap()
+    }
+
+    pub async fn try_receive_notification<T: notification::Notification>(
+        &mut self,
+    ) -> Option<T::Params> {
+        use futures::StreamExt as _;
+
+        loop {
+            let (method, params) = self.notifications_rx.next().await?;
+            if method == T::METHOD {
+                return Some(serde_json::from_str::<T::Params>(&params).unwrap());
+            } else {
+                log::info!("skipping message in fake language server {:?}", params);
+            }
+        }
+    }
+
+    pub fn handle_request<T, F, Fut>(
+        &self,
+        mut handler: F,
+    ) -> futures::channel::mpsc::UnboundedReceiver<()>
+    where
+        T: 'static + request::Request,
+        T::Params: 'static + Send,
+        F: 'static + Send + FnMut(T::Params, gpui2::AsyncAppContext) -> Fut,
+        Fut: 'static + Send + Future<Output = Result<T::Result>>,
+    {
+        let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded();
+        self.server.remove_request_handler::<T>();
+        self.server
+            .on_request::<T, _, _>(move |params, cx| {
+                let result = handler(params, cx.clone());
+                let responded_tx = responded_tx.clone();
+                async move {
+                    cx.executor().simulate_random_delay().await;
+                    let result = result.await;
+                    responded_tx.unbounded_send(()).ok();
+                    result
+                }
+            })
+            .detach();
+        responded_rx
+    }
+
+    pub fn handle_notification<T, F>(
+        &self,
+        mut handler: F,
+    ) -> futures::channel::mpsc::UnboundedReceiver<()>
+    where
+        T: 'static + notification::Notification,
+        T::Params: 'static + Send,
+        F: 'static + Send + FnMut(T::Params, gpui2::AsyncAppContext),
+    {
+        let (handled_tx, handled_rx) = futures::channel::mpsc::unbounded();
+        self.server.remove_notification_handler::<T>();
+        self.server
+            .on_notification::<T, _>(move |params, cx| {
+                handler(params, cx.clone());
+                handled_tx.unbounded_send(()).ok();
+            })
+            .detach();
+        handled_rx
+    }
+
+    pub fn remove_request_handler<T>(&mut self)
+    where
+        T: 'static + request::Request,
+    {
+        self.server.remove_request_handler::<T>();
+    }
+
+    pub async fn start_progress(&self, token: impl Into<String>) {
+        let token = token.into();
+        self.request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
+            token: NumberOrString::String(token.clone()),
+        })
+        .await
+        .unwrap();
+        self.notify::<notification::Progress>(ProgressParams {
+            token: NumberOrString::String(token),
+            value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(Default::default())),
+        });
+    }
+
+    pub fn end_progress(&self, token: impl Into<String>) {
+        self.notify::<notification::Progress>(ProgressParams {
+            token: NumberOrString::String(token.into()),
+            value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())),
+        });
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use gpui::TestAppContext;
+
+//     #[ctor::ctor]
+//     fn init_logger() {
+//         if std::env::var("RUST_LOG").is_ok() {
+//             env_logger::init();
+//         }
+//     }
+
+//     #[gpui::test]
+//     async fn test_fake(cx: &mut TestAppContext) {
+//         let (server, mut fake) =
+//             LanguageServer::fake("the-lsp".to_string(), Default::default(), cx.to_async());
+
+//         let (message_tx, message_rx) = channel::unbounded();
+//         let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
+//         server
+//             .on_notification::<notification::ShowMessage, _>(move |params, _| {
+//                 message_tx.try_send(params).unwrap()
+//             })
+//             .detach();
+//         server
+//             .on_notification::<notification::PublishDiagnostics, _>(move |params, _| {
+//                 diagnostics_tx.try_send(params).unwrap()
+//             })
+//             .detach();
+
+//         let server = server.initialize(None).await.unwrap();
+//         server
+//             .notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
+//                 text_document: TextDocumentItem::new(
+//                     Url::from_str("file://a/b").unwrap(),
+//                     "rust".to_string(),
+//                     0,
+//                     "".to_string(),
+//                 ),
+//             })
+//             .unwrap();
+//         assert_eq!(
+//             fake.receive_notification::<notification::DidOpenTextDocument>()
+//                 .await
+//                 .text_document
+//                 .uri
+//                 .as_str(),
+//             "file://a/b"
+//         );
+
+//         fake.notify::<notification::ShowMessage>(ShowMessageParams {
+//             typ: MessageType::ERROR,
+//             message: "ok".to_string(),
+//         });
+//         fake.notify::<notification::PublishDiagnostics>(PublishDiagnosticsParams {
+//             uri: Url::from_str("file://b/c").unwrap(),
+//             version: Some(5),
+//             diagnostics: vec![],
+//         });
+//         assert_eq!(message_rx.recv().await.unwrap().message, "ok");
+//         assert_eq!(
+//             diagnostics_rx.recv().await.unwrap().uri.as_str(),
+//             "file://b/c"
+//         );
+
+//         fake.handle_request::<request::Shutdown, _, _>(|_, _| async move { Ok(()) });
+
+//         drop(server);
+//         fake.receive_notification::<notification::Exit>().await;
+//     }
+// }

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

@@ -9,7 +9,6 @@ path = "src/node_runtime.rs"
 doctest = false
 
 [dependencies]
-gpui = { path = "../gpui" }
 util = { path = "../util" }
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
 async-tar = "0.4.2"

crates/prettier2/Cargo.toml 🔗

@@ -0,0 +1,35 @@
+[package]
+name = "prettier2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/prettier2.rs"
+doctest = false
+
+[features]
+test-support = []
+
+[dependencies]
+client2 = { path = "../client2" }
+collections = { path = "../collections"}
+language2 = { path = "../language2" }
+gpui2 = { path = "../gpui2" }
+fs2 = { path = "../fs2" }
+lsp2 = { path = "../lsp2" }
+node_runtime = { path = "../node_runtime"}
+util = { path = "../util" }
+
+log.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+anyhow.workspace = true
+futures.workspace = true
+parking_lot.workspace = true
+
+[dev-dependencies]
+language2 = { path = "../language2", features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+fs2 = { path = "../fs2",  features = ["test-support"] }

crates/prettier2/src/prettier2.rs 🔗

@@ -0,0 +1,468 @@
+use anyhow::Context;
+use collections::HashMap;
+use fs2::Fs;
+use gpui2::{AsyncAppContext, Model};
+use language2::{language_settings::language_settings, Buffer, Diff};
+use lsp2::{LanguageServer, LanguageServerId};
+use node_runtime::NodeRuntime;
+use serde::{Deserialize, Serialize};
+use std::{
+    collections::VecDeque,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::paths::DEFAULT_PRETTIER_DIR;
+
+pub enum Prettier {
+    Real(RealPrettier),
+    #[cfg(any(test, feature = "test-support"))]
+    Test(TestPrettier),
+}
+
+pub struct RealPrettier {
+    worktree_id: Option<usize>,
+    default: bool,
+    prettier_dir: PathBuf,
+    server: Arc<LanguageServer>,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct TestPrettier {
+    worktree_id: Option<usize>,
+    prettier_dir: PathBuf,
+    default: bool,
+}
+
+#[derive(Debug)]
+pub struct LocateStart {
+    pub worktree_root_path: Arc<Path>,
+    pub starting_path: Arc<Path>,
+}
+
+pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
+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",
+        ".prettierrc.json",
+        ".prettierrc.json5",
+        ".prettierrc.yaml",
+        ".prettierrc.yml",
+        ".prettierrc.toml",
+        ".prettierrc.js",
+        ".prettierrc.cjs",
+        "package.json",
+        "prettier.config.js",
+        "prettier.config.cjs",
+        ".editorconfig",
+    ];
+
+    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| !is_node_modules(path_component))
+                    .collect::<PathBuf>();
+                if worktree_root != starting_path.worktree_root_path.as_ref() {
+                    vec![worktree_root]
+                } else {
+                    if starting_path.starting_path.as_ref() == Path::new("") {
+                        worktree_root
+                            .parent()
+                            .map(|path| vec![path.to_path_buf()])
+                            .unwrap_or_default()
+                    } else {
+                        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;
+                            }
+                        }
+                        Vec::from(paths_to_check)
+                    }
+                }
+            }
+            None => Vec::new(),
+        };
+
+        match find_closest_prettier_dir(paths_to_check, fs.as_ref())
+            .await
+            .with_context(|| format!("finding prettier starting with {starting_path:?}"))?
+        {
+            Some(prettier_dir) => Ok(prettier_dir),
+            None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()),
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub async fn start(
+        worktree_id: Option<usize>,
+        _: LanguageServerId,
+        prettier_dir: PathBuf,
+        _: Arc<dyn NodeRuntime>,
+        _: AsyncAppContext,
+    ) -> anyhow::Result<Self> {
+        Ok(
+            #[cfg(any(test, feature = "test-support"))]
+            Self::Test(TestPrettier {
+                worktree_id,
+                default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
+                prettier_dir,
+            }),
+        )
+    }
+
+    #[cfg(not(any(test, feature = "test-support")))]
+    pub async fn start(
+        worktree_id: Option<usize>,
+        server_id: LanguageServerId,
+        prettier_dir: PathBuf,
+        node: Arc<dyn NodeRuntime>,
+        cx: AsyncAppContext,
+    ) -> anyhow::Result<Self> {
+        use lsp2::LanguageServerBinary;
+
+        let executor = cx.executor().clone();
+        anyhow::ensure!(
+            prettier_dir.is_dir(),
+            "Prettier dir {prettier_dir:?} is not a directory"
+        );
+        let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
+        anyhow::ensure!(
+            prettier_server.is_file(),
+            "no prettier server package found at {prettier_server:?}"
+        );
+
+        let node_path = executor
+            .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,
+                arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
+            },
+            Path::new("/"),
+            None,
+            cx,
+        )
+        .context("prettier server creation")?;
+        let server = executor
+            .spawn(server.initialize(None))
+            .await
+            .context("prettier server initialization")?;
+        Ok(Self::Real(RealPrettier {
+            worktree_id,
+            server,
+            default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
+            prettier_dir,
+        }))
+    }
+
+    pub async fn format(
+        &self,
+        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 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:?}"
+                        );
+                        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 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,
+                                        ),
+                                    ));
+                                }
+                                (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 {:?} inside {:?}",
+                                            plugin_name,
+                                            prettier_node_modules
+                                        );
+                                        None
+                                    }
+                                }
+                            })
+                            .collect();
+                        log::debug!(
+                            "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
+                            plugins,
+                            prettier_options,
+                            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)
+                    .await
+                    .context("prettier format request")?;
+                let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx))?;
+                Ok(diff_task.await)
+            }
+            #[cfg(any(test, feature = "test-support"))]
+            Self::Test(_) => Ok(buffer
+                .update(cx, |buffer, cx| {
+                    let formatted_text = buffer.text() + FORMAT_SUFFIX;
+                    buffer.diff(formatted_text, cx)
+                })?
+                .await),
+        }
+    }
+
+    pub async fn clear_cache(&self) -> anyhow::Result<()> {
+        match self {
+            Self::Real(local) => local
+                .server
+                .request::<ClearCache>(())
+                .await
+                .context("prettier clear cache"),
+            #[cfg(any(test, feature = "test-support"))]
+            Self::Test(_) => Ok(()),
+        }
+    }
+
+    pub fn server(&self) -> Option<&Arc<LanguageServer>> {
+        match self {
+            Self::Real(local) => Some(&local.server),
+            #[cfg(any(test, feature = "test-support"))]
+            Self::Test(_) => None,
+        }
+    }
+
+    pub fn is_default(&self) -> bool {
+        match self {
+            Self::Real(local) => local.default,
+            #[cfg(any(test, feature = "test-support"))]
+            Self::Test(test_prettier) => test_prettier.default,
+        }
+    }
+
+    pub fn prettier_dir(&self) -> &Path {
+        match self {
+            Self::Real(local) => &local.prettier_dir,
+            #[cfg(any(test, feature = "test-support"))]
+            Self::Test(test_prettier) => &test_prettier.prettier_dir,
+        }
+    }
+
+    pub fn worktree_id(&self) -> Option<usize> {
+        match self {
+            Self::Real(local) => local.worktree_id,
+            #[cfg(any(test, feature = "test-support"))]
+            Self::Test(test_prettier) => test_prettier.worktree_id,
+        }
+    }
+}
+
+async fn find_closest_prettier_dir(
+    paths_to_check: Vec<PathBuf>,
+    fs: &dyn Fs,
+) -> anyhow::Result<Option<PathBuf>> {
+    for path in paths_to_check {
+        let possible_package_json = path.join("package.json");
+        if let Some(package_json_metadata) = fs
+            .metadata(&possible_package_json)
+            .await
+            .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
+        {
+            if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
+                let package_json_contents = fs
+                    .load(&possible_package_json)
+                    .await
+                    .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
+                if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
+                    &package_json_contents,
+                ) {
+                    if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
+                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
+                            return Ok(Some(path));
+                        }
+                    }
+                    if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
+                    {
+                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
+                            return Ok(Some(path));
+                        }
+                    }
+                }
+            }
+        }
+
+        let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
+        if let Some(node_modules_location_metadata) = fs
+            .metadata(&possible_node_modules_location)
+            .await
+            .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
+        {
+            if node_modules_location_metadata.is_dir {
+                return Ok(Some(path));
+            }
+        }
+    }
+    Ok(None)
+}
+
+enum Format {}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct FormatParams {
+    text: String,
+    options: FormatOptions,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct FormatOptions {
+    plugins: Vec<PathBuf>,
+    parser: Option<String>,
+    #[serde(rename = "filepath")]
+    path: Option<PathBuf>,
+    prettier_options: Option<HashMap<String, serde_json::Value>>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct FormatResult {
+    text: String,
+}
+
+impl lsp2::request::Request for Format {
+    type Params = FormatParams;
+    type Result = FormatResult;
+    const METHOD: &'static str = "prettier/format";
+}
+
+enum ClearCache {}
+
+impl lsp2::request::Request for ClearCache {
+    type Params = ();
+    type Result = ();
+    const METHOD: &'static str = "prettier/clear_cache";
+}

crates/prettier2/src/prettier_server.js 🔗

@@ -0,0 +1,217 @@
+const { Buffer } = require('buffer');
+const fs = require("fs");
+const path = require("path");
+const { once } = require('events');
+
+const prettierContainerPath = process.argv[2];
+if (prettierContainerPath == null || prettierContainerPath.length == 0) {
+    process.stderr.write(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`);
+    process.exit(1);
+}
+fs.stat(prettierContainerPath, (err, stats) => {
+    if (err) {
+        process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`);
+        process.exit(1);
+    }
+
+    if (!stats.isDirectory()) {
+        process.stderr.write(`Path '${prettierContainerPath}' exists but is not a directory\n`);
+        process.exit(1);
+    }
+});
+const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier');
+
+class Prettier {
+    constructor(path, prettier, config) {
+        this.path = path;
+        this.prettier = prettier;
+        this.config = config;
+    }
+}
+
+(async () => {
+    let prettier;
+    let config;
+    try {
+        prettier = await loadPrettier(prettierPath);
+        config = await prettier.resolveConfig(prettierPath) || {};
+    } catch (e) {
+        process.stderr.write(`Failed to load prettier: ${e}\n`);
+        process.exit(1);
+    }
+    process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`);
+    process.stdin.resume();
+    handleBuffer(new Prettier(prettierPath, prettier, config));
+})()
+
+async function handleBuffer(prettier) {
+    for await (const messageText of readStdin()) {
+        let message;
+        try {
+            message = JSON.parse(messageText);
+        } catch (e) {
+            sendResponse(makeError(`Failed to parse message '${messageText}': ${e}`));
+            continue;
+        }
+        // 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 headerSeparator = "\r\n";
+const contentLengthHeaderName = 'Content-Length';
+
+async function* readStdin() {
+    let buffer = Buffer.alloc(0);
+    let streamEnded = false;
+    process.stdin.on('end', () => {
+        streamEnded = true;
+    });
+    process.stdin.on('data', (data) => {
+        buffer = Buffer.concat([buffer, data]);
+    });
+
+    async function handleStreamEnded(errorMessage) {
+        sendResponse(makeError(errorMessage));
+        buffer = Buffer.alloc(0);
+        messageLength = null;
+        await once(process.stdin, 'readable');
+        streamEnded = false;
+    }
+
+    try {
+        let headersLength = null;
+        let messageLength = null;
+        main_loop: while (true) {
+            if (messageLength === null) {
+                while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) {
+                    if (streamEnded) {
+                        await handleStreamEnded('Unexpected end of stream: headers not found');
+                        continue main_loop;
+                    } else if (buffer.length > contentLengthHeaderName.length * 10) {
+                        await handleStreamEnded(`Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`);
+                        continue main_loop;
+                    }
+                    await once(process.stdin, 'readable');
+                }
+                const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString('ascii');
+                const contentLengthHeader = headers.split(headerSeparator)
+                    .map(header => header.split(':'))
+                    .filter(header => header[2] === undefined)
+                    .filter(header => (header[1] || '').length > 0)
+                    .find(header => (header[0] || '').trim() === contentLengthHeaderName);
+                const contentLength = (contentLengthHeader || [])[1];
+                if (contentLength === undefined) {
+                    await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`);
+                    continue main_loop;
+                }
+                headersLength = headers.length + headerSeparator.length * 2;
+                messageLength = parseInt(contentLength, 10);
+            }
+
+            while (buffer.length < (headersLength + messageLength)) {
+                if (streamEnded) {
+                    await handleStreamEnded(
+                        `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`);
+                    continue main_loop;
+                }
+                await once(process.stdin, 'readable');
+            }
+
+            const messageEnd = headersLength + messageLength;
+            const message = buffer.subarray(headersLength, messageEnd);
+            buffer = buffer.subarray(messageEnd);
+            headersLength = null;
+            messageLength = null;
+            yield message.toString('utf8');
+        }
+    } catch (e) {
+        sendResponse(makeError(`Error reading stdin: ${e}`));
+    } finally {
+        process.stdin.off('data', () => { });
+    }
+}
+
+async function handleMessage(message, prettier) {
+    const { method, id, params } = message;
+    if (method === undefined) {
+        throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
+    }
+    if (id === undefined) {
+        throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
+    }
+
+    if (method === 'prettier/format') {
+        if (params === undefined || params.text === undefined) {
+            throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`);
+        }
+        if (params.options === undefined) {
+            throw new Error(`Message params.options is undefined: ${JSON.stringify(message)}`);
+        }
+
+        let resolvedConfig = {};
+        if (params.options.filepath !== undefined) {
+            resolvedConfig = await prettier.prettier.resolveConfig(params.options.filepath) || {};
+        }
+
+        const options = {
+            ...(params.options.prettierOptions || prettier.config),
+            ...resolvedConfig,
+            parser: params.options.parser,
+            plugins: params.options.plugins,
+            path: params.options.filepath
+        };
+        process.stderr.write(`Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${params.options.filepath || ''}' with options: ${JSON.stringify(options)}\n`);
+        const formattedText = await prettier.prettier.format(params.text, options);
+        sendResponse({ id, result: { text: formattedText } });
+    } else if (method === 'prettier/clear_cache') {
+        prettier.prettier.clearConfigCache();
+        prettier.config = await prettier.prettier.resolveConfig(prettier.path) || {};
+        sendResponse({ id, result: null });
+    } else if (method === 'initialize') {
+        sendResponse({
+            id,
+            result: {
+                "capabilities": {}
+            }
+        });
+    } else {
+        throw new Error(`Unknown method: ${method}`);
+    }
+}
+
+function makeError(message) {
+    return {
+        error: {
+            "code": -32600, // invalid request code
+            message,
+        }
+    };
+}
+
+function sendResponse(response) {
+    const responsePayloadString = JSON.stringify({
+        jsonrpc: "2.0",
+        ...response
+    });
+    const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(responsePayloadString)}${headerSeparator}${headerSeparator}`;
+    process.stdout.write(headers + responsePayloadString);
+}
+
+function loadPrettier(prettierPath) {
+    return new Promise((resolve, reject) => {
+        fs.access(prettierPath, fs.constants.F_OK, (err) => {
+            if (err) {
+                reject(`Path '${prettierPath}' does not exist.Error: ${err}`);
+            } else {
+                try {
+                    resolve(require(prettierPath));
+                } catch (err) {
+                    reject(`Error requiring prettier module from path '${prettierPath}'.Error: ${err}`);
+                }
+            }
+        });
+    });
+}

crates/project2/Cargo.toml 🔗

@@ -0,0 +1,84 @@
+[package]
+name = "project2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/project2.rs"
+doctest = false
+
+[features]
+test-support = [
+    "client2/test-support",
+    "db2/test-support",
+    "language2/test-support",
+    "settings2/test-support",
+    "text/test-support",
+    "prettier2/test-support",
+]
+
+[dependencies]
+text = { path = "../text" }
+copilot2 = { path = "../copilot2" }
+client2 = { path = "../client2" }
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+db2 = { path = "../db2" }
+fs2 = { path = "../fs2" }
+fsevent = { path = "../fsevent" }
+fuzzy2 = { path = "../fuzzy2" }
+git = { path = "../git" }
+gpui2 = { path = "../gpui2" }
+language2 = { path = "../language2" }
+lsp2 = { path = "../lsp2" }
+node_runtime = { path = "../node_runtime" }
+prettier2 = { path = "../prettier2" }
+rpc2 = { path = "../rpc2" }
+settings2 = { path = "../settings2" }
+sum_tree = { path = "../sum_tree" }
+terminal2 = { path = "../terminal2" }
+util = { path = "../util" }
+
+aho-corasick = "1.1"
+anyhow.workspace = true
+async-trait.workspace = true
+backtrace = "0.3"
+futures.workspace = true
+globset.workspace = true
+ignore = "0.4"
+lazy_static.workspace = true
+log.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+rand.workspace = true
+regex.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+sha2 = "0.10"
+similar = "1.3"
+smol.workspace = true
+thiserror.workspace = true
+toml.workspace = true
+itertools = "0.10"
+
+[dev-dependencies]
+ctor.workspace = true
+env_logger.workspace = true
+pretty_assertions.workspace = true
+client2 = { path = "../client2", features = ["test-support"] }
+collections = { path = "../collections", features = ["test-support"] }
+db2 = { path = "../db2", features = ["test-support"] }
+fs2 = { path = "../fs2",  features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+language2 = { path = "../language2", features = ["test-support"] }
+lsp2 = { path = "../lsp2", features = ["test-support"] }
+settings2 = { path = "../settings2", features = ["test-support"] }
+prettier2 = { path = "../prettier2", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+rpc2 = { path = "../rpc2", features = ["test-support"] }
+git2.workspace = true
+tempdir.workspace = true
+unindent.workspace = true

crates/project2/src/ignore.rs 🔗

@@ -0,0 +1,57 @@
+use ignore::gitignore::Gitignore;
+use std::{ffi::OsStr, path::Path, sync::Arc};
+
+pub enum IgnoreStack {
+    None,
+    Some {
+        abs_base_path: Arc<Path>,
+        ignore: Arc<Gitignore>,
+        parent: Arc<IgnoreStack>,
+    },
+    All,
+}
+
+impl IgnoreStack {
+    pub fn none() -> Arc<Self> {
+        Arc::new(Self::None)
+    }
+
+    pub fn all() -> Arc<Self> {
+        Arc::new(Self::All)
+    }
+
+    pub fn is_all(&self) -> bool {
+        matches!(self, IgnoreStack::All)
+    }
+
+    pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
+        match self.as_ref() {
+            IgnoreStack::All => self,
+            _ => Arc::new(Self::Some {
+                abs_base_path,
+                ignore,
+                parent: self,
+            }),
+        }
+    }
+
+    pub fn is_abs_path_ignored(&self, abs_path: &Path, is_dir: bool) -> bool {
+        if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) {
+            return true;
+        }
+
+        match self {
+            Self::None => false,
+            Self::All => true,
+            Self::Some {
+                abs_base_path,
+                ignore,
+                parent: prev,
+            } => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) {
+                ignore::Match::None => prev.is_abs_path_ignored(abs_path, is_dir),
+                ignore::Match::Ignore(_) => true,
+                ignore::Match::Whitelist(_) => false,
+            },
+        }
+    }
+}

crates/project2/src/lsp_command.rs 🔗

@@ -0,0 +1,2350 @@
+use crate::{
+    DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
+    InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
+    MarkupContent, Project, ProjectTransaction, ResolveState,
+};
+use anyhow::{anyhow, Context, Result};
+use async_trait::async_trait;
+use client2::proto::{self, PeerId};
+use futures::future;
+use gpui2::{AppContext, AsyncAppContext, Model};
+use language2::{
+    language_settings::{language_settings, InlayHintKind},
+    point_from_lsp, point_to_lsp,
+    proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
+    range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
+    CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
+    Unclipped,
+};
+use lsp2::{
+    CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
+    OneOf, ServerCapabilities,
+};
+use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
+use text::LineEnding;
+
+pub fn lsp_formatting_options(tab_size: u32) -> lsp2::FormattingOptions {
+    lsp2::FormattingOptions {
+        tab_size,
+        insert_spaces: true,
+        insert_final_newline: Some(true),
+        ..lsp2::FormattingOptions::default()
+    }
+}
+
+#[async_trait]
+pub(crate) trait LspCommand: 'static + Sized + Send {
+    type Response: 'static + Default + Send;
+    type LspRequest: 'static + Send + lsp2::request::Request;
+    type ProtoRequest: 'static + Send + proto::RequestMessage;
+
+    fn check_capabilities(&self, _: &lsp2::ServerCapabilities) -> bool {
+        true
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        buffer: &Buffer,
+        language_server: &Arc<LanguageServer>,
+        cx: &AppContext,
+    ) -> <Self::LspRequest as lsp2::request::Request>::Params;
+
+    async fn response_from_lsp(
+        self,
+        message: <Self::LspRequest as lsp2::request::Request>::Result,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        server_id: LanguageServerId,
+        cx: AsyncAppContext,
+    ) -> Result<Self::Response>;
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest;
+
+    async fn from_proto(
+        message: Self::ProtoRequest,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Self>;
+
+    fn response_to_proto(
+        response: Self::Response,
+        project: &mut Project,
+        peer_id: PeerId,
+        buffer_version: &clock::Global,
+        cx: &mut AppContext,
+    ) -> <Self::ProtoRequest as proto::RequestMessage>::Response;
+
+    async fn response_from_proto(
+        self,
+        message: <Self::ProtoRequest as proto::RequestMessage>::Response,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Self::Response>;
+
+    fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64;
+}
+
+pub(crate) struct PrepareRename {
+    pub position: PointUtf16,
+}
+
+pub(crate) struct PerformRename {
+    pub position: PointUtf16,
+    pub new_name: String,
+    pub push_to_history: bool,
+}
+
+pub(crate) struct GetDefinition {
+    pub position: PointUtf16,
+}
+
+pub(crate) struct GetTypeDefinition {
+    pub position: PointUtf16,
+}
+
+pub(crate) struct GetReferences {
+    pub position: PointUtf16,
+}
+
+pub(crate) struct GetDocumentHighlights {
+    pub position: PointUtf16,
+}
+
+pub(crate) struct GetHover {
+    pub position: PointUtf16,
+}
+
+pub(crate) struct GetCompletions {
+    pub position: PointUtf16,
+}
+
+pub(crate) struct GetCodeActions {
+    pub range: Range<Anchor>,
+}
+
+pub(crate) struct OnTypeFormatting {
+    pub position: PointUtf16,
+    pub trigger: String,
+    pub options: FormattingOptions,
+    pub push_to_history: bool,
+}
+
+pub(crate) struct InlayHints {
+    pub range: Range<Anchor>,
+}
+
+pub(crate) struct FormattingOptions {
+    tab_size: u32,
+}
+
+impl From<lsp2::FormattingOptions> for FormattingOptions {
+    fn from(value: lsp2::FormattingOptions) -> Self {
+        Self {
+            tab_size: value.tab_size,
+        }
+    }
+}
+
+#[async_trait]
+impl LspCommand for PrepareRename {
+    type Response = Option<Range<Anchor>>;
+    type LspRequest = lsp2::request::PrepareRenameRequest;
+    type ProtoRequest = proto::PrepareRename;
+
+    fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
+        if let Some(lsp2::OneOf::Right(rename)) = &capabilities.rename_provider {
+            rename.prepare_provider == Some(true)
+        } else {
+            false
+        }
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::TextDocumentPositionParams {
+        lsp2::TextDocumentPositionParams {
+            text_document: lsp2::TextDocumentIdentifier {
+                uri: lsp2::Url::from_file_path(path).unwrap(),
+            },
+            position: point_to_lsp(self.position),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp2::PrepareRenameResponse>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        _: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> Result<Option<Range<Anchor>>> {
+        buffer.update(&mut cx, |buffer, _| {
+            if let Some(
+                lsp2::PrepareRenameResponse::Range(range)
+                | lsp2::PrepareRenameResponse::RangeWithPlaceholder { range, .. },
+            ) = message
+            {
+                let Range { start, end } = range_from_lsp(range);
+                if buffer.clip_point_utf16(start, Bias::Left) == start.0
+                    && buffer.clip_point_utf16(end, Bias::Left) == end.0
+                {
+                    return Ok(Some(buffer.anchor_after(start)..buffer.anchor_before(end)));
+                }
+            }
+            Ok(None)
+        })?
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PrepareRename {
+        proto::PrepareRename {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language2::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::PrepareRename,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+        })
+    }
+
+    fn response_to_proto(
+        range: Option<Range<Anchor>>,
+        _: &mut Project,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        _: &mut AppContext,
+    ) -> proto::PrepareRenameResponse {
+        proto::PrepareRenameResponse {
+            can_rename: range.is_some(),
+            start: range
+                .as_ref()
+                .map(|range| language2::proto::serialize_anchor(&range.start)),
+            end: range
+                .as_ref()
+                .map(|range| language2::proto::serialize_anchor(&range.end)),
+            version: serialize_version(buffer_version),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::PrepareRenameResponse,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Option<Range<Anchor>>> {
+        if message.can_rename {
+            buffer
+                .update(&mut cx, |buffer, _| {
+                    buffer.wait_for_version(deserialize_version(&message.version))
+                })?
+                .await?;
+            let start = message.start.and_then(deserialize_anchor);
+            let end = message.end.and_then(deserialize_anchor);
+            Ok(start.zip(end).map(|(start, end)| start..end))
+        } else {
+            Ok(None)
+        }
+    }
+
+    fn buffer_id_from_proto(message: &proto::PrepareRename) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait]
+impl LspCommand for PerformRename {
+    type Response = ProjectTransaction;
+    type LspRequest = lsp2::request::Rename;
+    type ProtoRequest = proto::PerformRename;
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::RenameParams {
+        lsp2::RenameParams {
+            text_document_position: lsp2::TextDocumentPositionParams {
+                text_document: lsp2::TextDocumentIdentifier {
+                    uri: lsp2::Url::from_file_path(path).unwrap(),
+                },
+                position: point_to_lsp(self.position),
+            },
+            new_name: self.new_name.clone(),
+            work_done_progress_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp2::WorkspaceEdit>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        server_id: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> Result<ProjectTransaction> {
+        if let Some(edit) = message {
+            let (lsp_adapter, lsp_server) =
+                language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+            Project::deserialize_workspace_edit(
+                project,
+                edit,
+                self.push_to_history,
+                lsp_adapter,
+                lsp_server,
+                &mut cx,
+            )
+            .await
+        } else {
+            Ok(ProjectTransaction::default())
+        }
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PerformRename {
+        proto::PerformRename {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language2::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            new_name: self.new_name.clone(),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::PerformRename,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+            new_name: message.new_name,
+            push_to_history: false,
+        })
+    }
+
+    fn response_to_proto(
+        response: ProjectTransaction,
+        project: &mut Project,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &mut AppContext,
+    ) -> proto::PerformRenameResponse {
+        let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx);
+        proto::PerformRenameResponse {
+            transaction: Some(transaction),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::PerformRenameResponse,
+        project: Model<Project>,
+        _: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<ProjectTransaction> {
+        let message = message
+            .transaction
+            .ok_or_else(|| anyhow!("missing transaction"))?;
+        project
+            .update(&mut cx, |project, cx| {
+                project.deserialize_project_transaction(message, self.push_to_history, cx)
+            })?
+            .await
+    }
+
+    fn buffer_id_from_proto(message: &proto::PerformRename) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait]
+impl LspCommand for GetDefinition {
+    type Response = Vec<LocationLink>;
+    type LspRequest = lsp2::request::GotoDefinition;
+    type ProtoRequest = proto::GetDefinition;
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::GotoDefinitionParams {
+        lsp2::GotoDefinitionParams {
+            text_document_position_params: lsp2::TextDocumentPositionParams {
+                text_document: lsp2::TextDocumentIdentifier {
+                    uri: lsp2::Url::from_file_path(path).unwrap(),
+                },
+                position: point_to_lsp(self.position),
+            },
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp2::GotoDefinitionResponse>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        server_id: LanguageServerId,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<LocationLink>> {
+        location_links_from_lsp(message, project, buffer, server_id, cx).await
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition {
+        proto::GetDefinition {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language2::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetDefinition,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+        })
+    }
+
+    fn response_to_proto(
+        response: Vec<LocationLink>,
+        project: &mut Project,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &mut AppContext,
+    ) -> proto::GetDefinitionResponse {
+        let links = location_links_to_proto(response, project, peer_id, cx);
+        proto::GetDefinitionResponse { links }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetDefinitionResponse,
+        project: Model<Project>,
+        _: Model<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<LocationLink>> {
+        location_links_from_proto(message.links, project, cx).await
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait]
+impl LspCommand for GetTypeDefinition {
+    type Response = Vec<LocationLink>;
+    type LspRequest = lsp2::request::GotoTypeDefinition;
+    type ProtoRequest = proto::GetTypeDefinition;
+
+    fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
+        match &capabilities.type_definition_provider {
+            None => false,
+            Some(lsp2::TypeDefinitionProviderCapability::Simple(false)) => false,
+            _ => true,
+        }
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::GotoTypeDefinitionParams {
+        lsp2::GotoTypeDefinitionParams {
+            text_document_position_params: lsp2::TextDocumentPositionParams {
+                text_document: lsp2::TextDocumentIdentifier {
+                    uri: lsp2::Url::from_file_path(path).unwrap(),
+                },
+                position: point_to_lsp(self.position),
+            },
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp2::GotoTypeDefinitionResponse>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        server_id: LanguageServerId,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<LocationLink>> {
+        location_links_from_lsp(message, project, buffer, server_id, cx).await
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition {
+        proto::GetTypeDefinition {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language2::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetTypeDefinition,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+        })
+    }
+
+    fn response_to_proto(
+        response: Vec<LocationLink>,
+        project: &mut Project,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &mut AppContext,
+    ) -> proto::GetTypeDefinitionResponse {
+        let links = location_links_to_proto(response, project, peer_id, cx);
+        proto::GetTypeDefinitionResponse { links }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetTypeDefinitionResponse,
+        project: Model<Project>,
+        _: Model<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<LocationLink>> {
+        location_links_from_proto(message.links, project, cx).await
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> u64 {
+        message.buffer_id
+    }
+}
+
+fn language_server_for_buffer(
+    project: &Model<Project>,
+    buffer: &Model<Buffer>,
+    server_id: LanguageServerId,
+    cx: &mut AsyncAppContext,
+) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
+    project
+        .update(cx, |project, cx| {
+            project
+                .language_server_for_buffer(buffer.read(cx), server_id, cx)
+                .map(|(adapter, server)| (adapter.clone(), server.clone()))
+        })?
+        .ok_or_else(|| anyhow!("no language server found for buffer"))
+}
+
+async fn location_links_from_proto(
+    proto_links: Vec<proto::LocationLink>,
+    project: Model<Project>,
+    mut cx: AsyncAppContext,
+) -> Result<Vec<LocationLink>> {
+    let mut links = Vec::new();
+
+    for link in proto_links {
+        let origin = match link.origin {
+            Some(origin) => {
+                let buffer = project
+                    .update(&mut cx, |this, cx| {
+                        this.wait_for_remote_buffer(origin.buffer_id, cx)
+                    })?
+                    .await?;
+                let start = origin
+                    .start
+                    .and_then(deserialize_anchor)
+                    .ok_or_else(|| anyhow!("missing origin start"))?;
+                let end = origin
+                    .end
+                    .and_then(deserialize_anchor)
+                    .ok_or_else(|| anyhow!("missing origin end"))?;
+                buffer
+                    .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
+                    .await?;
+                Some(Location {
+                    buffer,
+                    range: start..end,
+                })
+            }
+            None => None,
+        };
+
+        let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
+        let buffer = project
+            .update(&mut cx, |this, cx| {
+                this.wait_for_remote_buffer(target.buffer_id, cx)
+            })?
+            .await?;
+        let start = target
+            .start
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("missing target start"))?;
+        let end = target
+            .end
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("missing target end"))?;
+        buffer
+            .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
+            .await?;
+        let target = Location {
+            buffer,
+            range: start..end,
+        };
+
+        links.push(LocationLink { origin, target })
+    }
+
+    Ok(links)
+}
+
+async fn location_links_from_lsp(
+    message: Option<lsp2::GotoDefinitionResponse>,
+    project: Model<Project>,
+    buffer: Model<Buffer>,
+    server_id: LanguageServerId,
+    mut cx: AsyncAppContext,
+) -> Result<Vec<LocationLink>> {
+    let message = match message {
+        Some(message) => message,
+        None => return Ok(Vec::new()),
+    };
+
+    let mut unresolved_links = Vec::new();
+    match message {
+        lsp2::GotoDefinitionResponse::Scalar(loc) => {
+            unresolved_links.push((None, loc.uri, loc.range));
+        }
+
+        lsp2::GotoDefinitionResponse::Array(locs) => {
+            unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range)));
+        }
+
+        lsp2::GotoDefinitionResponse::Link(links) => {
+            unresolved_links.extend(links.into_iter().map(|l| {
+                (
+                    l.origin_selection_range,
+                    l.target_uri,
+                    l.target_selection_range,
+                )
+            }));
+        }
+    }
+
+    let (lsp_adapter, language_server) =
+        language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+    let mut definitions = Vec::new();
+    for (origin_range, target_uri, target_range) in unresolved_links {
+        let target_buffer_handle = project
+            .update(&mut cx, |this, cx| {
+                this.open_local_buffer_via_lsp(
+                    target_uri,
+                    language_server.server_id(),
+                    lsp_adapter.name.clone(),
+                    cx,
+                )
+            })?
+            .await?;
+
+        buffer.update(&mut cx, |origin_buffer, cx| {
+            let origin_location = origin_range.map(|origin_range| {
+                let origin_start =
+                    origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
+                let origin_end =
+                    origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left);
+                Location {
+                    buffer: buffer.clone(),
+                    range: origin_buffer.anchor_after(origin_start)
+                        ..origin_buffer.anchor_before(origin_end),
+                }
+            });
+
+            let target_buffer = target_buffer_handle.read(cx);
+            let target_start =
+                target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
+            let target_end =
+                target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
+            let target_location = Location {
+                buffer: target_buffer_handle,
+                range: target_buffer.anchor_after(target_start)
+                    ..target_buffer.anchor_before(target_end),
+            };
+
+            definitions.push(LocationLink {
+                origin: origin_location,
+                target: target_location,
+            })
+        })?;
+    }
+    Ok(definitions)
+}
+
+fn location_links_to_proto(
+    links: Vec<LocationLink>,
+    project: &mut Project,
+    peer_id: PeerId,
+    cx: &mut AppContext,
+) -> Vec<proto::LocationLink> {
+    links
+        .into_iter()
+        .map(|definition| {
+            let origin = definition.origin.map(|origin| {
+                let buffer_id = project.create_buffer_for_peer(&origin.buffer, peer_id, cx);
+                proto::Location {
+                    start: Some(serialize_anchor(&origin.range.start)),
+                    end: Some(serialize_anchor(&origin.range.end)),
+                    buffer_id,
+                }
+            });
+
+            let buffer_id = project.create_buffer_for_peer(&definition.target.buffer, peer_id, cx);
+            let target = proto::Location {
+                start: Some(serialize_anchor(&definition.target.range.start)),
+                end: Some(serialize_anchor(&definition.target.range.end)),
+                buffer_id,
+            };
+
+            proto::LocationLink {
+                origin,
+                target: Some(target),
+            }
+        })
+        .collect()
+}
+
+#[async_trait]
+impl LspCommand for GetReferences {
+    type Response = Vec<Location>;
+    type LspRequest = lsp2::request::References;
+    type ProtoRequest = proto::GetReferences;
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::ReferenceParams {
+        lsp2::ReferenceParams {
+            text_document_position: lsp2::TextDocumentPositionParams {
+                text_document: lsp2::TextDocumentIdentifier {
+                    uri: lsp2::Url::from_file_path(path).unwrap(),
+                },
+                position: point_to_lsp(self.position),
+            },
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+            context: lsp2::ReferenceContext {
+                include_declaration: true,
+            },
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        locations: Option<Vec<lsp2::Location>>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        server_id: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<Location>> {
+        let mut references = Vec::new();
+        let (lsp_adapter, language_server) =
+            language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+
+        if let Some(locations) = locations {
+            for lsp_location in locations {
+                let target_buffer_handle = project
+                    .update(&mut cx, |this, cx| {
+                        this.open_local_buffer_via_lsp(
+                            lsp_location.uri,
+                            language_server.server_id(),
+                            lsp_adapter.name.clone(),
+                            cx,
+                        )
+                    })?
+                    .await?;
+
+                target_buffer_handle
+                    .clone()
+                    .update(&mut cx, |target_buffer, _| {
+                        let target_start = target_buffer
+                            .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left);
+                        let target_end = target_buffer
+                            .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left);
+                        references.push(Location {
+                            buffer: target_buffer_handle,
+                            range: target_buffer.anchor_after(target_start)
+                                ..target_buffer.anchor_before(target_end),
+                        });
+                    })?;
+            }
+        }
+
+        Ok(references)
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetReferences {
+        proto::GetReferences {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language2::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetReferences,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+        })
+    }
+
+    fn response_to_proto(
+        response: Vec<Location>,
+        project: &mut Project,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &mut AppContext,
+    ) -> proto::GetReferencesResponse {
+        let locations = response
+            .into_iter()
+            .map(|definition| {
+                let buffer_id = project.create_buffer_for_peer(&definition.buffer, peer_id, cx);
+                proto::Location {
+                    start: Some(serialize_anchor(&definition.range.start)),
+                    end: Some(serialize_anchor(&definition.range.end)),
+                    buffer_id,
+                }
+            })
+            .collect();
+        proto::GetReferencesResponse { locations }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetReferencesResponse,
+        project: Model<Project>,
+        _: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<Location>> {
+        let mut locations = Vec::new();
+        for location in message.locations {
+            let target_buffer = project
+                .update(&mut cx, |this, cx| {
+                    this.wait_for_remote_buffer(location.buffer_id, cx)
+                })?
+                .await?;
+            let start = location
+                .start
+                .and_then(deserialize_anchor)
+                .ok_or_else(|| anyhow!("missing target start"))?;
+            let end = location
+                .end
+                .and_then(deserialize_anchor)
+                .ok_or_else(|| anyhow!("missing target end"))?;
+            target_buffer
+                .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
+                .await?;
+            locations.push(Location {
+                buffer: target_buffer,
+                range: start..end,
+            })
+        }
+        Ok(locations)
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetReferences) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait]
+impl LspCommand for GetDocumentHighlights {
+    type Response = Vec<DocumentHighlight>;
+    type LspRequest = lsp2::request::DocumentHighlightRequest;
+    type ProtoRequest = proto::GetDocumentHighlights;
+
+    fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
+        capabilities.document_highlight_provider.is_some()
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::DocumentHighlightParams {
+        lsp2::DocumentHighlightParams {
+            text_document_position_params: lsp2::TextDocumentPositionParams {
+                text_document: lsp2::TextDocumentIdentifier {
+                    uri: lsp2::Url::from_file_path(path).unwrap(),
+                },
+                position: point_to_lsp(self.position),
+            },
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        lsp_highlights: Option<Vec<lsp2::DocumentHighlight>>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        _: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<DocumentHighlight>> {
+        buffer.update(&mut cx, |buffer, _| {
+            let mut lsp_highlights = lsp_highlights.unwrap_or_default();
+            lsp_highlights.sort_unstable_by_key(|h| (h.range.start, Reverse(h.range.end)));
+            lsp_highlights
+                .into_iter()
+                .map(|lsp_highlight| {
+                    let start = buffer
+                        .clip_point_utf16(point_from_lsp(lsp_highlight.range.start), Bias::Left);
+                    let end = buffer
+                        .clip_point_utf16(point_from_lsp(lsp_highlight.range.end), Bias::Left);
+                    DocumentHighlight {
+                        range: buffer.anchor_after(start)..buffer.anchor_before(end),
+                        kind: lsp_highlight
+                            .kind
+                            .unwrap_or(lsp2::DocumentHighlightKind::READ),
+                    }
+                })
+                .collect()
+        })
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentHighlights {
+        proto::GetDocumentHighlights {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language2::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetDocumentHighlights,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+        })
+    }
+
+    fn response_to_proto(
+        response: Vec<DocumentHighlight>,
+        _: &mut Project,
+        _: PeerId,
+        _: &clock::Global,
+        _: &mut AppContext,
+    ) -> proto::GetDocumentHighlightsResponse {
+        let highlights = response
+            .into_iter()
+            .map(|highlight| proto::DocumentHighlight {
+                start: Some(serialize_anchor(&highlight.range.start)),
+                end: Some(serialize_anchor(&highlight.range.end)),
+                kind: match highlight.kind {
+                    DocumentHighlightKind::TEXT => proto::document_highlight::Kind::Text.into(),
+                    DocumentHighlightKind::WRITE => proto::document_highlight::Kind::Write.into(),
+                    DocumentHighlightKind::READ => proto::document_highlight::Kind::Read.into(),
+                    _ => proto::document_highlight::Kind::Text.into(),
+                },
+            })
+            .collect();
+        proto::GetDocumentHighlightsResponse { highlights }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetDocumentHighlightsResponse,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<DocumentHighlight>> {
+        let mut highlights = Vec::new();
+        for highlight in message.highlights {
+            let start = highlight
+                .start
+                .and_then(deserialize_anchor)
+                .ok_or_else(|| anyhow!("missing target start"))?;
+            let end = highlight
+                .end
+                .and_then(deserialize_anchor)
+                .ok_or_else(|| anyhow!("missing target end"))?;
+            buffer
+                .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
+                .await?;
+            let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
+                Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
+                Some(proto::document_highlight::Kind::Read) => DocumentHighlightKind::READ,
+                Some(proto::document_highlight::Kind::Write) => DocumentHighlightKind::WRITE,
+                None => DocumentHighlightKind::TEXT,
+            };
+            highlights.push(DocumentHighlight {
+                range: start..end,
+                kind,
+            });
+        }
+        Ok(highlights)
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetDocumentHighlights) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait]
+impl LspCommand for GetHover {
+    type Response = Option<Hover>;
+    type LspRequest = lsp2::request::HoverRequest;
+    type ProtoRequest = proto::GetHover;
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::HoverParams {
+        lsp2::HoverParams {
+            text_document_position_params: lsp2::TextDocumentPositionParams {
+                text_document: lsp2::TextDocumentIdentifier {
+                    uri: lsp2::Url::from_file_path(path).unwrap(),
+                },
+                position: point_to_lsp(self.position),
+            },
+            work_done_progress_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp2::Hover>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        _: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self::Response> {
+        let Some(hover) = message else {
+            return Ok(None);
+        };
+
+        let (language, range) = buffer.update(&mut cx, |buffer, _| {
+            (
+                buffer.language().cloned(),
+                hover.range.map(|range| {
+                    let token_start =
+                        buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
+                    let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
+                    buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
+                }),
+            )
+        })?;
+
+        fn hover_blocks_from_marked_string(
+            marked_string: lsp2::MarkedString,
+        ) -> Option<HoverBlock> {
+            let block = match marked_string {
+                lsp2::MarkedString::String(content) => HoverBlock {
+                    text: content,
+                    kind: HoverBlockKind::Markdown,
+                },
+                lsp2::MarkedString::LanguageString(lsp2::LanguageString { language, value }) => {
+                    HoverBlock {
+                        text: value,
+                        kind: HoverBlockKind::Code { language },
+                    }
+                }
+            };
+            if block.text.is_empty() {
+                None
+            } else {
+                Some(block)
+            }
+        }
+
+        let contents = match hover.contents {
+            lsp2::HoverContents::Scalar(marked_string) => {
+                hover_blocks_from_marked_string(marked_string)
+                    .into_iter()
+                    .collect()
+            }
+            lsp2::HoverContents::Array(marked_strings) => marked_strings
+                .into_iter()
+                .filter_map(hover_blocks_from_marked_string)
+                .collect(),
+            lsp2::HoverContents::Markup(markup_content) => vec![HoverBlock {
+                text: markup_content.value,
+                kind: if markup_content.kind == lsp2::MarkupKind::Markdown {
+                    HoverBlockKind::Markdown
+                } else {
+                    HoverBlockKind::PlainText
+                },
+            }],
+        };
+
+        Ok(Some(Hover {
+            contents,
+            range,
+            language,
+        }))
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
+        proto::GetHover {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language2::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            version: serialize_version(&buffer.version),
+        }
+    }
+
+    async fn from_proto(
+        message: Self::ProtoRequest,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+        })
+    }
+
+    fn response_to_proto(
+        response: Self::Response,
+        _: &mut Project,
+        _: PeerId,
+        _: &clock::Global,
+        _: &mut AppContext,
+    ) -> proto::GetHoverResponse {
+        if let Some(response) = response {
+            let (start, end) = if let Some(range) = response.range {
+                (
+                    Some(language2::proto::serialize_anchor(&range.start)),
+                    Some(language2::proto::serialize_anchor(&range.end)),
+                )
+            } else {
+                (None, None)
+            };
+
+            let contents = response
+                .contents
+                .into_iter()
+                .map(|block| proto::HoverBlock {
+                    text: block.text,
+                    is_markdown: block.kind == HoverBlockKind::Markdown,
+                    language: if let HoverBlockKind::Code { language } = block.kind {
+                        Some(language)
+                    } else {
+                        None
+                    },
+                })
+                .collect();
+
+            proto::GetHoverResponse {
+                start,
+                end,
+                contents,
+            }
+        } else {
+            proto::GetHoverResponse {
+                start: None,
+                end: None,
+                contents: Vec::new(),
+            }
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetHoverResponse,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self::Response> {
+        let contents: Vec<_> = message
+            .contents
+            .into_iter()
+            .map(|block| HoverBlock {
+                text: block.text,
+                kind: if let Some(language) = block.language {
+                    HoverBlockKind::Code { language }
+                } else if block.is_markdown {
+                    HoverBlockKind::Markdown
+                } else {
+                    HoverBlockKind::PlainText
+                },
+            })
+            .collect();
+        if contents.is_empty() {
+            return Ok(None);
+        }
+
+        let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?;
+        let range = if let (Some(start), Some(end)) = (message.start, message.end) {
+            language2::proto::deserialize_anchor(start)
+                .and_then(|start| language2::proto::deserialize_anchor(end).map(|end| start..end))
+        } else {
+            None
+        };
+
+        Ok(Some(Hover {
+            contents,
+            range,
+            language,
+        }))
+    }
+
+    fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait]
+impl LspCommand for GetCompletions {
+    type Response = Vec<Completion>;
+    type LspRequest = lsp2::request::Completion;
+    type ProtoRequest = proto::GetCompletions;
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::CompletionParams {
+        lsp2::CompletionParams {
+            text_document_position: lsp2::TextDocumentPositionParams::new(
+                lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path(path).unwrap()),
+                point_to_lsp(self.position),
+            ),
+            context: Default::default(),
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        completions: Option<lsp2::CompletionResponse>,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        server_id: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<Completion>> {
+        let mut response_list = None;
+        let completions = if let Some(completions) = completions {
+            match completions {
+                lsp2::CompletionResponse::Array(completions) => completions,
+
+                lsp2::CompletionResponse::List(mut list) => {
+                    let items = std::mem::take(&mut list.items);
+                    response_list = Some(list);
+                    items
+                }
+            }
+        } else {
+            Default::default()
+        };
+
+        let completions = buffer.update(&mut cx, |buffer, _| {
+            let language = buffer.language().cloned();
+            let snapshot = buffer.snapshot();
+            let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
+
+            let mut range_for_token = None;
+            completions
+                .into_iter()
+                .filter_map(move |mut lsp_completion| {
+                    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.
+                        Some(lsp2::CompletionTextEdit::Edit(edit)) => {
+                            let range = range_from_lsp(edit.range);
+                            let start = snapshot.clip_point_utf16(range.start, Bias::Left);
+                            let end = snapshot.clip_point_utf16(range.end, Bias::Left);
+                            if start != range.start.0 || end != range.end.0 {
+                                log::info!("completion out of expected range");
+                                return None;
+                            }
+                            (
+                                snapshot.anchor_before(start)..snapshot.anchor_after(end),
+                                edit.new_text.clone(),
+                            )
+                        }
+
+                        // If the language server does not provide a range, then infer
+                        // the range based on the syntax tree.
+                        None => {
+                            if self.position != clipped_position {
+                                log::info!("completion out of expected range");
+                                return None;
+                            }
+
+                            let default_edit_range = response_list
+                                .as_ref()
+                                .and_then(|list| list.item_defaults.as_ref())
+                                .and_then(|defaults| defaults.edit_range.as_ref())
+                                .and_then(|range| match range {
+                                    CompletionListItemDefaultsEditRange::Range(r) => Some(r),
+                                    _ => None,
+                                });
+
+                            let range = if let Some(range) = default_edit_range {
+                                let range = range_from_lsp(range.clone());
+                                let start = snapshot.clip_point_utf16(range.start, Bias::Left);
+                                let end = snapshot.clip_point_utf16(range.end, Bias::Left);
+                                if start != range.start.0 || end != range.end.0 {
+                                    log::info!("completion out of expected range");
+                                    return None;
+                                }
+
+                                snapshot.anchor_before(start)..snapshot.anchor_after(end)
+                            } else {
+                                range_for_token
+                                    .get_or_insert_with(|| {
+                                        let offset = self.position.to_offset(&snapshot);
+                                        let (range, kind) = snapshot.surrounding_word(offset);
+                                        let range = if kind == Some(CharKind::Word) {
+                                            range
+                                        } else {
+                                            offset..offset
+                                        };
+
+                                        snapshot.anchor_before(range.start)
+                                            ..snapshot.anchor_after(range.end)
+                                    })
+                                    .clone()
+                            };
+
+                            let text = lsp_completion
+                                .insert_text
+                                .as_ref()
+                                .unwrap_or(&lsp_completion.label)
+                                .clone();
+                            (range, text)
+                        }
+
+                        Some(lsp2::CompletionTextEdit::InsertAndReplace(_)) => {
+                            log::info!("unsupported insert/replace completion");
+                            return None;
+                        }
+                    };
+
+                    let language = language.clone();
+                    LineEnding::normalize(&mut new_text);
+                    Some(async move {
+                        let mut label = None;
+                        if let Some(language) = language {
+                            language.process_completion(&mut lsp_completion).await;
+                            label = language.label_for_completion(&lsp_completion).await;
+                        }
+                        Completion {
+                            old_range,
+                            new_text,
+                            label: label.unwrap_or_else(|| {
+                                language2::CodeLabel::plain(
+                                    lsp_completion.label.clone(),
+                                    lsp_completion.filter_text.as_deref(),
+                                )
+                            }),
+                            server_id,
+                            lsp_completion,
+                        }
+                    })
+                })
+        })?;
+
+        Ok(future::join_all(completions).await)
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions {
+        let anchor = buffer.anchor_after(self.position);
+        proto::GetCompletions {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language2::proto::serialize_anchor(&anchor)),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetCompletions,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let version = deserialize_version(&message.version);
+        buffer
+            .update(&mut cx, |buffer, _| buffer.wait_for_version(version))?
+            .await?;
+        let position = message
+            .position
+            .and_then(language2::proto::deserialize_anchor)
+            .map(|p| {
+                buffer.update(&mut cx, |buffer, _| {
+                    buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left)
+                })
+            })
+            .ok_or_else(|| anyhow!("invalid position"))??;
+        Ok(Self { position })
+    }
+
+    fn response_to_proto(
+        completions: Vec<Completion>,
+        _: &mut Project,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        _: &mut AppContext,
+    ) -> proto::GetCompletionsResponse {
+        proto::GetCompletionsResponse {
+            completions: completions
+                .iter()
+                .map(language2::proto::serialize_completion)
+                .collect(),
+            version: serialize_version(&buffer_version),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetCompletionsResponse,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<Completion>> {
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+
+        let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?;
+        let completions = message.completions.into_iter().map(|completion| {
+            language2::proto::deserialize_completion(completion, language.clone())
+        });
+        future::try_join_all(completions).await
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait]
+impl LspCommand for GetCodeActions {
+    type Response = Vec<CodeAction>;
+    type LspRequest = lsp2::request::CodeActionRequest;
+    type ProtoRequest = proto::GetCodeActions;
+
+    fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
+        match &capabilities.code_action_provider {
+            None => false,
+            Some(lsp2::CodeActionProviderCapability::Simple(false)) => false,
+            _ => true,
+        }
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        buffer: &Buffer,
+        language_server: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::CodeActionParams {
+        let relevant_diagnostics = buffer
+            .snapshot()
+            .diagnostics_in_range::<_, usize>(self.range.clone(), false)
+            .map(|entry| entry.to_lsp_diagnostic_stub())
+            .collect();
+        lsp2::CodeActionParams {
+            text_document: lsp2::TextDocumentIdentifier::new(
+                lsp2::Url::from_file_path(path).unwrap(),
+            ),
+            range: range_to_lsp(self.range.to_point_utf16(buffer)),
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+            context: lsp2::CodeActionContext {
+                diagnostics: relevant_diagnostics,
+                only: language_server.code_action_kinds(),
+                ..lsp2::CodeActionContext::default()
+            },
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        actions: Option<lsp2::CodeActionResponse>,
+        _: Model<Project>,
+        _: Model<Buffer>,
+        server_id: LanguageServerId,
+        _: AsyncAppContext,
+    ) -> Result<Vec<CodeAction>> {
+        Ok(actions
+            .unwrap_or_default()
+            .into_iter()
+            .filter_map(|entry| {
+                if let lsp2::CodeActionOrCommand::CodeAction(lsp_action) = entry {
+                    Some(CodeAction {
+                        server_id,
+                        range: self.range.clone(),
+                        lsp_action,
+                    })
+                } else {
+                    None
+                }
+            })
+            .collect())
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeActions {
+        proto::GetCodeActions {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            start: Some(language2::proto::serialize_anchor(&self.range.start)),
+            end: Some(language2::proto::serialize_anchor(&self.range.end)),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetCodeActions,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let start = message
+            .start
+            .and_then(language2::proto::deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid start"))?;
+        let end = message
+            .end
+            .and_then(language2::proto::deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid end"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+
+        Ok(Self { range: start..end })
+    }
+
+    fn response_to_proto(
+        code_actions: Vec<CodeAction>,
+        _: &mut Project,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        _: &mut AppContext,
+    ) -> proto::GetCodeActionsResponse {
+        proto::GetCodeActionsResponse {
+            actions: code_actions
+                .iter()
+                .map(language2::proto::serialize_code_action)
+                .collect(),
+            version: serialize_version(&buffer_version),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetCodeActionsResponse,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<CodeAction>> {
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+        message
+            .actions
+            .into_iter()
+            .map(language2::proto::deserialize_code_action)
+            .collect()
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetCodeActions) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait]
+impl LspCommand for OnTypeFormatting {
+    type Response = Option<Transaction>;
+    type LspRequest = lsp2::request::OnTypeFormatting;
+    type ProtoRequest = proto::OnTypeFormatting;
+
+    fn check_capabilities(&self, server_capabilities: &lsp2::ServerCapabilities) -> bool {
+        let Some(on_type_formatting_options) =
+            &server_capabilities.document_on_type_formatting_provider
+        else {
+            return false;
+        };
+        on_type_formatting_options
+            .first_trigger_character
+            .contains(&self.trigger)
+            || on_type_formatting_options
+                .more_trigger_character
+                .iter()
+                .flatten()
+                .any(|chars| chars.contains(&self.trigger))
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::DocumentOnTypeFormattingParams {
+        lsp2::DocumentOnTypeFormattingParams {
+            text_document_position: lsp2::TextDocumentPositionParams::new(
+                lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path(path).unwrap()),
+                point_to_lsp(self.position),
+            ),
+            ch: self.trigger.clone(),
+            options: lsp_formatting_options(self.options.tab_size),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<Vec<lsp2::TextEdit>>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        server_id: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> Result<Option<Transaction>> {
+        if let Some(edits) = message {
+            let (lsp_adapter, lsp_server) =
+                language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+            Project::deserialize_edits(
+                project,
+                buffer,
+                edits,
+                self.push_to_history,
+                lsp_adapter,
+                lsp_server,
+                &mut cx,
+            )
+            .await
+        } else {
+            Ok(None)
+        }
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::OnTypeFormatting {
+        proto::OnTypeFormatting {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language2::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            trigger: self.trigger.clone(),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::OnTypeFormatting,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+
+        let tab_size = buffer.update(&mut cx, |buffer, cx| {
+            language_settings(buffer.language(), buffer.file(), cx).tab_size
+        })?;
+
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+            trigger: message.trigger.clone(),
+            options: lsp_formatting_options(tab_size.get()).into(),
+            push_to_history: false,
+        })
+    }
+
+    fn response_to_proto(
+        response: Option<Transaction>,
+        _: &mut Project,
+        _: PeerId,
+        _: &clock::Global,
+        _: &mut AppContext,
+    ) -> proto::OnTypeFormattingResponse {
+        proto::OnTypeFormattingResponse {
+            transaction: response
+                .map(|transaction| language2::proto::serialize_transaction(&transaction)),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::OnTypeFormattingResponse,
+        _: Model<Project>,
+        _: Model<Buffer>,
+        _: AsyncAppContext,
+    ) -> Result<Option<Transaction>> {
+        let Some(transaction) = message.transaction else {
+            return Ok(None);
+        };
+        Ok(Some(language2::proto::deserialize_transaction(
+            transaction,
+        )?))
+    }
+
+    fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> u64 {
+        message.buffer_id
+    }
+}
+
+impl InlayHints {
+    pub async fn lsp_to_project_hint(
+        lsp_hint: lsp2::InlayHint,
+        buffer_handle: &Model<Buffer>,
+        server_id: LanguageServerId,
+        resolve_state: ResolveState,
+        force_no_type_left_padding: bool,
+        cx: &mut AsyncAppContext,
+    ) -> anyhow::Result<InlayHint> {
+        let kind = lsp_hint.kind.and_then(|kind| match kind {
+            lsp2::InlayHintKind::TYPE => Some(InlayHintKind::Type),
+            lsp2::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
+            _ => None,
+        });
+
+        let position = buffer_handle.update(cx, |buffer, _| {
+            let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
+            if kind == Some(InlayHintKind::Parameter) {
+                buffer.anchor_before(position)
+            } else {
+                buffer.anchor_after(position)
+            }
+        })?;
+        let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id)
+            .await
+            .context("lsp to project inlay hint conversion")?;
+        let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
+            false
+        } else {
+            lsp_hint.padding_left.unwrap_or(false)
+        };
+
+        Ok(InlayHint {
+            position,
+            padding_left,
+            padding_right: lsp_hint.padding_right.unwrap_or(false),
+            label,
+            kind,
+            tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip {
+                lsp2::InlayHintTooltip::String(s) => InlayHintTooltip::String(s),
+                lsp2::InlayHintTooltip::MarkupContent(markup_content) => {
+                    InlayHintTooltip::MarkupContent(MarkupContent {
+                        kind: match markup_content.kind {
+                            lsp2::MarkupKind::PlainText => HoverBlockKind::PlainText,
+                            lsp2::MarkupKind::Markdown => HoverBlockKind::Markdown,
+                        },
+                        value: markup_content.value,
+                    })
+                }
+            }),
+            resolve_state,
+        })
+    }
+
+    async fn lsp_inlay_label_to_project(
+        lsp_label: lsp2::InlayHintLabel,
+        server_id: LanguageServerId,
+    ) -> anyhow::Result<InlayHintLabel> {
+        let label = match lsp_label {
+            lsp2::InlayHintLabel::String(s) => InlayHintLabel::String(s),
+            lsp2::InlayHintLabel::LabelParts(lsp_parts) => {
+                let mut parts = Vec::with_capacity(lsp_parts.len());
+                for lsp_part in lsp_parts {
+                    parts.push(InlayHintLabelPart {
+                        value: lsp_part.value,
+                        tooltip: lsp_part.tooltip.map(|tooltip| match tooltip {
+                            lsp2::InlayHintLabelPartTooltip::String(s) => {
+                                InlayHintLabelPartTooltip::String(s)
+                            }
+                            lsp2::InlayHintLabelPartTooltip::MarkupContent(markup_content) => {
+                                InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
+                                    kind: match markup_content.kind {
+                                        lsp2::MarkupKind::PlainText => HoverBlockKind::PlainText,
+                                        lsp2::MarkupKind::Markdown => HoverBlockKind::Markdown,
+                                    },
+                                    value: markup_content.value,
+                                })
+                            }
+                        }),
+                        location: Some(server_id).zip(lsp_part.location),
+                    });
+                }
+                InlayHintLabel::LabelParts(parts)
+            }
+        };
+
+        Ok(label)
+    }
+
+    pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint {
+        let (state, lsp_resolve_state) = match response_hint.resolve_state {
+            ResolveState::Resolved => (0, None),
+            ResolveState::CanResolve(server_id, resolve_data) => (
+                1,
+                resolve_data
+                    .map(|json_data| {
+                        serde_json::to_string(&json_data)
+                            .expect("failed to serialize resolve json data")
+                    })
+                    .map(|value| proto::resolve_state::LspResolveState {
+                        server_id: server_id.0 as u64,
+                        value,
+                    }),
+            ),
+            ResolveState::Resolving => (2, None),
+        };
+        let resolve_state = Some(proto::ResolveState {
+            state,
+            lsp_resolve_state,
+        });
+        proto::InlayHint {
+            position: Some(language2::proto::serialize_anchor(&response_hint.position)),
+            padding_left: response_hint.padding_left,
+            padding_right: response_hint.padding_right,
+            label: Some(proto::InlayHintLabel {
+                label: Some(match response_hint.label {
+                    InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
+                    InlayHintLabel::LabelParts(label_parts) => {
+                        proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
+                            parts: label_parts.into_iter().map(|label_part| {
+                                let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string());
+                                let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column });
+                                let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column });
+                                proto::InlayHintLabelPart {
+                                value: label_part.value,
+                                tooltip: label_part.tooltip.map(|tooltip| {
+                                    let proto_tooltip = match tooltip {
+                                        InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
+                                        InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
+                                            is_markdown: markup_content.kind == HoverBlockKind::Markdown,
+                                            value: markup_content.value,
+                                        }),
+                                    };
+                                    proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
+                                }),
+                                location_url,
+                                location_range_start,
+                                location_range_end,
+                                language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64),
+                            }}).collect()
+                        })
+                    }
+                }),
+            }),
+            kind: response_hint.kind.map(|kind| kind.name().to_string()),
+            tooltip: response_hint.tooltip.map(|response_tooltip| {
+                let proto_tooltip = match response_tooltip {
+                    InlayHintTooltip::String(s) => proto::inlay_hint_tooltip::Content::Value(s),
+                    InlayHintTooltip::MarkupContent(markup_content) => {
+                        proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent {
+                            is_markdown: markup_content.kind == HoverBlockKind::Markdown,
+                            value: markup_content.value,
+                        })
+                    }
+                };
+                proto::InlayHintTooltip {
+                    content: Some(proto_tooltip),
+                }
+            }),
+            resolve_state,
+        }
+    }
+
+    pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result<InlayHint> {
+        let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| {
+            panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",)
+        });
+        let resolve_state_data = resolve_state
+            .lsp_resolve_state.as_ref()
+            .map(|lsp_resolve_state| {
+                serde_json::from_str::<Option<lsp2::LSPAny>>(&lsp_resolve_state.value)
+                    .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}"))
+                    .map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state))
+            })
+            .transpose()?;
+        let resolve_state = match resolve_state.state {
+            0 => ResolveState::Resolved,
+            1 => {
+                let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| {
+                    format!(
+                        "No lsp resolve data for the hint that can be resolved: {message_hint:?}"
+                    )
+                })?;
+                ResolveState::CanResolve(server_id, lsp_resolve_state)
+            }
+            2 => ResolveState::Resolving,
+            invalid => {
+                anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}")
+            }
+        };
+        Ok(InlayHint {
+            position: message_hint
+                .position
+                .and_then(language2::proto::deserialize_anchor)
+                .context("invalid position")?,
+            label: match message_hint
+                .label
+                .and_then(|label| label.label)
+                .context("missing label")?
+            {
+                proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s),
+                proto::inlay_hint_label::Label::LabelParts(parts) => {
+                    let mut label_parts = Vec::new();
+                    for part in parts.parts {
+                        label_parts.push(InlayHintLabelPart {
+                            value: part.value,
+                            tooltip: part.tooltip.map(|tooltip| match tooltip.content {
+                                Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => {
+                                    InlayHintLabelPartTooltip::String(s)
+                                }
+                                Some(
+                                    proto::inlay_hint_label_part_tooltip::Content::MarkupContent(
+                                        markup_content,
+                                    ),
+                                ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
+                                    kind: if markup_content.is_markdown {
+                                        HoverBlockKind::Markdown
+                                    } else {
+                                        HoverBlockKind::PlainText
+                                    },
+                                    value: markup_content.value,
+                                }),
+                                None => InlayHintLabelPartTooltip::String(String::new()),
+                            }),
+                            location: {
+                                match part
+                                    .location_url
+                                    .zip(
+                                        part.location_range_start.and_then(|start| {
+                                            Some(start..part.location_range_end?)
+                                        }),
+                                    )
+                                    .zip(part.language_server_id)
+                                {
+                                    Some(((uri, range), server_id)) => Some((
+                                        LanguageServerId(server_id as usize),
+                                        lsp2::Location {
+                                            uri: lsp2::Url::parse(&uri)
+                                                .context("invalid uri in hint part {part:?}")?,
+                                            range: lsp2::Range::new(
+                                                point_to_lsp(PointUtf16::new(
+                                                    range.start.row,
+                                                    range.start.column,
+                                                )),
+                                                point_to_lsp(PointUtf16::new(
+                                                    range.end.row,
+                                                    range.end.column,
+                                                )),
+                                            ),
+                                        },
+                                    )),
+                                    None => None,
+                                }
+                            },
+                        });
+                    }
+
+                    InlayHintLabel::LabelParts(label_parts)
+                }
+            },
+            padding_left: message_hint.padding_left,
+            padding_right: message_hint.padding_right,
+            kind: message_hint
+                .kind
+                .as_deref()
+                .and_then(InlayHintKind::from_name),
+            tooltip: message_hint.tooltip.and_then(|tooltip| {
+                Some(match tooltip.content? {
+                    proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s),
+                    proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => {
+                        InlayHintTooltip::MarkupContent(MarkupContent {
+                            kind: if markup_content.is_markdown {
+                                HoverBlockKind::Markdown
+                            } else {
+                                HoverBlockKind::PlainText
+                            },
+                            value: markup_content.value,
+                        })
+                    }
+                })
+            }),
+            resolve_state,
+        })
+    }
+
+    pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp2::InlayHint {
+        lsp2::InlayHint {
+            position: point_to_lsp(hint.position.to_point_utf16(snapshot)),
+            kind: hint.kind.map(|kind| match kind {
+                InlayHintKind::Type => lsp2::InlayHintKind::TYPE,
+                InlayHintKind::Parameter => lsp2::InlayHintKind::PARAMETER,
+            }),
+            text_edits: None,
+            tooltip: hint.tooltip.and_then(|tooltip| {
+                Some(match tooltip {
+                    InlayHintTooltip::String(s) => lsp2::InlayHintTooltip::String(s),
+                    InlayHintTooltip::MarkupContent(markup_content) => {
+                        lsp2::InlayHintTooltip::MarkupContent(lsp2::MarkupContent {
+                            kind: match markup_content.kind {
+                                HoverBlockKind::PlainText => lsp2::MarkupKind::PlainText,
+                                HoverBlockKind::Markdown => lsp2::MarkupKind::Markdown,
+                                HoverBlockKind::Code { .. } => return None,
+                            },
+                            value: markup_content.value,
+                        })
+                    }
+                })
+            }),
+            label: match hint.label {
+                InlayHintLabel::String(s) => lsp2::InlayHintLabel::String(s),
+                InlayHintLabel::LabelParts(label_parts) => lsp2::InlayHintLabel::LabelParts(
+                    label_parts
+                        .into_iter()
+                        .map(|part| lsp2::InlayHintLabelPart {
+                            value: part.value,
+                            tooltip: part.tooltip.and_then(|tooltip| {
+                                Some(match tooltip {
+                                    InlayHintLabelPartTooltip::String(s) => {
+                                        lsp2::InlayHintLabelPartTooltip::String(s)
+                                    }
+                                    InlayHintLabelPartTooltip::MarkupContent(markup_content) => {
+                                        lsp2::InlayHintLabelPartTooltip::MarkupContent(
+                                            lsp2::MarkupContent {
+                                                kind: match markup_content.kind {
+                                                    HoverBlockKind::PlainText => {
+                                                        lsp2::MarkupKind::PlainText
+                                                    }
+                                                    HoverBlockKind::Markdown => {
+                                                        lsp2::MarkupKind::Markdown
+                                                    }
+                                                    HoverBlockKind::Code { .. } => return None,
+                                                },
+                                                value: markup_content.value,
+                                            },
+                                        )
+                                    }
+                                })
+                            }),
+                            location: part.location.map(|(_, location)| location),
+                            command: None,
+                        })
+                        .collect(),
+                ),
+            },
+            padding_left: Some(hint.padding_left),
+            padding_right: Some(hint.padding_right),
+            data: match hint.resolve_state {
+                ResolveState::CanResolve(_, data) => data,
+                ResolveState::Resolving | ResolveState::Resolved => None,
+            },
+        }
+    }
+
+    pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool {
+        capabilities
+            .inlay_hint_provider
+            .as_ref()
+            .and_then(|options| match options {
+                OneOf::Left(_is_supported) => None,
+                OneOf::Right(capabilities) => match capabilities {
+                    lsp2::InlayHintServerCapabilities::Options(o) => o.resolve_provider,
+                    lsp2::InlayHintServerCapabilities::RegistrationOptions(o) => {
+                        o.inlay_hint_options.resolve_provider
+                    }
+                },
+            })
+            .unwrap_or(false)
+    }
+}
+
+#[async_trait]
+impl LspCommand for InlayHints {
+    type Response = Vec<InlayHint>;
+    type LspRequest = lsp2::InlayHintRequest;
+    type ProtoRequest = proto::InlayHints;
+
+    fn check_capabilities(&self, server_capabilities: &lsp2::ServerCapabilities) -> bool {
+        let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else {
+            return false;
+        };
+        match inlay_hint_provider {
+            lsp2::OneOf::Left(enabled) => *enabled,
+            lsp2::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities {
+                lsp2::InlayHintServerCapabilities::Options(_) => true,
+                lsp2::InlayHintServerCapabilities::RegistrationOptions(_) => false,
+            },
+        }
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        buffer: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp2::InlayHintParams {
+        lsp2::InlayHintParams {
+            text_document: lsp2::TextDocumentIdentifier {
+                uri: lsp2::Url::from_file_path(path).unwrap(),
+            },
+            range: range_to_lsp(self.range.to_point_utf16(buffer)),
+            work_done_progress_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<Vec<lsp2::InlayHint>>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        server_id: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> anyhow::Result<Vec<InlayHint>> {
+        let (lsp_adapter, lsp_server) =
+            language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+        // `typescript-language-server` adds padding to the left for type hints, turning
+        // `const foo: boolean` into `const foo : boolean` which looks odd.
+        // `rust-analyzer` does not have the padding for this case, and we have to accomodate both.
+        //
+        // We could trim the whole string, but being pessimistic on par with the situation above,
+        // there might be a hint with multiple whitespaces at the end(s) which we need to display properly.
+        // Hence let's use a heuristic first to handle the most awkward case and look for more.
+        let force_no_type_left_padding =
+            lsp_adapter.name.0.as_ref() == "typescript-language-server";
+
+        let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| {
+            let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) {
+                ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone())
+            } else {
+                ResolveState::Resolved
+            };
+
+            let buffer = buffer.clone();
+            cx.spawn(move |mut cx| async move {
+                InlayHints::lsp_to_project_hint(
+                    lsp_hint,
+                    &buffer,
+                    server_id,
+                    resolve_state,
+                    force_no_type_left_padding,
+                    &mut cx,
+                )
+                .await
+            })
+        });
+        future::join_all(hints)
+            .await
+            .into_iter()
+            .collect::<anyhow::Result<_>>()
+            .context("lsp to project inlay hints conversion")
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
+        proto::InlayHints {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            start: Some(language2::proto::serialize_anchor(&self.range.start)),
+            end: Some(language2::proto::serialize_anchor(&self.range.end)),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::InlayHints,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let start = message
+            .start
+            .and_then(language2::proto::deserialize_anchor)
+            .context("invalid start")?;
+        let end = message
+            .end
+            .and_then(language2::proto::deserialize_anchor)
+            .context("invalid end")?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+
+        Ok(Self { range: start..end })
+    }
+
+    fn response_to_proto(
+        response: Vec<InlayHint>,
+        _: &mut Project,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        _: &mut AppContext,
+    ) -> proto::InlayHintsResponse {
+        proto::InlayHintsResponse {
+            hints: response
+                .into_iter()
+                .map(|response_hint| InlayHints::project_to_proto_hint(response_hint))
+                .collect(),
+            version: serialize_version(buffer_version),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::InlayHintsResponse,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> anyhow::Result<Vec<InlayHint>> {
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+
+        let mut hints = Vec::new();
+        for message_hint in message.hints {
+            hints.push(InlayHints::proto_to_project_hint(message_hint)?);
+        }
+
+        Ok(hints)
+    }
+
+    fn buffer_id_from_proto(message: &proto::InlayHints) -> u64 {
+        message.buffer_id
+    }
+}

crates/project2/src/project2.rs 🔗

@@ -0,0 +1,8975 @@
+mod ignore;
+mod lsp_command;
+pub mod project_settings;
+pub mod search;
+pub mod terminals;
+pub mod worktree;
+
+#[cfg(test)]
+mod project_tests;
+#[cfg(test)]
+mod worktree_tests;
+
+use anyhow::{anyhow, Context as _, Result};
+use client2::{proto, Client, Collaborator, TypedEnvelope, UserStore};
+use clock::ReplicaId;
+use collections::{hash_map, BTreeMap, HashMap, HashSet};
+use copilot2::Copilot;
+use futures::{
+    channel::{
+        mpsc::{self, UnboundedReceiver},
+        oneshot,
+    },
+    future::{self, try_join_all, Shared},
+    stream::FuturesUnordered,
+    AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
+};
+use globset::{Glob, GlobSet, GlobSetBuilder};
+use gpui2::{
+    AnyModel, AppContext, AsyncAppContext, Context, Entity, EventEmitter, Executor, Model,
+    ModelContext, Task, WeakModel,
+};
+use itertools::Itertools;
+use language2::{
+    language_settings::{
+        language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
+    },
+    point_to_lsp,
+    proto::{
+        deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
+        serialize_anchor, serialize_version, split_operations,
+    },
+    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::{
+    DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
+    DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
+};
+use lsp_command::*;
+use node_runtime::NodeRuntime;
+use parking_lot::Mutex;
+use postage::watch;
+use prettier2::{LocateStart, Prettier};
+use project_settings::{LspSettings, ProjectSettings};
+use rand::prelude::*;
+use search::SearchQuery;
+use serde::Serialize;
+use settings2::{Settings, SettingsStore};
+use sha2::{Digest, Sha256};
+use similar::{ChangeTag, TextDiff};
+use smol::channel::{Receiver, Sender};
+use std::{
+    cmp::{self, Ordering},
+    convert::TryInto,
+    hash::Hash,
+    mem,
+    num::NonZeroU32,
+    ops::Range,
+    path::{self, Component, Path, PathBuf},
+    process::Stdio,
+    str,
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+    time::{Duration, Instant},
+};
+use terminals::Terminals;
+use text::Anchor;
+use util::{
+    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>;
+}
+
+// Language server state is stored across 3 collections:
+//     language_servers =>
+//         a mapping from unique server id to LanguageServerState which can either be a task for a
+//         server in the process of starting, or a running server with adapter and language server arcs
+//     language_server_ids => a mapping from worktreeId and server name to the unique server id
+//     language_server_statuses => a mapping from unique server id to the current server status
+//
+// Multiple worktrees can map to the same language server for example when you jump to the definition
+// of a file in the standard library. So language_server_ids is used to look up which server is active
+// for a given worktree and language server name
+//
+// When starting a language server, first the id map is checked to make sure a server isn't already available
+// for that worktree. If there is one, it finishes early. Otherwise, a new id is allocated and and
+// the Starting variant of LanguageServerState is stored in the language_servers map.
+pub struct Project {
+    worktrees: Vec<WorktreeHandle>,
+    active_entry: Option<ProjectEntryId>,
+    buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>,
+    languages: Arc<LanguageRegistry>,
+    supplementary_language_servers:
+        HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
+    language_servers: HashMap<LanguageServerId, LanguageServerState>,
+    language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
+    language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
+    last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
+    client: Arc<client2::Client>,
+    next_entry_id: Arc<AtomicUsize>,
+    join_project_response_message_id: u32,
+    next_diagnostic_group_id: usize,
+    user_store: Model<UserStore>,
+    fs: Arc<dyn Fs>,
+    client_state: Option<ProjectClientState>,
+    collaborators: HashMap<proto::PeerId, Collaborator>,
+    client_subscriptions: Vec<client2::Subscription>,
+    _subscriptions: Vec<gpui2::Subscription>,
+    next_buffer_id: u64,
+    opened_buffer: (watch::Sender<()>, watch::Receiver<()>),
+    shared_buffers: HashMap<proto::PeerId, HashSet<u64>>,
+    #[allow(clippy::type_complexity)]
+    loading_buffers_by_path: HashMap<
+        ProjectPath,
+        postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
+    >,
+    #[allow(clippy::type_complexity)]
+    loading_local_worktrees:
+        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<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<WeakModel<Buffer>>,
+    git_diff_debouncer: DelayedDebounced,
+    nonce: u128,
+    _maintain_buffer_languages: Task<()>,
+    _maintain_workspace_config: Task<Result<()>>,
+    terminals: Terminals,
+    copilot_lsp_subscription: Option<gpui2::Subscription>,
+    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<()>>,
+}
+
+enum LanguageServerToQuery {
+    Primary,
+    Other(LanguageServerId),
+}
+
+impl DelayedDebounced {
+    fn new() -> DelayedDebounced {
+        DelayedDebounced {
+            task: None,
+            cancel_channel: None,
+        }
+    }
+
+    fn fire_new<F>(&mut self, delay: Duration, cx: &mut ModelContext<Project>, func: F)
+    where
+        F: 'static + Send + FnOnce(&mut Project, &mut ModelContext<Project>) -> Task<()>,
+    {
+        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(move |project, mut cx| async move {
+            let mut timer = cx.executor().timer(delay).fuse();
+            if let Some(previous_task) = previous_task {
+                previous_task.await;
+            }
+
+            futures::select_biased! {
+                _ = receiver => return,
+                    _ = timer => {}
+            }
+
+            if let Ok(task) = project.update(&mut cx, |project, cx| (func)(project, cx)) {
+                task.await;
+            }
+        }));
+    }
+}
+
+struct LspBufferSnapshot {
+    version: i32,
+    snapshot: TextBufferSnapshot,
+}
+
+/// Message ordered with respect to buffer operations
+enum BufferOrderedMessage {
+    Operation {
+        buffer_id: u64,
+        operation: proto::Operation,
+    },
+    LanguageServerUpdate {
+        language_server_id: LanguageServerId,
+        message: proto::update_language_server::Variant,
+    },
+    Resync,
+}
+
+enum LocalProjectUpdate {
+    WorktreesChanged,
+    CreateBufferForPeer {
+        peer_id: proto::PeerId,
+        buffer_id: u64,
+    },
+}
+
+enum OpenBuffer {
+    Strong(Model<Buffer>),
+    Weak(WeakModel<Buffer>),
+    Operations(Vec<Operation>),
+}
+
+#[derive(Clone)]
+enum WorktreeHandle {
+    Strong(Model<Worktree>),
+    Weak(WeakModel<Worktree>),
+}
+
+enum ProjectClientState {
+    Local {
+        remote_id: u64,
+        updates_tx: mpsc::UnboundedSender<LocalProjectUpdate>,
+        _send_updates: Task<Result<()>>,
+    },
+    Remote {
+        sharing_has_stopped: bool,
+        remote_id: u64,
+        replica_id: ReplicaId,
+    },
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Event {
+    LanguageServerAdded(LanguageServerId),
+    LanguageServerRemoved(LanguageServerId),
+    LanguageServerLog(LanguageServerId, String),
+    Notification(String),
+    ActiveEntryChanged(Option<ProjectEntryId>),
+    ActivateProjectPanel,
+    WorktreeAdded,
+    WorktreeRemoved(WorktreeId),
+    WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
+    DiskBasedDiagnosticsStarted {
+        language_server_id: LanguageServerId,
+    },
+    DiskBasedDiagnosticsFinished {
+        language_server_id: LanguageServerId,
+    },
+    DiagnosticsUpdated {
+        path: ProjectPath,
+        language_server_id: LanguageServerId,
+    },
+    RemoteIdChanged(Option<u64>),
+    DisconnectedFromHost,
+    Closed,
+    DeletedEntry(ProjectEntryId),
+    CollaboratorUpdated {
+        old_peer_id: proto::PeerId,
+        new_peer_id: proto::PeerId,
+    },
+    CollaboratorJoined(proto::PeerId),
+    CollaboratorLeft(proto::PeerId),
+    RefreshInlayHints,
+}
+
+pub enum LanguageServerState {
+    Starting(Task<Option<Arc<LanguageServer>>>),
+
+    Running {
+        language: Arc<Language>,
+        adapter: Arc<CachedLspAdapter>,
+        server: Arc<LanguageServer>,
+        watched_paths: HashMap<WorktreeId, GlobSet>,
+        simulate_disk_based_diagnostics_completion: Option<Task<()>>,
+    },
+}
+
+#[derive(Serialize)]
+pub struct LanguageServerStatus {
+    pub name: String,
+    pub pending_work: BTreeMap<String, LanguageServerProgress>,
+    pub has_pending_diagnostic_updates: bool,
+    progress_tokens: HashSet<String>,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct LanguageServerProgress {
+    pub message: Option<String>,
+    pub percentage: Option<usize>,
+    #[serde(skip_serializing)]
+    pub last_update_at: Instant,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
+pub struct ProjectPath {
+    pub worktree_id: WorktreeId,
+    pub path: Arc<Path>,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)]
+pub struct DiagnosticSummary {
+    pub error_count: usize,
+    pub warning_count: usize,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Location {
+    pub buffer: Model<Buffer>,
+    pub range: Range<language2::Anchor>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InlayHint {
+    pub position: language2::Anchor,
+    pub label: InlayHintLabel,
+    pub kind: Option<InlayHintKind>,
+    pub padding_left: bool,
+    pub padding_right: bool,
+    pub tooltip: Option<InlayHintTooltip>,
+    pub resolve_state: ResolveState,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ResolveState {
+    Resolved,
+    CanResolve(LanguageServerId, Option<lsp2::LSPAny>),
+    Resolving,
+}
+
+impl InlayHint {
+    pub fn text(&self) -> String {
+        match &self.label {
+            InlayHintLabel::String(s) => s.to_owned(),
+            InlayHintLabel::LabelParts(parts) => parts.iter().map(|part| &part.value).join(""),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum InlayHintLabel {
+    String(String),
+    LabelParts(Vec<InlayHintLabelPart>),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InlayHintLabelPart {
+    pub value: String,
+    pub tooltip: Option<InlayHintLabelPartTooltip>,
+    pub location: Option<(LanguageServerId, lsp2::Location)>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum InlayHintTooltip {
+    String(String),
+    MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum InlayHintLabelPartTooltip {
+    String(String),
+    MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MarkupContent {
+    pub kind: HoverBlockKind,
+    pub value: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct LocationLink {
+    pub origin: Option<Location>,
+    pub target: Location,
+}
+
+#[derive(Debug)]
+pub struct DocumentHighlight {
+    pub range: Range<language2::Anchor>,
+    pub kind: DocumentHighlightKind,
+}
+
+#[derive(Clone, Debug)]
+pub struct Symbol {
+    pub language_server_name: LanguageServerName,
+    pub source_worktree_id: WorktreeId,
+    pub path: ProjectPath,
+    pub label: CodeLabel,
+    pub name: String,
+    pub kind: lsp2::SymbolKind,
+    pub range: Range<Unclipped<PointUtf16>>,
+    pub signature: [u8; 32],
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct HoverBlock {
+    pub text: String,
+    pub kind: HoverBlockKind,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum HoverBlockKind {
+    PlainText,
+    Markdown,
+    Code { language: String },
+}
+
+#[derive(Debug)]
+pub struct Hover {
+    pub contents: Vec<HoverBlock>,
+    pub range: Option<Range<language2::Anchor>>,
+    pub language: Option<Arc<Language>>,
+}
+
+impl Hover {
+    pub fn is_empty(&self) -> bool {
+        self.contents.iter().all(|block| block.text.is_empty())
+    }
+}
+
+#[derive(Default)]
+pub struct ProjectTransaction(pub HashMap<Model<Buffer>, language2::Transaction>);
+
+impl DiagnosticSummary {
+    fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> Self {
+        let mut this = Self {
+            error_count: 0,
+            warning_count: 0,
+        };
+
+        for entry in diagnostics {
+            if entry.diagnostic.is_primary {
+                match entry.diagnostic.severity {
+                    DiagnosticSeverity::ERROR => this.error_count += 1,
+                    DiagnosticSeverity::WARNING => this.warning_count += 1,
+                    _ => {}
+                }
+            }
+        }
+
+        this
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.error_count == 0 && self.warning_count == 0
+    }
+
+    pub fn to_proto(
+        &self,
+        language_server_id: LanguageServerId,
+        path: &Path,
+    ) -> proto::DiagnosticSummary {
+        proto::DiagnosticSummary {
+            path: path.to_string_lossy().to_string(),
+            language_server_id: language_server_id.0 as u64,
+            error_count: self.error_count as u32,
+            warning_count: self.warning_count as u32,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ProjectEntryId(usize);
+
+impl ProjectEntryId {
+    pub const MAX: Self = Self(usize::MAX);
+
+    pub fn new(counter: &AtomicUsize) -> Self {
+        Self(counter.fetch_add(1, SeqCst))
+    }
+
+    pub fn from_proto(id: u64) -> Self {
+        Self(id as usize)
+    }
+
+    pub fn to_proto(&self) -> u64 {
+        self.0 as u64
+    }
+
+    pub fn to_usize(&self) -> usize {
+        self.0
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FormatTrigger {
+    Save,
+    Manual,
+}
+
+struct ProjectLspAdapterDelegate {
+    project: Model<Project>,
+    http_client: Arc<dyn HttpClient>,
+}
+
+impl FormatTrigger {
+    fn from_proto(value: i32) -> FormatTrigger {
+        match value {
+            0 => FormatTrigger::Save,
+            1 => FormatTrigger::Manual,
+            _ => FormatTrigger::Save,
+        }
+    }
+}
+#[derive(Clone, Debug, PartialEq)]
+enum SearchMatchCandidate {
+    OpenBuffer {
+        buffer: Model<Buffer>,
+        // This might be an unnamed file without representation on filesystem
+        path: Option<Arc<Path>>,
+    },
+    Path {
+        worktree_id: WorktreeId,
+        path: Arc<Path>,
+    },
+}
+
+type SearchMatchCandidateIndex = usize;
+impl SearchMatchCandidate {
+    fn path(&self) -> Option<Arc<Path>> {
+        match self {
+            SearchMatchCandidate::OpenBuffer { path, .. } => path.clone(),
+            SearchMatchCandidate::Path { path, .. } => Some(path.clone()),
+        }
+    }
+}
+
+impl Project {
+    pub fn init_settings(cx: &mut AppContext) {
+        ProjectSettings::register(cx);
+    }
+
+    pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
+        Self::init_settings(cx);
+
+        client.add_model_message_handler(Self::handle_add_collaborator);
+        client.add_model_message_handler(Self::handle_update_project_collaborator);
+        client.add_model_message_handler(Self::handle_remove_collaborator);
+        client.add_model_message_handler(Self::handle_buffer_reloaded);
+        client.add_model_message_handler(Self::handle_buffer_saved);
+        client.add_model_message_handler(Self::handle_start_language_server);
+        client.add_model_message_handler(Self::handle_update_language_server);
+        client.add_model_message_handler(Self::handle_update_project);
+        client.add_model_message_handler(Self::handle_unshare_project);
+        client.add_model_message_handler(Self::handle_create_buffer_for_peer);
+        client.add_model_message_handler(Self::handle_update_buffer_file);
+        client.add_model_request_handler(Self::handle_update_buffer);
+        client.add_model_message_handler(Self::handle_update_diagnostic_summary);
+        client.add_model_message_handler(Self::handle_update_worktree);
+        client.add_model_message_handler(Self::handle_update_worktree_settings);
+        client.add_model_request_handler(Self::handle_create_project_entry);
+        client.add_model_request_handler(Self::handle_rename_project_entry);
+        client.add_model_request_handler(Self::handle_copy_project_entry);
+        client.add_model_request_handler(Self::handle_delete_project_entry);
+        client.add_model_request_handler(Self::handle_expand_project_entry);
+        client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
+        client.add_model_request_handler(Self::handle_apply_code_action);
+        client.add_model_request_handler(Self::handle_on_type_formatting);
+        client.add_model_request_handler(Self::handle_inlay_hints);
+        client.add_model_request_handler(Self::handle_resolve_inlay_hint);
+        client.add_model_request_handler(Self::handle_refresh_inlay_hints);
+        client.add_model_request_handler(Self::handle_reload_buffers);
+        client.add_model_request_handler(Self::handle_synchronize_buffers);
+        client.add_model_request_handler(Self::handle_format_buffers);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetCodeActions>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetCompletions>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetHover>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetTypeDefinition>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
+        client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
+        client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
+        client.add_model_request_handler(Self::handle_search_project);
+        client.add_model_request_handler(Self::handle_get_project_symbols);
+        client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
+        client.add_model_request_handler(Self::handle_open_buffer_by_id);
+        client.add_model_request_handler(Self::handle_open_buffer_by_path);
+        client.add_model_request_handler(Self::handle_save_buffer);
+        client.add_model_message_handler(Self::handle_update_diff_base);
+    }
+
+    pub fn local(
+        client: Arc<Client>,
+        node: Arc<dyn NodeRuntime>,
+        user_store: Model<UserStore>,
+        languages: Arc<LanguageRegistry>,
+        fs: Arc<dyn Fs>,
+        cx: &mut AppContext,
+    ) -> 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();
+            let copilot_lsp_subscription =
+                Copilot::global(cx).map(|copilot| subscribe_for_copilot_events(&copilot, cx));
+            Self {
+                worktrees: Default::default(),
+                buffer_ordered_messages_tx: tx,
+                collaborators: Default::default(),
+                next_buffer_id: 0,
+                opened_buffers: Default::default(),
+                shared_buffers: Default::default(),
+                incomplete_remote_buffers: Default::default(),
+                loading_buffers_by_path: Default::default(),
+                loading_local_worktrees: Default::default(),
+                local_buffer_ids_by_path: Default::default(),
+                local_buffer_ids_by_entry_id: Default::default(),
+                buffer_snapshots: Default::default(),
+                join_project_response_message_id: 0,
+                client_state: None,
+                opened_buffer: watch::channel(),
+                client_subscriptions: Vec::new(),
+                _subscriptions: vec![
+                    cx.observe_global::<SettingsStore>(Self::on_settings_changed),
+                    cx.on_release(Self::release),
+                    cx.on_app_quit(Self::shutdown_language_servers),
+                ],
+                _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
+                _maintain_workspace_config: Self::maintain_workspace_config(cx),
+                active_entry: None,
+                languages,
+                client,
+                user_store,
+                fs,
+                next_entry_id: Default::default(),
+                next_diagnostic_group_id: Default::default(),
+                supplementary_language_servers: HashMap::default(),
+                language_servers: Default::default(),
+                language_server_ids: Default::default(),
+                language_server_statuses: Default::default(),
+                last_workspace_edits_by_language_server: Default::default(),
+                buffers_being_formatted: Default::default(),
+                buffers_needing_diff: Default::default(),
+                git_diff_debouncer: DelayedDebounced::new(),
+                nonce: StdRng::from_entropy().gen(),
+                terminals: Terminals {
+                    local_handles: Vec::new(),
+                },
+                copilot_lsp_subscription,
+                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(),
+            }
+        })
+    }
+
+    pub async fn remote(
+        remote_id: u64,
+        client: Arc<Client>,
+        user_store: Model<UserStore>,
+        languages: Arc<LanguageRegistry>,
+        fs: Arc<dyn Fs>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Model<Self>> {
+        client.authenticate_and_connect(true, &cx).await?;
+
+        let subscription = client.subscribe_to_entity(remote_id)?;
+        let response = client
+            .request_envelope(proto::JoinProject {
+                project_id: remote_id,
+            })
+            .await?;
+        let this = cx.build_model(|cx| {
+            let replica_id = response.payload.replica_id as ReplicaId;
+
+            let mut worktrees = Vec::new();
+            for worktree in response.payload.worktrees {
+                let worktree =
+                    Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx);
+                worktrees.push(worktree);
+            }
+
+            let (tx, rx) = mpsc::unbounded();
+            cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
+                .detach();
+            let copilot_lsp_subscription =
+                Copilot::global(cx).map(|copilot| subscribe_for_copilot_events(&copilot, cx));
+            let mut this = Self {
+                worktrees: Vec::new(),
+                buffer_ordered_messages_tx: tx,
+                loading_buffers_by_path: Default::default(),
+                next_buffer_id: 0,
+                opened_buffer: watch::channel(),
+                shared_buffers: Default::default(),
+                incomplete_remote_buffers: Default::default(),
+                loading_local_worktrees: Default::default(),
+                local_buffer_ids_by_path: Default::default(),
+                local_buffer_ids_by_entry_id: Default::default(),
+                active_entry: None,
+                collaborators: Default::default(),
+                join_project_response_message_id: response.message_id,
+                _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
+                _maintain_workspace_config: Self::maintain_workspace_config(cx),
+                languages,
+                user_store: user_store.clone(),
+                fs,
+                next_entry_id: Default::default(),
+                next_diagnostic_group_id: Default::default(),
+                client_subscriptions: Default::default(),
+                _subscriptions: vec![
+                    cx.on_release(Self::release),
+                    cx.on_app_quit(Self::shutdown_language_servers),
+                ],
+                client: client.clone(),
+                client_state: Some(ProjectClientState::Remote {
+                    sharing_has_stopped: false,
+                    remote_id,
+                    replica_id,
+                }),
+                supplementary_language_servers: HashMap::default(),
+                language_servers: Default::default(),
+                language_server_ids: Default::default(),
+                language_server_statuses: response
+                    .payload
+                    .language_servers
+                    .into_iter()
+                    .map(|server| {
+                        (
+                            LanguageServerId(server.id as usize),
+                            LanguageServerStatus {
+                                name: server.name,
+                                pending_work: Default::default(),
+                                has_pending_diagnostic_updates: false,
+                                progress_tokens: Default::default(),
+                            },
+                        )
+                    })
+                    .collect(),
+                last_workspace_edits_by_language_server: Default::default(),
+                opened_buffers: Default::default(),
+                buffers_being_formatted: Default::default(),
+                buffers_needing_diff: Default::default(),
+                git_diff_debouncer: DelayedDebounced::new(),
+                buffer_snapshots: Default::default(),
+                nonce: StdRng::from_entropy().gen(),
+                terminals: Terminals {
+                    local_handles: Vec::new(),
+                },
+                copilot_lsp_subscription,
+                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 {
+                let _ = this.add_worktree(&worktree, cx);
+            }
+            this
+        })?;
+        let subscription = subscription.set_model(&this, &mut cx);
+
+        let user_ids = response
+            .payload
+            .collaborators
+            .iter()
+            .map(|peer| peer.user_id)
+            .collect();
+        user_store
+            .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))?
+            .await?;
+
+        this.update(&mut cx, |this, cx| {
+            this.set_collaborators_from_proto(response.payload.collaborators, cx)?;
+            this.client_subscriptions.push(subscription);
+            anyhow::Ok(())
+        })??;
+
+        Ok(this)
+    }
+
+    fn release(&mut self, cx: &mut AppContext) {
+        match &self.client_state {
+            Some(ProjectClientState::Local { .. }) => {
+                let _ = self.unshare_internal(cx);
+            }
+            Some(ProjectClientState::Remote { remote_id, .. }) => {
+                let _ = self.client.send(proto::LeaveProject {
+                    project_id: *remote_id,
+                });
+                self.disconnected_from_host_internal(cx);
+            }
+            _ => {}
+        }
+    }
+
+    fn shutdown_language_servers(
+        &mut self,
+        _cx: &mut ModelContext<Self>,
+    ) -> impl Future<Output = ()> {
+        let shutdown_futures = self
+            .language_servers
+            .drain()
+            .map(|(_, server_state)| async {
+                use LanguageServerState::*;
+                match server_state {
+                    Running { server, .. } => server.shutdown()?.await,
+                    Starting(task) => task.await?.shutdown()?.await,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        async move {
+            futures::future::join_all(shutdown_futures).await;
+        }
+    }
+
+    // #[cfg(any(test, feature = "test-support"))]
+    // pub async fn test(
+    //     fs: Arc<dyn Fs>,
+    //     root_paths: impl IntoIterator<Item = &Path>,
+    //     cx: &mut gpui::TestAppContext,
+    // ) -> Handle<Project> {
+    //     let mut languages = LanguageRegistry::test();
+    //     languages.set_executor(cx.background());
+    //     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.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+    //     let project = cx.update(|cx| {
+    //         Project::local(
+    //             client,
+    //             node_runtime::FakeNodeRuntime::new(),
+    //             user_store,
+    //             Arc::new(languages),
+    //             fs,
+    //             cx,
+    //         )
+    //     });
+    //     for path in root_paths {
+    //         let (tree, _) = project
+    //             .update(cx, |project, cx| {
+    //                 project.find_or_create_local_worktree(path, true, cx)
+    //             })
+    //             .await
+    //             .unwrap();
+    //         tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
+    //             .await;
+    //     }
+    //     project
+    // }
+
+    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();
+        for buffer in self.opened_buffers.values() {
+            if let Some(buffer) = buffer.upgrade() {
+                let buffer = buffer.read(cx);
+                let buffer_file = File::from_dyn(buffer.file());
+                let buffer_language = buffer.language();
+                let settings = language_settings(buffer_language, buffer.file(), cx);
+                if let Some(language) = buffer_language {
+                    if settings.enable_language_server {
+                        if let Some(file) = buffer_file {
+                            language_servers_to_start
+                                .push((file.worktree.clone(), Arc::clone(language)));
+                        }
+                    }
+                    language_formatters_to_check.push((
+                        buffer_file.map(|f| f.worktree_id(cx)),
+                        Arc::clone(language),
+                        settings.clone(),
+                    ));
+                }
+            }
+        }
+
+        let mut language_servers_to_stop = Vec::new();
+        let mut language_servers_to_restart = Vec::new();
+        let languages = self.languages.to_vec();
+
+        let new_lsp_settings = ProjectSettings::get_global(cx).lsp.clone();
+        let current_lsp_settings = &self.current_lsp_settings;
+        for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
+            let language = languages.iter().find_map(|l| {
+                let adapter = l
+                    .lsp_adapters()
+                    .iter()
+                    .find(|adapter| &adapter.name == started_lsp_name)?;
+                Some((l, adapter))
+            });
+            if let Some((language, adapter)) = language {
+                let worktree = self.worktree_for_id(*worktree_id, cx);
+                let file = worktree.as_ref().and_then(|tree| {
+                    tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
+                });
+                if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
+                    language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
+                } else if let Some(worktree) = worktree {
+                    let server_name = &adapter.name.0;
+                    match (
+                        current_lsp_settings.get(server_name),
+                        new_lsp_settings.get(server_name),
+                    ) {
+                        (None, None) => {}
+                        (Some(_), None) | (None, Some(_)) => {
+                            language_servers_to_restart.push((worktree, Arc::clone(language)));
+                        }
+                        (Some(current_lsp_settings), Some(new_lsp_settings)) => {
+                            if current_lsp_settings != new_lsp_settings {
+                                language_servers_to_restart.push((worktree, Arc::clone(language)));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        self.current_lsp_settings = new_lsp_settings;
+
+        // Stop all newly-disabled language servers.
+        for (worktree_id, adapter_name) in language_servers_to_stop {
+            self.stop_language_server(worktree_id, adapter_name, cx)
+                .detach();
+        }
+
+        for (worktree, language, settings) in language_formatters_to_check {
+            self.install_default_formatters(worktree, &language, &settings, cx)
+                .detach_and_log_err(cx);
+        }
+
+        // Start all the newly-enabled language servers.
+        for (worktree, language) in language_servers_to_start {
+            let worktree_path = worktree.read(cx).abs_path();
+            self.start_language_servers(&worktree, worktree_path, language, cx);
+        }
+
+        // Restart all language servers with changed initialization options.
+        for (worktree, language) in language_servers_to_restart {
+            self.restart_language_servers(worktree, language, cx);
+        }
+
+        if self.copilot_lsp_subscription.is_none() {
+            if let Some(copilot) = Copilot::global(cx) {
+                for buffer in self.opened_buffers.values() {
+                    if let Some(buffer) = buffer.upgrade() {
+                        self.register_buffer_with_copilot(&buffer, cx);
+                    }
+                }
+                self.copilot_lsp_subscription = Some(subscribe_for_copilot_events(&copilot, cx));
+            }
+        }
+
+        cx.notify();
+    }
+
+    pub fn buffer_for_id(&self, remote_id: u64) -> Option<Model<Buffer>> {
+        self.opened_buffers
+            .get(&remote_id)
+            .and_then(|buffer| buffer.upgrade())
+    }
+
+    pub fn languages(&self) -> &Arc<LanguageRegistry> {
+        &self.languages
+    }
+
+    pub fn client(&self) -> Arc<Client> {
+        self.client.clone()
+    }
+
+    pub fn user_store(&self) -> Model<UserStore> {
+        self.user_store.clone()
+    }
+
+    pub fn opened_buffers(&self) -> Vec<Model<Buffer>> {
+        self.opened_buffers
+            .values()
+            .filter_map(|b| b.upgrade())
+            .collect()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn has_open_buffer(&self, path: impl Into<ProjectPath>, cx: &AppContext) -> bool {
+        let path = path.into();
+        if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) {
+            self.opened_buffers.iter().any(|(_, buffer)| {
+                if let Some(buffer) = buffer.upgrade() {
+                    if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
+                        if file.worktree == worktree && file.path() == &path.path {
+                            return true;
+                        }
+                    }
+                }
+                false
+            })
+        } else {
+            false
+        }
+    }
+
+    pub fn fs(&self) -> &Arc<dyn Fs> {
+        &self.fs
+    }
+
+    pub fn remote_id(&self) -> Option<u64> {
+        match self.client_state.as_ref()? {
+            ProjectClientState::Local { remote_id, .. }
+            | ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
+        }
+    }
+
+    pub fn replica_id(&self) -> ReplicaId {
+        match &self.client_state {
+            Some(ProjectClientState::Remote { replica_id, .. }) => *replica_id,
+            _ => 0,
+        }
+    }
+
+    fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {
+        if let Some(ProjectClientState::Local { updates_tx, .. }) = &mut self.client_state {
+            updates_tx
+                .unbounded_send(LocalProjectUpdate::WorktreesChanged)
+                .ok();
+        }
+        cx.notify();
+    }
+
+    pub fn collaborators(&self) -> &HashMap<proto::PeerId, Collaborator> {
+        &self.collaborators
+    }
+
+    pub fn host(&self) -> Option<&Collaborator> {
+        self.collaborators.values().find(|c| c.replica_id == 0)
+    }
+
+    /// Collect all worktrees, including ones that don't appear in the project panel
+    pub fn worktrees<'a>(&'a self) -> impl 'a + DoubleEndedIterator<Item = Model<Worktree>> {
+        self.worktrees
+            .iter()
+            .filter_map(move |worktree| worktree.upgrade())
+    }
+
+    /// Collect all user-visible worktrees, the ones that appear in the project panel
+    pub fn visible_worktrees<'a>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl 'a + DoubleEndedIterator<Item = Model<Worktree>> {
+        self.worktrees.iter().filter_map(|worktree| {
+            worktree.upgrade().and_then(|worktree| {
+                if worktree.read(cx).is_visible() {
+                    Some(worktree)
+                } else {
+                    None
+                }
+            })
+        })
+    }
+
+    pub fn worktree_root_names<'a>(&'a self, cx: &'a AppContext) -> impl Iterator<Item = &'a str> {
+        self.visible_worktrees(cx)
+            .map(|tree| tree.read(cx).root_name())
+    }
+
+    pub fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Model<Worktree>> {
+        self.worktrees()
+            .find(|worktree| worktree.read(cx).id() == id)
+    }
+
+    pub fn worktree_for_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        cx: &AppContext,
+    ) -> Option<Model<Worktree>> {
+        self.worktrees()
+            .find(|worktree| worktree.read(cx).contains_entry(entry_id))
+    }
+
+    pub fn worktree_id_for_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        cx: &AppContext,
+    ) -> Option<WorktreeId> {
+        self.worktree_for_entry(entry_id, cx)
+            .map(|worktree| worktree.read(cx).id())
+    }
+
+    pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
+        paths.iter().all(|path| self.contains_path(path, cx))
+    }
+
+    pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
+        for worktree in self.worktrees() {
+            let worktree = worktree.read(cx).as_local();
+            if worktree.map_or(false, |w| w.contains_abs_path(path)) {
+                return true;
+            }
+        }
+        false
+    }
+
+    pub fn create_entry(
+        &mut self,
+        project_path: impl Into<ProjectPath>,
+        is_directory: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<Entry>>> {
+        let project_path = project_path.into();
+        let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+        if self.is_local() {
+            Some(worktree.update(cx, |worktree, cx| {
+                worktree
+                    .as_local_mut()
+                    .unwrap()
+                    .create_entry(project_path.path, is_directory, cx)
+            }))
+        } else {
+            let client = self.client.clone();
+            let project_id = self.remote_id().unwrap();
+            Some(cx.spawn(move |_, mut cx| async move {
+                let response = client
+                    .request(proto::CreateProjectEntry {
+                        worktree_id: project_path.worktree_id.to_proto(),
+                        project_id,
+                        path: project_path.path.to_string_lossy().into(),
+                        is_directory,
+                    })
+                    .await?;
+                let entry = response
+                    .entry
+                    .ok_or_else(|| anyhow!("missing entry in response"))?;
+                worktree
+                    .update(&mut cx, |worktree, cx| {
+                        worktree.as_remote_mut().unwrap().insert_entry(
+                            entry,
+                            response.worktree_scan_id as usize,
+                            cx,
+                        )
+                    })?
+                    .await
+            }))
+        }
+    }
+
+    pub fn copy_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        new_path: impl Into<Arc<Path>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<Entry>>> {
+        let worktree = self.worktree_for_entry(entry_id, cx)?;
+        let new_path = new_path.into();
+        if self.is_local() {
+            worktree.update(cx, |worktree, cx| {
+                worktree
+                    .as_local_mut()
+                    .unwrap()
+                    .copy_entry(entry_id, new_path, cx)
+            })
+        } else {
+            let client = self.client.clone();
+            let project_id = self.remote_id().unwrap();
+
+            Some(cx.spawn(move |_, mut cx| async move {
+                let response = client
+                    .request(proto::CopyProjectEntry {
+                        project_id,
+                        entry_id: entry_id.to_proto(),
+                        new_path: new_path.to_string_lossy().into(),
+                    })
+                    .await?;
+                let entry = response
+                    .entry
+                    .ok_or_else(|| anyhow!("missing entry in response"))?;
+                worktree
+                    .update(&mut cx, |worktree, cx| {
+                        worktree.as_remote_mut().unwrap().insert_entry(
+                            entry,
+                            response.worktree_scan_id as usize,
+                            cx,
+                        )
+                    })?
+                    .await
+            }))
+        }
+    }
+
+    pub fn rename_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        new_path: impl Into<Arc<Path>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<Entry>>> {
+        let worktree = self.worktree_for_entry(entry_id, cx)?;
+        let new_path = new_path.into();
+        if self.is_local() {
+            worktree.update(cx, |worktree, cx| {
+                worktree
+                    .as_local_mut()
+                    .unwrap()
+                    .rename_entry(entry_id, new_path, cx)
+            })
+        } else {
+            let client = self.client.clone();
+            let project_id = self.remote_id().unwrap();
+
+            Some(cx.spawn(move |_, mut cx| async move {
+                let response = client
+                    .request(proto::RenameProjectEntry {
+                        project_id,
+                        entry_id: entry_id.to_proto(),
+                        new_path: new_path.to_string_lossy().into(),
+                    })
+                    .await?;
+                let entry = response
+                    .entry
+                    .ok_or_else(|| anyhow!("missing entry in response"))?;
+                worktree
+                    .update(&mut cx, |worktree, cx| {
+                        worktree.as_remote_mut().unwrap().insert_entry(
+                            entry,
+                            response.worktree_scan_id as usize,
+                            cx,
+                        )
+                    })?
+                    .await
+            }))
+        }
+    }
+
+    pub fn delete_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let worktree = self.worktree_for_entry(entry_id, cx)?;
+
+        cx.emit(Event::DeletedEntry(entry_id));
+
+        if self.is_local() {
+            worktree.update(cx, |worktree, cx| {
+                worktree.as_local_mut().unwrap().delete_entry(entry_id, cx)
+            })
+        } else {
+            let client = self.client.clone();
+            let project_id = self.remote_id().unwrap();
+            Some(cx.spawn(move |_, mut cx| async move {
+                let response = client
+                    .request(proto::DeleteProjectEntry {
+                        project_id,
+                        entry_id: entry_id.to_proto(),
+                    })
+                    .await?;
+                worktree
+                    .update(&mut cx, move |worktree, cx| {
+                        worktree.as_remote_mut().unwrap().delete_entry(
+                            entry_id,
+                            response.worktree_scan_id as usize,
+                            cx,
+                        )
+                    })?
+                    .await
+            }))
+        }
+    }
+
+    pub fn expand_entry(
+        &mut self,
+        worktree_id: WorktreeId,
+        entry_id: ProjectEntryId,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let worktree = self.worktree_for_id(worktree_id, cx)?;
+        if self.is_local() {
+            worktree.update(cx, |worktree, cx| {
+                worktree.as_local_mut().unwrap().expand_entry(entry_id, cx)
+            })
+        } else {
+            let worktree = worktree.downgrade();
+            let request = self.client.request(proto::ExpandProjectEntry {
+                project_id: self.remote_id().unwrap(),
+                entry_id: entry_id.to_proto(),
+            });
+            Some(cx.spawn(move |_, mut cx| async move {
+                let response = request.await?;
+                if let Some(worktree) = worktree.upgrade() {
+                    worktree
+                        .update(&mut cx, |worktree, _| {
+                            worktree
+                                .as_remote_mut()
+                                .unwrap()
+                                .wait_for_snapshot(response.worktree_scan_id as usize)
+                        })?
+                        .await?;
+                }
+                Ok(())
+            }))
+        }
+    }
+
+    pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Result<()> {
+        if self.client_state.is_some() {
+            return Err(anyhow!("project was already shared"));
+        }
+        self.client_subscriptions.push(
+            self.client
+                .subscribe_to_entity(project_id)?
+                .set_model(&cx.handle(), &mut cx.to_async()),
+        );
+
+        for open_buffer in self.opened_buffers.values_mut() {
+            match open_buffer {
+                OpenBuffer::Strong(_) => {}
+                OpenBuffer::Weak(buffer) => {
+                    if let Some(buffer) = buffer.upgrade() {
+                        *open_buffer = OpenBuffer::Strong(buffer);
+                    }
+                }
+                OpenBuffer::Operations(_) => unreachable!(),
+            }
+        }
+
+        for worktree_handle in self.worktrees.iter_mut() {
+            match worktree_handle {
+                WorktreeHandle::Strong(_) => {}
+                WorktreeHandle::Weak(worktree) => {
+                    if let Some(worktree) = worktree.upgrade() {
+                        *worktree_handle = WorktreeHandle::Strong(worktree);
+                    }
+                }
+            }
+        }
+
+        for (server_id, status) in &self.language_server_statuses {
+            self.client
+                .send(proto::StartLanguageServer {
+                    project_id,
+                    server: Some(proto::LanguageServer {
+                        id: server_id.0 as u64,
+                        name: status.name.clone(),
+                    }),
+                })
+                .log_err();
+        }
+
+        let store = cx.global::<SettingsStore>();
+        for worktree in self.worktrees() {
+            let worktree_id = worktree.read(cx).id().to_proto();
+            for (path, content) in store.local_settings(worktree.entity_id().as_u64() as usize) {
+                self.client
+                    .send(proto::UpdateWorktreeSettings {
+                        project_id,
+                        worktree_id,
+                        path: path.to_string_lossy().into(),
+                        content: Some(content),
+                    })
+                    .log_err();
+            }
+        }
+
+        let (updates_tx, mut updates_rx) = mpsc::unbounded();
+        let client = self.client.clone();
+        self.client_state = Some(ProjectClientState::Local {
+            remote_id: project_id,
+            updates_tx,
+            _send_updates: cx.spawn(move |this, mut cx| async move {
+                while let Some(update) = updates_rx.next().await {
+                    match update {
+                        LocalProjectUpdate::WorktreesChanged => {
+                            let worktrees = this.update(&mut cx, |this, _cx| {
+                                this.worktrees().collect::<Vec<_>>()
+                            })?;
+                            let update_project = this
+                                .update(&mut cx, |this, cx| {
+                                    this.client.request(proto::UpdateProject {
+                                        project_id,
+                                        worktrees: this.worktree_metadata_protos(cx),
+                                    })
+                                })?
+                                .await;
+                            if update_project.is_ok() {
+                                for worktree in worktrees {
+                                    worktree.update(&mut cx, |worktree, cx| {
+                                        let worktree = worktree.as_local_mut().unwrap();
+                                        worktree.share(project_id, cx).detach_and_log_err(cx)
+                                    })?;
+                                }
+                            }
+                        }
+                        LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id } => {
+                            let buffer = this.update(&mut cx, |this, _| {
+                                let buffer = this.opened_buffers.get(&buffer_id).unwrap();
+                                let shared_buffers =
+                                    this.shared_buffers.entry(peer_id).or_default();
+                                if shared_buffers.insert(buffer_id) {
+                                    if let OpenBuffer::Strong(buffer) = buffer {
+                                        Some(buffer.clone())
+                                    } else {
+                                        None
+                                    }
+                                } else {
+                                    None
+                                }
+                            })?;
+
+                            let Some(buffer) = buffer else { continue };
+                            let operations =
+                                buffer.update(&mut cx, |b, cx| b.serialize_ops(None, cx))?;
+                            let operations = operations.await;
+                            let state = buffer.update(&mut cx, |buffer, _| buffer.to_proto())?;
+
+                            let initial_state = proto::CreateBufferForPeer {
+                                project_id,
+                                peer_id: Some(peer_id),
+                                variant: Some(proto::create_buffer_for_peer::Variant::State(state)),
+                            };
+                            if client.send(initial_state).log_err().is_some() {
+                                let client = client.clone();
+                                cx.executor()
+                                    .spawn(async move {
+                                        let mut chunks = split_operations(operations).peekable();
+                                        while let Some(chunk) = chunks.next() {
+                                            let is_last = chunks.peek().is_none();
+                                            client.send(proto::CreateBufferForPeer {
+                                                project_id,
+                                                peer_id: Some(peer_id),
+                                                variant: Some(
+                                                    proto::create_buffer_for_peer::Variant::Chunk(
+                                                        proto::BufferChunk {
+                                                            buffer_id,
+                                                            operations: chunk,
+                                                            is_last,
+                                                        },
+                                                    ),
+                                                ),
+                                            })?;
+                                        }
+                                        anyhow::Ok(())
+                                    })
+                                    .await
+                                    .log_err();
+                            }
+                        }
+                    }
+                }
+                Ok(())
+            }),
+        });
+
+        self.metadata_changed(cx);
+        cx.emit(Event::RemoteIdChanged(Some(project_id)));
+        cx.notify();
+        Ok(())
+    }
+
+    pub fn reshared(
+        &mut self,
+        message: proto::ResharedProject,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        self.shared_buffers.clear();
+        self.set_collaborators_from_proto(message.collaborators, cx)?;
+        self.metadata_changed(cx);
+        Ok(())
+    }
+
+    pub fn rejoined(
+        &mut self,
+        message: proto::RejoinedProject,
+        message_id: u32,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        cx.update_global::<SettingsStore, _>(|store, cx| {
+            for worktree in &self.worktrees {
+                store
+                    .clear_local_settings(worktree.handle_id(), cx)
+                    .log_err();
+            }
+        });
+
+        self.join_project_response_message_id = message_id;
+        self.set_worktrees_from_proto(message.worktrees, cx)?;
+        self.set_collaborators_from_proto(message.collaborators, cx)?;
+        self.language_server_statuses = message
+            .language_servers
+            .into_iter()
+            .map(|server| {
+                (
+                    LanguageServerId(server.id as usize),
+                    LanguageServerStatus {
+                        name: server.name,
+                        pending_work: Default::default(),
+                        has_pending_diagnostic_updates: false,
+                        progress_tokens: Default::default(),
+                    },
+                )
+            })
+            .collect();
+        self.buffer_ordered_messages_tx
+            .unbounded_send(BufferOrderedMessage::Resync)
+            .unwrap();
+        cx.notify();
+        Ok(())
+    }
+
+    pub fn unshare(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
+        self.unshare_internal(cx)?;
+        self.metadata_changed(cx);
+        cx.notify();
+        Ok(())
+    }
+
+    fn unshare_internal(&mut self, cx: &mut AppContext) -> Result<()> {
+        if self.is_remote() {
+            return Err(anyhow!("attempted to unshare a remote project"));
+        }
+
+        if let Some(ProjectClientState::Local { remote_id, .. }) = self.client_state.take() {
+            self.collaborators.clear();
+            self.shared_buffers.clear();
+            self.client_subscriptions.clear();
+
+            for worktree_handle in self.worktrees.iter_mut() {
+                if let WorktreeHandle::Strong(worktree) = worktree_handle {
+                    let is_visible = worktree.update(cx, |worktree, _| {
+                        worktree.as_local_mut().unwrap().unshare();
+                        worktree.is_visible()
+                    });
+                    if !is_visible {
+                        *worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
+                    }
+                }
+            }
+
+            for open_buffer in self.opened_buffers.values_mut() {
+                // Wake up any tasks waiting for peers' edits to this buffer.
+                if let Some(buffer) = open_buffer.upgrade() {
+                    buffer.update(cx, |buffer, _| buffer.give_up_waiting());
+                }
+
+                if let OpenBuffer::Strong(buffer) = open_buffer {
+                    *open_buffer = OpenBuffer::Weak(buffer.downgrade());
+                }
+            }
+
+            self.client.send(proto::UnshareProject {
+                project_id: remote_id,
+            })?;
+
+            Ok(())
+        } else {
+            Err(anyhow!("attempted to unshare an unshared project"))
+        }
+    }
+
+    pub fn disconnected_from_host(&mut self, cx: &mut ModelContext<Self>) {
+        self.disconnected_from_host_internal(cx);
+        cx.emit(Event::DisconnectedFromHost);
+        cx.notify();
+    }
+
+    fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) {
+        if let Some(ProjectClientState::Remote {
+            sharing_has_stopped,
+            ..
+        }) = &mut self.client_state
+        {
+            *sharing_has_stopped = true;
+
+            self.collaborators.clear();
+
+            for worktree in &self.worktrees {
+                if let Some(worktree) = worktree.upgrade() {
+                    worktree.update(cx, |worktree, _| {
+                        if let Some(worktree) = worktree.as_remote_mut() {
+                            worktree.disconnected_from_host();
+                        }
+                    });
+                }
+            }
+
+            for open_buffer in self.opened_buffers.values_mut() {
+                // Wake up any tasks waiting for peers' edits to this buffer.
+                if let Some(buffer) = open_buffer.upgrade() {
+                    buffer.update(cx, |buffer, _| buffer.give_up_waiting());
+                }
+
+                if let OpenBuffer::Strong(buffer) = open_buffer {
+                    *open_buffer = OpenBuffer::Weak(buffer.downgrade());
+                }
+            }
+
+            // Wake up all futures currently waiting on a buffer to get opened,
+            // to give them a chance to fail now that we've disconnected.
+            *self.opened_buffer.0.borrow_mut() = ();
+        }
+    }
+
+    pub fn close(&mut self, cx: &mut ModelContext<Self>) {
+        cx.emit(Event::Closed);
+    }
+
+    pub fn is_read_only(&self) -> bool {
+        match &self.client_state {
+            Some(ProjectClientState::Remote {
+                sharing_has_stopped,
+                ..
+            }) => *sharing_has_stopped,
+            _ => false,
+        }
+    }
+
+    pub fn is_local(&self) -> bool {
+        match &self.client_state {
+            Some(ProjectClientState::Remote { .. }) => false,
+            _ => true,
+        }
+    }
+
+    pub fn is_remote(&self) -> bool {
+        !self.is_local()
+    }
+
+    pub fn create_buffer(
+        &mut self,
+        text: &str,
+        language: Option<Arc<Language>>,
+        cx: &mut ModelContext<Self>,
+    ) -> 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.build_model(|cx| {
+            Buffer::new(self.replica_id(), id, text).with_language(
+                language.unwrap_or_else(|| language2::PLAIN_TEXT.clone()),
+                cx,
+            )
+        });
+        self.register_buffer(&buffer, cx)?;
+        Ok(buffer)
+    }
+
+    pub fn open_path(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<(ProjectEntryId, AnyModel)>> {
+        let task = self.open_buffer(path, cx);
+        cx.spawn(move |_, mut cx| async move {
+            let buffer = task.await?;
+            let project_entry_id = buffer
+                .update(&mut cx, |buffer, cx| {
+                    File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+                })?
+                .ok_or_else(|| anyhow!("no project entry"))?;
+
+            let buffer: &AnyModel = &buffer;
+            Ok((project_entry_id, buffer.clone()))
+        })
+    }
+
+    pub fn open_local_buffer(
+        &mut self,
+        abs_path: impl AsRef<Path>,
+        cx: &mut ModelContext<Self>,
+    ) -> 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 {
+            Task::ready(Err(anyhow!("no such path")))
+        }
+    }
+
+    pub fn open_buffer(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        cx: &mut ModelContext<Self>,
+    ) -> 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
+        } else {
+            return Task::ready(Err(anyhow!("no such worktree")));
+        };
+
+        // If there is already a buffer for the given path, then return it.
+        let existing_buffer = self.get_open_buffer(&project_path, cx);
+        if let Some(existing_buffer) = existing_buffer {
+            return Task::ready(Ok(existing_buffer));
+        }
+
+        let loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
+            // If the given path is already being loaded, then wait for that existing
+            // task to complete and return the same buffer.
+            hash_map::Entry::Occupied(e) => e.get().clone(),
+
+            // Otherwise, record the fact that this path is now being loaded.
+            hash_map::Entry::Vacant(entry) => {
+                let (mut tx, rx) = postage::watch::channel();
+                entry.insert(rx.clone());
+
+                let load_buffer = if worktree.read(cx).is_local() {
+                    self.open_local_buffer_internal(&project_path.path, &worktree, cx)
+                } else {
+                    self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
+                };
+
+                cx.spawn(move |this, mut cx| async move {
+                    let load_result = load_buffer.await;
+                    *tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
+                        // Record the fact that the buffer is no longer loading.
+                        this.loading_buffers_by_path.remove(&project_path);
+                        let buffer = load_result.map_err(Arc::new)?;
+                        Ok(buffer)
+                    })?);
+                    anyhow::Ok(())
+                })
+                .detach();
+                rx
+            }
+        };
+
+        cx.executor().spawn(async move {
+            wait_for_loading_buffer(loading_watch)
+                .await
+                .map_err(|error| anyhow!("{}", error))
+        })
+    }
+
+    fn open_local_buffer_internal(
+        &mut self,
+        path: &Arc<Path>,
+        worktree: &Model<Worktree>,
+        cx: &mut ModelContext<Self>,
+    ) -> 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();
+            worktree.load_buffer(buffer_id, path, cx)
+        });
+        cx.spawn(move |this, mut cx| async move {
+            let buffer = load_buffer.await?;
+            this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))??;
+            Ok(buffer)
+        })
+    }
+
+    fn open_remote_buffer_internal(
+        &mut self,
+        path: &Arc<Path>,
+        worktree: &Model<Worktree>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Model<Buffer>>> {
+        let rpc = self.client.clone();
+        let project_id = self.remote_id().unwrap();
+        let remote_worktree_id = worktree.read(cx).id();
+        let path = path.clone();
+        let path_string = path.to_string_lossy().to_string();
+        cx.spawn(move |this, mut cx| async move {
+            let response = rpc
+                .request(proto::OpenBufferByPath {
+                    project_id,
+                    worktree_id: remote_worktree_id.to_proto(),
+                    path: path_string,
+                })
+                .await?;
+            this.update(&mut cx, |this, cx| {
+                this.wait_for_remote_buffer(response.buffer_id, cx)
+            })?
+            .await
+        })
+    }
+
+    /// LanguageServerName is owned, because it is inserted into a map
+    pub fn open_local_buffer_via_lsp(
+        &mut self,
+        abs_path: lsp2::Url,
+        language_server_id: LanguageServerId,
+        language_server_name: LanguageServerName,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Model<Buffer>>> {
+        cx.spawn(move |this, mut cx| async move {
+            let abs_path = abs_path
+                .to_file_path()
+                .map_err(|_| anyhow!("can't convert URI to path"))?;
+            let (worktree, relative_path) = if let Some(result) =
+                this.update(&mut cx, |this, cx| this.find_local_worktree(&abs_path, cx))?
+            {
+                result
+            } else {
+                let worktree = this
+                    .update(&mut cx, |this, cx| {
+                        this.create_local_worktree(&abs_path, false, cx)
+                    })?
+                    .await?;
+                this.update(&mut cx, |this, cx| {
+                    this.language_server_ids.insert(
+                        (worktree.read(cx).id(), language_server_name),
+                        language_server_id,
+                    );
+                })
+                .ok();
+                (worktree, PathBuf::new())
+            };
+
+            let project_path = ProjectPath {
+                worktree_id: worktree.update(&mut cx, |worktree, _| worktree.id())?,
+                path: relative_path.into(),
+            };
+            this.update(&mut cx, |this, cx| this.open_buffer(project_path, cx))?
+                .await
+        })
+    }
+
+    pub fn open_buffer_by_id(
+        &mut self,
+        id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Model<Buffer>>> {
+        if let Some(buffer) = self.buffer_for_id(id) {
+            Task::ready(Ok(buffer))
+        } else if self.is_local() {
+            Task::ready(Err(anyhow!("buffer {} does not exist", id)))
+        } else if let Some(project_id) = self.remote_id() {
+            let request = self
+                .client
+                .request(proto::OpenBufferById { project_id, id });
+            cx.spawn(move |this, mut cx| async move {
+                let buffer_id = request.await?.buffer_id;
+                this.update(&mut cx, |this, cx| {
+                    this.wait_for_remote_buffer(buffer_id, cx)
+                })?
+                .await
+            })
+        } else {
+            Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
+        }
+    }
+
+    pub fn save_buffers(
+        &self,
+        buffers: HashSet<Model<Buffer>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        cx.spawn(move |this, mut cx| async move {
+            let save_tasks = buffers.into_iter().filter_map(|buffer| {
+                this.update(&mut cx, |this, cx| this.save_buffer(buffer, cx))
+                    .ok()
+            });
+            try_join_all(save_tasks).await?;
+            Ok(())
+        })
+    }
+
+    pub fn save_buffer(
+        &self,
+        buffer: Model<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
+            return Task::ready(Err(anyhow!("buffer doesn't have a file")));
+        };
+        let worktree = file.worktree.clone();
+        let path = file.path.clone();
+        worktree.update(cx, |worktree, cx| match worktree {
+            Worktree::Local(worktree) => worktree.save_buffer(buffer, path, false, cx),
+            Worktree::Remote(worktree) => worktree.save_buffer(buffer, cx),
+        })
+    }
+
+    pub fn save_buffer_as(
+        &mut self,
+        buffer: Model<Buffer>,
+        abs_path: PathBuf,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx);
+        let old_file = File::from_dyn(buffer.read(cx).file())
+            .filter(|f| f.is_local())
+            .cloned();
+        cx.spawn(move |this, mut cx| async move {
+            if let Some(old_file) = &old_file {
+                this.update(&mut cx, |this, cx| {
+                    this.unregister_buffer_from_language_servers(&buffer, old_file, cx);
+                })?;
+            }
+            let (worktree, path) = worktree_task.await?;
+            worktree
+                .update(&mut cx, |worktree, cx| match worktree {
+                    Worktree::Local(worktree) => {
+                        worktree.save_buffer(buffer.clone(), path.into(), true, cx)
+                    }
+                    Worktree::Remote(_) => panic!("cannot remote buffers as new files"),
+                })?
+                .await?;
+
+            this.update(&mut cx, |this, cx| {
+                this.detect_language_for_buffer(&buffer, cx);
+                this.register_buffer_with_language_servers(&buffer, cx);
+            })?;
+            Ok(())
+        })
+    }
+
+    pub fn get_open_buffer(
+        &mut self,
+        path: &ProjectPath,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Model<Buffer>> {
+        let worktree = self.worktree_for_id(path.worktree_id, cx)?;
+        self.opened_buffers.values().find_map(|buffer| {
+            let buffer = buffer.upgrade()?;
+            let file = File::from_dyn(buffer.read(cx).file())?;
+            if file.worktree == worktree && file.path() == &path.path {
+                Some(buffer)
+            } else {
+                None
+            }
+        })
+    }
+
+    fn register_buffer(
+        &mut self,
+        buffer: &Model<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        self.request_buffer_diff_recalculation(buffer, cx);
+        buffer.update(cx, |buffer, _| {
+            buffer.set_language_registry(self.languages.clone())
+        });
+
+        let remote_id = buffer.read(cx).remote_id();
+        let is_remote = self.is_remote();
+        let open_buffer = if is_remote || self.is_shared() {
+            OpenBuffer::Strong(buffer.clone())
+        } else {
+            OpenBuffer::Weak(buffer.downgrade())
+        };
+
+        match self.opened_buffers.entry(remote_id) {
+            hash_map::Entry::Vacant(entry) => {
+                entry.insert(open_buffer);
+            }
+            hash_map::Entry::Occupied(mut entry) => {
+                if let OpenBuffer::Operations(operations) = entry.get_mut() {
+                    buffer.update(cx, |b, cx| b.apply_ops(operations.drain(..), cx))?;
+                } else if entry.get().upgrade().is_some() {
+                    if is_remote {
+                        return Ok(());
+                    } else {
+                        debug_panic!("buffer {} was already registered", remote_id);
+                        Err(anyhow!("buffer {} was already registered", remote_id))?;
+                    }
+                }
+                entry.insert(open_buffer);
+            }
+        }
+        cx.subscribe(buffer, |this, buffer, event, cx| {
+            this.on_buffer_event(buffer, event, cx);
+        })
+        .detach();
+
+        if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
+            if file.is_local {
+                self.local_buffer_ids_by_path.insert(
+                    ProjectPath {
+                        worktree_id: file.worktree_id(cx),
+                        path: file.path.clone(),
+                    },
+                    remote_id,
+                );
+
+                self.local_buffer_ids_by_entry_id
+                    .insert(file.entry_id, remote_id);
+            }
+        }
+
+        self.detect_language_for_buffer(buffer, cx);
+        self.register_buffer_with_language_servers(buffer, cx);
+        self.register_buffer_with_copilot(buffer, cx);
+        cx.observe_release(buffer, |this, buffer, cx| {
+            if let Some(file) = File::from_dyn(buffer.file()) {
+                if file.is_local() {
+                    let uri = lsp2::Url::from_file_path(file.abs_path(cx)).unwrap();
+                    for server in this.language_servers_for_buffer(buffer, cx) {
+                        server
+                            .1
+                            .notify::<lsp2::notification::DidCloseTextDocument>(
+                                lsp2::DidCloseTextDocumentParams {
+                                    text_document: lsp2::TextDocumentIdentifier::new(uri.clone()),
+                                },
+                            )
+                            .log_err();
+                    }
+                }
+            }
+        })
+        .detach();
+
+        *self.opened_buffer.0.borrow_mut() = ();
+        Ok(())
+    }
+
+    fn register_buffer_with_language_servers(
+        &mut self,
+        buffer_handle: &Model<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let buffer = buffer_handle.read(cx);
+        let buffer_id = buffer.remote_id();
+
+        if let Some(file) = File::from_dyn(buffer.file()) {
+            if !file.is_local() {
+                return;
+            }
+
+            let abs_path = file.abs_path(cx);
+            let uri = lsp2::Url::from_file_path(&abs_path)
+                .unwrap_or_else(|()| panic!("Failed to register file {abs_path:?}"));
+            let initial_snapshot = buffer.text_snapshot();
+            let language = buffer.language().cloned();
+            let worktree_id = file.worktree_id(cx);
+
+            if let Some(local_worktree) = file.worktree.read(cx).as_local() {
+                for (server_id, diagnostics) in local_worktree.diagnostics_for_path(file.path()) {
+                    self.update_buffer_diagnostics(buffer_handle, server_id, None, diagnostics, cx)
+                        .log_err();
+                }
+            }
+
+            if let Some(language) = language {
+                for adapter in language.lsp_adapters() {
+                    let language_id = adapter.language_ids.get(language.name().as_ref()).cloned();
+                    let server = self
+                        .language_server_ids
+                        .get(&(worktree_id, adapter.name.clone()))
+                        .and_then(|id| self.language_servers.get(id))
+                        .and_then(|server_state| {
+                            if let LanguageServerState::Running { server, .. } = server_state {
+                                Some(server.clone())
+                            } else {
+                                None
+                            }
+                        });
+                    let server = match server {
+                        Some(server) => server,
+                        None => continue,
+                    };
+
+                    server
+                        .notify::<lsp2::notification::DidOpenTextDocument>(
+                            lsp2::DidOpenTextDocumentParams {
+                                text_document: lsp2::TextDocumentItem::new(
+                                    uri.clone(),
+                                    language_id.unwrap_or_default(),
+                                    0,
+                                    initial_snapshot.text(),
+                                ),
+                            },
+                        )
+                        .log_err();
+
+                    buffer_handle.update(cx, |buffer, cx| {
+                        buffer.set_completion_triggers(
+                            server
+                                .capabilities()
+                                .completion_provider
+                                .as_ref()
+                                .and_then(|provider| provider.trigger_characters.clone())
+                                .unwrap_or_default(),
+                            cx,
+                        );
+                    });
+
+                    let snapshot = LspBufferSnapshot {
+                        version: 0,
+                        snapshot: initial_snapshot.clone(),
+                    };
+                    self.buffer_snapshots
+                        .entry(buffer_id)
+                        .or_default()
+                        .insert(server.server_id(), vec![snapshot]);
+                }
+            }
+        }
+    }
+
+    fn unregister_buffer_from_language_servers(
+        &mut self,
+        buffer: &Model<Buffer>,
+        old_file: &File,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let old_path = match old_file.as_local() {
+            Some(local) => local.abs_path(cx),
+            None => return,
+        };
+
+        buffer.update(cx, |buffer, cx| {
+            let worktree_id = old_file.worktree_id(cx);
+            let ids = &self.language_server_ids;
+
+            let language = buffer.language().cloned();
+            let adapters = language.iter().flat_map(|language| language.lsp_adapters());
+            for &server_id in adapters.flat_map(|a| ids.get(&(worktree_id, a.name.clone()))) {
+                buffer.update_diagnostics(server_id, Default::default(), cx);
+            }
+
+            self.buffer_snapshots.remove(&buffer.remote_id());
+            let file_url = lsp2::Url::from_file_path(old_path).unwrap();
+            for (_, language_server) in self.language_servers_for_buffer(buffer, cx) {
+                language_server
+                    .notify::<lsp2::notification::DidCloseTextDocument>(
+                        lsp2::DidCloseTextDocumentParams {
+                            text_document: lsp2::TextDocumentIdentifier::new(file_url.clone()),
+                        },
+                    )
+                    .log_err();
+            }
+        });
+    }
+
+    fn register_buffer_with_copilot(
+        &self,
+        buffer_handle: &Model<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(copilot) = Copilot::global(cx) {
+            copilot.update(cx, |copilot, cx| copilot.register_buffer(buffer_handle, cx));
+        }
+    }
+
+    async fn send_buffer_ordered_messages(
+        this: WeakModel<Self>,
+        rx: UnboundedReceiver<BufferOrderedMessage>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        const MAX_BATCH_SIZE: usize = 128;
+
+        let mut operations_by_buffer_id = HashMap::default();
+        async fn flush_operations(
+            this: &WeakModel<Project>,
+            operations_by_buffer_id: &mut HashMap<u64, Vec<proto::Operation>>,
+            needs_resync_with_host: &mut bool,
+            is_local: bool,
+            cx: &mut AsyncAppContext,
+        ) -> Result<()> {
+            for (buffer_id, operations) in operations_by_buffer_id.drain() {
+                let request = this.update(cx, |this, _| {
+                    let project_id = this.remote_id()?;
+                    Some(this.client.request(proto::UpdateBuffer {
+                        buffer_id,
+                        project_id,
+                        operations,
+                    }))
+                })?;
+                if let Some(request) = request {
+                    if request.await.is_err() && !is_local {
+                        *needs_resync_with_host = true;
+                        break;
+                    }
+                }
+            }
+            Ok(())
+        }
+
+        let mut needs_resync_with_host = false;
+        let mut changes = rx.ready_chunks(MAX_BATCH_SIZE);
+
+        while let Some(changes) = changes.next().await {
+            let is_local = this.update(&mut cx, |this, _| this.is_local())?;
+
+            for change in changes {
+                match change {
+                    BufferOrderedMessage::Operation {
+                        buffer_id,
+                        operation,
+                    } => {
+                        if needs_resync_with_host {
+                            continue;
+                        }
+
+                        operations_by_buffer_id
+                            .entry(buffer_id)
+                            .or_insert(Vec::new())
+                            .push(operation);
+                    }
+
+                    BufferOrderedMessage::Resync => {
+                        operations_by_buffer_id.clear();
+                        if this
+                            .update(&mut cx, |this, cx| this.synchronize_remote_buffers(cx))?
+                            .await
+                            .is_ok()
+                        {
+                            needs_resync_with_host = false;
+                        }
+                    }
+
+                    BufferOrderedMessage::LanguageServerUpdate {
+                        language_server_id,
+                        message,
+                    } => {
+                        flush_operations(
+                            &this,
+                            &mut operations_by_buffer_id,
+                            &mut needs_resync_with_host,
+                            is_local,
+                            &mut cx,
+                        )
+                        .await?;
+
+                        this.update(&mut cx, |this, _| {
+                            if let Some(project_id) = this.remote_id() {
+                                this.client
+                                    .send(proto::UpdateLanguageServer {
+                                        project_id,
+                                        language_server_id: language_server_id.0 as u64,
+                                        variant: Some(message),
+                                    })
+                                    .log_err();
+                            }
+                        })?;
+                    }
+                }
+            }
+
+            flush_operations(
+                &this,
+                &mut operations_by_buffer_id,
+                &mut needs_resync_with_host,
+                is_local,
+                &mut cx,
+            )
+            .await?;
+        }
+
+        Ok(())
+    }
+
+    fn on_buffer_event(
+        &mut self,
+        buffer: Model<Buffer>,
+        event: &BufferEvent,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<()> {
+        if matches!(
+            event,
+            BufferEvent::Edited { .. } | BufferEvent::Reloaded | BufferEvent::DiffBaseChanged
+        ) {
+            self.request_buffer_diff_recalculation(&buffer, cx);
+        }
+
+        match event {
+            BufferEvent::Operation(operation) => {
+                self.buffer_ordered_messages_tx
+                    .unbounded_send(BufferOrderedMessage::Operation {
+                        buffer_id: buffer.read(cx).remote_id(),
+                        operation: language2::proto::serialize_operation(operation),
+                    })
+                    .ok();
+            }
+
+            BufferEvent::Edited { .. } => {
+                let buffer = buffer.read(cx);
+                let file = File::from_dyn(buffer.file())?;
+                let abs_path = file.as_local()?.abs_path(cx);
+                let uri = lsp2::Url::from_file_path(abs_path).unwrap();
+                let next_snapshot = buffer.text_snapshot();
+
+                let language_servers: Vec<_> = self
+                    .language_servers_for_buffer(buffer, cx)
+                    .map(|i| i.1.clone())
+                    .collect();
+
+                for language_server in language_servers {
+                    let language_server = language_server.clone();
+
+                    let buffer_snapshots = self
+                        .buffer_snapshots
+                        .get_mut(&buffer.remote_id())
+                        .and_then(|m| m.get_mut(&language_server.server_id()))?;
+                    let previous_snapshot = buffer_snapshots.last()?;
+
+                    let build_incremental_change = || {
+                        buffer
+                            .edits_since::<(PointUtf16, usize)>(
+                                previous_snapshot.snapshot.version(),
+                            )
+                            .map(|edit| {
+                                let edit_start = edit.new.start.0;
+                                let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
+                                let new_text = next_snapshot
+                                    .text_for_range(edit.new.start.1..edit.new.end.1)
+                                    .collect();
+                                lsp2::TextDocumentContentChangeEvent {
+                                    range: Some(lsp2::Range::new(
+                                        point_to_lsp(edit_start),
+                                        point_to_lsp(edit_end),
+                                    )),
+                                    range_length: None,
+                                    text: new_text,
+                                }
+                            })
+                            .collect()
+                    };
+
+                    let document_sync_kind = language_server
+                        .capabilities()
+                        .text_document_sync
+                        .as_ref()
+                        .and_then(|sync| match sync {
+                            lsp2::TextDocumentSyncCapability::Kind(kind) => Some(*kind),
+                            lsp2::TextDocumentSyncCapability::Options(options) => options.change,
+                        });
+
+                    let content_changes: Vec<_> = match document_sync_kind {
+                        Some(lsp2::TextDocumentSyncKind::FULL) => {
+                            vec![lsp2::TextDocumentContentChangeEvent {
+                                range: None,
+                                range_length: None,
+                                text: next_snapshot.text(),
+                            }]
+                        }
+                        Some(lsp2::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(),
+                        _ => {
+                            #[cfg(any(test, feature = "test-support"))]
+                            {
+                                build_incremental_change()
+                            }
+
+                            #[cfg(not(any(test, feature = "test-support")))]
+                            {
+                                continue;
+                            }
+                        }
+                    };
+
+                    let next_version = previous_snapshot.version + 1;
+
+                    buffer_snapshots.push(LspBufferSnapshot {
+                        version: next_version,
+                        snapshot: next_snapshot.clone(),
+                    });
+
+                    language_server
+                        .notify::<lsp2::notification::DidChangeTextDocument>(
+                            lsp2::DidChangeTextDocumentParams {
+                                text_document: lsp2::VersionedTextDocumentIdentifier::new(
+                                    uri.clone(),
+                                    next_version,
+                                ),
+                                content_changes,
+                            },
+                        )
+                        .log_err();
+                }
+            }
+
+            BufferEvent::Saved => {
+                let file = File::from_dyn(buffer.read(cx).file())?;
+                let worktree_id = file.worktree_id(cx);
+                let abs_path = file.as_local()?.abs_path(cx);
+                let text_document = lsp2::TextDocumentIdentifier {
+                    uri: lsp2::Url::from_file_path(abs_path).unwrap(),
+                };
+
+                for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
+                    let text = include_text(server.as_ref()).then(|| buffer.read(cx).text());
+
+                    server
+                        .notify::<lsp2::notification::DidSaveTextDocument>(
+                            lsp2::DidSaveTextDocumentParams {
+                                text_document: text_document.clone(),
+                                text,
+                            },
+                        )
+                        .log_err();
+                }
+
+                let language_server_ids = self.language_server_ids_for_buffer(buffer.read(cx), cx);
+                for language_server_id in language_server_ids {
+                    if let Some(LanguageServerState::Running {
+                        adapter,
+                        simulate_disk_based_diagnostics_completion,
+                        ..
+                    }) = self.language_servers.get_mut(&language_server_id)
+                    {
+                        // After saving a buffer using a language server that doesn't provide
+                        // a disk-based progress token, kick off a timer that will reset every
+                        // time the buffer is saved. If the timer eventually fires, simulate
+                        // disk-based diagnostics being finished so that other pieces of UI
+                        // (e.g., project diagnostics view, diagnostic status bar) can update.
+                        // We don't emit an event right away because the language server might take
+                        // some time to publish diagnostics.
+                        if adapter.disk_based_diagnostics_progress_token.is_none() {
+                            const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration =
+                                Duration::from_secs(1);
+
+                            let task = cx.spawn(move |this, mut cx| async move {
+                                cx.executor().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await;
+                                if let Some(this) = this.upgrade() {
+                                    this.update(&mut cx, |this, cx| {
+                                        this.disk_based_diagnostics_finished(
+                                            language_server_id,
+                                            cx,
+                                        );
+                                        this.buffer_ordered_messages_tx
+                                            .unbounded_send(
+                                                BufferOrderedMessage::LanguageServerUpdate {
+                                                    language_server_id,
+                                                    message:proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(Default::default())
+                                                },
+                                            )
+                                            .ok();
+                                    }).ok();
+                                }
+                            });
+                            *simulate_disk_based_diagnostics_completion = Some(task);
+                        }
+                    }
+                }
+            }
+            BufferEvent::FileHandleChanged => {
+                let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
+                    return None;
+                };
+
+                match self.local_buffer_ids_by_entry_id.get(&file.entry_id) {
+                    Some(_) => {
+                        return None;
+                    }
+                    None => {
+                        let remote_id = buffer.read(cx).remote_id();
+                        self.local_buffer_ids_by_entry_id
+                            .insert(file.entry_id, remote_id);
+
+                        self.local_buffer_ids_by_path.insert(
+                            ProjectPath {
+                                worktree_id: file.worktree_id(cx),
+                                path: file.path.clone(),
+                            },
+                            remote_id,
+                        );
+                    }
+                }
+            }
+            _ => {}
+        }
+
+        None
+    }
+
+    fn request_buffer_diff_recalculation(
+        &mut self,
+        buffer: &Model<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.buffers_needing_diff.insert(buffer.downgrade());
+        let first_insertion = self.buffers_needing_diff.len() == 1;
+
+        let settings = ProjectSettings::get_global(cx);
+        let delay = if let Some(delay) = settings.git.gutter_debounce {
+            delay
+        } else {
+            if first_insertion {
+                let this = cx.weak_model();
+                cx.defer(move |cx| {
+                    if let Some(this) = this.upgrade() {
+                        this.update(cx, |this, cx| {
+                            this.recalculate_buffer_diffs(cx).detach();
+                        });
+                    }
+                });
+            }
+            return;
+        };
+
+        const MIN_DELAY: u64 = 50;
+        let delay = delay.max(MIN_DELAY);
+        let duration = Duration::from_millis(delay);
+
+        self.git_diff_debouncer
+            .fire_new(duration, cx, move |this, cx| {
+                this.recalculate_buffer_diffs(cx)
+            });
+    }
+
+    fn recalculate_buffer_diffs(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
+        let buffers = self.buffers_needing_diff.drain().collect::<Vec<_>>();
+        cx.spawn(move |this, mut cx| async move {
+            let tasks: Vec<_> = buffers
+                .iter()
+                .filter_map(|buffer| {
+                    let buffer = buffer.upgrade()?;
+                    buffer
+                        .update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx))
+                        .ok()
+                        .flatten()
+                })
+                .collect();
+
+            futures::future::join_all(tasks).await;
+
+            this.update(&mut cx, |this, cx| {
+                if !this.buffers_needing_diff.is_empty() {
+                    this.recalculate_buffer_diffs(cx).detach();
+                } else {
+                    // TODO: Would a `ModelContext<Project>.notify()` suffice here?
+                    for buffer in buffers {
+                        if let Some(buffer) = buffer.upgrade() {
+                            buffer.update(cx, |_, cx| cx.notify());
+                        }
+                    }
+                }
+            })
+            .ok();
+        })
+    }
+
+    fn language_servers_for_worktree(
+        &self,
+        worktree_id: WorktreeId,
+    ) -> impl Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<Language>, &Arc<LanguageServer>)> {
+        self.language_server_ids
+            .iter()
+            .filter_map(move |((language_server_worktree_id, _), id)| {
+                if *language_server_worktree_id == worktree_id {
+                    if let Some(LanguageServerState::Running {
+                        adapter,
+                        language,
+                        server,
+                        ..
+                    }) = self.language_servers.get(id)
+                    {
+                        return Some((adapter, language, server));
+                    }
+                }
+                None
+            })
+    }
+
+    fn maintain_buffer_languages(
+        languages: Arc<LanguageRegistry>,
+        cx: &mut ModelContext<Project>,
+    ) -> Task<()> {
+        let mut subscription = languages.subscribe();
+        let mut prev_reload_count = languages.reload_count();
+        cx.spawn(move |project, mut cx| async move {
+            while let Some(()) = subscription.next().await {
+                if let Some(project) = project.upgrade() {
+                    // If the language registry has been reloaded, then remove and
+                    // re-assign the languages on all open buffers.
+                    let reload_count = languages.reload_count();
+                    if reload_count > prev_reload_count {
+                        prev_reload_count = reload_count;
+                        project
+                            .update(&mut cx, |this, cx| {
+                                let buffers = this
+                                    .opened_buffers
+                                    .values()
+                                    .filter_map(|b| b.upgrade())
+                                    .collect::<Vec<_>>();
+                                for buffer in buffers {
+                                    if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned()
+                                    {
+                                        this.unregister_buffer_from_language_servers(
+                                            &buffer, &f, cx,
+                                        );
+                                        buffer
+                                            .update(cx, |buffer, cx| buffer.set_language(None, cx));
+                                    }
+                                }
+                            })
+                            .ok();
+                    }
+
+                    project
+                        .update(&mut cx, |project, cx| {
+                            let mut plain_text_buffers = Vec::new();
+                            let mut buffers_with_unknown_injections = Vec::new();
+                            for buffer in project.opened_buffers.values() {
+                                if let Some(handle) = buffer.upgrade() {
+                                    let buffer = &handle.read(cx);
+                                    if buffer.language().is_none()
+                                        || buffer.language() == Some(&*language2::PLAIN_TEXT)
+                                    {
+                                        plain_text_buffers.push(handle);
+                                    } else if buffer.contains_unknown_injections() {
+                                        buffers_with_unknown_injections.push(handle);
+                                    }
+                                }
+                            }
+
+                            for buffer in plain_text_buffers {
+                                project.detect_language_for_buffer(&buffer, cx);
+                                project.register_buffer_with_language_servers(&buffer, cx);
+                            }
+
+                            for buffer in buffers_with_unknown_injections {
+                                buffer.update(cx, |buffer, cx| buffer.reparse(cx));
+                            }
+                        })
+                        .ok();
+                }
+            }
+        })
+    }
+
+    fn maintain_workspace_config(cx: &mut ModelContext<Project>) -> Task<Result<()>> {
+        let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel();
+        let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx);
+
+        let settings_observation = cx.observe_global::<SettingsStore>(move |_, _| {
+            *settings_changed_tx.borrow_mut() = ();
+        });
+
+        cx.spawn(move |this, mut cx| async move {
+            while let Some(_) = settings_changed_rx.next().await {
+                let servers: Vec<_> = this.update(&mut cx, |this, _| {
+                    this.language_servers
+                        .values()
+                        .filter_map(|state| match state {
+                            LanguageServerState::Starting(_) => None,
+                            LanguageServerState::Running {
+                                adapter, server, ..
+                            } => Some((adapter.clone(), server.clone())),
+                        })
+                        .collect()
+                })?;
+
+                for (adapter, server) in servers {
+                    let workspace_config =
+                        cx.update(|cx| adapter.workspace_configuration(cx))?.await;
+                    server
+                        .notify::<lsp2::notification::DidChangeConfiguration>(
+                            lsp2::DidChangeConfigurationParams {
+                                settings: workspace_config.clone(),
+                            },
+                        )
+                        .ok();
+                }
+            }
+
+            drop(settings_observation);
+            anyhow::Ok(())
+        })
+    }
+
+    fn detect_language_for_buffer(
+        &mut self,
+        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.
+        let buffer = buffer_handle.read(cx);
+        let full_path = buffer.file()?.full_path(cx);
+        let content = buffer.as_rope();
+        let new_language = self
+            .languages
+            .language_for_file(&full_path, Some(content))
+            .now_or_never()?
+            .ok()?;
+        self.set_language_for_buffer(buffer_handle, new_language, cx);
+        None
+    }
+
+    pub fn set_language_for_buffer(
+        &mut self,
+        buffer: &Model<Buffer>,
+        new_language: Arc<Language>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        buffer.update(cx, |buffer, cx| {
+            if buffer.language().map_or(true, |old_language| {
+                !Arc::ptr_eq(old_language, &new_language)
+            }) {
+                buffer.set_language(Some(new_language.clone()), cx);
+            }
+        });
+
+        let buffer_file = buffer.read(cx).file().cloned();
+        let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
+        let buffer_file = File::from_dyn(buffer_file.as_ref());
+        let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
+
+        let task_buffer = buffer.clone();
+        let prettier_installation_task =
+            self.install_default_formatters(worktree, &new_language, &settings, cx);
+        cx.spawn(move |project, mut cx| async move {
+            prettier_installation_task.await?;
+            let _ = project
+                .update(&mut cx, |project, cx| {
+                    project.prettier_instance_for_buffer(&task_buffer, cx)
+                })?
+                .await;
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+
+        if let Some(file) = buffer_file {
+            let worktree = file.worktree.clone();
+            if let Some(tree) = worktree.read(cx).as_local() {
+                self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
+            }
+        }
+    }
+
+    fn start_language_servers(
+        &mut self,
+        worktree: &Model<Worktree>,
+        worktree_path: Arc<Path>,
+        language: Arc<Language>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx));
+        let settings = language_settings(Some(&language), root_file.map(|f| f as _).as_ref(), cx);
+        if !settings.enable_language_server {
+            return;
+        }
+
+        let worktree_id = worktree.read(cx).id();
+        for adapter in language.lsp_adapters() {
+            self.start_language_server(
+                worktree_id,
+                worktree_path.clone(),
+                adapter.clone(),
+                language.clone(),
+                cx,
+            );
+        }
+    }
+
+    fn start_language_server(
+        &mut self,
+        worktree_id: WorktreeId,
+        worktree_path: Arc<Path>,
+        adapter: Arc<CachedLspAdapter>,
+        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,
+            ProjectLspAdapterDelegate::new(self, cx),
+            cx,
+        ) {
+            Some(pending_server) => pending_server,
+            None => return,
+        };
+
+        let project_settings = ProjectSettings::get_global(cx);
+        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({
+            let adapter = adapter.clone();
+            let server_name = adapter.name.0.clone();
+            let language = language.clone();
+            let key = key.clone();
+
+            cx.spawn(move |this, mut cx| async move {
+                let result = Self::setup_and_insert_language_server(
+                    this.clone(),
+                    initialization_options,
+                    pending_server,
+                    adapter.clone(),
+                    language.clone(),
+                    server_id,
+                    key,
+                    &mut cx,
+                )
+                .await;
+
+                match result {
+                    Ok(server) => {
+                        stderr_capture.lock().take();
+                        server
+                    }
+
+                    Err(err) => {
+                        log::error!("failed to start language server {server_name:?}: {err}");
+                        log::error!("server stderr: {:?}", stderr_capture.lock().take());
+
+                        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
+                    }
+                }
+            })
+        });
+
+        self.language_servers.insert(server_id, state);
+        self.language_server_ids.insert(key, server_id);
+    }
+
+    fn reinstall_language_server(
+        &mut self,
+        language: Arc<Language>,
+        adapter: Arc<CachedLspAdapter>,
+        server_id: LanguageServerId,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<()>> {
+        log::info!("beginning to reinstall server");
+
+        let existing_server = match self.language_servers.remove(&server_id) {
+            Some(LanguageServerState::Running { server, .. }) => Some(server),
+            _ => None,
+        };
+
+        for worktree in &self.worktrees {
+            if let Some(worktree) = worktree.upgrade() {
+                let key = (worktree.read(cx).id(), adapter.name.clone());
+                self.language_server_ids.remove(&key);
+            }
+        }
+
+        Some(cx.spawn(move |this, mut cx| async move {
+            if let Some(task) = existing_server.and_then(|server| server.shutdown()) {
+                log::info!("shutting down existing server");
+                task.await;
+            }
+
+            // TODO: This is race-safe with regards to preventing new instances from
+            // starting while deleting, but existing instances in other projects are going
+            // to be very confused and messed up
+            let Some(task) = this
+                .update(&mut cx, |this, cx| {
+                    this.languages.delete_server_container(adapter.clone(), cx)
+                })
+                .log_err()
+            else {
+                return;
+            };
+            task.await;
+
+            this.update(&mut cx, |this, mut cx| {
+                let worktrees = this.worktrees.clone();
+                for worktree in worktrees {
+                    let worktree = match worktree.upgrade() {
+                        Some(worktree) => worktree.read(cx),
+                        None => continue,
+                    };
+                    let worktree_id = worktree.id();
+                    let root_path = worktree.abs_path();
+
+                    this.start_language_server(
+                        worktree_id,
+                        root_path,
+                        adapter.clone(),
+                        language.clone(),
+                        &mut cx,
+                    );
+                }
+            })
+            .ok();
+        }))
+    }
+
+    async fn setup_and_insert_language_server(
+        this: WeakModel<Self>,
+        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 language_server = Self::setup_pending_language_server(
+            this.clone(),
+            initialization_options,
+            pending_server,
+            adapter.clone(),
+            server_id,
+            cx,
+        )
+        .await?;
+
+        let this = match this.upgrade() {
+            Some(this) => this,
+            None => return Err(anyhow!("failed to upgrade project handle")),
+        };
+
+        this.update(cx, |this, cx| {
+            this.insert_newly_running_language_server(
+                language,
+                adapter,
+                language_server.clone(),
+                server_id,
+                key,
+                cx,
+            )
+        })??;
+
+        Ok(Some(language_server))
+    }
+
+    async fn setup_pending_language_server(
+        this: WeakModel<Self>,
+        initialization_options: Option<serde_json::Value>,
+        pending_server: PendingLanguageServer,
+        adapter: Arc<CachedLspAdapter>,
+        server_id: LanguageServerId,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Arc<LanguageServer>> {
+        let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx))?.await;
+        let language_server = pending_server.task.await?;
+
+        language_server
+            .on_notification::<lsp2::notification::PublishDiagnostics, _>({
+                let adapter = adapter.clone();
+                let this = this.clone();
+                move |mut params, mut cx| {
+                    let adapter = adapter.clone();
+                    if let Some(this) = this.upgrade() {
+                        adapter.process_diagnostics(&mut params);
+                        this.update(&mut cx, |this, cx| {
+                            this.update_diagnostics(
+                                server_id,
+                                params,
+                                &adapter.disk_based_diagnostic_sources,
+                                cx,
+                            )
+                            .log_err();
+                        })
+                        .ok();
+                    }
+                }
+            })
+            .detach();
+
+        language_server
+            .on_request::<lsp2::request::WorkspaceConfiguration, _, _>({
+                let adapter = adapter.clone();
+                move |params, cx| {
+                    let adapter = adapter.clone();
+                    async move {
+                        let workspace_config =
+                            cx.update(|cx| adapter.workspace_configuration(cx))?.await;
+                        Ok(params
+                            .items
+                            .into_iter()
+                            .map(|item| {
+                                if let Some(section) = &item.section {
+                                    workspace_config
+                                        .get(section)
+                                        .cloned()
+                                        .unwrap_or(serde_json::Value::Null)
+                                } else {
+                                    workspace_config.clone()
+                                }
+                            })
+                            .collect())
+                    }
+                }
+            })
+            .detach();
+
+        // Even though we don't have handling for these requests, respond to them to
+        // avoid stalling any language server like `gopls` which waits for a response
+        // to these requests when initializing.
+        language_server
+            .on_request::<lsp2::request::WorkDoneProgressCreate, _, _>({
+                let this = this.clone();
+                move |params, mut cx| {
+                    let this = this.clone();
+                    async move {
+                        this.update(&mut cx, |this, _| {
+                            if let Some(status) = this.language_server_statuses.get_mut(&server_id)
+                            {
+                                if let lsp2::NumberOrString::String(token) = params.token {
+                                    status.progress_tokens.insert(token);
+                                }
+                            }
+                        })?;
+
+                        Ok(())
+                    }
+                }
+            })
+            .detach();
+
+        language_server
+            .on_request::<lsp2::request::RegisterCapability, _, _>({
+                let this = this.clone();
+                move |params, mut cx| {
+                    let this = this.clone();
+                    async move {
+                        for reg in params.registrations {
+                            if reg.method == "workspace/didChangeWatchedFiles" {
+                                if let Some(options) = reg.register_options {
+                                    let options = serde_json::from_value(options)?;
+                                    this.update(&mut cx, |this, cx| {
+                                        this.on_lsp_did_change_watched_files(
+                                            server_id, options, cx,
+                                        );
+                                    })?;
+                                }
+                            }
+                        }
+                        Ok(())
+                    }
+                }
+            })
+            .detach();
+
+        language_server
+            .on_request::<lsp2::request::ApplyWorkspaceEdit, _, _>({
+                let adapter = adapter.clone();
+                let this = this.clone();
+                move |params, cx| {
+                    Self::on_lsp_workspace_edit(
+                        this.clone(),
+                        params,
+                        server_id,
+                        adapter.clone(),
+                        cx,
+                    )
+                }
+            })
+            .detach();
+
+        language_server
+            .on_request::<lsp2::request::InlayHintRefreshRequest, _, _>({
+                let this = this.clone();
+                move |(), mut cx| {
+                    let this = this.clone();
+                    async move {
+                        this.update(&mut cx, |project, cx| {
+                            cx.emit(Event::RefreshInlayHints);
+                            project.remote_id().map(|project_id| {
+                                project.client.send(proto::RefreshInlayHints { project_id })
+                            })
+                        })?
+                        .transpose()?;
+                        Ok(())
+                    }
+                }
+            })
+            .detach();
+
+        let disk_based_diagnostics_progress_token =
+            adapter.disk_based_diagnostics_progress_token.clone();
+
+        language_server
+            .on_notification::<lsp2::notification::Progress, _>(move |params, mut cx| {
+                if let Some(this) = this.upgrade() {
+                    this.update(&mut cx, |this, cx| {
+                        this.on_lsp_progress(
+                            params,
+                            server_id,
+                            disk_based_diagnostics_progress_token.clone(),
+                            cx,
+                        );
+                    })
+                    .ok();
+                }
+            })
+            .detach();
+
+        let language_server = language_server.initialize(initialization_options).await?;
+
+        language_server
+            .notify::<lsp2::notification::DidChangeConfiguration>(
+                lsp2::DidChangeConfigurationParams {
+                    settings: workspace_config,
+                },
+            )
+            .ok();
+
+        Ok(language_server)
+    }
+
+    fn insert_newly_running_language_server(
+        &mut self,
+        language: Arc<Language>,
+        adapter: Arc<CachedLspAdapter>,
+        language_server: Arc<LanguageServer>,
+        server_id: LanguageServerId,
+        key: (WorktreeId, LanguageServerName),
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        // If the language server for this key doesn't match the server id, don't store the
+        // server. Which will cause it to be dropped, killing the process
+        if self
+            .language_server_ids
+            .get(&key)
+            .map(|id| id != &server_id)
+            .unwrap_or(false)
+        {
+            return Ok(());
+        }
+
+        // Update language_servers collection with Running variant of LanguageServerState
+        // indicating that the server is up and running and ready
+        self.language_servers.insert(
+            server_id,
+            LanguageServerState::Running {
+                adapter: adapter.clone(),
+                language: language.clone(),
+                watched_paths: Default::default(),
+                server: language_server.clone(),
+                simulate_disk_based_diagnostics_completion: None,
+            },
+        );
+
+        self.language_server_statuses.insert(
+            server_id,
+            LanguageServerStatus {
+                name: language_server.name().to_string(),
+                pending_work: Default::default(),
+                has_pending_diagnostic_updates: false,
+                progress_tokens: Default::default(),
+            },
+        );
+
+        cx.emit(Event::LanguageServerAdded(server_id));
+
+        if let Some(project_id) = self.remote_id() {
+            self.client.send(proto::StartLanguageServer {
+                project_id,
+                server: Some(proto::LanguageServer {
+                    id: server_id.0 as u64,
+                    name: language_server.name().to_string(),
+                }),
+            })?;
+        }
+
+        // Tell the language server about every open buffer in the worktree that matches the language.
+        for buffer in self.opened_buffers.values() {
+            if let Some(buffer_handle) = buffer.upgrade() {
+                let buffer = buffer_handle.read(cx);
+                let file = match File::from_dyn(buffer.file()) {
+                    Some(file) => file,
+                    None => continue,
+                };
+                let language = match buffer.language() {
+                    Some(language) => language,
+                    None => continue,
+                };
+
+                if file.worktree.read(cx).id() != key.0
+                    || !language.lsp_adapters().iter().any(|a| a.name == key.1)
+                {
+                    continue;
+                }
+
+                let file = match file.as_local() {
+                    Some(file) => file,
+                    None => continue,
+                };
+
+                let versions = self
+                    .buffer_snapshots
+                    .entry(buffer.remote_id())
+                    .or_default()
+                    .entry(server_id)
+                    .or_insert_with(|| {
+                        vec![LspBufferSnapshot {
+                            version: 0,
+                            snapshot: buffer.text_snapshot(),
+                        }]
+                    });
+
+                let snapshot = versions.last().unwrap();
+                let version = snapshot.version;
+                let initial_snapshot = &snapshot.snapshot;
+                let uri = lsp2::Url::from_file_path(file.abs_path(cx)).unwrap();
+                language_server.notify::<lsp2::notification::DidOpenTextDocument>(
+                    lsp2::DidOpenTextDocumentParams {
+                        text_document: lsp2::TextDocumentItem::new(
+                            uri,
+                            adapter
+                                .language_ids
+                                .get(language.name().as_ref())
+                                .cloned()
+                                .unwrap_or_default(),
+                            version,
+                            initial_snapshot.text(),
+                        ),
+                    },
+                )?;
+
+                buffer_handle.update(cx, |buffer, cx| {
+                    buffer.set_completion_triggers(
+                        language_server
+                            .capabilities()
+                            .completion_provider
+                            .as_ref()
+                            .and_then(|provider| provider.trigger_characters.clone())
+                            .unwrap_or_default(),
+                        cx,
+                    )
+                });
+            }
+        }
+
+        cx.notify();
+        Ok(())
+    }
+
+    // Returns a list of all of the worktrees which no longer have a language server and the root path
+    // for the stopped server
+    fn stop_language_server(
+        &mut self,
+        worktree_id: WorktreeId,
+        adapter_name: LanguageServerName,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<(Option<PathBuf>, Vec<WorktreeId>)> {
+        let key = (worktree_id, adapter_name);
+        if let Some(server_id) = self.language_server_ids.remove(&key) {
+            log::info!("stopping language server {}", key.1 .0);
+
+            // Remove other entries for this language server as well
+            let mut orphaned_worktrees = vec![worktree_id];
+            let other_keys = self.language_server_ids.keys().cloned().collect::<Vec<_>>();
+            for other_key in other_keys {
+                if self.language_server_ids.get(&other_key) == Some(&server_id) {
+                    self.language_server_ids.remove(&other_key);
+                    orphaned_worktrees.push(other_key.0);
+                }
+            }
+
+            for buffer in self.opened_buffers.values() {
+                if let Some(buffer) = buffer.upgrade() {
+                    buffer.update(cx, |buffer, cx| {
+                        buffer.update_diagnostics(server_id, Default::default(), cx);
+                    });
+                }
+            }
+            for worktree in &self.worktrees {
+                if let Some(worktree) = worktree.upgrade() {
+                    worktree.update(cx, |worktree, cx| {
+                        if let Some(worktree) = worktree.as_local_mut() {
+                            worktree.clear_diagnostics_for_language_server(server_id, cx);
+                        }
+                    });
+                }
+            }
+
+            self.language_server_statuses.remove(&server_id);
+            cx.notify();
+
+            let server_state = self.language_servers.remove(&server_id);
+            cx.emit(Event::LanguageServerRemoved(server_id));
+            cx.spawn(move |this, mut cx| async move {
+                let mut root_path = None;
+
+                let server = match server_state {
+                    Some(LanguageServerState::Starting(task)) => task.await,
+                    Some(LanguageServerState::Running { server, .. }) => Some(server),
+                    None => None,
+                };
+
+                if let Some(server) = server {
+                    root_path = Some(server.root_path().clone());
+                    if let Some(shutdown) = server.shutdown() {
+                        shutdown.await;
+                    }
+                }
+
+                if let Some(this) = this.upgrade() {
+                    this.update(&mut cx, |this, cx| {
+                        this.language_server_statuses.remove(&server_id);
+                        cx.notify();
+                    })
+                    .ok();
+                }
+
+                (root_path, orphaned_worktrees)
+            })
+        } else {
+            Task::ready((None, Vec::new()))
+        }
+    }
+
+    pub fn restart_language_servers_for_buffers(
+        &mut self,
+        buffers: impl IntoIterator<Item = Model<Buffer>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<()> {
+        let language_server_lookup_info: HashSet<(Model<Worktree>, Arc<Language>)> = buffers
+            .into_iter()
+            .filter_map(|buffer| {
+                let buffer = buffer.read(cx);
+                let file = File::from_dyn(buffer.file())?;
+                let full_path = file.full_path(cx);
+                let language = self
+                    .languages
+                    .language_for_file(&full_path, Some(buffer.as_rope()))
+                    .now_or_never()?
+                    .ok()?;
+                Some((file.worktree.clone(), language))
+            })
+            .collect();
+        for (worktree, language) in language_server_lookup_info {
+            self.restart_language_servers(worktree, language, cx);
+        }
+
+        None
+    }
+
+    // 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: Model<Worktree>,
+        language: Arc<Language>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let worktree_id = worktree.read(cx).id();
+        let fallback_path = worktree.read(cx).abs_path();
+
+        let mut stops = Vec::new();
+        for adapter in language.lsp_adapters() {
+            stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx));
+        }
+
+        if stops.is_empty() {
+            return;
+        }
+        let mut stops = stops.into_iter();
+
+        cx.spawn(move |this, mut cx| async move {
+            let (original_root_path, mut orphaned_worktrees) = stops.next().unwrap().await;
+            for stop in stops {
+                let (_, worktrees) = stop.await;
+                orphaned_worktrees.extend_from_slice(&worktrees);
+            }
+
+            let this = match this.upgrade() {
+                Some(this) => this,
+                None => return,
+            };
+
+            this.update(&mut cx, |this, cx| {
+                // Attempt to restart using original server path. Fallback to passed in
+                // path if we could not retrieve the root path
+                let root_path = original_root_path
+                    .map(|path_buf| Arc::from(path_buf.as_path()))
+                    .unwrap_or(fallback_path);
+
+                this.start_language_servers(&worktree, root_path, language.clone(), cx);
+
+                // Lookup new server ids and set them for each of the orphaned worktrees
+                for adapter in language.lsp_adapters() {
+                    if let Some(new_server_id) = this
+                        .language_server_ids
+                        .get(&(worktree_id, adapter.name.clone()))
+                        .cloned()
+                    {
+                        for &orphaned_worktree in &orphaned_worktrees {
+                            this.language_server_ids
+                                .insert((orphaned_worktree, adapter.name.clone()), new_server_id);
+                        }
+                    }
+                }
+            })
+            .ok();
+        })
+        .detach();
+    }
+
+    fn check_errored_server(
+        language: Arc<Language>,
+        adapter: Arc<CachedLspAdapter>,
+        server_id: LanguageServerId,
+        installation_test_binary: Option<LanguageServerBinary>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if !adapter.can_be_reinstalled() {
+            log::info!(
+                "Validation check requested for {:?} but it cannot be reinstalled",
+                adapter.name.0
+            );
+            return;
+        }
+
+        cx.spawn(move |this, mut cx| async move {
+            log::info!("About to spawn test binary");
+
+            // A lack of test binary counts as a failure
+            let process = installation_test_binary.and_then(|binary| {
+                smol::process::Command::new(&binary.path)
+                    .current_dir(&binary.path)
+                    .args(binary.arguments)
+                    .stdin(Stdio::piped())
+                    .stdout(Stdio::piped())
+                    .stderr(Stdio::inherit())
+                    .kill_on_drop(true)
+                    .spawn()
+                    .ok()
+            });
+
+            const PROCESS_TIMEOUT: Duration = Duration::from_secs(5);
+            let mut timeout = cx.executor().timer(PROCESS_TIMEOUT).fuse();
+
+            let mut errored = false;
+            if let Some(mut process) = process {
+                futures::select! {
+                    status = process.status().fuse() => match status {
+                        Ok(status) => errored = !status.success(),
+                        Err(_) => errored = true,
+                    },
+
+                    _ = timeout => {
+                        log::info!("test binary time-ed out, this counts as a success");
+                        _ = process.kill();
+                    }
+                }
+            } else {
+                log::warn!("test binary failed to launch");
+                errored = true;
+            }
+
+            if errored {
+                log::warn!("test binary check failed");
+                let task = this
+                    .update(&mut cx, move |this, mut cx| {
+                        this.reinstall_language_server(language, adapter, server_id, &mut cx)
+                    })
+                    .ok()
+                    .flatten();
+
+                if let Some(task) = task {
+                    task.await;
+                }
+            }
+        })
+        .detach();
+    }
+
+    fn on_lsp_progress(
+        &mut self,
+        progress: lsp2::ProgressParams,
+        language_server_id: LanguageServerId,
+        disk_based_diagnostics_progress_token: Option<String>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let token = match progress.token {
+            lsp2::NumberOrString::String(token) => token,
+            lsp2::NumberOrString::Number(token) => {
+                log::info!("skipping numeric progress token {}", token);
+                return;
+            }
+        };
+        let lsp2::ProgressParamsValue::WorkDone(progress) = progress.value;
+        let language_server_status =
+            if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+                status
+            } else {
+                return;
+            };
+
+        if !language_server_status.progress_tokens.contains(&token) {
+            return;
+        }
+
+        let is_disk_based_diagnostics_progress = disk_based_diagnostics_progress_token
+            .as_ref()
+            .map_or(false, |disk_based_token| {
+                token.starts_with(disk_based_token)
+            });
+
+        match progress {
+            lsp2::WorkDoneProgress::Begin(report) => {
+                if is_disk_based_diagnostics_progress {
+                    language_server_status.has_pending_diagnostic_updates = true;
+                    self.disk_based_diagnostics_started(language_server_id, cx);
+                    self.buffer_ordered_messages_tx
+                        .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
+                            language_server_id,
+                            message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(Default::default())
+                        })
+                        .ok();
+                } else {
+                    self.on_lsp_work_start(
+                        language_server_id,
+                        token.clone(),
+                        LanguageServerProgress {
+                            message: report.message.clone(),
+                            percentage: report.percentage.map(|p| p as usize),
+                            last_update_at: Instant::now(),
+                        },
+                        cx,
+                    );
+                    self.buffer_ordered_messages_tx
+                        .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
+                            language_server_id,
+                            message: proto::update_language_server::Variant::WorkStart(
+                                proto::LspWorkStart {
+                                    token,
+                                    message: report.message,
+                                    percentage: report.percentage.map(|p| p as u32),
+                                },
+                            ),
+                        })
+                        .ok();
+                }
+            }
+            lsp2::WorkDoneProgress::Report(report) => {
+                if !is_disk_based_diagnostics_progress {
+                    self.on_lsp_work_progress(
+                        language_server_id,
+                        token.clone(),
+                        LanguageServerProgress {
+                            message: report.message.clone(),
+                            percentage: report.percentage.map(|p| p as usize),
+                            last_update_at: Instant::now(),
+                        },
+                        cx,
+                    );
+                    self.buffer_ordered_messages_tx
+                        .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
+                            language_server_id,
+                            message: proto::update_language_server::Variant::WorkProgress(
+                                proto::LspWorkProgress {
+                                    token,
+                                    message: report.message,
+                                    percentage: report.percentage.map(|p| p as u32),
+                                },
+                            ),
+                        })
+                        .ok();
+                }
+            }
+            lsp2::WorkDoneProgress::End(_) => {
+                language_server_status.progress_tokens.remove(&token);
+
+                if is_disk_based_diagnostics_progress {
+                    language_server_status.has_pending_diagnostic_updates = false;
+                    self.disk_based_diagnostics_finished(language_server_id, cx);
+                    self.buffer_ordered_messages_tx
+                        .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
+                            language_server_id,
+                            message:
+                                proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
+                                    Default::default(),
+                                ),
+                        })
+                        .ok();
+                } else {
+                    self.on_lsp_work_end(language_server_id, token.clone(), cx);
+                    self.buffer_ordered_messages_tx
+                        .unbounded_send(BufferOrderedMessage::LanguageServerUpdate {
+                            language_server_id,
+                            message: proto::update_language_server::Variant::WorkEnd(
+                                proto::LspWorkEnd { token },
+                            ),
+                        })
+                        .ok();
+                }
+            }
+        }
+    }
+
+    fn on_lsp_work_start(
+        &mut self,
+        language_server_id: LanguageServerId,
+        token: String,
+        progress: LanguageServerProgress,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+            status.pending_work.insert(token, progress);
+            cx.notify();
+        }
+    }
+
+    fn on_lsp_work_progress(
+        &mut self,
+        language_server_id: LanguageServerId,
+        token: String,
+        progress: LanguageServerProgress,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+            let entry = status
+                .pending_work
+                .entry(token)
+                .or_insert(LanguageServerProgress {
+                    message: Default::default(),
+                    percentage: Default::default(),
+                    last_update_at: progress.last_update_at,
+                });
+            if progress.message.is_some() {
+                entry.message = progress.message;
+            }
+            if progress.percentage.is_some() {
+                entry.percentage = progress.percentage;
+            }
+            entry.last_update_at = progress.last_update_at;
+            cx.notify();
+        }
+    }
+
+    fn on_lsp_work_end(
+        &mut self,
+        language_server_id: LanguageServerId,
+        token: String,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+            cx.emit(Event::RefreshInlayHints);
+            status.pending_work.remove(&token);
+            cx.notify();
+        }
+    }
+
+    fn on_lsp_did_change_watched_files(
+        &mut self,
+        language_server_id: LanguageServerId,
+        params: DidChangeWatchedFilesRegistrationOptions,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(LanguageServerState::Running { watched_paths, .. }) =
+            self.language_servers.get_mut(&language_server_id)
+        {
+            let mut builders = HashMap::default();
+            for watcher in params.watchers {
+                for worktree in &self.worktrees {
+                    if let Some(worktree) = worktree.upgrade() {
+                        let glob_is_inside_worktree = worktree.update(cx, |tree, _| {
+                            if let Some(abs_path) = tree.abs_path().to_str() {
+                                let relative_glob_pattern = match &watcher.glob_pattern {
+                                    lsp2::GlobPattern::String(s) => s
+                                        .strip_prefix(abs_path)
+                                        .and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR)),
+                                    lsp2::GlobPattern::Relative(rp) => {
+                                        let base_uri = match &rp.base_uri {
+                                            lsp2::OneOf::Left(workspace_folder) => {
+                                                &workspace_folder.uri
+                                            }
+                                            lsp2::OneOf::Right(base_uri) => base_uri,
+                                        };
+                                        base_uri.to_file_path().ok().and_then(|file_path| {
+                                            (file_path.to_str() == Some(abs_path))
+                                                .then_some(rp.pattern.as_str())
+                                        })
+                                    }
+                                };
+                                if let Some(relative_glob_pattern) = relative_glob_pattern {
+                                    let literal_prefix =
+                                        glob_literal_prefix(&relative_glob_pattern);
+                                    tree.as_local_mut()
+                                        .unwrap()
+                                        .add_path_prefix_to_scan(Path::new(literal_prefix).into());
+                                    if let Some(glob) = Glob::new(relative_glob_pattern).log_err() {
+                                        builders
+                                            .entry(tree.id())
+                                            .or_insert_with(|| GlobSetBuilder::new())
+                                            .add(glob);
+                                    }
+                                    return true;
+                                }
+                            }
+                            false
+                        });
+                        if glob_is_inside_worktree {
+                            break;
+                        }
+                    }
+                }
+            }
+
+            watched_paths.clear();
+            for (worktree_id, builder) in builders {
+                if let Ok(globset) = builder.build() {
+                    watched_paths.insert(worktree_id, globset);
+                }
+            }
+
+            cx.notify();
+        }
+    }
+
+    async fn on_lsp_workspace_edit(
+        this: WeakModel<Self>,
+        params: lsp2::ApplyWorkspaceEditParams,
+        server_id: LanguageServerId,
+        adapter: Arc<CachedLspAdapter>,
+        mut cx: AsyncAppContext,
+    ) -> Result<lsp2::ApplyWorkspaceEditResponse> {
+        let this = this
+            .upgrade()
+            .ok_or_else(|| anyhow!("project project closed"))?;
+        let language_server = this
+            .update(&mut cx, |this, _| this.language_server_for_id(server_id))?
+            .ok_or_else(|| anyhow!("language server not found"))?;
+        let transaction = Self::deserialize_workspace_edit(
+            this.clone(),
+            params.edit,
+            true,
+            adapter.clone(),
+            language_server.clone(),
+            &mut cx,
+        )
+        .await
+        .log_err();
+        this.update(&mut cx, |this, _| {
+            if let Some(transaction) = transaction {
+                this.last_workspace_edits_by_language_server
+                    .insert(server_id, transaction);
+            }
+        })?;
+        Ok(lsp2::ApplyWorkspaceEditResponse {
+            applied: true,
+            failed_change: None,
+            failure_reason: None,
+        })
+    }
+
+    pub fn language_server_statuses(
+        &self,
+    ) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
+        self.language_server_statuses.values()
+    }
+
+    pub fn update_diagnostics(
+        &mut self,
+        language_server_id: LanguageServerId,
+        mut params: lsp2::PublishDiagnosticsParams,
+        disk_based_sources: &[String],
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let abs_path = params
+            .uri
+            .to_file_path()
+            .map_err(|_| anyhow!("URI is not a file"))?;
+        let mut diagnostics = Vec::default();
+        let mut primary_diagnostic_group_ids = HashMap::default();
+        let mut sources_by_group_id = HashMap::default();
+        let mut supporting_diagnostics = HashMap::default();
+
+        // Ensure that primary diagnostics are always the most severe
+        params.diagnostics.sort_by_key(|item| item.severity);
+
+        for diagnostic in &params.diagnostics {
+            let source = diagnostic.source.as_ref();
+            let code = diagnostic.code.as_ref().map(|code| match code {
+                lsp2::NumberOrString::Number(code) => code.to_string(),
+                lsp2::NumberOrString::String(code) => code.clone(),
+            });
+            let range = range_from_lsp(diagnostic.range);
+            let is_supporting = diagnostic
+                .related_information
+                .as_ref()
+                .map_or(false, |infos| {
+                    infos.iter().any(|info| {
+                        primary_diagnostic_group_ids.contains_key(&(
+                            source,
+                            code.clone(),
+                            range_from_lsp(info.location.range),
+                        ))
+                    })
+                });
+
+            let is_unnecessary = diagnostic.tags.as_ref().map_or(false, |tags| {
+                tags.iter().any(|tag| *tag == DiagnosticTag::UNNECESSARY)
+            });
+
+            if is_supporting {
+                supporting_diagnostics.insert(
+                    (source, code.clone(), range),
+                    (diagnostic.severity, is_unnecessary),
+                );
+            } else {
+                let group_id = post_inc(&mut self.next_diagnostic_group_id);
+                let is_disk_based =
+                    source.map_or(false, |source| disk_based_sources.contains(source));
+
+                sources_by_group_id.insert(group_id, source);
+                primary_diagnostic_group_ids
+                    .insert((source, code.clone(), range.clone()), group_id);
+
+                diagnostics.push(DiagnosticEntry {
+                    range,
+                    diagnostic: Diagnostic {
+                        source: diagnostic.source.clone(),
+                        code: code.clone(),
+                        severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
+                        message: diagnostic.message.clone(),
+                        group_id,
+                        is_primary: true,
+                        is_valid: true,
+                        is_disk_based,
+                        is_unnecessary,
+                    },
+                });
+                if let Some(infos) = &diagnostic.related_information {
+                    for info in infos {
+                        if info.location.uri == params.uri && !info.message.is_empty() {
+                            let range = range_from_lsp(info.location.range);
+                            diagnostics.push(DiagnosticEntry {
+                                range,
+                                diagnostic: Diagnostic {
+                                    source: diagnostic.source.clone(),
+                                    code: code.clone(),
+                                    severity: DiagnosticSeverity::INFORMATION,
+                                    message: info.message.clone(),
+                                    group_id,
+                                    is_primary: false,
+                                    is_valid: true,
+                                    is_disk_based,
+                                    is_unnecessary: false,
+                                },
+                            });
+                        }
+                    }
+                }
+            }
+        }
+
+        for entry in &mut diagnostics {
+            let diagnostic = &mut entry.diagnostic;
+            if !diagnostic.is_primary {
+                let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap();
+                if let Some(&(severity, is_unnecessary)) = supporting_diagnostics.get(&(
+                    source,
+                    diagnostic.code.clone(),
+                    entry.range.clone(),
+                )) {
+                    if let Some(severity) = severity {
+                        diagnostic.severity = severity;
+                    }
+                    diagnostic.is_unnecessary = is_unnecessary;
+                }
+            }
+        }
+
+        self.update_diagnostic_entries(
+            language_server_id,
+            abs_path,
+            params.version,
+            diagnostics,
+            cx,
+        )?;
+        Ok(())
+    }
+
+    pub fn update_diagnostic_entries(
+        &mut self,
+        server_id: LanguageServerId,
+        abs_path: PathBuf,
+        version: Option<i32>,
+        diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
+        cx: &mut ModelContext<Project>,
+    ) -> Result<(), anyhow::Error> {
+        let (worktree, relative_path) = self
+            .find_local_worktree(&abs_path, cx)
+            .ok_or_else(|| anyhow!("no worktree found for diagnostics path {abs_path:?}"))?;
+
+        let project_path = ProjectPath {
+            worktree_id: worktree.read(cx).id(),
+            path: relative_path.into(),
+        };
+
+        if let Some(buffer) = self.get_open_buffer(&project_path, cx) {
+            self.update_buffer_diagnostics(&buffer, server_id, version, diagnostics.clone(), cx)?;
+        }
+
+        let updated = worktree.update(cx, |worktree, cx| {
+            worktree
+                .as_local_mut()
+                .ok_or_else(|| anyhow!("not a local worktree"))?
+                .update_diagnostics(server_id, project_path.path.clone(), diagnostics, cx)
+        })?;
+        if updated {
+            cx.emit(Event::DiagnosticsUpdated {
+                language_server_id: server_id,
+                path: project_path,
+            });
+        }
+        Ok(())
+    }
+
+    fn update_buffer_diagnostics(
+        &mut self,
+        buffer: &Model<Buffer>,
+        server_id: LanguageServerId,
+        version: Option<i32>,
+        mut diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering {
+            Ordering::Equal
+                .then_with(|| b.is_primary.cmp(&a.is_primary))
+                .then_with(|| a.is_disk_based.cmp(&b.is_disk_based))
+                .then_with(|| a.severity.cmp(&b.severity))
+                .then_with(|| a.message.cmp(&b.message))
+        }
+
+        let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx)?;
+
+        diagnostics.sort_unstable_by(|a, b| {
+            Ordering::Equal
+                .then_with(|| a.range.start.cmp(&b.range.start))
+                .then_with(|| b.range.end.cmp(&a.range.end))
+                .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic))
+        });
+
+        let mut sanitized_diagnostics = Vec::new();
+        let edits_since_save = Patch::new(
+            snapshot
+                .edits_since::<Unclipped<PointUtf16>>(buffer.read(cx).saved_version())
+                .collect(),
+        );
+        for entry in diagnostics {
+            let start;
+            let end;
+            if entry.diagnostic.is_disk_based {
+                // Some diagnostics are based on files on disk instead of buffers'
+                // current contents. Adjust these diagnostics' ranges to reflect
+                // any unsaved edits.
+                start = edits_since_save.old_to_new(entry.range.start);
+                end = edits_since_save.old_to_new(entry.range.end);
+            } else {
+                start = entry.range.start;
+                end = entry.range.end;
+            }
+
+            let mut range = snapshot.clip_point_utf16(start, Bias::Left)
+                ..snapshot.clip_point_utf16(end, Bias::Right);
+
+            // Expand empty ranges by one codepoint
+            if range.start == range.end {
+                // This will be go to the next boundary when being clipped
+                range.end.column += 1;
+                range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right);
+                if range.start == range.end && range.end.column > 0 {
+                    range.start.column -= 1;
+                    range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Left);
+                }
+            }
+
+            sanitized_diagnostics.push(DiagnosticEntry {
+                range,
+                diagnostic: entry.diagnostic,
+            });
+        }
+        drop(edits_since_save);
+
+        let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot);
+        buffer.update(cx, |buffer, cx| {
+            buffer.update_diagnostics(server_id, set, cx)
+        });
+        Ok(())
+    }
+
+    pub fn reload_buffers(
+        &self,
+        buffers: HashSet<Model<Buffer>>,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ProjectTransaction>> {
+        let mut local_buffers = Vec::new();
+        let mut remote_buffers = None;
+        for buffer_handle in buffers {
+            let buffer = buffer_handle.read(cx);
+            if buffer.is_dirty() {
+                if let Some(file) = File::from_dyn(buffer.file()) {
+                    if file.is_local() {
+                        local_buffers.push(buffer_handle);
+                    } else {
+                        remote_buffers.get_or_insert(Vec::new()).push(buffer_handle);
+                    }
+                }
+            }
+        }
+
+        let remote_buffers = self.remote_id().zip(remote_buffers);
+        let client = self.client.clone();
+
+        cx.spawn(move |this, mut cx| async move {
+            let mut project_transaction = ProjectTransaction::default();
+
+            if let Some((project_id, remote_buffers)) = remote_buffers {
+                let response = client
+                    .request(proto::ReloadBuffers {
+                        project_id,
+                        buffer_ids: remote_buffers
+                            .iter()
+                            .filter_map(|buffer| {
+                                buffer.update(&mut cx, |buffer, _| buffer.remote_id()).ok()
+                            })
+                            .collect(),
+                    })
+                    .await?
+                    .transaction
+                    .ok_or_else(|| anyhow!("missing transaction"))?;
+                project_transaction = this
+                    .update(&mut cx, |this, cx| {
+                        this.deserialize_project_transaction(response, push_to_history, cx)
+                    })?
+                    .await?;
+            }
+
+            for buffer in local_buffers {
+                let transaction = buffer
+                    .update(&mut cx, |buffer, cx| buffer.reload(cx))?
+                    .await?;
+                buffer.update(&mut cx, |buffer, cx| {
+                    if let Some(transaction) = transaction {
+                        if !push_to_history {
+                            buffer.forget_transaction(transaction.id);
+                        }
+                        project_transaction.0.insert(cx.handle(), transaction);
+                    }
+                })?;
+            }
+
+            Ok(project_transaction)
+        })
+    }
+
+    pub fn format(
+        &self,
+        buffers: HashSet<Model<Buffer>>,
+        push_to_history: bool,
+        trigger: FormatTrigger,
+        cx: &mut ModelContext<Project>,
+    ) -> Task<anyhow::Result<ProjectTransaction>> {
+        if self.is_local() {
+            let mut buffers_with_paths_and_servers = buffers
+                .into_iter()
+                .filter_map(|buffer_handle| {
+                    let buffer = buffer_handle.read(cx);
+                    let file = File::from_dyn(buffer.file())?;
+                    let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
+                    let server = self
+                        .primary_language_server_for_buffer(buffer, cx)
+                        .map(|s| s.1.clone());
+                    Some((buffer_handle, buffer_abs_path, server))
+                })
+                .collect::<Vec<_>>();
+
+            cx.spawn(move |this, mut cx| async move {
+                // Do not allow multiple concurrent formatting requests for the
+                // same buffer.
+                this.update(&mut cx, |this, cx| {
+                    buffers_with_paths_and_servers.retain(|(buffer, _, _)| {
+                        this.buffers_being_formatted
+                            .insert(buffer.read(cx).remote_id())
+                    });
+                })?;
+
+                let _cleanup = defer({
+                    let this = this.clone();
+                    let mut cx = cx.clone();
+                    let buffers = &buffers_with_paths_and_servers;
+                    move || {
+                        this.update(&mut cx, |this, cx| {
+                            for (buffer, _, _) in buffers {
+                                this.buffers_being_formatted
+                                    .remove(&buffer.read(cx).remote_id());
+                            }
+                        }).ok();
+                    }
+                });
+
+                let mut project_transaction = ProjectTransaction::default();
+                for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
+                    let settings = buffer.update(&mut cx, |buffer, cx| {
+                        language_settings(buffer.language(), buffer.file(), cx).clone()
+                    })?;
+
+                    let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
+                    let ensure_final_newline = settings.ensure_final_newline_on_save;
+                    let format_on_save = settings.format_on_save.clone();
+                    let formatter = settings.formatter.clone();
+                    let tab_size = settings.tab_size;
+
+                    // First, format buffer's whitespace according to the settings.
+                    let trailing_whitespace_diff = if remove_trailing_whitespace {
+                        Some(
+                            buffer
+                                .update(&mut cx, |b, cx| b.remove_trailing_whitespace(cx))?
+                                .await,
+                        )
+                    } else {
+                        None
+                    };
+                    let whitespace_transaction_id = buffer.update(&mut cx, |buffer, cx| {
+                        buffer.finalize_last_transaction();
+                        buffer.start_transaction();
+                        if let Some(diff) = trailing_whitespace_diff {
+                            buffer.apply_diff(diff, cx);
+                        }
+                        if ensure_final_newline {
+                            buffer.ensure_final_newline(cx);
+                        }
+                        buffer.end_transaction(cx)
+                    })?;
+
+                    // Currently, formatting operations are represented differently depending on
+                    // whether they come from a language server or an external command.
+                    enum FormatOperation {
+                        Lsp(Vec<(Range<Anchor>, String)>),
+                        External(Diff),
+                        Prettier(Diff),
+                    }
+
+                    // Apply language-specific formatting using either a language server
+                    // or external command.
+                    let mut format_operation = None;
+                    match (formatter, format_on_save) {
+                        (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
+
+                        (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
+                        | (_, FormatOnSave::LanguageServer) => {
+                            if let Some((language_server, buffer_abs_path)) =
+                                language_server.as_ref().zip(buffer_abs_path.as_ref())
+                            {
+                                format_operation = Some(FormatOperation::Lsp(
+                                    Self::format_via_lsp(
+                                        &this,
+                                        &buffer,
+                                        buffer_abs_path,
+                                        &language_server,
+                                        tab_size,
+                                        &mut cx,
+                                    )
+                                    .await
+                                    .context("failed to format via language server")?,
+                                ));
+                            }
+                        }
+
+                        (
+                            Formatter::External { command, arguments },
+                            FormatOnSave::On | FormatOnSave::Off,
+                        )
+                        | (_, FormatOnSave::External { command, arguments }) => {
+                            if let Some(buffer_abs_path) = buffer_abs_path {
+                                format_operation = Self::format_via_external_command(
+                                    buffer,
+                                    buffer_abs_path,
+                                    &command,
+                                    &arguments,
+                                    &mut cx,
+                                )
+                                .await
+                                .context(format!(
+                                    "failed to format via external command {:?}",
+                                    command
+                                ))?
+                                .map(FormatOperation::External);
+                            }
+                        }
+                        (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
+                            if let Some(prettier_task) = this
+                                .update(&mut cx, |project, cx| {
+                                    project.prettier_instance_for_buffer(buffer, cx)
+                                })?.await {
+                                    match prettier_task.await
+                                    {
+                                        Ok(prettier) => {
+                                            let buffer_path = buffer.update(&mut cx, |buffer, cx| {
+                                                File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
+                                            })?;
+                                            format_operation = Some(FormatOperation::Prettier(
+                                                prettier
+                                                    .format(buffer, buffer_path, &mut cx)
+                                                    .await
+                                                    .context("formatting via prettier")?,
+                                            ));
+                                        }
+                                        Err(e) => anyhow::bail!(
+                                            "Failed to create prettier instance for buffer during autoformatting: {e:#}"
+                                        ),
+                                    }
+                            } else if let Some((language_server, buffer_abs_path)) =
+                                language_server.as_ref().zip(buffer_abs_path.as_ref())
+                            {
+                                format_operation = Some(FormatOperation::Lsp(
+                                    Self::format_via_lsp(
+                                        &this,
+                                        &buffer,
+                                        buffer_abs_path,
+                                        &language_server,
+                                        tab_size,
+                                        &mut cx,
+                                    )
+                                    .await
+                                    .context("failed to format via language server")?,
+                                ));
+                            }
+                        }
+                        (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
+                            if let Some(prettier_task) = this
+                                .update(&mut cx, |project, cx| {
+                                    project.prettier_instance_for_buffer(buffer, cx)
+                                })?.await {
+                                    match prettier_task.await
+                                    {
+                                        Ok(prettier) => {
+                                            let buffer_path = buffer.update(&mut cx, |buffer, cx| {
+                                                File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
+                                            })?;
+                                            format_operation = Some(FormatOperation::Prettier(
+                                                prettier
+                                                    .format(buffer, buffer_path, &mut cx)
+                                                    .await
+                                                    .context("formatting via prettier")?,
+                                            ));
+                                        }
+                                        Err(e) => anyhow::bail!(
+                                            "Failed to create prettier instance for buffer during formatting: {e:#}"
+                                        ),
+                                    }
+                                }
+                        }
+                    };
+
+                    buffer.update(&mut cx, |b, cx| {
+                        // If the buffer had its whitespace formatted and was edited while the language-specific
+                        // formatting was being computed, avoid applying the language-specific formatting, because
+                        // it can't be grouped with the whitespace formatting in the undo history.
+                        if let Some(transaction_id) = whitespace_transaction_id {
+                            if b.peek_undo_stack()
+                                .map_or(true, |e| e.transaction_id() != transaction_id)
+                            {
+                                format_operation.take();
+                            }
+                        }
+
+                        // Apply any language-specific formatting, and group the two formatting operations
+                        // in the buffer's undo history.
+                        if let Some(operation) = format_operation {
+                            match operation {
+                                FormatOperation::Lsp(edits) => {
+                                    b.edit(edits, None, cx);
+                                }
+                                FormatOperation::External(diff) => {
+                                    b.apply_diff(diff, cx);
+                                }
+                                FormatOperation::Prettier(diff) => {
+                                    b.apply_diff(diff, cx);
+                                }
+                            }
+
+                            if let Some(transaction_id) = whitespace_transaction_id {
+                                b.group_until_transaction(transaction_id);
+                            }
+                        }
+
+                        if let Some(transaction) = b.finalize_last_transaction().cloned() {
+                            if !push_to_history {
+                                b.forget_transaction(transaction.id);
+                            }
+                            project_transaction.0.insert(buffer.clone(), transaction);
+                        }
+                    })?;
+                }
+
+                Ok(project_transaction)
+            })
+        } else {
+            let remote_id = self.remote_id();
+            let client = self.client.clone();
+            cx.spawn(move |this, mut cx| async move {
+                let mut project_transaction = ProjectTransaction::default();
+                if let Some(project_id) = remote_id {
+                    let response = client
+                        .request(proto::FormatBuffers {
+                            project_id,
+                            trigger: trigger as i32,
+                            buffer_ids: buffers
+                                .iter()
+                                .map(|buffer| {
+                                    buffer.update(&mut cx, |buffer, _| buffer.remote_id())
+                                })
+                                .collect::<Result<_>>()?,
+                        })
+                        .await?
+                        .transaction
+                        .ok_or_else(|| anyhow!("missing transaction"))?;
+                    project_transaction = this
+                        .update(&mut cx, |this, cx| {
+                            this.deserialize_project_transaction(response, push_to_history, cx)
+                        })?
+                        .await?;
+                }
+                Ok(project_transaction)
+            })
+        }
+    }
+
+    async fn format_via_lsp(
+        this: &WeakModel<Self>,
+        buffer: &Model<Buffer>,
+        abs_path: &Path,
+        language_server: &Arc<LanguageServer>,
+        tab_size: NonZeroU32,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Vec<(Range<Anchor>, String)>> {
+        let uri = lsp2::Url::from_file_path(abs_path)
+            .map_err(|_| anyhow!("failed to convert abs path to uri"))?;
+        let text_document = lsp2::TextDocumentIdentifier::new(uri);
+        let capabilities = &language_server.capabilities();
+
+        let formatting_provider = capabilities.document_formatting_provider.as_ref();
+        let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref();
+
+        let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) {
+            language_server
+                .request::<lsp2::request::Formatting>(lsp2::DocumentFormattingParams {
+                    text_document,
+                    options: lsp_command::lsp_formatting_options(tab_size.get()),
+                    work_done_progress_params: Default::default(),
+                })
+                .await?
+        } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) {
+            let buffer_start = lsp2::Position::new(0, 0);
+            let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?;
+
+            language_server
+                .request::<lsp2::request::RangeFormatting>(lsp2::DocumentRangeFormattingParams {
+                    text_document,
+                    range: lsp2::Range::new(buffer_start, buffer_end),
+                    options: lsp_command::lsp_formatting_options(tab_size.get()),
+                    work_done_progress_params: Default::default(),
+                })
+                .await?
+        } else {
+            None
+        };
+
+        if let Some(lsp_edits) = lsp_edits {
+            this.update(cx, |this, cx| {
+                this.edits_from_lsp(buffer, lsp_edits, language_server.server_id(), None, cx)
+            })?
+            .await
+        } else {
+            Ok(Vec::new())
+        }
+    }
+
+    async fn format_via_external_command(
+        buffer: &Model<Buffer>,
+        buffer_abs_path: &Path,
+        command: &str,
+        arguments: &[String],
+        cx: &mut AsyncAppContext,
+    ) -> Result<Option<Diff>> {
+        let working_dir_path = buffer.update(cx, |buffer, cx| {
+            let file = File::from_dyn(buffer.file())?;
+            let worktree = file.worktree.read(cx).as_local()?;
+            let mut worktree_path = worktree.abs_path().to_path_buf();
+            if worktree.root_entry()?.is_file() {
+                worktree_path.pop();
+            }
+            Some(worktree_path)
+        })?;
+
+        if let Some(working_dir_path) = working_dir_path {
+            let mut child =
+                smol::process::Command::new(command)
+                    .args(arguments.iter().map(|arg| {
+                        arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy())
+                    }))
+                    .current_dir(&working_dir_path)
+                    .stdin(smol::process::Stdio::piped())
+                    .stdout(smol::process::Stdio::piped())
+                    .stderr(smol::process::Stdio::piped())
+                    .spawn()?;
+            let stdin = child
+                .stdin
+                .as_mut()
+                .ok_or_else(|| anyhow!("failed to acquire stdin"))?;
+            let text = buffer.update(cx, |buffer, _| buffer.as_rope().clone())?;
+            for chunk in text.chunks() {
+                stdin.write_all(chunk.as_bytes()).await?;
+            }
+            stdin.flush().await?;
+
+            let output = child.output().await?;
+            if !output.status.success() {
+                return Err(anyhow!(
+                    "command failed with exit code {:?}:\nstdout: {}\nstderr: {}",
+                    output.status.code(),
+                    String::from_utf8_lossy(&output.stdout),
+                    String::from_utf8_lossy(&output.stderr),
+                ));
+            }
+
+            let stdout = String::from_utf8(output.stdout)?;
+            Ok(Some(
+                buffer
+                    .update(cx, |buffer, cx| buffer.diff(stdout, cx))?
+                    .await,
+            ))
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub fn definition<T: ToPointUtf16>(
+        &self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<LocationLink>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(
+            buffer.clone(),
+            LanguageServerToQuery::Primary,
+            GetDefinition { position },
+            cx,
+        )
+    }
+
+    pub fn type_definition<T: ToPointUtf16>(
+        &self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<LocationLink>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(
+            buffer.clone(),
+            LanguageServerToQuery::Primary,
+            GetTypeDefinition { position },
+            cx,
+        )
+    }
+
+    pub fn references<T: ToPointUtf16>(
+        &self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Location>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(
+            buffer.clone(),
+            LanguageServerToQuery::Primary,
+            GetReferences { position },
+            cx,
+        )
+    }
+
+    pub fn document_highlights<T: ToPointUtf16>(
+        &self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<DocumentHighlight>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(
+            buffer.clone(),
+            LanguageServerToQuery::Primary,
+            GetDocumentHighlights { position },
+            cx,
+        )
+    }
+
+    pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
+        if self.is_local() {
+            let mut requests = Vec::new();
+            for ((worktree_id, _), server_id) in self.language_server_ids.iter() {
+                let worktree_id = *worktree_id;
+                let worktree_handle = self.worktree_for_id(worktree_id, cx);
+                let worktree = match worktree_handle.and_then(|tree| tree.read(cx).as_local()) {
+                    Some(worktree) => worktree,
+                    None => continue,
+                };
+                let worktree_abs_path = worktree.abs_path().clone();
+
+                let (adapter, language, server) = match self.language_servers.get(server_id) {
+                    Some(LanguageServerState::Running {
+                        adapter,
+                        language,
+                        server,
+                        ..
+                    }) => (adapter.clone(), language.clone(), server),
+
+                    _ => continue,
+                };
+
+                requests.push(
+                    server
+                        .request::<lsp2::request::WorkspaceSymbolRequest>(
+                            lsp2::WorkspaceSymbolParams {
+                                query: query.to_string(),
+                                ..Default::default()
+                            },
+                        )
+                        .log_err()
+                        .map(move |response| {
+                            let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
+                                lsp2::WorkspaceSymbolResponse::Flat(flat_responses) => {
+                                    flat_responses.into_iter().map(|lsp_symbol| {
+                                        (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
+                                    }).collect::<Vec<_>>()
+                                }
+                                lsp2::WorkspaceSymbolResponse::Nested(nested_responses) => {
+                                    nested_responses.into_iter().filter_map(|lsp_symbol| {
+                                        let location = match lsp_symbol.location {
+                                            OneOf::Left(location) => location,
+                                            OneOf::Right(_) => {
+                                                error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
+                                                return None
+                                            }
+                                        };
+                                        Some((lsp_symbol.name, lsp_symbol.kind, location))
+                                    }).collect::<Vec<_>>()
+                                }
+                            }).unwrap_or_default();
+
+                            (
+                                adapter,
+                                language,
+                                worktree_id,
+                                worktree_abs_path,
+                                lsp_symbols,
+                            )
+                        }),
+                );
+            }
+
+            cx.spawn(move |this, mut cx| async move {
+                let responses = futures::future::join_all(requests).await;
+                let this = match this.upgrade() {
+                    Some(this) => this,
+                    None => return Ok(Vec::new()),
+                };
+
+                let symbols = this.update(&mut cx, |this, cx| {
+                    let mut symbols = Vec::new();
+                    for (
+                        adapter,
+                        adapter_language,
+                        source_worktree_id,
+                        worktree_abs_path,
+                        lsp_symbols,
+                    ) in responses
+                    {
+                        symbols.extend(lsp_symbols.into_iter().filter_map(
+                            |(symbol_name, symbol_kind, symbol_location)| {
+                                let abs_path = symbol_location.uri.to_file_path().ok()?;
+                                let mut worktree_id = source_worktree_id;
+                                let path;
+                                if let Some((worktree, rel_path)) =
+                                    this.find_local_worktree(&abs_path, cx)
+                                {
+                                    worktree_id = worktree.read(cx).id();
+                                    path = rel_path;
+                                } else {
+                                    path = relativize_path(&worktree_abs_path, &abs_path);
+                                }
+
+                                let project_path = ProjectPath {
+                                    worktree_id,
+                                    path: path.into(),
+                                };
+                                let signature = this.symbol_signature(&project_path);
+                                let adapter_language = adapter_language.clone();
+                                let language = this
+                                    .languages
+                                    .language_for_file(&project_path.path, None)
+                                    .unwrap_or_else(move |_| adapter_language);
+                                let language_server_name = adapter.name.clone();
+                                Some(async move {
+                                    let language = language.await;
+                                    let label =
+                                        language.label_for_symbol(&symbol_name, symbol_kind).await;
+
+                                    Symbol {
+                                        language_server_name,
+                                        source_worktree_id,
+                                        path: project_path,
+                                        label: label.unwrap_or_else(|| {
+                                            CodeLabel::plain(symbol_name.clone(), None)
+                                        }),
+                                        kind: symbol_kind,
+                                        name: symbol_name,
+                                        range: range_from_lsp(symbol_location.range),
+                                        signature,
+                                    }
+                                })
+                            },
+                        ));
+                    }
+
+                    symbols
+                })?;
+
+                Ok(futures::future::join_all(symbols).await)
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            let request = self.client.request(proto::GetProjectSymbols {
+                project_id,
+                query: query.to_string(),
+            });
+            cx.spawn(move |this, mut cx| async move {
+                let response = request.await?;
+                let mut symbols = Vec::new();
+                if let Some(this) = this.upgrade() {
+                    let new_symbols = this.update(&mut cx, |this, _| {
+                        response
+                            .symbols
+                            .into_iter()
+                            .map(|symbol| this.deserialize_symbol(symbol))
+                            .collect::<Vec<_>>()
+                    })?;
+                    symbols = futures::future::join_all(new_symbols)
+                        .await
+                        .into_iter()
+                        .filter_map(|symbol| symbol.log_err())
+                        .collect::<Vec<_>>();
+                }
+                Ok(symbols)
+            })
+        } else {
+            Task::ready(Ok(Default::default()))
+        }
+    }
+
+    pub fn open_buffer_for_symbol(
+        &mut self,
+        symbol: &Symbol,
+        cx: &mut ModelContext<Self>,
+    ) -> 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,
+                symbol.language_server_name.clone(),
+            )) {
+                *id
+            } else {
+                return Task::ready(Err(anyhow!(
+                    "language server for worktree and language not found"
+                )));
+            };
+
+            let worktree_abs_path = if let Some(worktree_abs_path) = self
+                .worktree_for_id(symbol.path.worktree_id, cx)
+                .and_then(|worktree| worktree.read(cx).as_local())
+                .map(|local_worktree| local_worktree.abs_path())
+            {
+                worktree_abs_path
+            } else {
+                return Task::ready(Err(anyhow!("worktree not found for symbol")));
+            };
+            let symbol_abs_path = worktree_abs_path.join(&symbol.path.path);
+            let symbol_uri = if let Ok(uri) = lsp2::Url::from_file_path(symbol_abs_path) {
+                uri
+            } else {
+                return Task::ready(Err(anyhow!("invalid symbol path")));
+            };
+
+            self.open_local_buffer_via_lsp(
+                symbol_uri,
+                language_server_id,
+                symbol.language_server_name.clone(),
+                cx,
+            )
+        } else if let Some(project_id) = self.remote_id() {
+            let request = self.client.request(proto::OpenBufferForSymbol {
+                project_id,
+                symbol: Some(serialize_symbol(symbol)),
+            });
+            cx.spawn(move |this, mut cx| async move {
+                let response = request.await?;
+                this.update(&mut cx, |this, cx| {
+                    this.wait_for_remote_buffer(response.buffer_id, cx)
+                })?
+                .await
+            })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
+    pub fn hover<T: ToPointUtf16>(
+        &self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Option<Hover>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(
+            buffer.clone(),
+            LanguageServerToQuery::Primary,
+            GetHover { position },
+            cx,
+        )
+    }
+
+    pub fn completions<T: ToOffset + ToPointUtf16>(
+        &self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        if self.is_local() {
+            let snapshot = buffer.read(cx).snapshot();
+            let offset = position.to_offset(&snapshot);
+            let scope = snapshot.language_scope_at(offset);
+
+            let server_ids: Vec<_> = self
+                .language_servers_for_buffer(buffer.read(cx), cx)
+                .filter(|(_, server)| server.capabilities().completion_provider.is_some())
+                .filter(|(adapter, _)| {
+                    scope
+                        .as_ref()
+                        .map(|scope| scope.language_allowed(&adapter.name))
+                        .unwrap_or(true)
+                })
+                .map(|(_, server)| server.server_id())
+                .collect();
+
+            let buffer = buffer.clone();
+            cx.spawn(move |this, mut cx| async move {
+                let mut tasks = Vec::with_capacity(server_ids.len());
+                this.update(&mut cx, |this, cx| {
+                    for server_id in server_ids {
+                        tasks.push(this.request_lsp(
+                            buffer.clone(),
+                            LanguageServerToQuery::Other(server_id),
+                            GetCompletions { position },
+                            cx,
+                        ));
+                    }
+                })?;
+
+                let mut completions = Vec::new();
+                for task in tasks {
+                    if let Ok(new_completions) = task.await {
+                        completions.extend_from_slice(&new_completions);
+                    }
+                }
+
+                Ok(completions)
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            self.send_lsp_proto_request(buffer.clone(), project_id, GetCompletions { position }, cx)
+        } else {
+            Task::ready(Ok(Default::default()))
+        }
+    }
+
+    pub fn apply_additional_edits_for_completion(
+        &self,
+        buffer_handle: Model<Buffer>,
+        completion: Completion,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Option<Transaction>>> {
+        let buffer = buffer_handle.read(cx);
+        let buffer_id = buffer.remote_id();
+
+        if self.is_local() {
+            let server_id = completion.server_id;
+            let lang_server = match self.language_server_for_buffer(buffer, server_id, cx) {
+                Some((_, server)) => server.clone(),
+                _ => return Task::ready(Ok(Default::default())),
+            };
+
+            cx.spawn(move |this, mut cx| async move {
+                let can_resolve = lang_server
+                    .capabilities()
+                    .completion_provider
+                    .as_ref()
+                    .and_then(|options| options.resolve_provider)
+                    .unwrap_or(false);
+                let additional_text_edits = if can_resolve {
+                    lang_server
+                        .request::<lsp2::request::ResolveCompletionItem>(completion.lsp_completion)
+                        .await?
+                        .additional_text_edits
+                } else {
+                    completion.lsp_completion.additional_text_edits
+                };
+                if let Some(edits) = additional_text_edits {
+                    let edits = this
+                        .update(&mut cx, |this, cx| {
+                            this.edits_from_lsp(
+                                &buffer_handle,
+                                edits,
+                                lang_server.server_id(),
+                                None,
+                                cx,
+                            )
+                        })?
+                        .await?;
+
+                    buffer_handle.update(&mut cx, |buffer, cx| {
+                        buffer.finalize_last_transaction();
+                        buffer.start_transaction();
+
+                        for (range, text) in edits {
+                            let primary = &completion.old_range;
+                            let start_within = primary.start.cmp(&range.start, buffer).is_le()
+                                && primary.end.cmp(&range.start, buffer).is_ge();
+                            let end_within = range.start.cmp(&primary.end, buffer).is_le()
+                                && range.end.cmp(&primary.end, buffer).is_ge();
+
+                            //Skip additional edits which overlap with the primary completion edit
+                            //https://github.com/zed-industries/zed/pull/1871
+                            if !start_within && !end_within {
+                                buffer.edit([(range, text)], None, cx);
+                            }
+                        }
+
+                        let transaction = if buffer.end_transaction(cx).is_some() {
+                            let transaction = buffer.finalize_last_transaction().unwrap().clone();
+                            if !push_to_history {
+                                buffer.forget_transaction(transaction.id);
+                            }
+                            Some(transaction)
+                        } else {
+                            None
+                        };
+                        Ok(transaction)
+                    })?
+                } else {
+                    Ok(None)
+                }
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            let client = self.client.clone();
+            cx.spawn(move |_, mut cx| async move {
+                let response = client
+                    .request(proto::ApplyCompletionAdditionalEdits {
+                        project_id,
+                        buffer_id,
+                        completion: Some(language2::proto::serialize_completion(&completion)),
+                    })
+                    .await?;
+
+                if let Some(transaction) = response.transaction {
+                    let transaction = language2::proto::deserialize_transaction(transaction)?;
+                    buffer_handle
+                        .update(&mut cx, |buffer, _| {
+                            buffer.wait_for_edits(transaction.edit_ids.iter().copied())
+                        })?
+                        .await?;
+                    if push_to_history {
+                        buffer_handle.update(&mut cx, |buffer, _| {
+                            buffer.push_transaction(transaction.clone(), Instant::now());
+                        })?;
+                    }
+                    Ok(Some(transaction))
+                } else {
+                    Ok(None)
+                }
+            })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
+    pub fn code_actions<T: Clone + ToOffset>(
+        &self,
+        buffer_handle: &Model<Buffer>,
+        range: Range<T>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<CodeAction>>> {
+        let buffer = buffer_handle.read(cx);
+        let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
+        self.request_lsp(
+            buffer_handle.clone(),
+            LanguageServerToQuery::Primary,
+            GetCodeActions { range },
+            cx,
+        )
+    }
+
+    pub fn apply_code_action(
+        &self,
+        buffer_handle: Model<Buffer>,
+        mut action: CodeAction,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ProjectTransaction>> {
+        if self.is_local() {
+            let buffer = buffer_handle.read(cx);
+            let (lsp_adapter, lang_server) = if let Some((adapter, server)) =
+                self.language_server_for_buffer(buffer, action.server_id, cx)
+            {
+                (adapter.clone(), server.clone())
+            } else {
+                return Task::ready(Ok(Default::default()));
+            };
+            let range = action.range.to_point_utf16(buffer);
+
+            cx.spawn(move |this, mut cx| async move {
+                if let Some(lsp_range) = action
+                    .lsp_action
+                    .data
+                    .as_mut()
+                    .and_then(|d| d.get_mut("codeActionParams"))
+                    .and_then(|d| d.get_mut("range"))
+                {
+                    *lsp_range = serde_json::to_value(&range_to_lsp(range)).unwrap();
+                    action.lsp_action = lang_server
+                        .request::<lsp2::request::CodeActionResolveRequest>(action.lsp_action)
+                        .await?;
+                } else {
+                    let actions = this
+                        .update(&mut cx, |this, cx| {
+                            this.code_actions(&buffer_handle, action.range, cx)
+                        })?
+                        .await?;
+                    action.lsp_action = actions
+                        .into_iter()
+                        .find(|a| a.lsp_action.title == action.lsp_action.title)
+                        .ok_or_else(|| anyhow!("code action is outdated"))?
+                        .lsp_action;
+                }
+
+                if let Some(edit) = action.lsp_action.edit {
+                    if edit.changes.is_some() || edit.document_changes.is_some() {
+                        return Self::deserialize_workspace_edit(
+                            this.upgrade().ok_or_else(|| anyhow!("no app present"))?,
+                            edit,
+                            push_to_history,
+                            lsp_adapter.clone(),
+                            lang_server.clone(),
+                            &mut cx,
+                        )
+                        .await;
+                    }
+                }
+
+                if let Some(command) = action.lsp_action.command {
+                    this.update(&mut cx, |this, _| {
+                        this.last_workspace_edits_by_language_server
+                            .remove(&lang_server.server_id());
+                    })?;
+
+                    let result = lang_server
+                        .request::<lsp2::request::ExecuteCommand>(lsp2::ExecuteCommandParams {
+                            command: command.command,
+                            arguments: command.arguments.unwrap_or_default(),
+                            ..Default::default()
+                        })
+                        .await;
+
+                    if let Err(err) = result {
+                        // TODO: LSP ERROR
+                        return Err(err);
+                    }
+
+                    return Ok(this.update(&mut cx, |this, _| {
+                        this.last_workspace_edits_by_language_server
+                            .remove(&lang_server.server_id())
+                            .unwrap_or_default()
+                    })?);
+                }
+
+                Ok(ProjectTransaction::default())
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            let client = self.client.clone();
+            let request = proto::ApplyCodeAction {
+                project_id,
+                buffer_id: buffer_handle.read(cx).remote_id(),
+                action: Some(language2::proto::serialize_code_action(&action)),
+            };
+            cx.spawn(move |this, mut cx| async move {
+                let response = client
+                    .request(request)
+                    .await?
+                    .transaction
+                    .ok_or_else(|| anyhow!("missing transaction"))?;
+                this.update(&mut cx, |this, cx| {
+                    this.deserialize_project_transaction(response, push_to_history, cx)
+                })?
+                .await
+            })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
+    fn apply_on_type_formatting(
+        &self,
+        buffer: Model<Buffer>,
+        position: Anchor,
+        trigger: String,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Option<Transaction>>> {
+        if self.is_local() {
+            cx.spawn(move |this, mut cx| async move {
+                // Do not allow multiple concurrent formatting requests for the
+                // same buffer.
+                this.update(&mut cx, |this, cx| {
+                    this.buffers_being_formatted
+                        .insert(buffer.read(cx).remote_id())
+                })?;
+
+                let _cleanup = defer({
+                    let this = this.clone();
+                    let mut cx = cx.clone();
+                    let closure_buffer = buffer.clone();
+                    move || {
+                        this.update(&mut cx, |this, cx| {
+                            this.buffers_being_formatted
+                                .remove(&closure_buffer.read(cx).remote_id());
+                        })
+                        .ok();
+                    }
+                });
+
+                buffer
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_edits(Some(position.timestamp))
+                    })?
+                    .await?;
+                this.update(&mut cx, |this, cx| {
+                    let position = position.to_point_utf16(buffer.read(cx));
+                    this.on_type_format(buffer, position, trigger, false, cx)
+                })?
+                .await
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            let client = self.client.clone();
+            let request = proto::OnTypeFormatting {
+                project_id,
+                buffer_id: buffer.read(cx).remote_id(),
+                position: Some(serialize_anchor(&position)),
+                trigger,
+                version: serialize_version(&buffer.read(cx).version()),
+            };
+            cx.spawn(move |_, _| async move {
+                client
+                    .request(request)
+                    .await?
+                    .transaction
+                    .map(language2::proto::deserialize_transaction)
+                    .transpose()
+            })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
+    async fn deserialize_edits(
+        this: Model<Self>,
+        buffer_to_edit: Model<Buffer>,
+        edits: Vec<lsp2::TextEdit>,
+        push_to_history: bool,
+        _: Arc<CachedLspAdapter>,
+        language_server: Arc<LanguageServer>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Option<Transaction>> {
+        let edits = this
+            .update(cx, |this, cx| {
+                this.edits_from_lsp(
+                    &buffer_to_edit,
+                    edits,
+                    language_server.server_id(),
+                    None,
+                    cx,
+                )
+            })?
+            .await?;
+
+        let transaction = buffer_to_edit.update(cx, |buffer, cx| {
+            buffer.finalize_last_transaction();
+            buffer.start_transaction();
+            for (range, text) in edits {
+                buffer.edit([(range, text)], None, cx);
+            }
+
+            if buffer.end_transaction(cx).is_some() {
+                let transaction = buffer.finalize_last_transaction().unwrap().clone();
+                if !push_to_history {
+                    buffer.forget_transaction(transaction.id);
+                }
+                Some(transaction)
+            } else {
+                None
+            }
+        })?;
+
+        Ok(transaction)
+    }
+
+    async fn deserialize_workspace_edit(
+        this: Model<Self>,
+        edit: lsp2::WorkspaceEdit,
+        push_to_history: bool,
+        lsp_adapter: Arc<CachedLspAdapter>,
+        language_server: Arc<LanguageServer>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<ProjectTransaction> {
+        let fs = this.update(cx, |this, _| this.fs.clone())?;
+        let mut operations = Vec::new();
+        if let Some(document_changes) = edit.document_changes {
+            match document_changes {
+                lsp2::DocumentChanges::Edits(edits) => {
+                    operations.extend(edits.into_iter().map(lsp2::DocumentChangeOperation::Edit))
+                }
+                lsp2::DocumentChanges::Operations(ops) => operations = ops,
+            }
+        } else if let Some(changes) = edit.changes {
+            operations.extend(changes.into_iter().map(|(uri, edits)| {
+                lsp2::DocumentChangeOperation::Edit(lsp2::TextDocumentEdit {
+                    text_document: lsp2::OptionalVersionedTextDocumentIdentifier {
+                        uri,
+                        version: None,
+                    },
+                    edits: edits.into_iter().map(OneOf::Left).collect(),
+                })
+            }));
+        }
+
+        let mut project_transaction = ProjectTransaction::default();
+        for operation in operations {
+            match operation {
+                lsp2::DocumentChangeOperation::Op(lsp2::ResourceOp::Create(op)) => {
+                    let abs_path = op
+                        .uri
+                        .to_file_path()
+                        .map_err(|_| anyhow!("can't convert URI to path"))?;
+
+                    if let Some(parent_path) = abs_path.parent() {
+                        fs.create_dir(parent_path).await?;
+                    }
+                    if abs_path.ends_with("/") {
+                        fs.create_dir(&abs_path).await?;
+                    } else {
+                        fs.create_file(
+                            &abs_path,
+                            op.options
+                                .map(|options| fs2::CreateOptions {
+                                    overwrite: options.overwrite.unwrap_or(false),
+                                    ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
+                                })
+                                .unwrap_or_default(),
+                        )
+                        .await?;
+                    }
+                }
+
+                lsp2::DocumentChangeOperation::Op(lsp2::ResourceOp::Rename(op)) => {
+                    let source_abs_path = op
+                        .old_uri
+                        .to_file_path()
+                        .map_err(|_| anyhow!("can't convert URI to path"))?;
+                    let target_abs_path = op
+                        .new_uri
+                        .to_file_path()
+                        .map_err(|_| anyhow!("can't convert URI to path"))?;
+                    fs.rename(
+                        &source_abs_path,
+                        &target_abs_path,
+                        op.options
+                            .map(|options| fs2::RenameOptions {
+                                overwrite: options.overwrite.unwrap_or(false),
+                                ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
+                            })
+                            .unwrap_or_default(),
+                    )
+                    .await?;
+                }
+
+                lsp2::DocumentChangeOperation::Op(lsp2::ResourceOp::Delete(op)) => {
+                    let abs_path = op
+                        .uri
+                        .to_file_path()
+                        .map_err(|_| anyhow!("can't convert URI to path"))?;
+                    let options = op
+                        .options
+                        .map(|options| fs2::RemoveOptions {
+                            recursive: options.recursive.unwrap_or(false),
+                            ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
+                        })
+                        .unwrap_or_default();
+                    if abs_path.ends_with("/") {
+                        fs.remove_dir(&abs_path, options).await?;
+                    } else {
+                        fs.remove_file(&abs_path, options).await?;
+                    }
+                }
+
+                lsp2::DocumentChangeOperation::Edit(op) => {
+                    let buffer_to_edit = this
+                        .update(cx, |this, cx| {
+                            this.open_local_buffer_via_lsp(
+                                op.text_document.uri,
+                                language_server.server_id(),
+                                lsp_adapter.name.clone(),
+                                cx,
+                            )
+                        })?
+                        .await?;
+
+                    let edits = this
+                        .update(cx, |this, cx| {
+                            let edits = op.edits.into_iter().map(|edit| match edit {
+                                OneOf::Left(edit) => edit,
+                                OneOf::Right(edit) => edit.text_edit,
+                            });
+                            this.edits_from_lsp(
+                                &buffer_to_edit,
+                                edits,
+                                language_server.server_id(),
+                                op.text_document.version,
+                                cx,
+                            )
+                        })?
+                        .await?;
+
+                    let transaction = buffer_to_edit.update(cx, |buffer, cx| {
+                        buffer.finalize_last_transaction();
+                        buffer.start_transaction();
+                        for (range, text) in edits {
+                            buffer.edit([(range, text)], None, cx);
+                        }
+                        let transaction = if buffer.end_transaction(cx).is_some() {
+                            let transaction = buffer.finalize_last_transaction().unwrap().clone();
+                            if !push_to_history {
+                                buffer.forget_transaction(transaction.id);
+                            }
+                            Some(transaction)
+                        } else {
+                            None
+                        };
+
+                        transaction
+                    })?;
+                    if let Some(transaction) = transaction {
+                        project_transaction.0.insert(buffer_to_edit, transaction);
+                    }
+                }
+            }
+        }
+
+        Ok(project_transaction)
+    }
+
+    pub fn prepare_rename<T: ToPointUtf16>(
+        &self,
+        buffer: Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Option<Range<Anchor>>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(
+            buffer,
+            LanguageServerToQuery::Primary,
+            PrepareRename { position },
+            cx,
+        )
+    }
+
+    pub fn perform_rename<T: ToPointUtf16>(
+        &self,
+        buffer: Model<Buffer>,
+        position: T,
+        new_name: String,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ProjectTransaction>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(
+            buffer,
+            LanguageServerToQuery::Primary,
+            PerformRename {
+                position,
+                new_name,
+                push_to_history,
+            },
+            cx,
+        )
+    }
+
+    pub fn on_type_format<T: ToPointUtf16>(
+        &self,
+        buffer: Model<Buffer>,
+        position: T,
+        trigger: String,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Option<Transaction>>> {
+        let (position, tab_size) = buffer.update(cx, |buffer, cx| {
+            let position = position.to_point_utf16(buffer);
+            (
+                position,
+                language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx)
+                    .tab_size,
+            )
+        });
+        self.request_lsp(
+            buffer.clone(),
+            LanguageServerToQuery::Primary,
+            OnTypeFormatting {
+                position,
+                trigger,
+                options: lsp_command::lsp_formatting_options(tab_size.get()).into(),
+                push_to_history,
+            },
+            cx,
+        )
+    }
+
+    pub fn inlay_hints<T: ToOffset>(
+        &self,
+        buffer_handle: Model<Buffer>,
+        range: Range<T>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<anyhow::Result<Vec<InlayHint>>> {
+        let buffer = buffer_handle.read(cx);
+        let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
+        let range_start = range.start;
+        let range_end = range.end;
+        let buffer_id = buffer.remote_id();
+        let buffer_version = buffer.version().clone();
+        let lsp_request = InlayHints { range };
+
+        if self.is_local() {
+            let lsp_request_task = self.request_lsp(
+                buffer_handle.clone(),
+                LanguageServerToQuery::Primary,
+                lsp_request,
+                cx,
+            );
+            cx.spawn(move |_, mut cx| async move {
+                buffer_handle
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp])
+                    })?
+                    .await
+                    .context("waiting for inlay hint request range edits")?;
+                lsp_request_task.await.context("inlay hints LSP request")
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            let client = self.client.clone();
+            let request = proto::InlayHints {
+                project_id,
+                buffer_id,
+                start: Some(serialize_anchor(&range_start)),
+                end: Some(serialize_anchor(&range_end)),
+                version: serialize_version(&buffer_version),
+            };
+            cx.spawn(move |project, cx| async move {
+                let response = client
+                    .request(request)
+                    .await
+                    .context("inlay hints proto request")?;
+                let hints_request_result = LspCommand::response_from_proto(
+                    lsp_request,
+                    response,
+                    project.upgrade().ok_or_else(|| anyhow!("No project"))?,
+                    buffer_handle.clone(),
+                    cx,
+                )
+                .await;
+
+                hints_request_result.context("inlay hints proto response conversion")
+            })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
+    pub fn resolve_inlay_hint(
+        &self,
+        hint: InlayHint,
+        buffer_handle: Model<Buffer>,
+        server_id: LanguageServerId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<anyhow::Result<InlayHint>> {
+        if self.is_local() {
+            let buffer = buffer_handle.read(cx);
+            let (_, lang_server) = if let Some((adapter, server)) =
+                self.language_server_for_buffer(buffer, server_id, cx)
+            {
+                (adapter.clone(), server.clone())
+            } else {
+                return Task::ready(Ok(hint));
+            };
+            if !InlayHints::can_resolve_inlays(lang_server.capabilities()) {
+                return Task::ready(Ok(hint));
+            }
+
+            let buffer_snapshot = buffer.snapshot();
+            cx.spawn(move |_, mut cx| async move {
+                let resolve_task = lang_server.request::<lsp2::request::InlayHintResolveRequest>(
+                    InlayHints::project_to_lsp_hint(hint, &buffer_snapshot),
+                );
+                let resolved_hint = resolve_task
+                    .await
+                    .context("inlay hint resolve LSP request")?;
+                let resolved_hint = InlayHints::lsp_to_project_hint(
+                    resolved_hint,
+                    &buffer_handle,
+                    server_id,
+                    ResolveState::Resolved,
+                    false,
+                    &mut cx,
+                )
+                .await?;
+                Ok(resolved_hint)
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            let client = self.client.clone();
+            let request = proto::ResolveInlayHint {
+                project_id,
+                buffer_id: buffer_handle.read(cx).remote_id(),
+                language_server_id: server_id.0 as u64,
+                hint: Some(InlayHints::project_to_proto_hint(hint.clone())),
+            };
+            cx.spawn(move |_, _| async move {
+                let response = client
+                    .request(request)
+                    .await
+                    .context("inlay hints proto request")?;
+                match response.hint {
+                    Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint)
+                        .context("inlay hints proto resolve response conversion"),
+                    None => Ok(hint),
+                }
+            })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
+    #[allow(clippy::type_complexity)]
+    pub fn search(
+        &self,
+        query: SearchQuery,
+        cx: &mut ModelContext<Self>,
+    ) -> Receiver<(Model<Buffer>, Vec<Range<Anchor>>)> {
+        if self.is_local() {
+            self.search_local(query, cx)
+        } else if let Some(project_id) = self.remote_id() {
+            let (tx, rx) = smol::channel::unbounded();
+            let request = self.client.request(query.to_proto(project_id));
+            cx.spawn(move |this, mut cx| async move {
+                let response = request.await?;
+                let mut result = HashMap::default();
+                for location in response.locations {
+                    let target_buffer = this
+                        .update(&mut cx, |this, cx| {
+                            this.wait_for_remote_buffer(location.buffer_id, cx)
+                        })?
+                        .await?;
+                    let start = location
+                        .start
+                        .and_then(deserialize_anchor)
+                        .ok_or_else(|| anyhow!("missing target start"))?;
+                    let end = location
+                        .end
+                        .and_then(deserialize_anchor)
+                        .ok_or_else(|| anyhow!("missing target end"))?;
+                    result
+                        .entry(target_buffer)
+                        .or_insert(Vec::new())
+                        .push(start..end)
+                }
+                for (buffer, ranges) in result {
+                    let _ = tx.send((buffer, ranges)).await;
+                }
+                Result::<(), anyhow::Error>::Ok(())
+            })
+            .detach_and_log_err(cx);
+            rx
+        } else {
+            unimplemented!();
+        }
+    }
+
+    pub fn search_local(
+        &self,
+        query: SearchQuery,
+        cx: &mut ModelContext<Self>,
+    ) -> 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.
+        // The Receiver obtained from this function returns matches sorted by buffer path. Files without a buffer path are reported first.
+        //
+        // It gets a bit hairy though, because we must account for files that do not have a persistent representation
+        // on FS. Namely, if you have an untitled buffer or unsaved changes in a buffer, we want to scan that too.
+        //
+        // 1. We initialize a queue of match candidates and feed all opened buffers into it (== unsaved files / untitled buffers).
+        //    Then, we go through a worktree and check for files that do match a predicate. If the file had an opened version, we skip the scan
+        //    of FS version for that file altogether - after all, what we have in memory is more up-to-date than what's in FS.
+        // 2. At this point, we have a list of all potentially matching buffers/files.
+        //    We sort that list by buffer path - this list is retained for later use.
+        //    We ensure that all buffers are now opened and available in project.
+        // 3. We run a scan over all the candidate buffers on multiple background threads.
+        //    We cannot assume that there will even be a match - while at least one match
+        //    is guaranteed for files obtained from FS, the buffers we got from memory (unsaved files/unnamed buffers) might not have a match at all.
+        //    There is also an auxilliary background thread responsible for result gathering.
+        //    This is where the sorted list of buffers comes into play to maintain sorted order; Whenever this background thread receives a notification (buffer has/doesn't have matches),
+        //    it keeps it around. It reports matches in sorted order, though it accepts them in unsorted order as well.
+        //    As soon as the match info on next position in sorted order becomes available, it reports it (if it's a match) or skips to the next
+        //    entry - which might already be available thanks to out-of-order processing.
+        //
+        // We could also report matches fully out-of-order, without maintaining a sorted list of matching paths.
+        // This however would mean that project search (that is the main user of this function) would have to do the sorting itself, on the go.
+        // This isn't as straightforward as running an insertion sort sadly, and would also mean that it would have to care about maintaining match index
+        // in face of constantly updating list of sorted matches.
+        // Meanwhile, this implementation offers index stability, since the matches are already reported in a sorted order.
+        let snapshots = self
+            .visible_worktrees(cx)
+            .filter_map(|tree| {
+                let tree = tree.read(cx).as_local()?;
+                Some(tree.snapshot())
+            })
+            .collect::<Vec<_>>();
+
+        let background = cx.executor().clone();
+        let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum();
+        if path_count == 0 {
+            let (_, rx) = smol::channel::bounded(1024);
+            return rx;
+        }
+        let workers = background.num_cpus().min(path_count);
+        let (matching_paths_tx, matching_paths_rx) = smol::channel::bounded(1024);
+        let mut unnamed_files = vec![];
+        let opened_buffers = self
+            .opened_buffers
+            .iter()
+            .filter_map(|(_, b)| {
+                let buffer = b.upgrade()?;
+                let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
+                if let Some(path) = snapshot.file().map(|file| file.path()) {
+                    Some((path.clone(), (buffer, snapshot)))
+                } else {
+                    unnamed_files.push(buffer);
+                    None
+                }
+            })
+            .collect();
+        cx.executor()
+            .spawn(Self::background_search(
+                unnamed_files,
+                opened_buffers,
+                cx.executor().clone(),
+                self.fs.clone(),
+                workers,
+                query.clone(),
+                path_count,
+                snapshots,
+                matching_paths_tx,
+            ))
+            .detach();
+
+        let (buffers, buffers_rx) = Self::sort_candidates_and_open_buffers(matching_paths_rx, cx);
+        let background = cx.executor().clone();
+        let (result_tx, result_rx) = smol::channel::bounded(1024);
+        cx.executor()
+            .spawn(async move {
+                let Ok(buffers) = buffers.await else {
+                    return;
+                };
+
+                let buffers_len = buffers.len();
+                if buffers_len == 0 {
+                    return;
+                }
+                let query = &query;
+                let (finished_tx, mut finished_rx) = smol::channel::unbounded();
+                background
+                    .scoped(|scope| {
+                        #[derive(Clone)]
+                        struct FinishedStatus {
+                            entry: Option<(Model<Buffer>, Vec<Range<Anchor>>)>,
+                            buffer_index: SearchMatchCandidateIndex,
+                        }
+
+                        for _ in 0..workers {
+                            let finished_tx = finished_tx.clone();
+                            let mut buffers_rx = buffers_rx.clone();
+                            scope.spawn(async move {
+                                while let Some((entry, buffer_index)) = buffers_rx.next().await {
+                                    let buffer_matches = if let Some((_, snapshot)) = entry.as_ref()
+                                    {
+                                        if query.file_matches(
+                                            snapshot.file().map(|file| file.path().as_ref()),
+                                        ) {
+                                            query
+                                                .search(&snapshot, None)
+                                                .await
+                                                .iter()
+                                                .map(|range| {
+                                                    snapshot.anchor_before(range.start)
+                                                        ..snapshot.anchor_after(range.end)
+                                                })
+                                                .collect()
+                                        } else {
+                                            Vec::new()
+                                        }
+                                    } else {
+                                        Vec::new()
+                                    };
+
+                                    let status = if !buffer_matches.is_empty() {
+                                        let entry = if let Some((buffer, _)) = entry.as_ref() {
+                                            Some((buffer.clone(), buffer_matches))
+                                        } else {
+                                            None
+                                        };
+                                        FinishedStatus {
+                                            entry,
+                                            buffer_index,
+                                        }
+                                    } else {
+                                        FinishedStatus {
+                                            entry: None,
+                                            buffer_index,
+                                        }
+                                    };
+                                    if finished_tx.send(status).await.is_err() {
+                                        break;
+                                    }
+                                }
+                            });
+                        }
+                        // Report sorted matches
+                        scope.spawn(async move {
+                            let mut current_index = 0;
+                            let mut scratch = vec![None; buffers_len];
+                            while let Some(status) = finished_rx.next().await {
+                                debug_assert!(
+                                    scratch[status.buffer_index].is_none(),
+                                    "Got match status of position {} twice",
+                                    status.buffer_index
+                                );
+                                let index = status.buffer_index;
+                                scratch[index] = Some(status);
+                                while current_index < buffers_len {
+                                    let Some(current_entry) = scratch[current_index].take() else {
+                                        // We intentionally **do not** increment `current_index` here. When next element arrives
+                                        // from `finished_rx`, we will inspect the same position again, hoping for it to be Some(_)
+                                        // this time.
+                                        break;
+                                    };
+                                    if let Some(entry) = current_entry.entry {
+                                        result_tx.send(entry).await.log_err();
+                                    }
+                                    current_index += 1;
+                                }
+                                if current_index == buffers_len {
+                                    break;
+                                }
+                            }
+                        });
+                    })
+                    .await;
+            })
+            .detach();
+        result_rx
+    }
+
+    /// Pick paths that might potentially contain a match of a given search query.
+    async fn background_search(
+        unnamed_buffers: Vec<Model<Buffer>>,
+        opened_buffers: HashMap<Arc<Path>, (Model<Buffer>, BufferSnapshot)>,
+        executor: Executor,
+        fs: Arc<dyn Fs>,
+        workers: usize,
+        query: SearchQuery,
+        path_count: usize,
+        snapshots: Vec<LocalSnapshot>,
+        matching_paths_tx: Sender<SearchMatchCandidate>,
+    ) {
+        let fs = &fs;
+        let query = &query;
+        let matching_paths_tx = &matching_paths_tx;
+        let snapshots = &snapshots;
+        let paths_per_worker = (path_count + workers - 1) / workers;
+        for buffer in unnamed_buffers {
+            matching_paths_tx
+                .send(SearchMatchCandidate::OpenBuffer {
+                    buffer: buffer.clone(),
+                    path: None,
+                })
+                .await
+                .log_err();
+        }
+        for (path, (buffer, _)) in opened_buffers.iter() {
+            matching_paths_tx
+                .send(SearchMatchCandidate::OpenBuffer {
+                    buffer: buffer.clone(),
+                    path: Some(path.clone()),
+                })
+                .await
+                .log_err();
+        }
+        executor
+            .scoped(|scope| {
+                for worker_ix in 0..workers {
+                    let worker_start_ix = worker_ix * paths_per_worker;
+                    let worker_end_ix = worker_start_ix + paths_per_worker;
+                    let unnamed_buffers = opened_buffers.clone();
+                    scope.spawn(async move {
+                        let mut snapshot_start_ix = 0;
+                        let mut abs_path = PathBuf::new();
+                        for snapshot in snapshots {
+                            let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count();
+                            if worker_end_ix <= snapshot_start_ix {
+                                break;
+                            } else if worker_start_ix > snapshot_end_ix {
+                                snapshot_start_ix = snapshot_end_ix;
+                                continue;
+                            } else {
+                                let start_in_snapshot =
+                                    worker_start_ix.saturating_sub(snapshot_start_ix);
+                                let end_in_snapshot =
+                                    cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
+
+                                for entry in snapshot
+                                    .files(false, start_in_snapshot)
+                                    .take(end_in_snapshot - start_in_snapshot)
+                                {
+                                    if matching_paths_tx.is_closed() {
+                                        break;
+                                    }
+                                    if unnamed_buffers.contains_key(&entry.path) {
+                                        continue;
+                                    }
+                                    let matches = if query.file_matches(Some(&entry.path)) {
+                                        abs_path.clear();
+                                        abs_path.push(&snapshot.abs_path());
+                                        abs_path.push(&entry.path);
+                                        if let Some(file) = fs.open_sync(&abs_path).await.log_err()
+                                        {
+                                            query.detect(file).unwrap_or(false)
+                                        } else {
+                                            false
+                                        }
+                                    } else {
+                                        false
+                                    };
+
+                                    if matches {
+                                        let project_path = SearchMatchCandidate::Path {
+                                            worktree_id: snapshot.id(),
+                                            path: entry.path.clone(),
+                                        };
+                                        if matching_paths_tx.send(project_path).await.is_err() {
+                                            break;
+                                        }
+                                    }
+                                }
+
+                                snapshot_start_ix = snapshot_end_ix;
+                            }
+                        }
+                    });
+                }
+            })
+            .await;
+    }
+
+    fn request_lsp<R: LspCommand>(
+        &self,
+        buffer_handle: Model<Buffer>,
+        server: LanguageServerToQuery,
+        request: R,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<R::Response>>
+    where
+        <R::LspRequest as lsp2::request::Request>::Result: Send,
+        <R::LspRequest as lsp2::request::Request>::Params: Send,
+    {
+        let buffer = buffer_handle.read(cx);
+        if self.is_local() {
+            let language_server = match server {
+                LanguageServerToQuery::Primary => {
+                    match self.primary_language_server_for_buffer(buffer, cx) {
+                        Some((_, server)) => Some(Arc::clone(server)),
+                        None => return Task::ready(Ok(Default::default())),
+                    }
+                }
+                LanguageServerToQuery::Other(id) => self
+                    .language_server_for_buffer(buffer, id, cx)
+                    .map(|(_, server)| Arc::clone(server)),
+            };
+            let file = File::from_dyn(buffer.file()).and_then(File::as_local);
+            if let (Some(file), Some(language_server)) = (file, language_server) {
+                let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx);
+                return cx.spawn(move |this, cx| async move {
+                    if !request.check_capabilities(language_server.capabilities()) {
+                        return Ok(Default::default());
+                    }
+
+                    let result = language_server.request::<R::LspRequest>(lsp_params).await;
+                    let response = match result {
+                        Ok(response) => response,
+
+                        Err(err) => {
+                            log::warn!(
+                                "Generic lsp request to {} failed: {}",
+                                language_server.name(),
+                                err
+                            );
+                            return Err(err);
+                        }
+                    };
+
+                    request
+                        .response_from_lsp(
+                            response,
+                            this.upgrade().ok_or_else(|| anyhow!("no app context"))?,
+                            buffer_handle,
+                            language_server.server_id(),
+                            cx,
+                        )
+                        .await
+                });
+            }
+        } else if let Some(project_id) = self.remote_id() {
+            return self.send_lsp_proto_request(buffer_handle, project_id, request, cx);
+        }
+
+        Task::ready(Ok(Default::default()))
+    }
+
+    fn send_lsp_proto_request<R: LspCommand>(
+        &self,
+        buffer: Model<Buffer>,
+        project_id: u64,
+        request: R,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Task<anyhow::Result<<R as LspCommand>::Response>> {
+        let rpc = self.client.clone();
+        let message = request.to_proto(project_id, buffer.read(cx));
+        cx.spawn(move |this, mut cx| async move {
+            // Ensure the project is still alive by the time the task
+            // is scheduled.
+            this.upgrade().context("project dropped")?;
+            let response = rpc.request(message).await?;
+            let this = this.upgrade().context("project dropped")?;
+            if this.update(&mut cx, |this, _| this.is_read_only())? {
+                Err(anyhow!("disconnected before completing request"))
+            } else {
+                request
+                    .response_from_proto(response, this, buffer, cx)
+                    .await
+            }
+        })
+    }
+
+    fn sort_candidates_and_open_buffers(
+        mut matching_paths_rx: Receiver<SearchMatchCandidate>,
+        cx: &mut ModelContext<Self>,
+    ) -> (
+        futures::channel::oneshot::Receiver<Vec<SearchMatchCandidate>>,
+        Receiver<(
+            Option<(Model<Buffer>, BufferSnapshot)>,
+            SearchMatchCandidateIndex,
+        )>,
+    ) {
+        let (buffers_tx, buffers_rx) = smol::channel::bounded(1024);
+        let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel();
+        cx.spawn(move |this, cx| async move {
+            let mut buffers = vec![];
+            while let Some(entry) = matching_paths_rx.next().await {
+                buffers.push(entry);
+            }
+            buffers.sort_by_key(|candidate| candidate.path());
+            let matching_paths = buffers.clone();
+            let _ = sorted_buffers_tx.send(buffers);
+            for (index, candidate) in matching_paths.into_iter().enumerate() {
+                if buffers_tx.is_closed() {
+                    break;
+                }
+                let this = this.clone();
+                let buffers_tx = buffers_tx.clone();
+                cx.spawn(move |mut cx| async move {
+                    let buffer = match candidate {
+                        SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer),
+                        SearchMatchCandidate::Path { worktree_id, path } => this
+                            .update(&mut cx, |this, cx| {
+                                this.open_buffer((worktree_id, path), cx)
+                            })?
+                            .await
+                            .log_err(),
+                    };
+                    if let Some(buffer) = buffer {
+                        let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
+                        buffers_tx
+                            .send((Some((buffer, snapshot)), index))
+                            .await
+                            .log_err();
+                    } else {
+                        buffers_tx.send((None, index)).await.log_err();
+                    }
+
+                    Ok::<_, anyhow::Error>(())
+                })
+                .detach();
+            }
+        })
+        .detach();
+        (sorted_buffers_rx, buffers_rx)
+    }
+
+    pub fn find_or_create_local_worktree(
+        &mut self,
+        abs_path: impl AsRef<Path>,
+        visible: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> 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)))
+        } else {
+            let worktree = self.create_local_worktree(abs_path, visible, cx);
+            cx.executor()
+                .spawn(async move { Ok((worktree.await?, PathBuf::new())) })
+        }
+    }
+
+    pub fn find_local_worktree(
+        &self,
+        abs_path: &Path,
+        cx: &AppContext,
+    ) -> Option<(Model<Worktree>, PathBuf)> {
+        for tree in &self.worktrees {
+            if let Some(tree) = tree.upgrade() {
+                if let Some(relative_path) = tree
+                    .read(cx)
+                    .as_local()
+                    .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
+                {
+                    return Some((tree.clone(), relative_path.into()));
+                }
+            }
+        }
+        None
+    }
+
+    pub fn is_shared(&self) -> bool {
+        match &self.client_state {
+            Some(ProjectClientState::Local { .. }) => true,
+            _ => false,
+        }
+    }
+
+    fn create_local_worktree(
+        &mut self,
+        abs_path: impl AsRef<Path>,
+        visible: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Model<Worktree>>> {
+        let fs = self.fs.clone();
+        let client = self.client.clone();
+        let next_entry_id = self.next_entry_id.clone();
+        let path: Arc<Path> = abs_path.as_ref().into();
+        let task = self
+            .loading_local_worktrees
+            .entry(path.clone())
+            .or_insert_with(|| {
+                cx.spawn(move |project, mut cx| {
+                    async move {
+                        let worktree = Worktree::local(
+                            client.clone(),
+                            path.clone(),
+                            visible,
+                            fs,
+                            next_entry_id,
+                            &mut cx,
+                        )
+                        .await;
+
+                        project.update(&mut cx, |project, _| {
+                            project.loading_local_worktrees.remove(&path);
+                        })?;
+
+                        let worktree = worktree?;
+                        project
+                            .update(&mut cx, |project, cx| project.add_worktree(&worktree, cx))?;
+                        Ok(worktree)
+                    }
+                    .map_err(Arc::new)
+                })
+                .shared()
+            })
+            .clone();
+        cx.executor().spawn(async move {
+            match task.await {
+                Ok(worktree) => Ok(worktree),
+                Err(err) => Err(anyhow!("{}", err)),
+            }
+        })
+    }
+
+    pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
+        self.worktrees.retain(|worktree| {
+            if let Some(worktree) = worktree.upgrade() {
+                let id = worktree.read(cx).id();
+                if id == id_to_remove {
+                    cx.emit(Event::WorktreeRemoved(id));
+                    false
+                } else {
+                    true
+                }
+            } else {
+                false
+            }
+        });
+        self.metadata_changed(cx);
+    }
+
+    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 {
+                worktree::Event::UpdatedEntries(changes) => {
+                    this.update_local_worktree_buffers(&worktree, changes, cx);
+                    this.update_local_worktree_language_servers(&worktree, changes, cx);
+                    this.update_local_worktree_settings(&worktree, changes, cx);
+                    this.update_prettier_settings(&worktree, changes, cx);
+                    cx.emit(Event::WorktreeUpdatedEntries(
+                        worktree.read(cx).id(),
+                        changes.clone(),
+                    ));
+                }
+                worktree::Event::UpdatedGitRepositories(updated_repos) => {
+                    this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
+                }
+            })
+            .detach();
+        }
+
+        let push_strong_handle = {
+            let worktree = worktree.read(cx);
+            self.is_shared() || worktree.is_visible() || worktree.is_remote()
+        };
+        if push_strong_handle {
+            self.worktrees
+                .push(WorktreeHandle::Strong(worktree.clone()));
+        } else {
+            self.worktrees
+                .push(WorktreeHandle::Weak(worktree.downgrade()));
+        }
+
+        let handle_id = worktree.entity_id();
+        cx.observe_release(worktree, move |this, worktree, cx| {
+            let _ = this.remove_worktree(worktree.id(), cx);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store
+                    .clear_local_settings(handle_id.as_u64() as usize, cx)
+                    .log_err()
+            });
+        })
+        .detach();
+
+        cx.emit(Event::WorktreeAdded);
+        self.metadata_changed(cx);
+    }
+
+    fn update_local_worktree_buffers(
+        &mut self,
+        worktree_handle: &Model<Worktree>,
+        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        cx: &mut ModelContext<Self>,
+    ) {
+        let snapshot = worktree_handle.read(cx).snapshot();
+
+        let mut renamed_buffers = Vec::new();
+        for (path, entry_id, _) in changes {
+            let worktree_id = worktree_handle.read(cx).id();
+            let project_path = ProjectPath {
+                worktree_id,
+                path: path.clone(),
+            };
+
+            let buffer_id = match self.local_buffer_ids_by_entry_id.get(entry_id) {
+                Some(&buffer_id) => buffer_id,
+                None => match self.local_buffer_ids_by_path.get(&project_path) {
+                    Some(&buffer_id) => buffer_id,
+                    None => {
+                        continue;
+                    }
+                },
+            };
+
+            let open_buffer = self.opened_buffers.get(&buffer_id);
+            let buffer = if let Some(buffer) = open_buffer.and_then(|buffer| buffer.upgrade()) {
+                buffer
+            } else {
+                self.opened_buffers.remove(&buffer_id);
+                self.local_buffer_ids_by_path.remove(&project_path);
+                self.local_buffer_ids_by_entry_id.remove(entry_id);
+                continue;
+            };
+
+            buffer.update(cx, |buffer, cx| {
+                if let Some(old_file) = File::from_dyn(buffer.file()) {
+                    if old_file.worktree != *worktree_handle {
+                        return;
+                    }
+
+                    let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
+                        File {
+                            is_local: true,
+                            entry_id: entry.id,
+                            mtime: entry.mtime,
+                            path: entry.path.clone(),
+                            worktree: worktree_handle.clone(),
+                            is_deleted: false,
+                        }
+                    } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
+                        File {
+                            is_local: true,
+                            entry_id: entry.id,
+                            mtime: entry.mtime,
+                            path: entry.path.clone(),
+                            worktree: worktree_handle.clone(),
+                            is_deleted: false,
+                        }
+                    } else {
+                        File {
+                            is_local: true,
+                            entry_id: old_file.entry_id,
+                            path: old_file.path().clone(),
+                            mtime: old_file.mtime(),
+                            worktree: worktree_handle.clone(),
+                            is_deleted: true,
+                        }
+                    };
+
+                    let old_path = old_file.abs_path(cx);
+                    if new_file.abs_path(cx) != old_path {
+                        renamed_buffers.push((cx.handle(), old_file.clone()));
+                        self.local_buffer_ids_by_path.remove(&project_path);
+                        self.local_buffer_ids_by_path.insert(
+                            ProjectPath {
+                                worktree_id,
+                                path: path.clone(),
+                            },
+                            buffer_id,
+                        );
+                    }
+
+                    if new_file.entry_id != *entry_id {
+                        self.local_buffer_ids_by_entry_id.remove(entry_id);
+                        self.local_buffer_ids_by_entry_id
+                            .insert(new_file.entry_id, buffer_id);
+                    }
+
+                    if new_file != *old_file {
+                        if let Some(project_id) = self.remote_id() {
+                            self.client
+                                .send(proto::UpdateBufferFile {
+                                    project_id,
+                                    buffer_id: buffer_id as u64,
+                                    file: Some(new_file.to_proto()),
+                                })
+                                .log_err();
+                        }
+
+                        buffer.file_updated(Arc::new(new_file), cx).detach();
+                    }
+                }
+            });
+        }
+
+        for (buffer, old_file) in renamed_buffers {
+            self.unregister_buffer_from_language_servers(&buffer, &old_file, cx);
+            self.detect_language_for_buffer(&buffer, cx);
+            self.register_buffer_with_language_servers(&buffer, cx);
+        }
+    }
+
+    fn update_local_worktree_language_servers(
+        &mut self,
+        worktree_handle: &Model<Worktree>,
+        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        cx: &mut ModelContext<Self>,
+    ) {
+        if changes.is_empty() {
+            return;
+        }
+
+        let worktree_id = worktree_handle.read(cx).id();
+        let mut language_server_ids = self
+            .language_server_ids
+            .iter()
+            .filter_map(|((server_worktree_id, _), server_id)| {
+                (*server_worktree_id == worktree_id).then_some(*server_id)
+            })
+            .collect::<Vec<_>>();
+        language_server_ids.sort();
+        language_server_ids.dedup();
+
+        let abs_path = worktree_handle.read(cx).abs_path();
+        for server_id in &language_server_ids {
+            if let Some(LanguageServerState::Running {
+                server,
+                watched_paths,
+                ..
+            }) = self.language_servers.get(server_id)
+            {
+                if let Some(watched_paths) = watched_paths.get(&worktree_id) {
+                    let params = lsp2::DidChangeWatchedFilesParams {
+                        changes: changes
+                            .iter()
+                            .filter_map(|(path, _, change)| {
+                                if !watched_paths.is_match(&path) {
+                                    return None;
+                                }
+                                let typ = match change {
+                                    PathChange::Loaded => return None,
+                                    PathChange::Added => lsp2::FileChangeType::CREATED,
+                                    PathChange::Removed => lsp2::FileChangeType::DELETED,
+                                    PathChange::Updated => lsp2::FileChangeType::CHANGED,
+                                    PathChange::AddedOrUpdated => lsp2::FileChangeType::CHANGED,
+                                };
+                                Some(lsp2::FileEvent {
+                                    uri: lsp2::Url::from_file_path(abs_path.join(path)).unwrap(),
+                                    typ,
+                                })
+                            })
+                            .collect(),
+                    };
+
+                    if !params.changes.is_empty() {
+                        server
+                            .notify::<lsp2::notification::DidChangeWatchedFiles>(params)
+                            .log_err();
+                    }
+                }
+            }
+        }
+    }
+
+    fn update_local_worktree_buffers_git_repos(
+        &mut self,
+        worktree_handle: Model<Worktree>,
+        changed_repos: &UpdatedGitRepositoriesSet,
+        cx: &mut ModelContext<Self>,
+    ) {
+        debug_assert!(worktree_handle.read(cx).is_local());
+
+        // Identify the loading buffers whose containing repository that has changed.
+        let future_buffers = self
+            .loading_buffers_by_path
+            .iter()
+            .filter_map(|(project_path, receiver)| {
+                if project_path.worktree_id != worktree_handle.read(cx).id() {
+                    return None;
+                }
+                let path = &project_path.path;
+                changed_repos
+                    .iter()
+                    .find(|(work_dir, _)| path.starts_with(work_dir))?;
+                let receiver = receiver.clone();
+                let path = path.clone();
+                Some(async move {
+                    wait_for_loading_buffer(receiver)
+                        .await
+                        .ok()
+                        .map(|buffer| (buffer, path))
+                })
+            })
+            .collect::<FuturesUnordered<_>>();
+
+        // Identify the current buffers whose containing repository has changed.
+        let current_buffers = self
+            .opened_buffers
+            .values()
+            .filter_map(|buffer| {
+                let buffer = buffer.upgrade()?;
+                let file = File::from_dyn(buffer.read(cx).file())?;
+                if file.worktree != worktree_handle {
+                    return None;
+                }
+                let path = file.path();
+                changed_repos
+                    .iter()
+                    .find(|(work_dir, _)| path.starts_with(work_dir))?;
+                Some((buffer, path.clone()))
+            })
+            .collect::<Vec<_>>();
+
+        if future_buffers.len() + current_buffers.len() == 0 {
+            return;
+        }
+
+        let remote_id = self.remote_id();
+        let client = self.client.clone();
+        cx.spawn(move |_, mut cx| async move {
+            // Wait for all of the buffers to load.
+            let future_buffers = future_buffers.collect::<Vec<_>>().await;
+
+            // Reload the diff base for every buffer whose containing git repository has changed.
+            let snapshot =
+                worktree_handle.update(&mut cx, |tree, _| tree.as_local().unwrap().snapshot())?;
+            let diff_bases_by_buffer = cx
+                .executor()
+                .spawn(async move {
+                    future_buffers
+                        .into_iter()
+                        .filter_map(|e| e)
+                        .chain(current_buffers)
+                        .filter_map(|(buffer, path)| {
+                            let (work_directory, repo) =
+                                snapshot.repository_and_work_directory_for_path(&path)?;
+                            let repo = snapshot.get_local_repo(&repo)?;
+                            let relative_path = path.strip_prefix(&work_directory).ok()?;
+                            let base_text = repo.repo_ptr.lock().load_index_text(&relative_path);
+                            Some((buffer, base_text))
+                        })
+                        .collect::<Vec<_>>()
+                })
+                .await;
+
+            // Assign the new diff bases on all of the buffers.
+            for (buffer, diff_base) in diff_bases_by_buffer {
+                let buffer_id = buffer.update(&mut cx, |buffer, cx| {
+                    buffer.set_diff_base(diff_base.clone(), cx);
+                    buffer.remote_id()
+                })?;
+                if let Some(project_id) = remote_id {
+                    client
+                        .send(proto::UpdateDiffBase {
+                            project_id,
+                            buffer_id,
+                            diff_base,
+                        })
+                        .log_err();
+                }
+            }
+
+            anyhow::Ok(())
+        })
+        .detach();
+    }
+
+    fn update_local_worktree_settings(
+        &mut self,
+        worktree: &Model<Worktree>,
+        changes: &UpdatedEntriesSet,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let project_id = self.remote_id();
+        let worktree_id = worktree.entity_id();
+        let worktree = worktree.read(cx).as_local().unwrap();
+        let remote_worktree_id = worktree.id();
+
+        let mut settings_contents = Vec::new();
+        for (path, _, change) in changes.iter() {
+            if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) {
+                let settings_dir = Arc::from(
+                    path.ancestors()
+                        .nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count())
+                        .unwrap(),
+                );
+                let fs = self.fs.clone();
+                let removed = *change == PathChange::Removed;
+                let abs_path = worktree.absolutize(path);
+                settings_contents.push(async move {
+                    (settings_dir, (!removed).then_some(fs.load(&abs_path).await))
+                });
+            }
+        }
+
+        if settings_contents.is_empty() {
+            return;
+        }
+
+        let client = self.client.clone();
+        cx.spawn(move |_, cx| async move {
+            let settings_contents: Vec<(Arc<Path>, _)> =
+                futures::future::join_all(settings_contents).await;
+            cx.update(|cx| {
+                cx.update_global::<SettingsStore, _>(|store, cx| {
+                    for (directory, file_content) in settings_contents {
+                        let file_content = file_content.and_then(|content| content.log_err());
+                        store
+                            .set_local_settings(
+                                worktree_id.as_u64() as usize,
+                                directory.clone(),
+                                file_content.as_ref().map(String::as_str),
+                                cx,
+                            )
+                            .log_err();
+                        if let Some(remote_id) = project_id {
+                            client
+                                .send(proto::UpdateWorktreeSettings {
+                                    project_id: remote_id,
+                                    worktree_id: remote_worktree_id.to_proto(),
+                                    path: directory.to_string_lossy().into_owned(),
+                                    content: file_content,
+                                })
+                                .log_err();
+                        }
+                    }
+                });
+            })
+            .ok();
+        })
+        .detach();
+    }
+
+    fn update_prettier_settings(
+        &self,
+        worktree: &Model<Worktree>,
+        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        cx: &mut ModelContext<'_, Project>,
+    ) {
+        let prettier_config_files = Prettier::CONFIG_FILE_NAMES
+            .iter()
+            .map(Path::new)
+            .collect::<HashSet<_>>();
+
+        let prettier_config_file_changed = changes
+            .iter()
+            .filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
+            .filter(|(path, _, _)| {
+                !path
+                    .components()
+                    .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
+            })
+            .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
+        let current_worktree_id = worktree.read(cx).id();
+        if let Some((config_path, _, _)) = prettier_config_file_changed {
+            log::info!(
+                "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
+            );
+            let prettiers_to_reload = self
+                .prettier_instances
+                .iter()
+                .filter_map(|((worktree_id, prettier_path), prettier_task)| {
+                    if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) {
+                        Some((*worktree_id, prettier_path.clone(), prettier_task.clone()))
+                    } else {
+                        None
+                    }
+                })
+                .collect::<Vec<_>>();
+
+            cx.executor()
+                .spawn(async move {
+                    for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
+                        async move {
+                            prettier_task.await?
+                                .clear_cache()
+                                .await
+                                .with_context(|| {
+                                    format!(
+                                        "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
+                                    )
+                                })
+                                .map_err(Arc::new)
+                        }
+                    }))
+                    .await
+                    {
+                        if let Err(e) = task_result {
+                            log::error!("Failed to clear cache for prettier: {e:#}");
+                        }
+                    }
+                })
+                .detach();
+        }
+    }
+
+    pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
+        let new_active_entry = entry.and_then(|project_path| {
+            let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+            let entry = worktree.read(cx).entry_for_path(project_path.path)?;
+            Some(entry.id)
+        });
+        if new_active_entry != self.active_entry {
+            self.active_entry = new_active_entry;
+            cx.emit(Event::ActiveEntryChanged(new_active_entry));
+        }
+    }
+
+    pub fn language_servers_running_disk_based_diagnostics(
+        &self,
+    ) -> impl Iterator<Item = LanguageServerId> + '_ {
+        self.language_server_statuses
+            .iter()
+            .filter_map(|(id, status)| {
+                if status.has_pending_diagnostic_updates {
+                    Some(*id)
+                } else {
+                    None
+                }
+            })
+    }
+
+    pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
+        let mut summary = DiagnosticSummary::default();
+        for (_, _, path_summary) in self.diagnostic_summaries(cx) {
+            summary.error_count += path_summary.error_count;
+            summary.warning_count += path_summary.warning_count;
+        }
+        summary
+    }
+
+    pub fn diagnostic_summaries<'a>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl Iterator<Item = (ProjectPath, LanguageServerId, DiagnosticSummary)> + 'a {
+        self.visible_worktrees(cx).flat_map(move |worktree| {
+            let worktree = worktree.read(cx);
+            let worktree_id = worktree.id();
+            worktree
+                .diagnostic_summaries()
+                .map(move |(path, server_id, summary)| {
+                    (ProjectPath { worktree_id, path }, server_id, summary)
+                })
+        })
+    }
+
+    pub fn disk_based_diagnostics_started(
+        &mut self,
+        language_server_id: LanguageServerId,
+        cx: &mut ModelContext<Self>,
+    ) {
+        cx.emit(Event::DiskBasedDiagnosticsStarted { language_server_id });
+    }
+
+    pub fn disk_based_diagnostics_finished(
+        &mut self,
+        language_server_id: LanguageServerId,
+        cx: &mut ModelContext<Self>,
+    ) {
+        cx.emit(Event::DiskBasedDiagnosticsFinished { language_server_id });
+    }
+
+    pub fn active_entry(&self) -> Option<ProjectEntryId> {
+        self.active_entry
+    }
+
+    pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Entry> {
+        self.worktree_for_id(path.worktree_id, cx)?
+            .read(cx)
+            .entry_for_path(&path.path)
+            .cloned()
+    }
+
+    pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option<ProjectPath> {
+        let worktree = self.worktree_for_entry(entry_id, cx)?;
+        let worktree = worktree.read(cx);
+        let worktree_id = worktree.id();
+        let path = worktree.entry_for_id(entry_id)?.path.clone();
+        Some(ProjectPath { worktree_id, path })
+    }
+
+    pub fn absolute_path(&self, project_path: &ProjectPath, cx: &AppContext) -> Option<PathBuf> {
+        let workspace_root = self
+            .worktree_for_id(project_path.worktree_id, cx)?
+            .read(cx)
+            .abs_path();
+        let project_path = project_path.path.as_ref();
+
+        Some(if project_path == Path::new("") {
+            workspace_root.to_path_buf()
+        } else {
+            workspace_root.join(project_path)
+        })
+    }
+
+    // RPC message handlers
+
+    async fn handle_unshare_project(
+        this: Model<Self>,
+        _: TypedEnvelope<proto::UnshareProject>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            if this.is_local() {
+                this.unshare(cx)?;
+            } else {
+                this.disconnected_from_host(cx);
+            }
+            Ok(())
+        })?
+    }
+
+    async fn handle_add_collaborator(
+        this: Model<Self>,
+        mut envelope: TypedEnvelope<proto::AddProjectCollaborator>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let collaborator = envelope
+            .payload
+            .collaborator
+            .take()
+            .ok_or_else(|| anyhow!("empty collaborator"))?;
+
+        let collaborator = Collaborator::from_proto(collaborator)?;
+        this.update(&mut cx, |this, cx| {
+            this.shared_buffers.remove(&collaborator.peer_id);
+            cx.emit(Event::CollaboratorJoined(collaborator.peer_id));
+            this.collaborators
+                .insert(collaborator.peer_id, collaborator);
+            cx.notify();
+        })?;
+
+        Ok(())
+    }
+
+    async fn handle_update_project_collaborator(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::UpdateProjectCollaborator>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let old_peer_id = envelope
+            .payload
+            .old_peer_id
+            .ok_or_else(|| anyhow!("missing old peer id"))?;
+        let new_peer_id = envelope
+            .payload
+            .new_peer_id
+            .ok_or_else(|| anyhow!("missing new peer id"))?;
+        this.update(&mut cx, |this, cx| {
+            let collaborator = this
+                .collaborators
+                .remove(&old_peer_id)
+                .ok_or_else(|| anyhow!("received UpdateProjectCollaborator for unknown peer"))?;
+            let is_host = collaborator.replica_id == 0;
+            this.collaborators.insert(new_peer_id, collaborator);
+
+            let buffers = this.shared_buffers.remove(&old_peer_id);
+            log::info!(
+                "peer {} became {}. moving buffers {:?}",
+                old_peer_id,
+                new_peer_id,
+                &buffers
+            );
+            if let Some(buffers) = buffers {
+                this.shared_buffers.insert(new_peer_id, buffers);
+            }
+
+            if is_host {
+                this.opened_buffers
+                    .retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
+                this.buffer_ordered_messages_tx
+                    .unbounded_send(BufferOrderedMessage::Resync)
+                    .unwrap();
+            }
+
+            cx.emit(Event::CollaboratorUpdated {
+                old_peer_id,
+                new_peer_id,
+            });
+            cx.notify();
+            Ok(())
+        })?
+    }
+
+    async fn handle_remove_collaborator(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::RemoveProjectCollaborator>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            let peer_id = envelope
+                .payload
+                .peer_id
+                .ok_or_else(|| anyhow!("invalid peer id"))?;
+            let replica_id = this
+                .collaborators
+                .remove(&peer_id)
+                .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?
+                .replica_id;
+            for buffer in this.opened_buffers.values() {
+                if let Some(buffer) = buffer.upgrade() {
+                    buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
+                }
+            }
+            this.shared_buffers.remove(&peer_id);
+
+            cx.emit(Event::CollaboratorLeft(peer_id));
+            cx.notify();
+            Ok(())
+        })?
+    }
+
+    async fn handle_update_project(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::UpdateProject>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            // Don't handle messages that were sent before the response to us joining the project
+            if envelope.message_id > this.join_project_response_message_id {
+                this.set_worktrees_from_proto(envelope.payload.worktrees, cx)?;
+            }
+            Ok(())
+        })?
+    }
+
+    async fn handle_update_worktree(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::UpdateWorktree>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+            if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
+                worktree.update(cx, |worktree, _| {
+                    let worktree = worktree.as_remote_mut().unwrap();
+                    worktree.update_from_remote(envelope.payload);
+                });
+            }
+            Ok(())
+        })?
+    }
+
+    async fn handle_update_worktree_settings(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+            if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
+                cx.update_global::<SettingsStore, _>(|store, cx| {
+                    store
+                        .set_local_settings(
+                            worktree.entity_id().as_u64() as usize,
+                            PathBuf::from(&envelope.payload.path).into(),
+                            envelope.payload.content.as_ref().map(String::as_str),
+                            cx,
+                        )
+                        .log_err();
+                });
+            }
+            Ok(())
+        })?
+    }
+
+    async fn handle_create_project_entry(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::CreateProjectEntry>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ProjectEntryResponse> {
+        let worktree = this.update(&mut cx, |this, cx| {
+            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+            this.worktree_for_id(worktree_id, cx)
+                .ok_or_else(|| anyhow!("worktree not found"))
+        })??;
+        let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())?;
+        let entry = worktree
+            .update(&mut cx, |worktree, cx| {
+                let worktree = worktree.as_local_mut().unwrap();
+                let path = PathBuf::from(envelope.payload.path);
+                worktree.create_entry(path, envelope.payload.is_directory, cx)
+            })?
+            .await?;
+        Ok(proto::ProjectEntryResponse {
+            entry: Some((&entry).into()),
+            worktree_scan_id: worktree_scan_id as u64,
+        })
+    }
+
+    async fn handle_rename_project_entry(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::RenameProjectEntry>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ProjectEntryResponse> {
+        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
+        let worktree = this.update(&mut cx, |this, cx| {
+            this.worktree_for_entry(entry_id, cx)
+                .ok_or_else(|| anyhow!("worktree not found"))
+        })??;
+        let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())?;
+        let entry = worktree
+            .update(&mut cx, |worktree, cx| {
+                let new_path = PathBuf::from(envelope.payload.new_path);
+                worktree
+                    .as_local_mut()
+                    .unwrap()
+                    .rename_entry(entry_id, new_path, cx)
+                    .ok_or_else(|| anyhow!("invalid entry"))
+            })??
+            .await?;
+        Ok(proto::ProjectEntryResponse {
+            entry: Some((&entry).into()),
+            worktree_scan_id: worktree_scan_id as u64,
+        })
+    }
+
+    async fn handle_copy_project_entry(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::CopyProjectEntry>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ProjectEntryResponse> {
+        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
+        let worktree = this.update(&mut cx, |this, cx| {
+            this.worktree_for_entry(entry_id, cx)
+                .ok_or_else(|| anyhow!("worktree not found"))
+        })??;
+        let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())?;
+        let entry = worktree
+            .update(&mut cx, |worktree, cx| {
+                let new_path = PathBuf::from(envelope.payload.new_path);
+                worktree
+                    .as_local_mut()
+                    .unwrap()
+                    .copy_entry(entry_id, new_path, cx)
+                    .ok_or_else(|| anyhow!("invalid entry"))
+            })??
+            .await?;
+        Ok(proto::ProjectEntryResponse {
+            entry: Some((&entry).into()),
+            worktree_scan_id: worktree_scan_id as u64,
+        })
+    }
+
+    async fn handle_delete_project_entry(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::DeleteProjectEntry>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ProjectEntryResponse> {
+        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
+
+        this.update(&mut cx, |_, cx| cx.emit(Event::DeletedEntry(entry_id)))?;
+
+        let worktree = this.update(&mut cx, |this, cx| {
+            this.worktree_for_entry(entry_id, cx)
+                .ok_or_else(|| anyhow!("worktree not found"))
+        })??;
+        let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())?;
+        worktree
+            .update(&mut cx, |worktree, cx| {
+                worktree
+                    .as_local_mut()
+                    .unwrap()
+                    .delete_entry(entry_id, cx)
+                    .ok_or_else(|| anyhow!("invalid entry"))
+            })??
+            .await?;
+        Ok(proto::ProjectEntryResponse {
+            entry: None,
+            worktree_scan_id: worktree_scan_id as u64,
+        })
+    }
+
+    async fn handle_expand_project_entry(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::ExpandProjectEntry>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ExpandProjectEntryResponse> {
+        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
+        let worktree = this
+            .update(&mut cx, |this, cx| this.worktree_for_entry(entry_id, cx))?
+            .ok_or_else(|| anyhow!("invalid request"))?;
+        worktree
+            .update(&mut cx, |worktree, cx| {
+                worktree
+                    .as_local_mut()
+                    .unwrap()
+                    .expand_entry(entry_id, cx)
+                    .ok_or_else(|| anyhow!("invalid entry"))
+            })??
+            .await?;
+        let worktree_scan_id = worktree.update(&mut cx, |worktree, _| worktree.scan_id())? as u64;
+        Ok(proto::ExpandProjectEntryResponse { worktree_scan_id })
+    }
+
+    async fn handle_update_diagnostic_summary(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+            if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
+                if let Some(summary) = envelope.payload.summary {
+                    let project_path = ProjectPath {
+                        worktree_id,
+                        path: Path::new(&summary.path).into(),
+                    };
+                    worktree.update(cx, |worktree, _| {
+                        worktree
+                            .as_remote_mut()
+                            .unwrap()
+                            .update_diagnostic_summary(project_path.path.clone(), &summary);
+                    });
+                    cx.emit(Event::DiagnosticsUpdated {
+                        language_server_id: LanguageServerId(summary.language_server_id as usize),
+                        path: project_path,
+                    });
+                }
+            }
+            Ok(())
+        })?
+    }
+
+    async fn handle_start_language_server(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::StartLanguageServer>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let server = envelope
+            .payload
+            .server
+            .ok_or_else(|| anyhow!("invalid server"))?;
+        this.update(&mut cx, |this, cx| {
+            this.language_server_statuses.insert(
+                LanguageServerId(server.id as usize),
+                LanguageServerStatus {
+                    name: server.name,
+                    pending_work: Default::default(),
+                    has_pending_diagnostic_updates: false,
+                    progress_tokens: Default::default(),
+                },
+            );
+            cx.notify();
+        })?;
+        Ok(())
+    }
+
+    async fn handle_update_language_server(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::UpdateLanguageServer>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            let language_server_id = LanguageServerId(envelope.payload.language_server_id as usize);
+
+            match envelope
+                .payload
+                .variant
+                .ok_or_else(|| anyhow!("invalid variant"))?
+            {
+                proto::update_language_server::Variant::WorkStart(payload) => {
+                    this.on_lsp_work_start(
+                        language_server_id,
+                        payload.token,
+                        LanguageServerProgress {
+                            message: payload.message,
+                            percentage: payload.percentage.map(|p| p as usize),
+                            last_update_at: Instant::now(),
+                        },
+                        cx,
+                    );
+                }
+
+                proto::update_language_server::Variant::WorkProgress(payload) => {
+                    this.on_lsp_work_progress(
+                        language_server_id,
+                        payload.token,
+                        LanguageServerProgress {
+                            message: payload.message,
+                            percentage: payload.percentage.map(|p| p as usize),
+                            last_update_at: Instant::now(),
+                        },
+                        cx,
+                    );
+                }
+
+                proto::update_language_server::Variant::WorkEnd(payload) => {
+                    this.on_lsp_work_end(language_server_id, payload.token, cx);
+                }
+
+                proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => {
+                    this.disk_based_diagnostics_started(language_server_id, cx);
+                }
+
+                proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => {
+                    this.disk_based_diagnostics_finished(language_server_id, cx)
+                }
+            }
+
+            Ok(())
+        })?
+    }
+
+    async fn handle_update_buffer(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::UpdateBuffer>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::Ack> {
+        this.update(&mut cx, |this, cx| {
+            let payload = envelope.payload.clone();
+            let buffer_id = payload.buffer_id;
+            let ops = payload
+                .operations
+                .into_iter()
+                .map(language2::proto::deserialize_operation)
+                .collect::<Result<Vec<_>, _>>()?;
+            let is_remote = this.is_remote();
+            match this.opened_buffers.entry(buffer_id) {
+                hash_map::Entry::Occupied(mut e) => match e.get_mut() {
+                    OpenBuffer::Strong(buffer) => {
+                        buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?;
+                    }
+                    OpenBuffer::Operations(operations) => operations.extend_from_slice(&ops),
+                    OpenBuffer::Weak(_) => {}
+                },
+                hash_map::Entry::Vacant(e) => {
+                    assert!(
+                        is_remote,
+                        "received buffer update from {:?}",
+                        envelope.original_sender_id
+                    );
+                    e.insert(OpenBuffer::Operations(ops));
+                }
+            }
+            Ok(proto::Ack {})
+        })?
+    }
+
+    async fn handle_create_buffer_for_peer(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::CreateBufferForPeer>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            match envelope
+                .payload
+                .variant
+                .ok_or_else(|| anyhow!("missing variant"))?
+            {
+                proto::create_buffer_for_peer::Variant::State(mut state) => {
+                    let mut buffer_file = None;
+                    if let Some(file) = state.file.take() {
+                        let worktree_id = WorktreeId::from_proto(file.worktree_id);
+                        let worktree = this.worktree_for_id(worktree_id, cx).ok_or_else(|| {
+                            anyhow!("no worktree found for id {}", file.worktree_id)
+                        })?;
+                        buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?)
+                            as Arc<dyn language2::File>);
+                    }
+
+                    let buffer_id = state.id;
+                    let buffer = cx.build_model(|_| {
+                        Buffer::from_proto(this.replica_id(), state, buffer_file).unwrap()
+                    });
+                    this.incomplete_remote_buffers
+                        .insert(buffer_id, Some(buffer));
+                }
+                proto::create_buffer_for_peer::Variant::Chunk(chunk) => {
+                    let buffer = this
+                        .incomplete_remote_buffers
+                        .get(&chunk.buffer_id)
+                        .cloned()
+                        .flatten()
+                        .ok_or_else(|| {
+                            anyhow!(
+                                "received chunk for buffer {} without initial state",
+                                chunk.buffer_id
+                            )
+                        })?;
+                    let operations = chunk
+                        .operations
+                        .into_iter()
+                        .map(language2::proto::deserialize_operation)
+                        .collect::<Result<Vec<_>>>()?;
+                    buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
+
+                    if chunk.is_last {
+                        this.incomplete_remote_buffers.remove(&chunk.buffer_id);
+                        this.register_buffer(&buffer, cx)?;
+                    }
+                }
+            }
+
+            Ok(())
+        })?
+    }
+
+    async fn handle_update_diff_base(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::UpdateDiffBase>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            let buffer_id = envelope.payload.buffer_id;
+            let diff_base = envelope.payload.diff_base;
+            if let Some(buffer) = this
+                .opened_buffers
+                .get_mut(&buffer_id)
+                .and_then(|b| b.upgrade())
+                .or_else(|| {
+                    this.incomplete_remote_buffers
+                        .get(&buffer_id)
+                        .cloned()
+                        .flatten()
+                })
+            {
+                buffer.update(cx, |buffer, cx| buffer.set_diff_base(diff_base, cx));
+            }
+            Ok(())
+        })?
+    }
+
+    async fn handle_update_buffer_file(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::UpdateBufferFile>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let buffer_id = envelope.payload.buffer_id;
+
+        this.update(&mut cx, |this, cx| {
+            let payload = envelope.payload.clone();
+            if let Some(buffer) = this
+                .opened_buffers
+                .get(&buffer_id)
+                .and_then(|b| b.upgrade())
+                .or_else(|| {
+                    this.incomplete_remote_buffers
+                        .get(&buffer_id)
+                        .cloned()
+                        .flatten()
+                })
+            {
+                let file = payload.file.ok_or_else(|| anyhow!("invalid file"))?;
+                let worktree = this
+                    .worktree_for_id(WorktreeId::from_proto(file.worktree_id), cx)
+                    .ok_or_else(|| anyhow!("no such worktree"))?;
+                let file = File::from_proto(file, worktree, cx)?;
+                buffer.update(cx, |buffer, cx| {
+                    buffer.file_updated(Arc::new(file), cx).detach();
+                });
+                this.detect_language_for_buffer(&buffer, cx);
+            }
+            Ok(())
+        })?
+    }
+
+    async fn handle_save_buffer(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::SaveBuffer>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::BufferSaved> {
+        let buffer_id = envelope.payload.buffer_id;
+        let (project_id, buffer) = this.update(&mut cx, |this, _cx| {
+            let project_id = this.remote_id().ok_or_else(|| anyhow!("not connected"))?;
+            let buffer = this
+                .opened_buffers
+                .get(&buffer_id)
+                .and_then(|buffer| buffer.upgrade())
+                .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?;
+            anyhow::Ok((project_id, buffer))
+        })??;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&envelope.payload.version))
+            })?
+            .await?;
+        let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
+
+        this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
+            .await?;
+        Ok(buffer.update(&mut cx, |buffer, _| proto::BufferSaved {
+            project_id,
+            buffer_id,
+            version: serialize_version(buffer.saved_version()),
+            mtime: Some(buffer.saved_mtime().into()),
+            fingerprint: language2::proto::serialize_fingerprint(
+                buffer.saved_version_fingerprint(),
+            ),
+        })?)
+    }
+
+    async fn handle_reload_buffers(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::ReloadBuffers>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ReloadBuffersResponse> {
+        let sender_id = envelope.original_sender_id()?;
+        let reload = this.update(&mut cx, |this, cx| {
+            let mut buffers = HashSet::default();
+            for buffer_id in &envelope.payload.buffer_ids {
+                buffers.insert(
+                    this.opened_buffers
+                        .get(buffer_id)
+                        .and_then(|buffer| buffer.upgrade())
+                        .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?,
+                );
+            }
+            Ok::<_, anyhow::Error>(this.reload_buffers(buffers, false, cx))
+        })??;
+
+        let project_transaction = reload.await?;
+        let project_transaction = this.update(&mut cx, |this, cx| {
+            this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx)
+        })?;
+        Ok(proto::ReloadBuffersResponse {
+            transaction: Some(project_transaction),
+        })
+    }
+
+    async fn handle_synchronize_buffers(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::SynchronizeBuffers>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::SynchronizeBuffersResponse> {
+        let project_id = envelope.payload.project_id;
+        let mut response = proto::SynchronizeBuffersResponse {
+            buffers: Default::default(),
+        };
+
+        this.update(&mut cx, |this, cx| {
+            let Some(guest_id) = envelope.original_sender_id else {
+                error!("missing original_sender_id on SynchronizeBuffers request");
+                return;
+            };
+
+            this.shared_buffers.entry(guest_id).or_default().clear();
+            for buffer in envelope.payload.buffers {
+                let buffer_id = buffer.id;
+                let remote_version = language2::proto::deserialize_version(&buffer.version);
+                if let Some(buffer) = this.buffer_for_id(buffer_id) {
+                    this.shared_buffers
+                        .entry(guest_id)
+                        .or_default()
+                        .insert(buffer_id);
+
+                    let buffer = buffer.read(cx);
+                    response.buffers.push(proto::BufferVersion {
+                        id: buffer_id,
+                        version: language2::proto::serialize_version(&buffer.version),
+                    });
+
+                    let operations = buffer.serialize_ops(Some(remote_version), cx);
+                    let client = this.client.clone();
+                    if let Some(file) = buffer.file() {
+                        client
+                            .send(proto::UpdateBufferFile {
+                                project_id,
+                                buffer_id: buffer_id as u64,
+                                file: Some(file.to_proto()),
+                            })
+                            .log_err();
+                    }
+
+                    client
+                        .send(proto::UpdateDiffBase {
+                            project_id,
+                            buffer_id: buffer_id as u64,
+                            diff_base: buffer.diff_base().map(Into::into),
+                        })
+                        .log_err();
+
+                    client
+                        .send(proto::BufferReloaded {
+                            project_id,
+                            buffer_id,
+                            version: language2::proto::serialize_version(buffer.saved_version()),
+                            mtime: Some(buffer.saved_mtime().into()),
+                            fingerprint: language2::proto::serialize_fingerprint(
+                                buffer.saved_version_fingerprint(),
+                            ),
+                            line_ending: language2::proto::serialize_line_ending(
+                                buffer.line_ending(),
+                            ) as i32,
+                        })
+                        .log_err();
+
+                    cx.executor()
+                        .spawn(
+                            async move {
+                                let operations = operations.await;
+                                for chunk in split_operations(operations) {
+                                    client
+                                        .request(proto::UpdateBuffer {
+                                            project_id,
+                                            buffer_id,
+                                            operations: chunk,
+                                        })
+                                        .await?;
+                                }
+                                anyhow::Ok(())
+                            }
+                            .log_err(),
+                        )
+                        .detach();
+                }
+            }
+        })?;
+
+        Ok(response)
+    }
+
+    async fn handle_format_buffers(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::FormatBuffers>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::FormatBuffersResponse> {
+        let sender_id = envelope.original_sender_id()?;
+        let format = this.update(&mut cx, |this, cx| {
+            let mut buffers = HashSet::default();
+            for buffer_id in &envelope.payload.buffer_ids {
+                buffers.insert(
+                    this.opened_buffers
+                        .get(buffer_id)
+                        .and_then(|buffer| buffer.upgrade())
+                        .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?,
+                );
+            }
+            let trigger = FormatTrigger::from_proto(envelope.payload.trigger);
+            Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, cx))
+        })??;
+
+        let project_transaction = format.await?;
+        let project_transaction = this.update(&mut cx, |this, cx| {
+            this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx)
+        })?;
+        Ok(proto::FormatBuffersResponse {
+            transaction: Some(project_transaction),
+        })
+    }
+
+    async fn handle_apply_additional_edits_for_completion(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::ApplyCompletionAdditionalEdits>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ApplyCompletionAdditionalEditsResponse> {
+        let (buffer, completion) = this.update(&mut cx, |this, cx| {
+            let buffer = this
+                .opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade())
+                .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
+            let language = buffer.read(cx).language();
+            let completion = language2::proto::deserialize_completion(
+                envelope
+                    .payload
+                    .completion
+                    .ok_or_else(|| anyhow!("invalid completion"))?,
+                language.cloned(),
+            );
+            Ok::<_, anyhow::Error>((buffer, completion))
+        })??;
+
+        let completion = completion.await?;
+
+        let apply_additional_edits = this.update(&mut cx, |this, cx| {
+            this.apply_additional_edits_for_completion(buffer, completion, false, cx)
+        })?;
+
+        Ok(proto::ApplyCompletionAdditionalEditsResponse {
+            transaction: apply_additional_edits
+                .await?
+                .as_ref()
+                .map(language2::proto::serialize_transaction),
+        })
+    }
+
+    async fn handle_apply_code_action(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::ApplyCodeAction>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ApplyCodeActionResponse> {
+        let sender_id = envelope.original_sender_id()?;
+        let action = language2::proto::deserialize_code_action(
+            envelope
+                .payload
+                .action
+                .ok_or_else(|| anyhow!("invalid action"))?,
+        )?;
+        let apply_code_action = this.update(&mut cx, |this, cx| {
+            let buffer = this
+                .opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade())
+                .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
+            Ok::<_, anyhow::Error>(this.apply_code_action(buffer, action, false, cx))
+        })??;
+
+        let project_transaction = apply_code_action.await?;
+        let project_transaction = this.update(&mut cx, |this, cx| {
+            this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx)
+        })?;
+        Ok(proto::ApplyCodeActionResponse {
+            transaction: Some(project_transaction),
+        })
+    }
+
+    async fn handle_on_type_formatting(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::OnTypeFormatting>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::OnTypeFormattingResponse> {
+        let on_type_formatting = this.update(&mut cx, |this, cx| {
+            let buffer = this
+                .opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade())
+                .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
+            let position = envelope
+                .payload
+                .position
+                .and_then(deserialize_anchor)
+                .ok_or_else(|| anyhow!("invalid position"))?;
+            Ok::<_, anyhow::Error>(this.apply_on_type_formatting(
+                buffer,
+                position,
+                envelope.payload.trigger.clone(),
+                cx,
+            ))
+        })??;
+
+        let transaction = on_type_formatting
+            .await?
+            .as_ref()
+            .map(language2::proto::serialize_transaction);
+        Ok(proto::OnTypeFormattingResponse { transaction })
+    }
+
+    async fn handle_inlay_hints(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::InlayHints>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::InlayHintsResponse> {
+        let sender_id = envelope.original_sender_id()?;
+        let buffer = this.update(&mut cx, |this, _| {
+            this.opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade())
+                .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
+        })??;
+        let buffer_version = deserialize_version(&envelope.payload.version);
+
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(buffer_version.clone())
+            })?
+            .await
+            .with_context(|| {
+                format!(
+                    "waiting for version {:?} for buffer {}",
+                    buffer_version,
+                    buffer.entity_id()
+                )
+            })?;
+
+        let start = envelope
+            .payload
+            .start
+            .and_then(deserialize_anchor)
+            .context("missing range start")?;
+        let end = envelope
+            .payload
+            .end
+            .and_then(deserialize_anchor)
+            .context("missing range end")?;
+        let buffer_hints = this
+            .update(&mut cx, |project, cx| {
+                project.inlay_hints(buffer, start..end, cx)
+            })?
+            .await
+            .context("inlay hints fetch")?;
+
+        Ok(this.update(&mut cx, |project, cx| {
+            InlayHints::response_to_proto(buffer_hints, project, sender_id, &buffer_version, cx)
+        })?)
+    }
+
+    async fn handle_resolve_inlay_hint(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::ResolveInlayHint>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ResolveInlayHintResponse> {
+        let proto_hint = envelope
+            .payload
+            .hint
+            .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint");
+        let hint = InlayHints::proto_to_project_hint(proto_hint)
+            .context("resolved proto inlay hint conversion")?;
+        let buffer = this.update(&mut cx, |this, _cx| {
+            this.opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade())
+                .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
+        })??;
+        let response_hint = this
+            .update(&mut cx, |project, cx| {
+                project.resolve_inlay_hint(
+                    hint,
+                    buffer,
+                    LanguageServerId(envelope.payload.language_server_id as usize),
+                    cx,
+                )
+            })?
+            .await
+            .context("inlay hints fetch")?;
+        Ok(proto::ResolveInlayHintResponse {
+            hint: Some(InlayHints::project_to_proto_hint(response_hint)),
+        })
+    }
+
+    async fn handle_refresh_inlay_hints(
+        this: Model<Self>,
+        _: TypedEnvelope<proto::RefreshInlayHints>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::Ack> {
+        this.update(&mut cx, |_, cx| {
+            cx.emit(Event::RefreshInlayHints);
+        })?;
+        Ok(proto::Ack {})
+    }
+
+    async fn handle_lsp_command<T: LspCommand>(
+        this: Model<Self>,
+        envelope: TypedEnvelope<T::ProtoRequest>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<<T::ProtoRequest as proto::RequestMessage>::Response>
+    where
+        <T::LspRequest as lsp2::request::Request>::Params: Send,
+        <T::LspRequest as lsp2::request::Request>::Result: Send,
+    {
+        let sender_id = envelope.original_sender_id()?;
+        let buffer_id = T::buffer_id_from_proto(&envelope.payload);
+        let buffer_handle = this.update(&mut cx, |this, _cx| {
+            this.opened_buffers
+                .get(&buffer_id)
+                .and_then(|buffer| buffer.upgrade())
+                .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))
+        })??;
+        let request = T::from_proto(
+            envelope.payload,
+            this.clone(),
+            buffer_handle.clone(),
+            cx.clone(),
+        )
+        .await?;
+        let buffer_version = buffer_handle.update(&mut cx, |buffer, _| buffer.version())?;
+        let response = this
+            .update(&mut cx, |this, cx| {
+                this.request_lsp(buffer_handle, LanguageServerToQuery::Primary, request, cx)
+            })?
+            .await?;
+        this.update(&mut cx, |this, cx| {
+            Ok(T::response_to_proto(
+                response,
+                this,
+                sender_id,
+                &buffer_version,
+                cx,
+            ))
+        })?
+    }
+
+    async fn handle_get_project_symbols(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::GetProjectSymbols>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::GetProjectSymbolsResponse> {
+        let symbols = this
+            .update(&mut cx, |this, cx| {
+                this.symbols(&envelope.payload.query, cx)
+            })?
+            .await?;
+
+        Ok(proto::GetProjectSymbolsResponse {
+            symbols: symbols.iter().map(serialize_symbol).collect(),
+        })
+    }
+
+    async fn handle_search_project(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::SearchProject>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::SearchProjectResponse> {
+        let peer_id = envelope.original_sender_id()?;
+        let query = SearchQuery::from_proto(envelope.payload)?;
+        let mut result = this.update(&mut cx, |this, cx| this.search(query, cx))?;
+
+        cx.spawn(move |mut cx| async move {
+            let mut locations = Vec::new();
+            while let Some((buffer, ranges)) = result.next().await {
+                for range in ranges {
+                    let start = serialize_anchor(&range.start);
+                    let end = serialize_anchor(&range.end);
+                    let buffer_id = this.update(&mut cx, |this, cx| {
+                        this.create_buffer_for_peer(&buffer, peer_id, cx)
+                    })?;
+                    locations.push(proto::Location {
+                        buffer_id,
+                        start: Some(start),
+                        end: Some(end),
+                    });
+                }
+            }
+            Ok(proto::SearchProjectResponse { locations })
+        })
+        .await
+    }
+
+    async fn handle_open_buffer_for_symbol(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::OpenBufferForSymbol>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::OpenBufferForSymbolResponse> {
+        let peer_id = envelope.original_sender_id()?;
+        let symbol = envelope
+            .payload
+            .symbol
+            .ok_or_else(|| anyhow!("invalid symbol"))?;
+        let symbol = this
+            .update(&mut cx, |this, _| this.deserialize_symbol(symbol))?
+            .await?;
+        let symbol = this.update(&mut cx, |this, _| {
+            let signature = this.symbol_signature(&symbol.path);
+            if signature == symbol.signature {
+                Ok(symbol)
+            } else {
+                Err(anyhow!("invalid symbol signature"))
+            }
+        })??;
+        let buffer = this
+            .update(&mut cx, |this, cx| this.open_buffer_for_symbol(&symbol, cx))?
+            .await?;
+
+        Ok(proto::OpenBufferForSymbolResponse {
+            buffer_id: this.update(&mut cx, |this, cx| {
+                this.create_buffer_for_peer(&buffer, peer_id, cx)
+            })?,
+        })
+    }
+
+    fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] {
+        let mut hasher = Sha256::new();
+        hasher.update(project_path.worktree_id.to_proto().to_be_bytes());
+        hasher.update(project_path.path.to_string_lossy().as_bytes());
+        hasher.update(self.nonce.to_be_bytes());
+        hasher.finalize().as_slice().try_into().unwrap()
+    }
+
+    async fn handle_open_buffer_by_id(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::OpenBufferById>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::OpenBufferResponse> {
+        let peer_id = envelope.original_sender_id()?;
+        let buffer = this
+            .update(&mut cx, |this, cx| {
+                this.open_buffer_by_id(envelope.payload.id, cx)
+            })?
+            .await?;
+        this.update(&mut cx, |this, cx| {
+            Ok(proto::OpenBufferResponse {
+                buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx),
+            })
+        })?
+    }
+
+    async fn handle_open_buffer_by_path(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::OpenBufferByPath>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::OpenBufferResponse> {
+        let peer_id = envelope.original_sender_id()?;
+        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+        let open_buffer = this.update(&mut cx, |this, cx| {
+            this.open_buffer(
+                ProjectPath {
+                    worktree_id,
+                    path: PathBuf::from(envelope.payload.path).into(),
+                },
+                cx,
+            )
+        })?;
+
+        let buffer = open_buffer.await?;
+        this.update(&mut cx, |this, cx| {
+            Ok(proto::OpenBufferResponse {
+                buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx),
+            })
+        })?
+    }
+
+    fn serialize_project_transaction_for_peer(
+        &mut self,
+        project_transaction: ProjectTransaction,
+        peer_id: proto::PeerId,
+        cx: &mut AppContext,
+    ) -> proto::ProjectTransaction {
+        let mut serialized_transaction = proto::ProjectTransaction {
+            buffer_ids: Default::default(),
+            transactions: Default::default(),
+        };
+        for (buffer, transaction) in project_transaction.0 {
+            serialized_transaction
+                .buffer_ids
+                .push(self.create_buffer_for_peer(&buffer, peer_id, cx));
+            serialized_transaction
+                .transactions
+                .push(language2::proto::serialize_transaction(&transaction));
+        }
+        serialized_transaction
+    }
+
+    fn deserialize_project_transaction(
+        &mut self,
+        message: proto::ProjectTransaction,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ProjectTransaction>> {
+        cx.spawn(move |this, mut cx| async move {
+            let mut project_transaction = ProjectTransaction::default();
+            for (buffer_id, transaction) in message.buffer_ids.into_iter().zip(message.transactions)
+            {
+                let buffer = this
+                    .update(&mut cx, |this, cx| {
+                        this.wait_for_remote_buffer(buffer_id, cx)
+                    })?
+                    .await?;
+                let transaction = language2::proto::deserialize_transaction(transaction)?;
+                project_transaction.0.insert(buffer, transaction);
+            }
+
+            for (buffer, transaction) in &project_transaction.0 {
+                buffer
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_edits(transaction.edit_ids.iter().copied())
+                    })?
+                    .await?;
+
+                if push_to_history {
+                    buffer.update(&mut cx, |buffer, _| {
+                        buffer.push_transaction(transaction.clone(), Instant::now());
+                    })?;
+                }
+            }
+
+            Ok(project_transaction)
+        })
+    }
+
+    fn create_buffer_for_peer(
+        &mut self,
+        buffer: &Model<Buffer>,
+        peer_id: proto::PeerId,
+        cx: &mut AppContext,
+    ) -> u64 {
+        let buffer_id = buffer.read(cx).remote_id();
+        if let Some(ProjectClientState::Local { updates_tx, .. }) = &self.client_state {
+            updates_tx
+                .unbounded_send(LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id })
+                .ok();
+        }
+        buffer_id
+    }
+
+    fn wait_for_remote_buffer(
+        &mut self,
+        id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Model<Buffer>>> {
+        let mut opened_buffer_rx = self.opened_buffer.1.clone();
+
+        cx.spawn(move |this, mut cx| async move {
+            let buffer = loop {
+                let Some(this) = this.upgrade() else {
+                    return Err(anyhow!("project dropped"));
+                };
+
+                let buffer = this.update(&mut cx, |this, _cx| {
+                    this.opened_buffers
+                        .get(&id)
+                        .and_then(|buffer| buffer.upgrade())
+                })?;
+
+                if let Some(buffer) = buffer {
+                    break buffer;
+                } else if this.update(&mut cx, |this, _| this.is_read_only())? {
+                    return Err(anyhow!("disconnected before buffer {} could be opened", id));
+                }
+
+                this.update(&mut cx, |this, _| {
+                    this.incomplete_remote_buffers.entry(id).or_default();
+                })?;
+                drop(this);
+
+                opened_buffer_rx
+                    .next()
+                    .await
+                    .ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
+            };
+
+            Ok(buffer)
+        })
+    }
+
+    fn synchronize_remote_buffers(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        let project_id = match self.client_state.as_ref() {
+            Some(ProjectClientState::Remote {
+                sharing_has_stopped,
+                remote_id,
+                ..
+            }) => {
+                if *sharing_has_stopped {
+                    return Task::ready(Err(anyhow!(
+                        "can't synchronize remote buffers on a readonly project"
+                    )));
+                } else {
+                    *remote_id
+                }
+            }
+            Some(ProjectClientState::Local { .. }) | None => {
+                return Task::ready(Err(anyhow!(
+                    "can't synchronize remote buffers on a local project"
+                )))
+            }
+        };
+
+        let client = self.client.clone();
+        cx.spawn(move |this, mut cx| async move {
+            let (buffers, incomplete_buffer_ids) = this.update(&mut cx, |this, cx| {
+                let buffers = this
+                    .opened_buffers
+                    .iter()
+                    .filter_map(|(id, buffer)| {
+                        let buffer = buffer.upgrade()?;
+                        Some(proto::BufferVersion {
+                            id: *id,
+                            version: language2::proto::serialize_version(&buffer.read(cx).version),
+                        })
+                    })
+                    .collect();
+                let incomplete_buffer_ids = this
+                    .incomplete_remote_buffers
+                    .keys()
+                    .copied()
+                    .collect::<Vec<_>>();
+
+                (buffers, incomplete_buffer_ids)
+            })?;
+            let response = client
+                .request(proto::SynchronizeBuffers {
+                    project_id,
+                    buffers,
+                })
+                .await?;
+
+            let send_updates_for_buffers = this.update(&mut cx, |this, cx| {
+                response
+                    .buffers
+                    .into_iter()
+                    .map(|buffer| {
+                        let client = client.clone();
+                        let buffer_id = buffer.id;
+                        let remote_version = language2::proto::deserialize_version(&buffer.version);
+                        if let Some(buffer) = this.buffer_for_id(buffer_id) {
+                            let operations =
+                                buffer.read(cx).serialize_ops(Some(remote_version), cx);
+                            cx.executor().spawn(async move {
+                                let operations = operations.await;
+                                for chunk in split_operations(operations) {
+                                    client
+                                        .request(proto::UpdateBuffer {
+                                            project_id,
+                                            buffer_id,
+                                            operations: chunk,
+                                        })
+                                        .await?;
+                                }
+                                anyhow::Ok(())
+                            })
+                        } else {
+                            Task::ready(Ok(()))
+                        }
+                    })
+                    .collect::<Vec<_>>()
+            })?;
+
+            // Any incomplete buffers have open requests waiting. Request that the host sends
+            // creates these buffers for us again to unblock any waiting futures.
+            for id in incomplete_buffer_ids {
+                cx.executor()
+                    .spawn(client.request(proto::OpenBufferById { project_id, id }))
+                    .detach();
+            }
+
+            futures::future::join_all(send_updates_for_buffers)
+                .await
+                .into_iter()
+                .collect()
+        })
+    }
+
+    pub fn worktree_metadata_protos(&self, cx: &AppContext) -> Vec<proto::WorktreeMetadata> {
+        self.worktrees()
+            .map(|worktree| {
+                let worktree = worktree.read(cx);
+                proto::WorktreeMetadata {
+                    id: worktree.id().to_proto(),
+                    root_name: worktree.root_name().into(),
+                    visible: worktree.is_visible(),
+                    abs_path: worktree.abs_path().to_string_lossy().into(),
+                }
+            })
+            .collect()
+    }
+
+    fn set_worktrees_from_proto(
+        &mut self,
+        worktrees: Vec<proto::WorktreeMetadata>,
+        cx: &mut ModelContext<Project>,
+    ) -> Result<()> {
+        let replica_id = self.replica_id();
+        let remote_id = self.remote_id().ok_or_else(|| anyhow!("invalid project"))?;
+
+        let mut old_worktrees_by_id = self
+            .worktrees
+            .drain(..)
+            .filter_map(|worktree| {
+                let worktree = worktree.upgrade()?;
+                Some((worktree.read(cx).id(), worktree))
+            })
+            .collect::<HashMap<_, _>>();
+
+        for worktree in worktrees {
+            if let Some(old_worktree) =
+                old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id))
+            {
+                self.worktrees.push(WorktreeHandle::Strong(old_worktree));
+            } else {
+                let worktree =
+                    Worktree::remote(remote_id, replica_id, worktree, self.client.clone(), cx);
+                let _ = self.add_worktree(&worktree, cx);
+            }
+        }
+
+        self.metadata_changed(cx);
+        for id in old_worktrees_by_id.keys() {
+            cx.emit(Event::WorktreeRemoved(*id));
+        }
+
+        Ok(())
+    }
+
+    fn set_collaborators_from_proto(
+        &mut self,
+        messages: Vec<proto::Collaborator>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let mut collaborators = HashMap::default();
+        for message in messages {
+            let collaborator = Collaborator::from_proto(message)?;
+            collaborators.insert(collaborator.peer_id, collaborator);
+        }
+        for old_peer_id in self.collaborators.keys() {
+            if !collaborators.contains_key(old_peer_id) {
+                cx.emit(Event::CollaboratorLeft(*old_peer_id));
+            }
+        }
+        self.collaborators = collaborators;
+        Ok(())
+    }
+
+    fn deserialize_symbol(
+        &self,
+        serialized_symbol: proto::Symbol,
+    ) -> impl Future<Output = Result<Symbol>> {
+        let languages = self.languages.clone();
+        async move {
+            let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
+            let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
+            let start = serialized_symbol
+                .start
+                .ok_or_else(|| anyhow!("invalid start"))?;
+            let end = serialized_symbol
+                .end
+                .ok_or_else(|| anyhow!("invalid end"))?;
+            let kind = unsafe { mem::transmute(serialized_symbol.kind) };
+            let path = ProjectPath {
+                worktree_id,
+                path: PathBuf::from(serialized_symbol.path).into(),
+            };
+            let language = languages
+                .language_for_file(&path.path, None)
+                .await
+                .log_err();
+            Ok(Symbol {
+                language_server_name: LanguageServerName(
+                    serialized_symbol.language_server_name.into(),
+                ),
+                source_worktree_id,
+                path,
+                label: {
+                    match language {
+                        Some(language) => {
+                            language
+                                .label_for_symbol(&serialized_symbol.name, kind)
+                                .await
+                        }
+                        None => None,
+                    }
+                    .unwrap_or_else(|| CodeLabel::plain(serialized_symbol.name.clone(), None))
+                },
+
+                name: serialized_symbol.name,
+                range: Unclipped(PointUtf16::new(start.row, start.column))
+                    ..Unclipped(PointUtf16::new(end.row, end.column)),
+                kind,
+                signature: serialized_symbol
+                    .signature
+                    .try_into()
+                    .map_err(|_| anyhow!("invalid signature"))?,
+            })
+        }
+    }
+
+    async fn handle_buffer_saved(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::BufferSaved>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let fingerprint = deserialize_fingerprint(&envelope.payload.fingerprint)?;
+        let version = deserialize_version(&envelope.payload.version);
+        let mtime = envelope
+            .payload
+            .mtime
+            .ok_or_else(|| anyhow!("missing mtime"))?
+            .into();
+
+        this.update(&mut cx, |this, cx| {
+            let buffer = this
+                .opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade())
+                .or_else(|| {
+                    this.incomplete_remote_buffers
+                        .get(&envelope.payload.buffer_id)
+                        .and_then(|b| b.clone())
+                });
+            if let Some(buffer) = buffer {
+                buffer.update(cx, |buffer, cx| {
+                    buffer.did_save(version, fingerprint, mtime, cx);
+                });
+            }
+            Ok(())
+        })?
+    }
+
+    async fn handle_buffer_reloaded(
+        this: Model<Self>,
+        envelope: TypedEnvelope<proto::BufferReloaded>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let payload = envelope.payload;
+        let version = deserialize_version(&payload.version);
+        let fingerprint = deserialize_fingerprint(&payload.fingerprint)?;
+        let line_ending = deserialize_line_ending(
+            proto::LineEnding::from_i32(payload.line_ending)
+                .ok_or_else(|| anyhow!("missing line ending"))?,
+        );
+        let mtime = payload
+            .mtime
+            .ok_or_else(|| anyhow!("missing mtime"))?
+            .into();
+        this.update(&mut cx, |this, cx| {
+            let buffer = this
+                .opened_buffers
+                .get(&payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade())
+                .or_else(|| {
+                    this.incomplete_remote_buffers
+                        .get(&payload.buffer_id)
+                        .cloned()
+                        .flatten()
+                });
+            if let Some(buffer) = buffer {
+                buffer.update(cx, |buffer, cx| {
+                    buffer.did_reload(version, fingerprint, line_ending, mtime, cx);
+                });
+            }
+            Ok(())
+        })?
+    }
+
+    #[allow(clippy::type_complexity)]
+    fn edits_from_lsp(
+        &mut self,
+        buffer: &Model<Buffer>,
+        lsp_edits: impl 'static + Send + IntoIterator<Item = lsp2::TextEdit>,
+        server_id: LanguageServerId,
+        version: Option<i32>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<(Range<Anchor>, String)>>> {
+        let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx);
+        cx.executor().spawn(async move {
+            let snapshot = snapshot?;
+            let mut lsp_edits = lsp_edits
+                .into_iter()
+                .map(|edit| (range_from_lsp(edit.range), edit.new_text))
+                .collect::<Vec<_>>();
+            lsp_edits.sort_by_key(|(range, _)| range.start);
+
+            let mut lsp_edits = lsp_edits.into_iter().peekable();
+            let mut edits = Vec::new();
+            while let Some((range, mut new_text)) = lsp_edits.next() {
+                // Clip invalid ranges provided by the language server.
+                let mut range = snapshot.clip_point_utf16(range.start, Bias::Left)
+                    ..snapshot.clip_point_utf16(range.end, Bias::Left);
+
+                // Combine any LSP edits that are adjacent.
+                //
+                // Also, combine LSP edits that are separated from each other by only
+                // a newline. This is important because for some code actions,
+                // Rust-analyzer rewrites the entire buffer via a series of edits that
+                // are separated by unchanged newline characters.
+                //
+                // In order for the diffing logic below to work properly, any edits that
+                // cancel each other out must be combined into one.
+                while let Some((next_range, next_text)) = lsp_edits.peek() {
+                    if next_range.start.0 > range.end {
+                        if next_range.start.0.row > range.end.row + 1
+                            || next_range.start.0.column > 0
+                            || snapshot.clip_point_utf16(
+                                Unclipped(PointUtf16::new(range.end.row, u32::MAX)),
+                                Bias::Left,
+                            ) > range.end
+                        {
+                            break;
+                        }
+                        new_text.push('\n');
+                    }
+                    range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left);
+                    new_text.push_str(next_text);
+                    lsp_edits.next();
+                }
+
+                // For multiline edits, perform a diff of the old and new text so that
+                // we can identify the changes more precisely, preserving the locations
+                // of any anchors positioned in the unchanged regions.
+                if range.end.row > range.start.row {
+                    let mut offset = range.start.to_offset(&snapshot);
+                    let old_text = snapshot.text_for_range(range).collect::<String>();
+
+                    let diff = TextDiff::from_lines(old_text.as_str(), &new_text);
+                    let mut moved_since_edit = true;
+                    for change in diff.iter_all_changes() {
+                        let tag = change.tag();
+                        let value = change.value();
+                        match tag {
+                            ChangeTag::Equal => {
+                                offset += value.len();
+                                moved_since_edit = true;
+                            }
+                            ChangeTag::Delete => {
+                                let start = snapshot.anchor_after(offset);
+                                let end = snapshot.anchor_before(offset + value.len());
+                                if moved_since_edit {
+                                    edits.push((start..end, String::new()));
+                                } else {
+                                    edits.last_mut().unwrap().0.end = end;
+                                }
+                                offset += value.len();
+                                moved_since_edit = false;
+                            }
+                            ChangeTag::Insert => {
+                                if moved_since_edit {
+                                    let anchor = snapshot.anchor_after(offset);
+                                    edits.push((anchor..anchor, value.to_string()));
+                                } else {
+                                    edits.last_mut().unwrap().1.push_str(value);
+                                }
+                                moved_since_edit = false;
+                            }
+                        }
+                    }
+                } else if range.end == range.start {
+                    let anchor = snapshot.anchor_after(range.start);
+                    edits.push((anchor..anchor, new_text));
+                } else {
+                    let edit_start = snapshot.anchor_after(range.start);
+                    let edit_end = snapshot.anchor_before(range.end);
+                    edits.push((edit_start..edit_end, new_text));
+                }
+            }
+
+            Ok(edits)
+        })
+    }
+
+    fn buffer_snapshot_for_lsp_version(
+        &mut self,
+        buffer: &Model<Buffer>,
+        server_id: LanguageServerId,
+        version: Option<i32>,
+        cx: &AppContext,
+    ) -> Result<TextBufferSnapshot> {
+        const OLD_VERSIONS_TO_RETAIN: i32 = 10;
+
+        if let Some(version) = version {
+            let buffer_id = buffer.read(cx).remote_id();
+            let snapshots = self
+                .buffer_snapshots
+                .get_mut(&buffer_id)
+                .and_then(|m| m.get_mut(&server_id))
+                .ok_or_else(|| {
+                    anyhow!("no snapshots found for buffer {buffer_id} and server {server_id}")
+                })?;
+
+            let found_snapshot = snapshots
+                .binary_search_by_key(&version, |e| e.version)
+                .map(|ix| snapshots[ix].snapshot.clone())
+                .map_err(|_| {
+                    anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}")
+                })?;
+
+            snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version);
+            Ok(found_snapshot)
+        } else {
+            Ok((buffer.read(cx)).text_snapshot())
+        }
+    }
+
+    pub fn language_servers(
+        &self,
+    ) -> impl '_ + Iterator<Item = (LanguageServerId, LanguageServerName, WorktreeId)> {
+        self.language_server_ids
+            .iter()
+            .map(|((worktree_id, server_name), server_id)| {
+                (*server_id, server_name.clone(), *worktree_id)
+            })
+    }
+
+    pub fn supplementary_language_servers(
+        &self,
+    ) -> impl '_
+           + Iterator<
+        Item = (
+            &LanguageServerId,
+            &(LanguageServerName, Arc<LanguageServer>),
+        ),
+    > {
+        self.supplementary_language_servers.iter()
+    }
+
+    pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
+        if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) {
+            Some(server.clone())
+        } else if let Some((_, server)) = self.supplementary_language_servers.get(&id) {
+            Some(Arc::clone(server))
+        } else {
+            None
+        }
+    }
+
+    pub fn language_servers_for_buffer(
+        &self,
+        buffer: &Buffer,
+        cx: &AppContext,
+    ) -> impl Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
+        self.language_server_ids_for_buffer(buffer, cx)
+            .into_iter()
+            .filter_map(|server_id| match self.language_servers.get(&server_id)? {
+                LanguageServerState::Running {
+                    adapter, server, ..
+                } => Some((adapter, server)),
+                _ => None,
+            })
+    }
+
+    fn primary_language_server_for_buffer(
+        &self,
+        buffer: &Buffer,
+        cx: &AppContext,
+    ) -> Option<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
+        self.language_servers_for_buffer(buffer, cx).next()
+    }
+
+    pub fn language_server_for_buffer(
+        &self,
+        buffer: &Buffer,
+        server_id: LanguageServerId,
+        cx: &AppContext,
+    ) -> Option<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
+        self.language_servers_for_buffer(buffer, cx)
+            .find(|(_, s)| s.server_id() == server_id)
+    }
+
+    fn language_server_ids_for_buffer(
+        &self,
+        buffer: &Buffer,
+        cx: &AppContext,
+    ) -> Vec<LanguageServerId> {
+        if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
+            let worktree_id = file.worktree_id(cx);
+            language
+                .lsp_adapters()
+                .iter()
+                .flat_map(|adapter| {
+                    let key = (worktree_id, adapter.name.clone());
+                    self.language_server_ids.get(&key).copied()
+                })
+                .collect()
+        } else {
+            Vec::new()
+        }
+    }
+
+    fn prettier_instance_for_buffer(
+        &mut self,
+        buffer: &Model<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
+        let buffer = buffer.read(cx);
+        let buffer_file = buffer.file();
+        let Some(buffer_language) = buffer.language() else {
+            return Task::ready(None);
+        };
+        if buffer_language.prettier_parser_name().is_none() {
+            return Task::ready(None);
+        }
+
+        let buffer_file = File::from_dyn(buffer_file);
+        let buffer_path = buffer_file.map(|file| Arc::clone(file.path()));
+        let worktree_path = buffer_file
+            .as_ref()
+            .and_then(|file| Some(file.worktree.read(cx).abs_path()));
+        let worktree_id = buffer_file.map(|file| file.worktree_id(cx));
+        if self.is_local() || worktree_id.is_none() || worktree_path.is_none() {
+            let Some(node) = self.node.as_ref().map(Arc::clone) else {
+                return Task::ready(None);
+            };
+            let fs = self.fs.clone();
+            cx.spawn(move |this, mut cx| async move {
+                let prettier_dir = match cx
+                    .executor()
+                    .spawn(Prettier::locate(
+                        worktree_path.zip(buffer_path).map(
+                            |(worktree_root_path, starting_path)| LocateStart {
+                                worktree_root_path,
+                                starting_path,
+                            },
+                        ),
+                        fs,
+                    ))
+                    .await
+                {
+                    Ok(path) => path,
+                    Err(e) => {
+                        return Some(
+                            Task::ready(Err(Arc::new(e.context(
+                                "determining prettier path for worktree {worktree_path:?}",
+                            ))))
+                            .shared(),
+                        );
+                    }
+                };
+
+                if let Some(existing_prettier) = this
+                    .update(&mut cx, |project, _| {
+                        project
+                            .prettier_instances
+                            .get(&(worktree_id, prettier_dir.clone()))
+                            .cloned()
+                    })
+                    .ok()
+                    .flatten()
+                {
+                    return Some(existing_prettier);
+                }
+
+                log::info!("Found prettier in {prettier_dir:?}, starting.");
+                let task_prettier_dir = prettier_dir.clone();
+                let new_prettier_task = cx
+                    .spawn({
+                        let this = this.clone();
+                        move |mut cx| async move {
+                            let new_server_id = this.update(&mut cx, |this, _| {
+                                this.languages.next_language_server_id()
+                            })?;
+                            let prettier = Prettier::start(
+                                worktree_id.map(|id| id.to_usize()),
+                                new_server_id,
+                                task_prettier_dir,
+                                node,
+                                cx.clone(),
+                            )
+                            .await
+                            .context("prettier start")
+                            .map_err(Arc::new)?;
+                            log::info!("Started prettier in {:?}", prettier.prettier_dir());
+
+                            if let Some(prettier_server) = prettier.server() {
+                                this.update(&mut cx, |project, cx| {
+                                    let name = if prettier.is_default() {
+                                        LanguageServerName(Arc::from("prettier (default)"))
+                                    } else {
+                                        let prettier_dir = prettier.prettier_dir();
+                                        let worktree_path = prettier
+                                            .worktree_id()
+                                            .map(WorktreeId::from_usize)
+                                            .and_then(|id| project.worktree_for_id(id, cx))
+                                            .map(|worktree| worktree.read(cx).abs_path());
+                                        match worktree_path {
+                                            Some(worktree_path) => {
+                                                if worktree_path.as_ref() == prettier_dir {
+                                                    LanguageServerName(Arc::from(format!(
+                                                        "prettier ({})",
+                                                        prettier_dir
+                                                            .file_name()
+                                                            .and_then(|name| name.to_str())
+                                                            .unwrap_or_default()
+                                                    )))
+                                                } else {
+                                                    let dir_to_display = match prettier_dir
+                                                        .strip_prefix(&worktree_path)
+                                                        .ok()
+                                                    {
+                                                        Some(relative_path) => relative_path,
+                                                        None => prettier_dir,
+                                                    };
+                                                    LanguageServerName(Arc::from(format!(
+                                                        "prettier ({})",
+                                                        dir_to_display.display(),
+                                                    )))
+                                                }
+                                            }
+                                            None => LanguageServerName(Arc::from(format!(
+                                                "prettier ({})",
+                                                prettier_dir.display(),
+                                            ))),
+                                        }
+                                    };
+
+                                    project
+                                        .supplementary_language_servers
+                                        .insert(new_server_id, (name, Arc::clone(prettier_server)));
+                                    cx.emit(Event::LanguageServerAdded(new_server_id));
+                                })?;
+                            }
+                            Ok(Arc::new(prettier)).map_err(Arc::new)
+                        }
+                    })
+                    .shared();
+                this.update(&mut cx, |project, _| {
+                    project
+                        .prettier_instances
+                        .insert((worktree_id, prettier_dir), new_prettier_task.clone());
+                })
+                .ok();
+                Some(new_prettier_task)
+            })
+        } else if self.remote_id().is_some() {
+            return Task::ready(None);
+        } else {
+            Task::ready(Some(
+                Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
+            ))
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    fn install_default_formatters(
+        &mut self,
+        _: Option<WorktreeId>,
+        _: &Language,
+        _: &LanguageSettings,
+        _: &mut ModelContext<Self>,
+    ) -> Task<anyhow::Result<()>> {
+        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,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<anyhow::Result<()>> {
+        match &language_settings.formatter {
+            Formatter::Prettier { .. } | Formatter::Auto => {}
+            Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())),
+        };
+        let Some(node) = self.node.as_ref().cloned() else {
+            return Task::ready(Ok(()));
+        };
+
+        let mut prettier_plugins = None;
+        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 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);
+                        }
+                    })
+                    .ok();
+                }
+            })
+            .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.spawn_on_main(move |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(());
+            }
+
+            cx.spawn(move |_| async move {
+                let prettier_wrapper_path = default_prettier_dir.join(prettier2::PRETTIER_SERVER_FILE);
+                // method creates parent directory if it doesn't exist
+                fs.save(&prettier_wrapper_path, &text::Rope::from(prettier2::PRETTIER_SERVER_JS), text::LineEnding::Unix).await
+                .with_context(|| format!("writing {} file at {prettier_wrapper_path:?}", prettier2::PRETTIER_SERVER_FILE))?;
+
+                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(())
+            }).await
+        })
+    }
+}
+
+fn subscribe_for_copilot_events(
+    copilot: &Model<Copilot>,
+    cx: &mut ModelContext<'_, Project>,
+) -> gpui2::Subscription {
+    cx.subscribe(
+        copilot,
+        |project, copilot, copilot_event, cx| match copilot_event {
+            copilot2::Event::CopilotLanguageServerStarted => {
+                match copilot.read(cx).language_server() {
+                    Some((name, copilot_server)) => {
+                        // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
+                        if !copilot_server.has_notification_handler::<copilot2::request::LogMessage>() {
+                            let new_server_id = copilot_server.server_id();
+                            let weak_project = cx.weak_model();
+                            let copilot_log_subscription = copilot_server
+                                .on_notification::<copilot2::request::LogMessage, _>(
+                                    move |params, mut cx| {
+                                        weak_project.update(&mut cx, |_, cx| {
+                                            cx.emit(Event::LanguageServerLog(
+                                                new_server_id,
+                                                params.message,
+                                            ));
+                                        }).ok();
+                                    },
+                                );
+                            project.supplementary_language_servers.insert(new_server_id, (name.clone(), Arc::clone(copilot_server)));
+                            project.copilot_log_subscription = Some(copilot_log_subscription);
+                            cx.emit(Event::LanguageServerAdded(new_server_id));
+                        }
+                    }
+                    None => debug_panic!("Received Copilot language server started event, but no language server is running"),
+                }
+            }
+        },
+    )
+}
+
+fn glob_literal_prefix<'a>(glob: &'a str) -> &'a str {
+    let mut literal_end = 0;
+    for (i, part) in glob.split(path::MAIN_SEPARATOR).enumerate() {
+        if part.contains(&['*', '?', '{', '}']) {
+            break;
+        } else {
+            if i > 0 {
+                // Acount for separator prior to this part
+                literal_end += path::MAIN_SEPARATOR.len_utf8();
+            }
+            literal_end += part.len();
+        }
+    }
+    &glob[..literal_end]
+}
+
+impl WorktreeHandle {
+    pub fn upgrade(&self) -> Option<Model<Worktree>> {
+        match self {
+            WorktreeHandle::Strong(handle) => Some(handle.clone()),
+            WorktreeHandle::Weak(handle) => handle.upgrade(),
+        }
+    }
+
+    pub fn handle_id(&self) -> usize {
+        match self {
+            WorktreeHandle::Strong(handle) => handle.entity_id().as_u64() as usize,
+            WorktreeHandle::Weak(handle) => handle.entity_id().as_u64() as usize,
+        }
+    }
+}
+
+impl OpenBuffer {
+    pub fn upgrade(&self) -> Option<Model<Buffer>> {
+        match self {
+            OpenBuffer::Strong(handle) => Some(handle.clone()),
+            OpenBuffer::Weak(handle) => handle.upgrade(),
+            OpenBuffer::Operations(_) => None,
+        }
+    }
+}
+
+pub struct PathMatchCandidateSet {
+    pub snapshot: Snapshot,
+    pub include_ignored: bool,
+    pub include_root_name: bool,
+}
+
+impl<'a> fuzzy2::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
+    type Candidates = PathMatchCandidateSetIter<'a>;
+
+    fn id(&self) -> usize {
+        self.snapshot.id().to_usize()
+    }
+
+    fn len(&self) -> usize {
+        if self.include_ignored {
+            self.snapshot.file_count()
+        } else {
+            self.snapshot.visible_file_count()
+        }
+    }
+
+    fn prefix(&self) -> Arc<str> {
+        if self.snapshot.root_entry().map_or(false, |e| e.is_file()) {
+            self.snapshot.root_name().into()
+        } else if self.include_root_name {
+            format!("{}/", self.snapshot.root_name()).into()
+        } else {
+            "".into()
+        }
+    }
+
+    fn candidates(&'a self, start: usize) -> Self::Candidates {
+        PathMatchCandidateSetIter {
+            traversal: self.snapshot.files(self.include_ignored, start),
+        }
+    }
+}
+
+pub struct PathMatchCandidateSetIter<'a> {
+    traversal: Traversal<'a>,
+}
+
+impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
+    type Item = fuzzy2::PathMatchCandidate<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.traversal.next().map(|entry| {
+            if let EntryKind::File(char_bag) = entry.kind {
+                fuzzy2::PathMatchCandidate {
+                    path: &entry.path,
+                    char_bag,
+                }
+            } else {
+                unreachable!()
+            }
+        })
+    }
+}
+
+impl EventEmitter for Project {
+    type Event = Event;
+}
+
+impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
+    fn from((worktree_id, path): (WorktreeId, P)) -> Self {
+        Self {
+            worktree_id,
+            path: path.as_ref().into(),
+        }
+    }
+}
+
+impl ProjectLspAdapterDelegate {
+    fn new(project: &Project, cx: &ModelContext<Project>) -> Arc<Self> {
+        Arc::new(Self {
+            project: cx.handle(),
+            http_client: project.client.http_client(),
+        })
+    }
+}
+
+impl LspAdapterDelegate for ProjectLspAdapterDelegate {
+    fn show_notification(&self, message: &str, cx: &mut AppContext) {
+        self.project
+            .update(cx, |_, cx| cx.emit(Event::Notification(message.to_owned())));
+    }
+
+    fn http_client(&self) -> Arc<dyn HttpClient> {
+        self.http_client.clone()
+    }
+}
+
+fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
+    proto::Symbol {
+        language_server_name: symbol.language_server_name.0.to_string(),
+        source_worktree_id: symbol.source_worktree_id.to_proto(),
+        worktree_id: symbol.path.worktree_id.to_proto(),
+        path: symbol.path.path.to_string_lossy().to_string(),
+        name: symbol.name.clone(),
+        kind: unsafe { mem::transmute(symbol.kind) },
+        start: Some(proto::PointUtf16 {
+            row: symbol.range.start.0.row,
+            column: symbol.range.start.0.column,
+        }),
+        end: Some(proto::PointUtf16 {
+            row: symbol.range.end.0.row,
+            column: symbol.range.end.0.column,
+        }),
+        signature: symbol.signature.to_vec(),
+    }
+}
+
+fn relativize_path(base: &Path, path: &Path) -> PathBuf {
+    let mut path_components = path.components();
+    let mut base_components = base.components();
+    let mut components: Vec<Component> = Vec::new();
+    loop {
+        match (path_components.next(), base_components.next()) {
+            (None, None) => break,
+            (Some(a), None) => {
+                components.push(a);
+                components.extend(path_components.by_ref());
+                break;
+            }
+            (None, _) => components.push(Component::ParentDir),
+            (Some(a), Some(b)) if components.is_empty() && a == b => (),
+            (Some(a), Some(b)) if b == Component::CurDir => components.push(a),
+            (Some(a), Some(_)) => {
+                components.push(Component::ParentDir);
+                for _ in base_components {
+                    components.push(Component::ParentDir);
+                }
+                components.push(a);
+                components.extend(path_components.by_ref());
+                break;
+            }
+        }
+    }
+    components.iter().map(|c| c.as_os_str()).collect()
+}
+
+impl Item for Buffer {
+    fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
+        File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))
+    }
+
+    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
+        File::from_dyn(self.file()).map(|file| ProjectPath {
+            worktree_id: file.worktree_id(cx),
+            path: file.path().clone(),
+        })
+    }
+}
+
+async fn wait_for_loading_buffer(
+    mut receiver: postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
+) -> Result<Model<Buffer>, Arc<anyhow::Error>> {
+    loop {
+        if let Some(result) = receiver.borrow().as_ref() {
+            match result {
+                Ok(buffer) => return Ok(buffer.to_owned()),
+                Err(e) => return Err(e.to_owned()),
+            }
+        }
+        receiver.next().await;
+    }
+}
+
+fn include_text(server: &lsp2::LanguageServer) -> bool {
+    server
+        .capabilities()
+        .text_document_sync
+        .as_ref()
+        .and_then(|sync| match sync {
+            lsp2::TextDocumentSyncCapability::Kind(_) => None,
+            lsp2::TextDocumentSyncCapability::Options(options) => options.save.as_ref(),
+        })
+        .and_then(|save_options| match save_options {
+            lsp2::TextDocumentSyncSaveOptions::Supported(_) => None,
+            lsp2::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text,
+        })
+        .unwrap_or(false)
+}

crates/project2/src/project_settings.rs 🔗

@@ -0,0 +1,48 @@
+use collections::HashMap;
+use gpui2::AppContext;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings2::Settings;
+use std::sync::Arc;
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ProjectSettings {
+    #[serde(default)]
+    pub lsp: HashMap<Arc<str>, LspSettings>,
+    #[serde(default)]
+    pub git: GitSettings,
+}
+
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct GitSettings {
+    pub git_gutter: Option<GitGutterSetting>,
+    pub gutter_debounce: Option<u64>,
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitGutterSetting {
+    #[default]
+    TrackedFiles,
+    Hide,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct LspSettings {
+    pub initialization_options: Option<serde_json::Value>,
+}
+
+impl Settings for ProjectSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = Self;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/project2/src/project_tests.rs 🔗

@@ -0,0 +1,4077 @@
+// use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *};
+// use fs::{FakeFs, RealFs};
+// use futures::{future, StreamExt};
+// use gpui::{executor::Deterministic, test::subscribe, AppContext};
+// use language2::{
+//     language_settings::{AllLanguageSettings, LanguageSettingsContent},
+//     tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
+//     LineEnding, OffsetRangeExt, Point, ToPoint,
+// };
+// use lsp2::Url;
+// use parking_lot::Mutex;
+// use pretty_assertions::assert_eq;
+// use serde_json::json;
+// use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
+// use unindent::Unindent as _;
+// use util::{assert_set_eq, test::temp_tree};
+
+// #[cfg(test)]
+// #[ctor::ctor]
+// fn init_logger() {
+//     if std::env::var("RUST_LOG").is_ok() {
+//         env_logger::init();
+//     }
+// }
+
+// #[gpui::test]
+// async fn test_symlinks(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+//     cx.foreground().allow_parking();
+
+//     let dir = temp_tree(json!({
+//         "root": {
+//             "apple": "",
+//             "banana": {
+//                 "carrot": {
+//                     "date": "",
+//                     "endive": "",
+//                 }
+//             },
+//             "fennel": {
+//                 "grape": "",
+//             }
+//         }
+//     }));
+
+//     let root_link_path = dir.path().join("root_link");
+//     unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
+//     unix::fs::symlink(
+//         &dir.path().join("root/fennel"),
+//         &dir.path().join("root/finnochio"),
+//     )
+//     .unwrap();
+
+//     let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
+//     project.read_with(cx, |project, cx| {
+//         let tree = project.worktrees(cx).next().unwrap().read(cx);
+//         assert_eq!(tree.file_count(), 5);
+//         assert_eq!(
+//             tree.inode_for_path("fennel/grape"),
+//             tree.inode_for_path("finnochio/grape")
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_managing_project_specific_settings(
+//     deterministic: Arc<Deterministic>,
+//     cx: &mut gpui::TestAppContext,
+// ) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/the-root",
+//         json!({
+//             ".zed": {
+//                 "settings.json": r#"{ "tab_size": 8 }"#
+//             },
+//             "a": {
+//                 "a.rs": "fn a() {\n    A\n}"
+//             },
+//             "b": {
+//                 ".zed": {
+//                     "settings.json": r#"{ "tab_size": 2 }"#
+//                 },
+//                 "b.rs": "fn b() {\n  B\n}"
+//             }
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+//     let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+
+//     deterministic.run_until_parked();
+//     cx.read(|cx| {
+//         let tree = worktree.read(cx);
+
+//         let settings_a = language_settings(
+//             None,
+//             Some(
+//                 &(File::for_entry(
+//                     tree.entry_for_path("a/a.rs").unwrap().clone(),
+//                     worktree.clone(),
+//                 ) as _),
+//             ),
+//             cx,
+//         );
+//         let settings_b = language_settings(
+//             None,
+//             Some(
+//                 &(File::for_entry(
+//                     tree.entry_for_path("b/b.rs").unwrap().clone(),
+//                     worktree.clone(),
+//                 ) as _),
+//             ),
+//             cx,
+//         );
+
+//         assert_eq!(settings_a.tab_size.get(), 8);
+//         assert_eq!(settings_b.tab_size.get(), 2);
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_managing_language_servers(
+//     deterministic: Arc<Deterministic>,
+//     cx: &mut gpui::TestAppContext,
+// ) {
+//     init_test(cx);
+
+//     let mut rust_language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut json_language = Language::new(
+//         LanguageConfig {
+//             name: "JSON".into(),
+//             path_suffixes: vec!["json".to_string()],
+//             ..Default::default()
+//         },
+//         None,
+//     );
+//     let mut fake_rust_servers = rust_language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             name: "the-rust-language-server",
+//             capabilities: lsp::ServerCapabilities {
+//                 completion_provider: Some(lsp::CompletionOptions {
+//                     trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
+//                     ..Default::default()
+//                 }),
+//                 ..Default::default()
+//             },
+//             ..Default::default()
+//         }))
+//         .await;
+//     let mut fake_json_servers = json_language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             name: "the-json-language-server",
+//             capabilities: lsp::ServerCapabilities {
+//                 completion_provider: Some(lsp::CompletionOptions {
+//                     trigger_characters: Some(vec![":".to_string()]),
+//                     ..Default::default()
+//                 }),
+//                 ..Default::default()
+//             },
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/the-root",
+//         json!({
+//             "test.rs": "const A: i32 = 1;",
+//             "test2.rs": "",
+//             "Cargo.toml": "a = 1",
+//             "package.json": "{\"a\": 1}",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+
+//     // Open a buffer without an associated language server.
+//     let toml_buffer = project
+//         .update(cx, |project, cx| {
+//             project.open_local_buffer("/the-root/Cargo.toml", cx)
+//         })
+//         .await
+//         .unwrap();
+
+//     // Open a buffer with an associated language server before the language for it has been loaded.
+//     let rust_buffer = project
+//         .update(cx, |project, cx| {
+//             project.open_local_buffer("/the-root/test.rs", cx)
+//         })
+//         .await
+//         .unwrap();
+//     rust_buffer.read_with(cx, |buffer, _| {
+//         assert_eq!(buffer.language().map(|l| l.name()), None);
+//     });
+
+//     // Now we add the languages to the project, and ensure they get assigned to all
+//     // the relevant open buffers.
+//     project.update(cx, |project, _| {
+//         project.languages.add(Arc::new(json_language));
+//         project.languages.add(Arc::new(rust_language));
+//     });
+//     deterministic.run_until_parked();
+//     rust_buffer.read_with(cx, |buffer, _| {
+//         assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
+//     });
+
+//     // A server is started up, and it is notified about Rust files.
+//     let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
+//     assert_eq!(
+//         fake_rust_server
+//             .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::TextDocumentItem {
+//             uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
+//             version: 0,
+//             text: "const A: i32 = 1;".to_string(),
+//             language_id: Default::default()
+//         }
+//     );
+
+//     // The buffer is configured based on the language server's capabilities.
+//     rust_buffer.read_with(cx, |buffer, _| {
+//         assert_eq!(
+//             buffer.completion_triggers(),
+//             &[".".to_string(), "::".to_string()]
+//         );
+//     });
+//     toml_buffer.read_with(cx, |buffer, _| {
+//         assert!(buffer.completion_triggers().is_empty());
+//     });
+
+//     // Edit a buffer. The changes are reported to the language server.
+//     rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
+//     assert_eq!(
+//         fake_rust_server
+//             .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::VersionedTextDocumentIdentifier::new(
+//             lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
+//             1
+//         )
+//     );
+
+//     // Open a third buffer with a different associated language server.
+//     let json_buffer = project
+//         .update(cx, |project, cx| {
+//             project.open_local_buffer("/the-root/package.json", cx)
+//         })
+//         .await
+//         .unwrap();
+
+//     // A json language server is started up and is only notified about the json buffer.
+//     let mut fake_json_server = fake_json_servers.next().await.unwrap();
+//     assert_eq!(
+//         fake_json_server
+//             .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::TextDocumentItem {
+//             uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
+//             version: 0,
+//             text: "{\"a\": 1}".to_string(),
+//             language_id: Default::default()
+//         }
+//     );
+
+//     // This buffer is configured based on the second language server's
+//     // capabilities.
+//     json_buffer.read_with(cx, |buffer, _| {
+//         assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
+//     });
+
+//     // When opening another buffer whose language server is already running,
+//     // it is also configured based on the existing language server's capabilities.
+//     let rust_buffer2 = project
+//         .update(cx, |project, cx| {
+//             project.open_local_buffer("/the-root/test2.rs", cx)
+//         })
+//         .await
+//         .unwrap();
+//     rust_buffer2.read_with(cx, |buffer, _| {
+//         assert_eq!(
+//             buffer.completion_triggers(),
+//             &[".".to_string(), "::".to_string()]
+//         );
+//     });
+
+//     // Changes are reported only to servers matching the buffer's language.
+//     toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
+//     rust_buffer2.update(cx, |buffer, cx| {
+//         buffer.edit([(0..0, "let x = 1;")], None, cx)
+//     });
+//     assert_eq!(
+//         fake_rust_server
+//             .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::VersionedTextDocumentIdentifier::new(
+//             lsp2::Url::from_file_path("/the-root/test2.rs").unwrap(),
+//             1
+//         )
+//     );
+
+//     // Save notifications are reported to all servers.
+//     project
+//         .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
+//         .await
+//         .unwrap();
+//     assert_eq!(
+//         fake_rust_server
+//             .receive_notification::<lsp2::notification::DidSaveTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::TextDocumentIdentifier::new(
+//             lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap()
+//         )
+//     );
+//     assert_eq!(
+//         fake_json_server
+//             .receive_notification::<lsp2::notification::DidSaveTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::TextDocumentIdentifier::new(
+//             lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap()
+//         )
+//     );
+
+//     // Renames are reported only to servers matching the buffer's language.
+//     fs.rename(
+//         Path::new("/the-root/test2.rs"),
+//         Path::new("/the-root/test3.rs"),
+//         Default::default(),
+//     )
+//     .await
+//     .unwrap();
+//     assert_eq!(
+//         fake_rust_server
+//             .receive_notification::<lsp2::notification::DidCloseTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test2.rs").unwrap()),
+//     );
+//     assert_eq!(
+//         fake_rust_server
+//             .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::TextDocumentItem {
+//             uri: lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),
+//             version: 0,
+//             text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
+//             language_id: Default::default()
+//         },
+//     );
+
+//     rust_buffer2.update(cx, |buffer, cx| {
+//         buffer.update_diagnostics(
+//             LanguageServerId(0),
+//             DiagnosticSet::from_sorted_entries(
+//                 vec![DiagnosticEntry {
+//                     diagnostic: Default::default(),
+//                     range: Anchor::MIN..Anchor::MAX,
+//                 }],
+//                 &buffer.snapshot(),
+//             ),
+//             cx,
+//         );
+//         assert_eq!(
+//             buffer
+//                 .snapshot()
+//                 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
+//                 .count(),
+//             1
+//         );
+//     });
+
+//     // When the rename changes the extension of the file, the buffer gets closed on the old
+//     // language server and gets opened on the new one.
+//     fs.rename(
+//         Path::new("/the-root/test3.rs"),
+//         Path::new("/the-root/test3.json"),
+//         Default::default(),
+//     )
+//     .await
+//     .unwrap();
+//     assert_eq!(
+//         fake_rust_server
+//             .receive_notification::<lsp2::notification::DidCloseTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),),
+//     );
+//     assert_eq!(
+//         fake_json_server
+//             .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::TextDocumentItem {
+//             uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
+//             version: 0,
+//             text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
+//             language_id: Default::default()
+//         },
+//     );
+
+//     // We clear the diagnostics, since the language has changed.
+//     rust_buffer2.read_with(cx, |buffer, _| {
+//         assert_eq!(
+//             buffer
+//                 .snapshot()
+//                 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
+//                 .count(),
+//             0
+//         );
+//     });
+
+//     // The renamed file's version resets after changing language server.
+//     rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
+//     assert_eq!(
+//         fake_json_server
+//             .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::VersionedTextDocumentIdentifier::new(
+//             lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
+//             1
+//         )
+//     );
+
+//     // Restart language servers
+//     project.update(cx, |project, cx| {
+//         project.restart_language_servers_for_buffers(
+//             vec![rust_buffer.clone(), json_buffer.clone()],
+//             cx,
+//         );
+//     });
+
+//     let mut rust_shutdown_requests = fake_rust_server
+//         .handle_request::<lsp2::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
+//     let mut json_shutdown_requests = fake_json_server
+//         .handle_request::<lsp2::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
+//     futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
+
+//     let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
+//     let mut fake_json_server = fake_json_servers.next().await.unwrap();
+
+//     // Ensure rust document is reopened in new rust language server
+//     assert_eq!(
+//         fake_rust_server
+//             .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//             .await
+//             .text_document,
+//         lsp2::TextDocumentItem {
+//             uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
+//             version: 0,
+//             text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
+//             language_id: Default::default()
+//         }
+//     );
+
+//     // Ensure json documents are reopened in new json language server
+//     assert_set_eq!(
+//         [
+//             fake_json_server
+//                 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//                 .await
+//                 .text_document,
+//             fake_json_server
+//                 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//                 .await
+//                 .text_document,
+//         ],
+//         [
+//             lsp2::TextDocumentItem {
+//                 uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
+//                 version: 0,
+//                 text: json_buffer.read_with(cx, |buffer, _| buffer.text()),
+//                 language_id: Default::default()
+//             },
+//             lsp2::TextDocumentItem {
+//                 uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
+//                 version: 0,
+//                 text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
+//                 language_id: Default::default()
+//             }
+//         ]
+//     );
+
+//     // Close notifications are reported only to servers matching the buffer's language.
+//     cx.update(|_| drop(json_buffer));
+//     let close_message = lsp2::DidCloseTextDocumentParams {
+//         text_document: lsp2::TextDocumentIdentifier::new(
+//             lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
+//         ),
+//     };
+//     assert_eq!(
+//         fake_json_server
+//             .receive_notification::<lsp2::notification::DidCloseTextDocument>()
+//             .await,
+//         close_message,
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             name: "the-language-server",
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/the-root",
+//         json!({
+//             ".gitignore": "target\n",
+//             "src": {
+//                 "a.rs": "",
+//                 "b.rs": "",
+//             },
+//             "target": {
+//                 "x": {
+//                     "out": {
+//                         "x.rs": ""
+//                     }
+//                 },
+//                 "y": {
+//                     "out": {
+//                         "y.rs": "",
+//                     }
+//                 },
+//                 "z": {
+//                     "out": {
+//                         "z.rs": ""
+//                     }
+//                 }
+//             }
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+//     project.update(cx, |project, _| {
+//         project.languages.add(Arc::new(language));
+//     });
+//     cx.foreground().run_until_parked();
+
+//     // Start the language server by opening a buffer with a compatible file extension.
+//     let _buffer = project
+//         .update(cx, |project, cx| {
+//             project.open_local_buffer("/the-root/src/a.rs", cx)
+//         })
+//         .await
+//         .unwrap();
+
+//     // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
+//     project.read_with(cx, |project, cx| {
+//         let worktree = project.worktrees(cx).next().unwrap();
+//         assert_eq!(
+//             worktree
+//                 .read(cx)
+//                 .snapshot()
+//                 .entries(true)
+//                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 (Path::new(""), false),
+//                 (Path::new(".gitignore"), false),
+//                 (Path::new("src"), false),
+//                 (Path::new("src/a.rs"), false),
+//                 (Path::new("src/b.rs"), false),
+//                 (Path::new("target"), true),
+//             ]
+//         );
+//     });
+
+//     let prev_read_dir_count = fs.read_dir_call_count();
+
+//     // Keep track of the FS events reported to the language server.
+//     let fake_server = fake_servers.next().await.unwrap();
+//     let file_changes = Arc::new(Mutex::new(Vec::new()));
+//     fake_server
+//         .request::<lsp2::request::RegisterCapability>(lsp2::RegistrationParams {
+//             registrations: vec![lsp2::Registration {
+//                 id: Default::default(),
+//                 method: "workspace/didChangeWatchedFiles".to_string(),
+//                 register_options: serde_json::to_value(
+//                     lsp::DidChangeWatchedFilesRegistrationOptions {
+//                         watchers: vec![
+//                             lsp2::FileSystemWatcher {
+//                                 glob_pattern: lsp2::GlobPattern::String(
+//                                     "/the-root/Cargo.toml".to_string(),
+//                                 ),
+//                                 kind: None,
+//                             },
+//                             lsp2::FileSystemWatcher {
+//                                 glob_pattern: lsp2::GlobPattern::String(
+//                                     "/the-root/src/*.{rs,c}".to_string(),
+//                                 ),
+//                                 kind: None,
+//                             },
+//                             lsp2::FileSystemWatcher {
+//                                 glob_pattern: lsp2::GlobPattern::String(
+//                                     "/the-root/target/y/**/*.rs".to_string(),
+//                                 ),
+//                                 kind: None,
+//                             },
+//                         ],
+//                     },
+//                 )
+//                 .ok(),
+//             }],
+//         })
+//         .await
+//         .unwrap();
+//     fake_server.handle_notification::<lsp2::notification::DidChangeWatchedFiles, _>({
+//         let file_changes = file_changes.clone();
+//         move |params, _| {
+//             let mut file_changes = file_changes.lock();
+//             file_changes.extend(params.changes);
+//             file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
+//         }
+//     });
+
+//     cx.foreground().run_until_parked();
+//     assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
+//     assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
+
+//     // Now the language server has asked us to watch an ignored directory path,
+//     // so we recursively load it.
+//     project.read_with(cx, |project, cx| {
+//         let worktree = project.worktrees(cx).next().unwrap();
+//         assert_eq!(
+//             worktree
+//                 .read(cx)
+//                 .snapshot()
+//                 .entries(true)
+//                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 (Path::new(""), false),
+//                 (Path::new(".gitignore"), false),
+//                 (Path::new("src"), false),
+//                 (Path::new("src/a.rs"), false),
+//                 (Path::new("src/b.rs"), false),
+//                 (Path::new("target"), true),
+//                 (Path::new("target/x"), true),
+//                 (Path::new("target/y"), true),
+//                 (Path::new("target/y/out"), true),
+//                 (Path::new("target/y/out/y.rs"), true),
+//                 (Path::new("target/z"), true),
+//             ]
+//         );
+//     });
+
+//     // Perform some file system mutations, two of which match the watched patterns,
+//     // and one of which does not.
+//     fs.create_file("/the-root/src/c.rs".as_ref(), Default::default())
+//         .await
+//         .unwrap();
+//     fs.create_file("/the-root/src/d.txt".as_ref(), Default::default())
+//         .await
+//         .unwrap();
+//     fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default())
+//         .await
+//         .unwrap();
+//     fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default())
+//         .await
+//         .unwrap();
+//     fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default())
+//         .await
+//         .unwrap();
+
+//     // The language server receives events for the FS mutations that match its watch patterns.
+//     cx.foreground().run_until_parked();
+//     assert_eq!(
+//         &*file_changes.lock(),
+//         &[
+//             lsp2::FileEvent {
+//                 uri: lsp2::Url::from_file_path("/the-root/src/b.rs").unwrap(),
+//                 typ: lsp2::FileChangeType::DELETED,
+//             },
+//             lsp2::FileEvent {
+//                 uri: lsp2::Url::from_file_path("/the-root/src/c.rs").unwrap(),
+//                 typ: lsp2::FileChangeType::CREATED,
+//             },
+//             lsp2::FileEvent {
+//                 uri: lsp2::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(),
+//                 typ: lsp2::FileChangeType::CREATED,
+//             },
+//         ]
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.rs": "let a = 1;",
+//             "b.rs": "let b = 2;"
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
+
+//     let buffer_a = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+//     let buffer_b = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
+//         .await
+//         .unwrap();
+
+//     project.update(cx, |project, cx| {
+//         project
+//             .update_diagnostics(
+//                 LanguageServerId(0),
+//                 lsp::PublishDiagnosticsParams {
+//                     uri: Url::from_file_path("/dir/a.rs").unwrap(),
+//                     version: None,
+//                     diagnostics: vec![lsp2::Diagnostic {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 4),
+//                             lsp2::Position::new(0, 5),
+//                         ),
+//                         severity: Some(lsp2::DiagnosticSeverity::ERROR),
+//                         message: "error 1".to_string(),
+//                         ..Default::default()
+//                     }],
+//                 },
+//                 &[],
+//                 cx,
+//             )
+//             .unwrap();
+//         project
+//             .update_diagnostics(
+//                 LanguageServerId(0),
+//                 lsp::PublishDiagnosticsParams {
+//                     uri: Url::from_file_path("/dir/b.rs").unwrap(),
+//                     version: None,
+//                     diagnostics: vec![lsp2::Diagnostic {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 4),
+//                             lsp2::Position::new(0, 5),
+//                         ),
+//                         severity: Some(lsp2::DiagnosticSeverity::WARNING),
+//                         message: "error 2".to_string(),
+//                         ..Default::default()
+//                     }],
+//                 },
+//                 &[],
+//                 cx,
+//             )
+//             .unwrap();
+//     });
+
+//     buffer_a.read_with(cx, |buffer, _| {
+//         let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+//         assert_eq!(
+//             chunks
+//                 .iter()
+//                 .map(|(s, d)| (s.as_str(), *d))
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 ("let ", None),
+//                 ("a", Some(DiagnosticSeverity::ERROR)),
+//                 (" = 1;", None),
+//             ]
+//         );
+//     });
+//     buffer_b.read_with(cx, |buffer, _| {
+//         let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+//         assert_eq!(
+//             chunks
+//                 .iter()
+//                 .map(|(s, d)| (s.as_str(), *d))
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 ("let ", None),
+//                 ("b", Some(DiagnosticSeverity::WARNING)),
+//                 (" = 2;", None),
+//             ]
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//             "dir": {
+//                 "a.rs": "let a = 1;",
+//             },
+//             "other.rs": "let b = c;"
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
+
+//     let (worktree, _) = project
+//         .update(cx, |project, cx| {
+//             project.find_or_create_local_worktree("/root/other.rs", false, cx)
+//         })
+//         .await
+//         .unwrap();
+//     let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
+
+//     project.update(cx, |project, cx| {
+//         project
+//             .update_diagnostics(
+//                 LanguageServerId(0),
+//                 lsp::PublishDiagnosticsParams {
+//                     uri: Url::from_file_path("/root/other.rs").unwrap(),
+//                     version: None,
+//                     diagnostics: vec![lsp2::Diagnostic {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 8),
+//                             lsp2::Position::new(0, 9),
+//                         ),
+//                         severity: Some(lsp2::DiagnosticSeverity::ERROR),
+//                         message: "unknown variable 'c'".to_string(),
+//                         ..Default::default()
+//                     }],
+//                 },
+//                 &[],
+//                 cx,
+//             )
+//             .unwrap();
+//     });
+
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+//         .await
+//         .unwrap();
+//     buffer.read_with(cx, |buffer, _| {
+//         let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+//         assert_eq!(
+//             chunks
+//                 .iter()
+//                 .map(|(s, d)| (s.as_str(), *d))
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 ("let b = ", None),
+//                 ("c", Some(DiagnosticSeverity::ERROR)),
+//                 (";", None),
+//             ]
+//         );
+//     });
+
+//     project.read_with(cx, |project, cx| {
+//         assert_eq!(project.diagnostic_summaries(cx).next(), None);
+//         assert_eq!(project.diagnostic_summary(cx).error_count, 0);
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let progress_token = "the-progress-token";
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             disk_based_diagnostics_progress_token: Some(progress_token.into()),
+//             disk_based_diagnostics_sources: vec!["disk".into()],
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.rs": "fn a() { A }",
+//             "b.rs": "const y: i32 = 1",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+//     let worktree_id = project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
+
+//     // Cause worktree to start the fake language server
+//     let _buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
+//         .await
+//         .unwrap();
+
+//     let mut events = subscribe(&project, cx);
+
+//     let fake_server = fake_servers.next().await.unwrap();
+//     assert_eq!(
+//         events.next().await.unwrap(),
+//         Event::LanguageServerAdded(LanguageServerId(0)),
+//     );
+
+//     fake_server
+//         .start_progress(format!("{}/0", progress_token))
+//         .await;
+//     assert_eq!(
+//         events.next().await.unwrap(),
+//         Event::DiskBasedDiagnosticsStarted {
+//             language_server_id: LanguageServerId(0),
+//         }
+//     );
+
+//     fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+//         uri: Url::from_file_path("/dir/a.rs").unwrap(),
+//         version: None,
+//         diagnostics: vec![lsp2::Diagnostic {
+//             range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+//             severity: Some(lsp2::DiagnosticSeverity::ERROR),
+//             message: "undefined variable 'A'".to_string(),
+//             ..Default::default()
+//         }],
+//     });
+//     assert_eq!(
+//         events.next().await.unwrap(),
+//         Event::DiagnosticsUpdated {
+//             language_server_id: LanguageServerId(0),
+//             path: (worktree_id, Path::new("a.rs")).into()
+//         }
+//     );
+
+//     fake_server.end_progress(format!("{}/0", progress_token));
+//     assert_eq!(
+//         events.next().await.unwrap(),
+//         Event::DiskBasedDiagnosticsFinished {
+//             language_server_id: LanguageServerId(0)
+//         }
+//     );
+
+//     let buffer = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     buffer.read_with(cx, |buffer, _| {
+//         let snapshot = buffer.snapshot();
+//         let diagnostics = snapshot
+//             .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
+//             .collect::<Vec<_>>();
+//         assert_eq!(
+//             diagnostics,
+//             &[DiagnosticEntry {
+//                 range: Point::new(0, 9)..Point::new(0, 10),
+//                 diagnostic: Diagnostic {
+//                     severity: lsp2::DiagnosticSeverity::ERROR,
+//                     message: "undefined variable 'A'".to_string(),
+//                     group_id: 0,
+//                     is_primary: true,
+//                     ..Default::default()
+//                 }
+//             }]
+//         )
+//     });
+
+//     // Ensure publishing empty diagnostics twice only results in one update event.
+//     fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+//         uri: Url::from_file_path("/dir/a.rs").unwrap(),
+//         version: None,
+//         diagnostics: Default::default(),
+//     });
+//     assert_eq!(
+//         events.next().await.unwrap(),
+//         Event::DiagnosticsUpdated {
+//             language_server_id: LanguageServerId(0),
+//             path: (worktree_id, Path::new("a.rs")).into()
+//         }
+//     );
+
+//     fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+//         uri: Url::from_file_path("/dir/a.rs").unwrap(),
+//         version: None,
+//         diagnostics: Default::default(),
+//     });
+//     cx.foreground().run_until_parked();
+//     assert_eq!(futures::poll!(events.next()), Poll::Pending);
+// }
+
+// #[gpui::test]
+// async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let progress_token = "the-progress-token";
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         None,
+//     );
+//     let mut fake_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             disk_based_diagnostics_sources: vec!["disk".into()],
+//             disk_based_diagnostics_progress_token: Some(progress_token.into()),
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     // Simulate diagnostics starting to update.
+//     let fake_server = fake_servers.next().await.unwrap();
+//     fake_server.start_progress(progress_token).await;
+
+//     // Restart the server before the diagnostics finish updating.
+//     project.update(cx, |project, cx| {
+//         project.restart_language_servers_for_buffers([buffer], cx);
+//     });
+//     let mut events = subscribe(&project, cx);
+
+//     // Simulate the newly started server sending more diagnostics.
+//     let fake_server = fake_servers.next().await.unwrap();
+//     assert_eq!(
+//         events.next().await.unwrap(),
+//         Event::LanguageServerAdded(LanguageServerId(1))
+//     );
+//     fake_server.start_progress(progress_token).await;
+//     assert_eq!(
+//         events.next().await.unwrap(),
+//         Event::DiskBasedDiagnosticsStarted {
+//             language_server_id: LanguageServerId(1)
+//         }
+//     );
+//     project.read_with(cx, |project, _| {
+//         assert_eq!(
+//             project
+//                 .language_servers_running_disk_based_diagnostics()
+//                 .collect::<Vec<_>>(),
+//             [LanguageServerId(1)]
+//         );
+//     });
+
+//     // All diagnostics are considered done, despite the old server's diagnostic
+//     // task never completing.
+//     fake_server.end_progress(progress_token);
+//     assert_eq!(
+//         events.next().await.unwrap(),
+//         Event::DiskBasedDiagnosticsFinished {
+//             language_server_id: LanguageServerId(1)
+//         }
+//     );
+//     project.read_with(cx, |project, _| {
+//         assert_eq!(
+//             project
+//                 .language_servers_running_disk_based_diagnostics()
+//                 .collect::<Vec<_>>(),
+//             [LanguageServerId(0); 0]
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         None,
+//     );
+//     let mut fake_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     // Publish diagnostics
+//     let fake_server = fake_servers.next().await.unwrap();
+//     fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+//         uri: Url::from_file_path("/dir/a.rs").unwrap(),
+//         version: None,
+//         diagnostics: vec![lsp2::Diagnostic {
+//             range: lsp2::Range::new(lsp2::Position::new(0, 0), lsp2::Position::new(0, 0)),
+//             severity: Some(lsp2::DiagnosticSeverity::ERROR),
+//             message: "the message".to_string(),
+//             ..Default::default()
+//         }],
+//     });
+
+//     cx.foreground().run_until_parked();
+//     buffer.read_with(cx, |buffer, _| {
+//         assert_eq!(
+//             buffer
+//                 .snapshot()
+//                 .diagnostics_in_range::<_, usize>(0..1, false)
+//                 .map(|entry| entry.diagnostic.message.clone())
+//                 .collect::<Vec<_>>(),
+//             ["the message".to_string()]
+//         );
+//     });
+//     project.read_with(cx, |project, cx| {
+//         assert_eq!(
+//             project.diagnostic_summary(cx),
+//             DiagnosticSummary {
+//                 error_count: 1,
+//                 warning_count: 0,
+//             }
+//         );
+//     });
+
+//     project.update(cx, |project, cx| {
+//         project.restart_language_servers_for_buffers([buffer.clone()], cx);
+//     });
+
+//     // The diagnostics are cleared.
+//     cx.foreground().run_until_parked();
+//     buffer.read_with(cx, |buffer, _| {
+//         assert_eq!(
+//             buffer
+//                 .snapshot()
+//                 .diagnostics_in_range::<_, usize>(0..1, false)
+//                 .map(|entry| entry.diagnostic.message.clone())
+//                 .collect::<Vec<_>>(),
+//             Vec::<String>::new(),
+//         );
+//     });
+//     project.read_with(cx, |project, cx| {
+//         assert_eq!(
+//             project.diagnostic_summary(cx),
+//             DiagnosticSummary {
+//                 error_count: 0,
+//                 warning_count: 0,
+//             }
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         None,
+//     );
+//     let mut fake_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             name: "the-lsp",
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     // Before restarting the server, report diagnostics with an unknown buffer version.
+//     let fake_server = fake_servers.next().await.unwrap();
+//     fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+//         uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+//         version: Some(10000),
+//         diagnostics: Vec::new(),
+//     });
+//     cx.foreground().run_until_parked();
+
+//     project.update(cx, |project, cx| {
+//         project.restart_language_servers_for_buffers([buffer.clone()], cx);
+//     });
+//     let mut fake_server = fake_servers.next().await.unwrap();
+//     let notification = fake_server
+//         .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//         .await
+//         .text_document;
+//     assert_eq!(notification.version, 0);
+// }
+
+// #[gpui::test]
+// async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut rust = Language::new(
+//         LanguageConfig {
+//             name: Arc::from("Rust"),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         None,
+//     );
+//     let mut fake_rust_servers = rust
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             name: "rust-lsp",
+//             ..Default::default()
+//         }))
+//         .await;
+//     let mut js = Language::new(
+//         LanguageConfig {
+//             name: Arc::from("JavaScript"),
+//             path_suffixes: vec!["js".to_string()],
+//             ..Default::default()
+//         },
+//         None,
+//     );
+//     let mut fake_js_servers = js
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             name: "js-lsp",
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
+//         .await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| {
+//         project.languages.add(Arc::new(rust));
+//         project.languages.add(Arc::new(js));
+//     });
+
+//     let _rs_buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+//     let _js_buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
+//         .await
+//         .unwrap();
+
+//     let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
+//     assert_eq!(
+//         fake_rust_server_1
+//             .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//             .await
+//             .text_document
+//             .uri
+//             .as_str(),
+//         "file:///dir/a.rs"
+//     );
+
+//     let mut fake_js_server = fake_js_servers.next().await.unwrap();
+//     assert_eq!(
+//         fake_js_server
+//             .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//             .await
+//             .text_document
+//             .uri
+//             .as_str(),
+//         "file:///dir/b.js"
+//     );
+
+//     // Disable Rust language server, ensuring only that server gets stopped.
+//     cx.update(|cx| {
+//         cx.update_global(|settings: &mut SettingsStore, cx| {
+//             settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+//                 settings.languages.insert(
+//                     Arc::from("Rust"),
+//                     LanguageSettingsContent {
+//                         enable_language_server: Some(false),
+//                         ..Default::default()
+//                     },
+//                 );
+//             });
+//         })
+//     });
+//     fake_rust_server_1
+//         .receive_notification::<lsp2::notification::Exit>()
+//         .await;
+
+//     // Enable Rust and disable JavaScript language servers, ensuring that the
+//     // former gets started again and that the latter stops.
+//     cx.update(|cx| {
+//         cx.update_global(|settings: &mut SettingsStore, cx| {
+//             settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+//                 settings.languages.insert(
+//                     Arc::from("Rust"),
+//                     LanguageSettingsContent {
+//                         enable_language_server: Some(true),
+//                         ..Default::default()
+//                     },
+//                 );
+//                 settings.languages.insert(
+//                     Arc::from("JavaScript"),
+//                     LanguageSettingsContent {
+//                         enable_language_server: Some(false),
+//                         ..Default::default()
+//                     },
+//                 );
+//             });
+//         })
+//     });
+//     let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
+//     assert_eq!(
+//         fake_rust_server_2
+//             .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//             .await
+//             .text_document
+//             .uri
+//             .as_str(),
+//         "file:///dir/a.rs"
+//     );
+//     fake_js_server
+//         .receive_notification::<lsp2::notification::Exit>()
+//         .await;
+// }
+
+// #[gpui::test(iterations = 3)]
+// async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             disk_based_diagnostics_sources: vec!["disk".into()],
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let text = "
+//         fn a() { A }
+//         fn b() { BB }
+//         fn c() { CCC }
+//     "
+//     .unindent();
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree("/dir", json!({ "a.rs": text })).await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     let mut fake_server = fake_servers.next().await.unwrap();
+//     let open_notification = fake_server
+//         .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//         .await;
+
+//     // Edit the buffer, moving the content down
+//     buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
+//     let change_notification_1 = fake_server
+//         .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+//         .await;
+//     assert!(change_notification_1.text_document.version > open_notification.text_document.version);
+
+//     // Report some diagnostics for the initial version of the buffer
+//     fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+//         uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+//         version: Some(open_notification.text_document.version),
+//         diagnostics: vec![
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+//                 severity: Some(DiagnosticSeverity::ERROR),
+//                 message: "undefined variable 'A'".to_string(),
+//                 source: Some("disk".to_string()),
+//                 ..Default::default()
+//             },
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)),
+//                 severity: Some(DiagnosticSeverity::ERROR),
+//                 message: "undefined variable 'BB'".to_string(),
+//                 source: Some("disk".to_string()),
+//                 ..Default::default()
+//             },
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(2, 9), lsp2::Position::new(2, 12)),
+//                 severity: Some(DiagnosticSeverity::ERROR),
+//                 source: Some("disk".to_string()),
+//                 message: "undefined variable 'CCC'".to_string(),
+//                 ..Default::default()
+//             },
+//         ],
+//     });
+
+//     // The diagnostics have moved down since they were created.
+//     buffer.next_notification(cx).await;
+//     cx.foreground().run_until_parked();
+//     buffer.read_with(cx, |buffer, _| {
+//         assert_eq!(
+//             buffer
+//                 .snapshot()
+//                 .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 DiagnosticEntry {
+//                     range: Point::new(3, 9)..Point::new(3, 11),
+//                     diagnostic: Diagnostic {
+//                         source: Some("disk".into()),
+//                         severity: DiagnosticSeverity::ERROR,
+//                         message: "undefined variable 'BB'".to_string(),
+//                         is_disk_based: true,
+//                         group_id: 1,
+//                         is_primary: true,
+//                         ..Default::default()
+//                     },
+//                 },
+//                 DiagnosticEntry {
+//                     range: Point::new(4, 9)..Point::new(4, 12),
+//                     diagnostic: Diagnostic {
+//                         source: Some("disk".into()),
+//                         severity: DiagnosticSeverity::ERROR,
+//                         message: "undefined variable 'CCC'".to_string(),
+//                         is_disk_based: true,
+//                         group_id: 2,
+//                         is_primary: true,
+//                         ..Default::default()
+//                     }
+//                 }
+//             ]
+//         );
+//         assert_eq!(
+//             chunks_with_diagnostics(buffer, 0..buffer.len()),
+//             [
+//                 ("\n\nfn a() { ".to_string(), None),
+//                 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
+//                 (" }\nfn b() { ".to_string(), None),
+//                 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
+//                 (" }\nfn c() { ".to_string(), None),
+//                 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
+//                 (" }\n".to_string(), None),
+//             ]
+//         );
+//         assert_eq!(
+//             chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
+//             [
+//                 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
+//                 (" }\nfn c() { ".to_string(), None),
+//                 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
+//             ]
+//         );
+//     });
+
+//     // Ensure overlapping diagnostics are highlighted correctly.
+//     fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+//         uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+//         version: Some(open_notification.text_document.version),
+//         diagnostics: vec![
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+//                 severity: Some(DiagnosticSeverity::ERROR),
+//                 message: "undefined variable 'A'".to_string(),
+//                 source: Some("disk".to_string()),
+//                 ..Default::default()
+//             },
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 12)),
+//                 severity: Some(DiagnosticSeverity::WARNING),
+//                 message: "unreachable statement".to_string(),
+//                 source: Some("disk".to_string()),
+//                 ..Default::default()
+//             },
+//         ],
+//     });
+
+//     buffer.next_notification(cx).await;
+//     cx.foreground().run_until_parked();
+//     buffer.read_with(cx, |buffer, _| {
+//         assert_eq!(
+//             buffer
+//                 .snapshot()
+//                 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 DiagnosticEntry {
+//                     range: Point::new(2, 9)..Point::new(2, 12),
+//                     diagnostic: Diagnostic {
+//                         source: Some("disk".into()),
+//                         severity: DiagnosticSeverity::WARNING,
+//                         message: "unreachable statement".to_string(),
+//                         is_disk_based: true,
+//                         group_id: 4,
+//                         is_primary: true,
+//                         ..Default::default()
+//                     }
+//                 },
+//                 DiagnosticEntry {
+//                     range: Point::new(2, 9)..Point::new(2, 10),
+//                     diagnostic: Diagnostic {
+//                         source: Some("disk".into()),
+//                         severity: DiagnosticSeverity::ERROR,
+//                         message: "undefined variable 'A'".to_string(),
+//                         is_disk_based: true,
+//                         group_id: 3,
+//                         is_primary: true,
+//                         ..Default::default()
+//                     },
+//                 }
+//             ]
+//         );
+//         assert_eq!(
+//             chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
+//             [
+//                 ("fn a() { ".to_string(), None),
+//                 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
+//                 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
+//                 ("\n".to_string(), None),
+//             ]
+//         );
+//         assert_eq!(
+//             chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
+//             [
+//                 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
+//                 ("\n".to_string(), None),
+//             ]
+//         );
+//     });
+
+//     // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
+//     // changes since the last save.
+//     buffer.update(cx, |buffer, cx| {
+//         buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "    ")], None, cx);
+//         buffer.edit(
+//             [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
+//             None,
+//             cx,
+//         );
+//         buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
+//     });
+//     let change_notification_2 = fake_server
+//         .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+//         .await;
+//     assert!(
+//         change_notification_2.text_document.version > change_notification_1.text_document.version
+//     );
+
+//     // Handle out-of-order diagnostics
+//     fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+//         uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+//         version: Some(change_notification_2.text_document.version),
+//         diagnostics: vec![
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)),
+//                 severity: Some(DiagnosticSeverity::ERROR),
+//                 message: "undefined variable 'BB'".to_string(),
+//                 source: Some("disk".to_string()),
+//                 ..Default::default()
+//             },
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+//                 severity: Some(DiagnosticSeverity::WARNING),
+//                 message: "undefined variable 'A'".to_string(),
+//                 source: Some("disk".to_string()),
+//                 ..Default::default()
+//             },
+//         ],
+//     });
+
+//     buffer.next_notification(cx).await;
+//     cx.foreground().run_until_parked();
+//     buffer.read_with(cx, |buffer, _| {
+//         assert_eq!(
+//             buffer
+//                 .snapshot()
+//                 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 DiagnosticEntry {
+//                     range: Point::new(2, 21)..Point::new(2, 22),
+//                     diagnostic: Diagnostic {
+//                         source: Some("disk".into()),
+//                         severity: DiagnosticSeverity::WARNING,
+//                         message: "undefined variable 'A'".to_string(),
+//                         is_disk_based: true,
+//                         group_id: 6,
+//                         is_primary: true,
+//                         ..Default::default()
+//                     }
+//                 },
+//                 DiagnosticEntry {
+//                     range: Point::new(3, 9)..Point::new(3, 14),
+//                     diagnostic: Diagnostic {
+//                         source: Some("disk".into()),
+//                         severity: DiagnosticSeverity::ERROR,
+//                         message: "undefined variable 'BB'".to_string(),
+//                         is_disk_based: true,
+//                         group_id: 5,
+//                         is_primary: true,
+//                         ..Default::default()
+//                     },
+//                 }
+//             ]
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let text = concat!(
+//         "let one = ;\n", //
+//         "let two = \n",
+//         "let three = 3;\n",
+//     );
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree("/dir", json!({ "a.rs": text })).await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     project.update(cx, |project, cx| {
+//         project
+//             .update_buffer_diagnostics(
+//                 &buffer,
+//                 LanguageServerId(0),
+//                 None,
+//                 vec![
+//                     DiagnosticEntry {
+//                         range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
+//                         diagnostic: Diagnostic {
+//                             severity: DiagnosticSeverity::ERROR,
+//                             message: "syntax error 1".to_string(),
+//                             ..Default::default()
+//                         },
+//                     },
+//                     DiagnosticEntry {
+//                         range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
+//                         diagnostic: Diagnostic {
+//                             severity: DiagnosticSeverity::ERROR,
+//                             message: "syntax error 2".to_string(),
+//                             ..Default::default()
+//                         },
+//                     },
+//                 ],
+//                 cx,
+//             )
+//             .unwrap();
+//     });
+
+//     // An empty range is extended forward to include the following character.
+//     // At the end of a line, an empty range is extended backward to include
+//     // the preceding character.
+//     buffer.read_with(cx, |buffer, _| {
+//         let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+//         assert_eq!(
+//             chunks
+//                 .iter()
+//                 .map(|(s, d)| (s.as_str(), *d))
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 ("let one = ", None),
+//                 (";", Some(DiagnosticSeverity::ERROR)),
+//                 ("\nlet two =", None),
+//                 (" ", Some(DiagnosticSeverity::ERROR)),
+//                 ("\nlet three = 3;\n", None)
+//             ]
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
+//         .await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+
+//     project.update(cx, |project, cx| {
+//         project
+//             .update_diagnostic_entries(
+//                 LanguageServerId(0),
+//                 Path::new("/dir/a.rs").to_owned(),
+//                 None,
+//                 vec![DiagnosticEntry {
+//                     range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
+//                     diagnostic: Diagnostic {
+//                         severity: DiagnosticSeverity::ERROR,
+//                         is_primary: true,
+//                         message: "syntax error a1".to_string(),
+//                         ..Default::default()
+//                     },
+//                 }],
+//                 cx,
+//             )
+//             .unwrap();
+//         project
+//             .update_diagnostic_entries(
+//                 LanguageServerId(1),
+//                 Path::new("/dir/a.rs").to_owned(),
+//                 None,
+//                 vec![DiagnosticEntry {
+//                     range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
+//                     diagnostic: Diagnostic {
+//                         severity: DiagnosticSeverity::ERROR,
+//                         is_primary: true,
+//                         message: "syntax error b1".to_string(),
+//                         ..Default::default()
+//                     },
+//                 }],
+//                 cx,
+//             )
+//             .unwrap();
+
+//         assert_eq!(
+//             project.diagnostic_summary(cx),
+//             DiagnosticSummary {
+//                 error_count: 2,
+//                 warning_count: 0,
+//             }
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
+
+//     let text = "
+//         fn a() {
+//             f1();
+//         }
+//         fn b() {
+//             f2();
+//         }
+//         fn c() {
+//             f3();
+//         }
+//     "
+//     .unindent();
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.rs": text.clone(),
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     let mut fake_server = fake_servers.next().await.unwrap();
+//     let lsp_document_version = fake_server
+//         .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+//         .await
+//         .text_document
+//         .version;
+
+//     // Simulate editing the buffer after the language server computes some edits.
+//     buffer.update(cx, |buffer, cx| {
+//         buffer.edit(
+//             [(
+//                 Point::new(0, 0)..Point::new(0, 0),
+//                 "// above first function\n",
+//             )],
+//             None,
+//             cx,
+//         );
+//         buffer.edit(
+//             [(
+//                 Point::new(2, 0)..Point::new(2, 0),
+//                 "    // inside first function\n",
+//             )],
+//             None,
+//             cx,
+//         );
+//         buffer.edit(
+//             [(
+//                 Point::new(6, 4)..Point::new(6, 4),
+//                 "// inside second function ",
+//             )],
+//             None,
+//             cx,
+//         );
+
+//         assert_eq!(
+//             buffer.text(),
+//             "
+//                 // above first function
+//                 fn a() {
+//                     // inside first function
+//                     f1();
+//                 }
+//                 fn b() {
+//                     // inside second function f2();
+//                 }
+//                 fn c() {
+//                     f3();
+//                 }
+//             "
+//             .unindent()
+//         );
+//     });
+
+//     let edits = project
+//         .update(cx, |project, cx| {
+//             project.edits_from_lsp(
+//                 &buffer,
+//                 vec![
+//                     // replace body of first function
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 0),
+//                             lsp2::Position::new(3, 0),
+//                         ),
+//                         new_text: "
+//                             fn a() {
+//                                 f10();
+//                             }
+//                             "
+//                         .unindent(),
+//                     },
+//                     // edit inside second function
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(4, 6),
+//                             lsp2::Position::new(4, 6),
+//                         ),
+//                         new_text: "00".into(),
+//                     },
+//                     // edit inside third function via two distinct edits
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(7, 5),
+//                             lsp2::Position::new(7, 5),
+//                         ),
+//                         new_text: "4000".into(),
+//                     },
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(7, 5),
+//                             lsp2::Position::new(7, 6),
+//                         ),
+//                         new_text: "".into(),
+//                     },
+//                 ],
+//                 LanguageServerId(0),
+//                 Some(lsp_document_version),
+//                 cx,
+//             )
+//         })
+//         .await
+//         .unwrap();
+
+//     buffer.update(cx, |buffer, cx| {
+//         for (range, new_text) in edits {
+//             buffer.edit([(range, new_text)], None, cx);
+//         }
+//         assert_eq!(
+//             buffer.text(),
+//             "
+//                 // above first function
+//                 fn a() {
+//                     // inside first function
+//                     f10();
+//                 }
+//                 fn b() {
+//                     // inside second function f200();
+//                 }
+//                 fn c() {
+//                     f4000();
+//                 }
+//                 "
+//             .unindent()
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let text = "
+//         use a::b;
+//         use a::c;
+
+//         fn f() {
+//             b();
+//             c();
+//         }
+//     "
+//     .unindent();
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.rs": text.clone(),
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     // Simulate the language server sending us a small edit in the form of a very large diff.
+//     // Rust-analyzer does this when performing a merge-imports code action.
+//     let edits = project
+//         .update(cx, |project, cx| {
+//             project.edits_from_lsp(
+//                 &buffer,
+//                 [
+//                     // Replace the first use statement without editing the semicolon.
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 4),
+//                             lsp2::Position::new(0, 8),
+//                         ),
+//                         new_text: "a::{b, c}".into(),
+//                     },
+//                     // Reinsert the remainder of the file between the semicolon and the final
+//                     // newline of the file.
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 9),
+//                             lsp2::Position::new(0, 9),
+//                         ),
+//                         new_text: "\n\n".into(),
+//                     },
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 9),
+//                             lsp2::Position::new(0, 9),
+//                         ),
+//                         new_text: "
+//                             fn f() {
+//                                 b();
+//                                 c();
+//                             }"
+//                         .unindent(),
+//                     },
+//                     // Delete everything after the first newline of the file.
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(1, 0),
+//                             lsp2::Position::new(7, 0),
+//                         ),
+//                         new_text: "".into(),
+//                     },
+//                 ],
+//                 LanguageServerId(0),
+//                 None,
+//                 cx,
+//             )
+//         })
+//         .await
+//         .unwrap();
+
+//     buffer.update(cx, |buffer, cx| {
+//         let edits = edits
+//             .into_iter()
+//             .map(|(range, text)| {
+//                 (
+//                     range.start.to_point(buffer)..range.end.to_point(buffer),
+//                     text,
+//                 )
+//             })
+//             .collect::<Vec<_>>();
+
+//         assert_eq!(
+//             edits,
+//             [
+//                 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
+//                 (Point::new(1, 0)..Point::new(2, 0), "".into())
+//             ]
+//         );
+
+//         for (range, new_text) in edits {
+//             buffer.edit([(range, new_text)], None, cx);
+//         }
+//         assert_eq!(
+//             buffer.text(),
+//             "
+//                 use a::{b, c};
+
+//                 fn f() {
+//                     b();
+//                     c();
+//                 }
+//             "
+//             .unindent()
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let text = "
+//         use a::b;
+//         use a::c;
+
+//         fn f() {
+//             b();
+//             c();
+//         }
+//     "
+//     .unindent();
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.rs": text.clone(),
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     // Simulate the language server sending us edits in a non-ordered fashion,
+//     // with ranges sometimes being inverted or pointing to invalid locations.
+//     let edits = project
+//         .update(cx, |project, cx| {
+//             project.edits_from_lsp(
+//                 &buffer,
+//                 [
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 9),
+//                             lsp2::Position::new(0, 9),
+//                         ),
+//                         new_text: "\n\n".into(),
+//                     },
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 8),
+//                             lsp2::Position::new(0, 4),
+//                         ),
+//                         new_text: "a::{b, c}".into(),
+//                     },
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(1, 0),
+//                             lsp2::Position::new(99, 0),
+//                         ),
+//                         new_text: "".into(),
+//                     },
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 9),
+//                             lsp2::Position::new(0, 9),
+//                         ),
+//                         new_text: "
+//                             fn f() {
+//                                 b();
+//                                 c();
+//                             }"
+//                         .unindent(),
+//                     },
+//                 ],
+//                 LanguageServerId(0),
+//                 None,
+//                 cx,
+//             )
+//         })
+//         .await
+//         .unwrap();
+
+//     buffer.update(cx, |buffer, cx| {
+//         let edits = edits
+//             .into_iter()
+//             .map(|(range, text)| {
+//                 (
+//                     range.start.to_point(buffer)..range.end.to_point(buffer),
+//                     text,
+//                 )
+//             })
+//             .collect::<Vec<_>>();
+
+//         assert_eq!(
+//             edits,
+//             [
+//                 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
+//                 (Point::new(1, 0)..Point::new(2, 0), "".into())
+//             ]
+//         );
+
+//         for (range, new_text) in edits {
+//             buffer.edit([(range, new_text)], None, cx);
+//         }
+//         assert_eq!(
+//             buffer.text(),
+//             "
+//                 use a::{b, c};
+
+//                 fn f() {
+//                     b();
+//                     c();
+//                 }
+//             "
+//             .unindent()
+//         );
+//     });
+// }
+
+// fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
+//     buffer: &Buffer,
+//     range: Range<T>,
+// ) -> Vec<(String, Option<DiagnosticSeverity>)> {
+//     let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
+//     for chunk in buffer.snapshot().chunks(range, true) {
+//         if chunks.last().map_or(false, |prev_chunk| {
+//             prev_chunk.1 == chunk.diagnostic_severity
+//         }) {
+//             chunks.last_mut().unwrap().0.push_str(chunk.text);
+//         } else {
+//             chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
+//         }
+//     }
+//     chunks
+// }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_definition(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.rs": "const fn a() { A }",
+//             "b.rs": "const y: i32 = crate::a()",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
+//         .await
+//         .unwrap();
+
+//     let fake_server = fake_servers.next().await.unwrap();
+//     fake_server.handle_request::<lsp2::request::GotoDefinition, _, _>(|params, _| async move {
+//         let params = params.text_document_position_params;
+//         assert_eq!(
+//             params.text_document.uri.to_file_path().unwrap(),
+//             Path::new("/dir/b.rs"),
+//         );
+//         assert_eq!(params.position, lsp2::Position::new(0, 22));
+
+//         Ok(Some(lsp2::GotoDefinitionResponse::Scalar(
+//             lsp2::Location::new(
+//                 lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+//                 lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+//             ),
+//         )))
+//     });
+
+//     let mut definitions = project
+//         .update(cx, |project, cx| project.definition(&buffer, 22, cx))
+//         .await
+//         .unwrap();
+
+//     // Assert no new language server started
+//     cx.foreground().run_until_parked();
+//     assert!(fake_servers.try_next().is_err());
+
+//     assert_eq!(definitions.len(), 1);
+//     let definition = definitions.pop().unwrap();
+//     cx.update(|cx| {
+//         let target_buffer = definition.target.buffer.read(cx);
+//         assert_eq!(
+//             target_buffer
+//                 .file()
+//                 .unwrap()
+//                 .as_local()
+//                 .unwrap()
+//                 .abs_path(cx),
+//             Path::new("/dir/a.rs"),
+//         );
+//         assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
+//         assert_eq!(
+//             list_worktrees(&project, cx),
+//             [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]
+//         );
+
+//         drop(definition);
+//     });
+//     cx.read(|cx| {
+//         assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
+//     });
+
+//     fn list_worktrees<'a>(
+//         project: &'a ModelHandle<Project>,
+//         cx: &'a AppContext,
+//     ) -> Vec<(&'a Path, bool)> {
+//         project
+//             .read(cx)
+//             .worktrees(cx)
+//             .map(|worktree| {
+//                 let worktree = worktree.read(cx);
+//                 (
+//                     worktree.as_local().unwrap().abs_path().as_ref(),
+//                     worktree.is_visible(),
+//                 )
+//             })
+//             .collect::<Vec<_>>()
+//     }
+// }
+
+// #[gpui::test]
+// async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "TypeScript".into(),
+//             path_suffixes: vec!["ts".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_typescript::language_typescript()),
+//     );
+//     let mut fake_language_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             capabilities: lsp::ServerCapabilities {
+//                 completion_provider: Some(lsp::CompletionOptions {
+//                     trigger_characters: Some(vec![":".to_string()]),
+//                     ..Default::default()
+//                 }),
+//                 ..Default::default()
+//             },
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.ts": "",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+//     let buffer = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
+//         .await
+//         .unwrap();
+
+//     let fake_server = fake_language_servers.next().await.unwrap();
+
+//     let text = "let a = b.fqn";
+//     buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
+//     let completions = project.update(cx, |project, cx| {
+//         project.completions(&buffer, text.len(), cx)
+//     });
+
+//     fake_server
+//         .handle_request::<lsp2::request::Completion, _, _>(|_, _| async move {
+//             Ok(Some(lsp2::CompletionResponse::Array(vec![
+//                 lsp2::CompletionItem {
+//                     label: "fullyQualifiedName?".into(),
+//                     insert_text: Some("fullyQualifiedName".into()),
+//                     ..Default::default()
+//                 },
+//             ])))
+//         })
+//         .next()
+//         .await;
+//     let completions = completions.await.unwrap();
+//     let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+//     assert_eq!(completions.len(), 1);
+//     assert_eq!(completions[0].new_text, "fullyQualifiedName");
+//     assert_eq!(
+//         completions[0].old_range.to_offset(&snapshot),
+//         text.len() - 3..text.len()
+//     );
+
+//     let text = "let a = \"atoms/cmp\"";
+//     buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
+//     let completions = project.update(cx, |project, cx| {
+//         project.completions(&buffer, text.len() - 1, cx)
+//     });
+
+//     fake_server
+//         .handle_request::<lsp2::request::Completion, _, _>(|_, _| async move {
+//             Ok(Some(lsp2::CompletionResponse::Array(vec![
+//                 lsp2::CompletionItem {
+//                     label: "component".into(),
+//                     ..Default::default()
+//                 },
+//             ])))
+//         })
+//         .next()
+//         .await;
+//     let completions = completions.await.unwrap();
+//     let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+//     assert_eq!(completions.len(), 1);
+//     assert_eq!(completions[0].new_text, "component");
+//     assert_eq!(
+//         completions[0].old_range.to_offset(&snapshot),
+//         text.len() - 4..text.len() - 1
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "TypeScript".into(),
+//             path_suffixes: vec!["ts".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_typescript::language_typescript()),
+//     );
+//     let mut fake_language_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             capabilities: lsp::ServerCapabilities {
+//                 completion_provider: Some(lsp::CompletionOptions {
+//                     trigger_characters: Some(vec![":".to_string()]),
+//                     ..Default::default()
+//                 }),
+//                 ..Default::default()
+//             },
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.ts": "",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+//     let buffer = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
+//         .await
+//         .unwrap();
+
+//     let fake_server = fake_language_servers.next().await.unwrap();
+
+//     let text = "let a = b.fqn";
+//     buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
+//     let completions = project.update(cx, |project, cx| {
+//         project.completions(&buffer, text.len(), cx)
+//     });
+
+//     fake_server
+//         .handle_request::<lsp2::request::Completion, _, _>(|_, _| async move {
+//             Ok(Some(lsp2::CompletionResponse::Array(vec![
+//                 lsp2::CompletionItem {
+//                     label: "fullyQualifiedName?".into(),
+//                     insert_text: Some("fully\rQualified\r\nName".into()),
+//                     ..Default::default()
+//                 },
+//             ])))
+//         })
+//         .next()
+//         .await;
+//     let completions = completions.await.unwrap();
+//     assert_eq!(completions.len(), 1);
+//     assert_eq!(completions[0].new_text, "fully\nQualified\nName");
+// }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "TypeScript".into(),
+//             path_suffixes: vec!["ts".to_string()],
+//             ..Default::default()
+//         },
+//         None,
+//     );
+//     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.ts": "a",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+//     let buffer = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
+//         .await
+//         .unwrap();
+
+//     let fake_server = fake_language_servers.next().await.unwrap();
+
+//     // Language server returns code actions that contain commands, and not edits.
+//     let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
+//     fake_server
+//         .handle_request::<lsp2::request::CodeActionRequest, _, _>(|_, _| async move {
+//             Ok(Some(vec![
+//                 lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction {
+//                     title: "The code action".into(),
+//                     command: Some(lsp::Command {
+//                         title: "The command".into(),
+//                         command: "_the/command".into(),
+//                         arguments: Some(vec![json!("the-argument")]),
+//                     }),
+//                     ..Default::default()
+//                 }),
+//                 lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction {
+//                     title: "two".into(),
+//                     ..Default::default()
+//                 }),
+//             ]))
+//         })
+//         .next()
+//         .await;
+
+//     let action = actions.await.unwrap()[0].clone();
+//     let apply = project.update(cx, |project, cx| {
+//         project.apply_code_action(buffer.clone(), action, true, cx)
+//     });
+
+//     // Resolving the code action does not populate its edits. In absence of
+//     // edits, we must execute the given command.
+//     fake_server.handle_request::<lsp2::request::CodeActionResolveRequest, _, _>(
+//         |action, _| async move { Ok(action) },
+//     );
+
+//     // While executing the command, the language server sends the editor
+//     // a `workspaceEdit` request.
+//     fake_server
+//         .handle_request::<lsp2::request::ExecuteCommand, _, _>({
+//             let fake = fake_server.clone();
+//             move |params, _| {
+//                 assert_eq!(params.command, "_the/command");
+//                 let fake = fake.clone();
+//                 async move {
+//                     fake.server
+//                         .request::<lsp2::request::ApplyWorkspaceEdit>(
+//                             lsp2::ApplyWorkspaceEditParams {
+//                                 label: None,
+//                                 edit: lsp::WorkspaceEdit {
+//                                     changes: Some(
+//                                         [(
+//                                             lsp2::Url::from_file_path("/dir/a.ts").unwrap(),
+//                                             vec![lsp2::TextEdit {
+//                                                 range: lsp2::Range::new(
+//                                                     lsp2::Position::new(0, 0),
+//                                                     lsp2::Position::new(0, 0),
+//                                                 ),
+//                                                 new_text: "X".into(),
+//                                             }],
+//                                         )]
+//                                         .into_iter()
+//                                         .collect(),
+//                                     ),
+//                                     ..Default::default()
+//                                 },
+//                             },
+//                         )
+//                         .await
+//                         .unwrap();
+//                     Ok(Some(json!(null)))
+//                 }
+//             }
+//         })
+//         .next()
+//         .await;
+
+//     // Applying the code action returns a project transaction containing the edits
+//     // sent by the language server in its `workspaceEdit` request.
+//     let transaction = apply.await.unwrap();
+//     assert!(transaction.0.contains_key(&buffer));
+//     buffer.update(cx, |buffer, cx| {
+//         assert_eq!(buffer.text(), "Xa");
+//         buffer.undo(cx);
+//         assert_eq!(buffer.text(), "a");
+//     });
+// }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_save_file(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "file1": "the old contents",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+//     let buffer = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+//         .await
+//         .unwrap();
+//     buffer.update(cx, |buffer, cx| {
+//         assert_eq!(buffer.text(), "the old contents");
+//         buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
+//     });
+
+//     project
+//         .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
+//         .await
+//         .unwrap();
+
+//     let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
+//     assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
+// }
+
+// #[gpui::test]
+// async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "file1": "the old contents",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
+//     let buffer = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+//         .await
+//         .unwrap();
+//     buffer.update(cx, |buffer, cx| {
+//         buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
+//     });
+
+//     project
+//         .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
+//         .await
+//         .unwrap();
+
+//     let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
+//     assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
+// }
+
+// #[gpui::test]
+// async fn test_save_as(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree("/dir", json!({})).await;
+
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+//     let languages = project.read_with(cx, |project, _| project.languages().clone());
+//     languages.register(
+//         "/some/path",
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".into()],
+//             ..Default::default()
+//         },
+//         tree_sitter_rust::language(),
+//         vec![],
+//         |_| Default::default(),
+//     );
+
+//     let buffer = project.update(cx, |project, cx| {
+//         project.create_buffer("", None, cx).unwrap()
+//     });
+//     buffer.update(cx, |buffer, cx| {
+//         buffer.edit([(0..0, "abc")], None, cx);
+//         assert!(buffer.is_dirty());
+//         assert!(!buffer.has_conflict());
+//         assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
+//     });
+//     project
+//         .update(cx, |project, cx| {
+//             project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
+//         })
+//         .await
+//         .unwrap();
+//     assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
+
+//     cx.foreground().run_until_parked();
+//     buffer.read_with(cx, |buffer, cx| {
+//         assert_eq!(
+//             buffer.file().unwrap().full_path(cx),
+//             Path::new("dir/file1.rs")
+//         );
+//         assert!(!buffer.is_dirty());
+//         assert!(!buffer.has_conflict());
+//         assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
+//     });
+
+//     let opened_buffer = project
+//         .update(cx, |project, cx| {
+//             project.open_local_buffer("/dir/file1.rs", cx)
+//         })
+//         .await
+//         .unwrap();
+//     assert_eq!(opened_buffer, buffer);
+// }
+
+// #[gpui::test(retries = 5)]
+// async fn test_rescan_and_remote_updates(
+//     deterministic: Arc<Deterministic>,
+//     cx: &mut gpui::TestAppContext,
+// ) {
+//     init_test(cx);
+//     cx.foreground().allow_parking();
+
+//     let dir = temp_tree(json!({
+//         "a": {
+//             "file1": "",
+//             "file2": "",
+//             "file3": "",
+//         },
+//         "b": {
+//             "c": {
+//                 "file4": "",
+//                 "file5": "",
+//             }
+//         }
+//     }));
+
+//     let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
+//     let rpc = project.read_with(cx, |p, _| p.client.clone());
+
+//     let buffer_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| {
+//         let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
+//         async move { buffer.await.unwrap() }
+//     };
+//     let id_for_path = |path: &'static str, cx: &gpui2::TestAppContext| {
+//         project.read_with(cx, |project, cx| {
+//             let tree = project.worktrees(cx).next().unwrap();
+//             tree.read(cx)
+//                 .entry_for_path(path)
+//                 .unwrap_or_else(|| panic!("no entry for path {}", path))
+//                 .id
+//         })
+//     };
+
+//     let buffer2 = buffer_for_path("a/file2", cx).await;
+//     let buffer3 = buffer_for_path("a/file3", cx).await;
+//     let buffer4 = buffer_for_path("b/c/file4", cx).await;
+//     let buffer5 = buffer_for_path("b/c/file5", cx).await;
+
+//     let file2_id = id_for_path("a/file2", cx);
+//     let file3_id = id_for_path("a/file3", cx);
+//     let file4_id = id_for_path("b/c/file4", cx);
+
+//     // Create a remote copy of this worktree.
+//     let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+
+//     let metadata = tree.read_with(cx, |tree, _| tree.as_local().unwrap().metadata_proto());
+
+//     let updates = Arc::new(Mutex::new(Vec::new()));
+//     tree.update(cx, |tree, cx| {
+//         let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
+//             let updates = updates.clone();
+//             move |update| {
+//                 updates.lock().push(update);
+//                 async { true }
+//             }
+//         });
+//     });
+
+//     let remote = cx.update(|cx| Worktree::remote(1, 1, metadata, rpc.clone(), cx));
+//     deterministic.run_until_parked();
+
+//     cx.read(|cx| {
+//         assert!(!buffer2.read(cx).is_dirty());
+//         assert!(!buffer3.read(cx).is_dirty());
+//         assert!(!buffer4.read(cx).is_dirty());
+//         assert!(!buffer5.read(cx).is_dirty());
+//     });
+
+//     // Rename and delete files and directories.
+//     tree.flush_fs_events(cx).await;
+//     std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
+//     std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
+//     std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
+//     std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
+//     tree.flush_fs_events(cx).await;
+
+//     let expected_paths = vec![
+//         "a",
+//         "a/file1",
+//         "a/file2.new",
+//         "b",
+//         "d",
+//         "d/file3",
+//         "d/file4",
+//     ];
+
+//     cx.read(|app| {
+//         assert_eq!(
+//             tree.read(app)
+//                 .paths()
+//                 .map(|p| p.to_str().unwrap())
+//                 .collect::<Vec<_>>(),
+//             expected_paths
+//         );
+
+//         assert_eq!(id_for_path("a/file2.new", cx), file2_id);
+//         assert_eq!(id_for_path("d/file3", cx), file3_id);
+//         assert_eq!(id_for_path("d/file4", cx), file4_id);
+
+//         assert_eq!(
+//             buffer2.read(app).file().unwrap().path().as_ref(),
+//             Path::new("a/file2.new")
+//         );
+//         assert_eq!(
+//             buffer3.read(app).file().unwrap().path().as_ref(),
+//             Path::new("d/file3")
+//         );
+//         assert_eq!(
+//             buffer4.read(app).file().unwrap().path().as_ref(),
+//             Path::new("d/file4")
+//         );
+//         assert_eq!(
+//             buffer5.read(app).file().unwrap().path().as_ref(),
+//             Path::new("b/c/file5")
+//         );
+
+//         assert!(!buffer2.read(app).file().unwrap().is_deleted());
+//         assert!(!buffer3.read(app).file().unwrap().is_deleted());
+//         assert!(!buffer4.read(app).file().unwrap().is_deleted());
+//         assert!(buffer5.read(app).file().unwrap().is_deleted());
+//     });
+
+//     // Update the remote worktree. Check that it becomes consistent with the
+//     // local worktree.
+//     deterministic.run_until_parked();
+//     remote.update(cx, |remote, _| {
+//         for update in updates.lock().drain(..) {
+//             remote.as_remote_mut().unwrap().update_from_remote(update);
+//         }
+//     });
+//     deterministic.run_until_parked();
+//     remote.read_with(cx, |remote, _| {
+//         assert_eq!(
+//             remote
+//                 .paths()
+//                 .map(|p| p.to_str().unwrap())
+//                 .collect::<Vec<_>>(),
+//             expected_paths
+//         );
+//     });
+// }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_buffer_identity_across_renames(
+//     deterministic: Arc<Deterministic>,
+//     cx: &mut gpui::TestAppContext,
+// ) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a": {
+//                 "file1": "",
+//             }
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs, [Path::new("/dir")], cx).await;
+//     let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+//     let tree_id = tree.read_with(cx, |tree, _| tree.id());
+
+//     let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
+//         project.read_with(cx, |project, cx| {
+//             let tree = project.worktrees(cx).next().unwrap();
+//             tree.read(cx)
+//                 .entry_for_path(path)
+//                 .unwrap_or_else(|| panic!("no entry for path {}", path))
+//                 .id
+//         })
+//     };
+
+//     let dir_id = id_for_path("a", cx);
+//     let file_id = id_for_path("a/file1", cx);
+//     let buffer = project
+//         .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
+//         .await
+//         .unwrap();
+//     buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
+
+//     project
+//         .update(cx, |project, cx| {
+//             project.rename_entry(dir_id, Path::new("b"), cx)
+//         })
+//         .unwrap()
+//         .await
+//         .unwrap();
+//     deterministic.run_until_parked();
+//     assert_eq!(id_for_path("b", cx), dir_id);
+//     assert_eq!(id_for_path("b/file1", cx), file_id);
+//     buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
+// }
+
+// #[gpui2::test]
+// async fn test_buffer_deduping(cx: &mut gpui2::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "a.txt": "a-contents",
+//             "b.txt": "b-contents",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+//     // Spawn multiple tasks to open paths, repeating some paths.
+//     let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
+//         (
+//             p.open_local_buffer("/dir/a.txt", cx),
+//             p.open_local_buffer("/dir/b.txt", cx),
+//             p.open_local_buffer("/dir/a.txt", cx),
+//         )
+//     });
+
+//     let buffer_a_1 = buffer_a_1.await.unwrap();
+//     let buffer_a_2 = buffer_a_2.await.unwrap();
+//     let buffer_b = buffer_b.await.unwrap();
+//     assert_eq!(buffer_a_1.read_with(cx, |b, _| b.text()), "a-contents");
+//     assert_eq!(buffer_b.read_with(cx, |b, _| b.text()), "b-contents");
+
+//     // There is only one buffer per path.
+//     let buffer_a_id = buffer_a_1.id();
+//     assert_eq!(buffer_a_2.id(), buffer_a_id);
+
+//     // Open the same path again while it is still open.
+//     drop(buffer_a_1);
+//     let buffer_a_3 = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
+//         .await
+//         .unwrap();
+
+//     // There's still only one buffer per path.
+//     assert_eq!(buffer_a_3.id(), buffer_a_id);
+// }
+
+// #[gpui2::test]
+// async fn test_buffer_is_dirty(cx: &mut gpui2::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "file1": "abc",
+//             "file2": "def",
+//             "file3": "ghi",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+//     let buffer1 = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+//         .await
+//         .unwrap();
+//     let events = Rc::new(RefCell::new(Vec::new()));
+
+//     // initially, the buffer isn't dirty.
+//     buffer1.update(cx, |buffer, cx| {
+//         cx.subscribe(&buffer1, {
+//             let events = events.clone();
+//             move |_, _, event, _| match event {
+//                 BufferEvent::Operation(_) => {}
+//                 _ => events.borrow_mut().push(event.clone()),
+//             }
+//         })
+//         .detach();
+
+//         assert!(!buffer.is_dirty());
+//         assert!(events.borrow().is_empty());
+
+//         buffer.edit([(1..2, "")], None, cx);
+//     });
+
+//     // after the first edit, the buffer is dirty, and emits a dirtied event.
+//     buffer1.update(cx, |buffer, cx| {
+//         assert!(buffer.text() == "ac");
+//         assert!(buffer.is_dirty());
+//         assert_eq!(
+//             *events.borrow(),
+//             &[language2::Event::Edited, language2::Event::DirtyChanged]
+//         );
+//         events.borrow_mut().clear();
+//         buffer.did_save(
+//             buffer.version(),
+//             buffer.as_rope().fingerprint(),
+//             buffer.file().unwrap().mtime(),
+//             cx,
+//         );
+//     });
+
+//     // after saving, the buffer is not dirty, and emits a saved event.
+//     buffer1.update(cx, |buffer, cx| {
+//         assert!(!buffer.is_dirty());
+//         assert_eq!(*events.borrow(), &[language2::Event::Saved]);
+//         events.borrow_mut().clear();
+
+//         buffer.edit([(1..1, "B")], None, cx);
+//         buffer.edit([(2..2, "D")], None, cx);
+//     });
+
+//     // after editing again, the buffer is dirty, and emits another dirty event.
+//     buffer1.update(cx, |buffer, cx| {
+//         assert!(buffer.text() == "aBDc");
+//         assert!(buffer.is_dirty());
+//         assert_eq!(
+//             *events.borrow(),
+//             &[
+//                 language2::Event::Edited,
+//                 language2::Event::DirtyChanged,
+//                 language2::Event::Edited,
+//             ],
+//         );
+//         events.borrow_mut().clear();
+
+//         // After restoring the buffer to its previously-saved state,
+//         // the buffer is not considered dirty anymore.
+//         buffer.edit([(1..3, "")], None, cx);
+//         assert!(buffer.text() == "ac");
+//         assert!(!buffer.is_dirty());
+//     });
+
+//     assert_eq!(
+//         *events.borrow(),
+//         &[language2::Event::Edited, language2::Event::DirtyChanged]
+//     );
+
+//     // When a file is deleted, the buffer is considered dirty.
+//     let events = Rc::new(RefCell::new(Vec::new()));
+//     let buffer2 = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
+//         .await
+//         .unwrap();
+//     buffer2.update(cx, |_, cx| {
+//         cx.subscribe(&buffer2, {
+//             let events = events.clone();
+//             move |_, _, event, _| events.borrow_mut().push(event.clone())
+//         })
+//         .detach();
+//     });
+
+//     fs.remove_file("/dir/file2".as_ref(), Default::default())
+//         .await
+//         .unwrap();
+//     cx.foreground().run_until_parked();
+//     buffer2.read_with(cx, |buffer, _| assert!(buffer.is_dirty()));
+//     assert_eq!(
+//         *events.borrow(),
+//         &[
+//             language2::Event::DirtyChanged,
+//             language2::Event::FileHandleChanged
+//         ]
+//     );
+
+//     // When a file is already dirty when deleted, we don't emit a Dirtied event.
+//     let events = Rc::new(RefCell::new(Vec::new()));
+//     let buffer3 = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
+//         .await
+//         .unwrap();
+//     buffer3.update(cx, |_, cx| {
+//         cx.subscribe(&buffer3, {
+//             let events = events.clone();
+//             move |_, _, event, _| events.borrow_mut().push(event.clone())
+//         })
+//         .detach();
+//     });
+
+//     buffer3.update(cx, |buffer, cx| {
+//         buffer.edit([(0..0, "x")], None, cx);
+//     });
+//     events.borrow_mut().clear();
+//     fs.remove_file("/dir/file3".as_ref(), Default::default())
+//         .await
+//         .unwrap();
+//     cx.foreground().run_until_parked();
+//     assert_eq!(*events.borrow(), &[language2::Event::FileHandleChanged]);
+//     cx.read(|cx| assert!(buffer3.read(cx).is_dirty()));
+// }
+
+// #[gpui::test]
+// async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let initial_contents = "aaa\nbbbbb\nc\n";
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "the-file": initial_contents,
+//         }),
+//     )
+//     .await;
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+//     let buffer = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
+//         .await
+//         .unwrap();
+
+//     let anchors = (0..3)
+//         .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1))))
+//         .collect::<Vec<_>>();
+
+//     // Change the file on disk, adding two new lines of text, and removing
+//     // one line.
+//     buffer.read_with(cx, |buffer, _| {
+//         assert!(!buffer.is_dirty());
+//         assert!(!buffer.has_conflict());
+//     });
+//     let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
+//     fs.save(
+//         "/dir/the-file".as_ref(),
+//         &new_contents.into(),
+//         LineEnding::Unix,
+//     )
+//     .await
+//     .unwrap();
+
+//     // Because the buffer was not modified, it is reloaded from disk. Its
+//     // contents are edited according to the diff between the old and new
+//     // file contents.
+//     cx.foreground().run_until_parked();
+//     buffer.update(cx, |buffer, _| {
+//         assert_eq!(buffer.text(), new_contents);
+//         assert!(!buffer.is_dirty());
+//         assert!(!buffer.has_conflict());
+
+//         let anchor_positions = anchors
+//             .iter()
+//             .map(|anchor| anchor.to_point(&*buffer))
+//             .collect::<Vec<_>>();
+//         assert_eq!(
+//             anchor_positions,
+//             [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
+//         );
+//     });
+
+//     // Modify the buffer
+//     buffer.update(cx, |buffer, cx| {
+//         buffer.edit([(0..0, " ")], None, cx);
+//         assert!(buffer.is_dirty());
+//         assert!(!buffer.has_conflict());
+//     });
+
+//     // Change the file on disk again, adding blank lines to the beginning.
+//     fs.save(
+//         "/dir/the-file".as_ref(),
+//         &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
+//         LineEnding::Unix,
+//     )
+//     .await
+//     .unwrap();
+
+//     // Because the buffer is modified, it doesn't reload from disk, but is
+//     // marked as having a conflict.
+//     cx.foreground().run_until_parked();
+//     buffer.read_with(cx, |buffer, _| {
+//         assert!(buffer.has_conflict());
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "file1": "a\nb\nc\n",
+//             "file2": "one\r\ntwo\r\nthree\r\n",
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+//     let buffer1 = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+//         .await
+//         .unwrap();
+//     let buffer2 = project
+//         .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
+//         .await
+//         .unwrap();
+
+//     buffer1.read_with(cx, |buffer, _| {
+//         assert_eq!(buffer.text(), "a\nb\nc\n");
+//         assert_eq!(buffer.line_ending(), LineEnding::Unix);
+//     });
+//     buffer2.read_with(cx, |buffer, _| {
+//         assert_eq!(buffer.text(), "one\ntwo\nthree\n");
+//         assert_eq!(buffer.line_ending(), LineEnding::Windows);
+//     });
+
+//     // Change a file's line endings on disk from unix to windows. The buffer's
+//     // state updates correctly.
+//     fs.save(
+//         "/dir/file1".as_ref(),
+//         &"aaa\nb\nc\n".into(),
+//         LineEnding::Windows,
+//     )
+//     .await
+//     .unwrap();
+//     cx.foreground().run_until_parked();
+//     buffer1.read_with(cx, |buffer, _| {
+//         assert_eq!(buffer.text(), "aaa\nb\nc\n");
+//         assert_eq!(buffer.line_ending(), LineEnding::Windows);
+//     });
+
+//     // Save a file with windows line endings. The file is written correctly.
+//     buffer2.update(cx, |buffer, cx| {
+//         buffer.set_text("one\ntwo\nthree\nfour\n", cx);
+//     });
+//     project
+//         .update(cx, |project, cx| project.save_buffer(buffer2, cx))
+//         .await
+//         .unwrap();
+//     assert_eq!(
+//         fs.load("/dir/file2".as_ref()).await.unwrap(),
+//         "one\r\ntwo\r\nthree\r\nfour\r\n",
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/the-dir",
+//         json!({
+//             "a.rs": "
+//                 fn foo(mut v: Vec<usize>) {
+//                     for x in &v {
+//                         v.push(1);
+//                     }
+//                 }
+//             "
+//             .unindent(),
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
+//     let buffer = project
+//         .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap();
+//     let message = lsp::PublishDiagnosticsParams {
+//         uri: buffer_uri.clone(),
+//         diagnostics: vec![
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)),
+//                 severity: Some(DiagnosticSeverity::WARNING),
+//                 message: "error 1".to_string(),
+//                 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+//                     location: lsp::Location {
+//                         uri: buffer_uri.clone(),
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(1, 8),
+//                             lsp2::Position::new(1, 9),
+//                         ),
+//                     },
+//                     message: "error 1 hint 1".to_string(),
+//                 }]),
+//                 ..Default::default()
+//             },
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)),
+//                 severity: Some(DiagnosticSeverity::HINT),
+//                 message: "error 1 hint 1".to_string(),
+//                 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+//                     location: lsp::Location {
+//                         uri: buffer_uri.clone(),
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(1, 8),
+//                             lsp2::Position::new(1, 9),
+//                         ),
+//                     },
+//                     message: "original diagnostic".to_string(),
+//                 }]),
+//                 ..Default::default()
+//             },
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(2, 8), lsp2::Position::new(2, 17)),
+//                 severity: Some(DiagnosticSeverity::ERROR),
+//                 message: "error 2".to_string(),
+//                 related_information: Some(vec![
+//                     lsp::DiagnosticRelatedInformation {
+//                         location: lsp::Location {
+//                             uri: buffer_uri.clone(),
+//                             range: lsp2::Range::new(
+//                                 lsp2::Position::new(1, 13),
+//                                 lsp2::Position::new(1, 15),
+//                             ),
+//                         },
+//                         message: "error 2 hint 1".to_string(),
+//                     },
+//                     lsp::DiagnosticRelatedInformation {
+//                         location: lsp::Location {
+//                             uri: buffer_uri.clone(),
+//                             range: lsp2::Range::new(
+//                                 lsp2::Position::new(1, 13),
+//                                 lsp2::Position::new(1, 15),
+//                             ),
+//                         },
+//                         message: "error 2 hint 2".to_string(),
+//                     },
+//                 ]),
+//                 ..Default::default()
+//             },
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)),
+//                 severity: Some(DiagnosticSeverity::HINT),
+//                 message: "error 2 hint 1".to_string(),
+//                 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+//                     location: lsp::Location {
+//                         uri: buffer_uri.clone(),
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(2, 8),
+//                             lsp2::Position::new(2, 17),
+//                         ),
+//                     },
+//                     message: "original diagnostic".to_string(),
+//                 }]),
+//                 ..Default::default()
+//             },
+//             lsp2::Diagnostic {
+//                 range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)),
+//                 severity: Some(DiagnosticSeverity::HINT),
+//                 message: "error 2 hint 2".to_string(),
+//                 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+//                     location: lsp::Location {
+//                         uri: buffer_uri,
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(2, 8),
+//                             lsp2::Position::new(2, 17),
+//                         ),
+//                     },
+//                     message: "original diagnostic".to_string(),
+//                 }]),
+//                 ..Default::default()
+//             },
+//         ],
+//         version: None,
+//     };
+
+//     project
+//         .update(cx, |p, cx| {
+//             p.update_diagnostics(LanguageServerId(0), message, &[], cx)
+//         })
+//         .unwrap();
+//     let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+
+//     assert_eq!(
+//         buffer
+//             .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
+//             .collect::<Vec<_>>(),
+//         &[
+//             DiagnosticEntry {
+//                 range: Point::new(1, 8)..Point::new(1, 9),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::WARNING,
+//                     message: "error 1".to_string(),
+//                     group_id: 1,
+//                     is_primary: true,
+//                     ..Default::default()
+//                 }
+//             },
+//             DiagnosticEntry {
+//                 range: Point::new(1, 8)..Point::new(1, 9),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::HINT,
+//                     message: "error 1 hint 1".to_string(),
+//                     group_id: 1,
+//                     is_primary: false,
+//                     ..Default::default()
+//                 }
+//             },
+//             DiagnosticEntry {
+//                 range: Point::new(1, 13)..Point::new(1, 15),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::HINT,
+//                     message: "error 2 hint 1".to_string(),
+//                     group_id: 0,
+//                     is_primary: false,
+//                     ..Default::default()
+//                 }
+//             },
+//             DiagnosticEntry {
+//                 range: Point::new(1, 13)..Point::new(1, 15),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::HINT,
+//                     message: "error 2 hint 2".to_string(),
+//                     group_id: 0,
+//                     is_primary: false,
+//                     ..Default::default()
+//                 }
+//             },
+//             DiagnosticEntry {
+//                 range: Point::new(2, 8)..Point::new(2, 17),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::ERROR,
+//                     message: "error 2".to_string(),
+//                     group_id: 0,
+//                     is_primary: true,
+//                     ..Default::default()
+//                 }
+//             }
+//         ]
+//     );
+
+//     assert_eq!(
+//         buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
+//         &[
+//             DiagnosticEntry {
+//                 range: Point::new(1, 13)..Point::new(1, 15),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::HINT,
+//                     message: "error 2 hint 1".to_string(),
+//                     group_id: 0,
+//                     is_primary: false,
+//                     ..Default::default()
+//                 }
+//             },
+//             DiagnosticEntry {
+//                 range: Point::new(1, 13)..Point::new(1, 15),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::HINT,
+//                     message: "error 2 hint 2".to_string(),
+//                     group_id: 0,
+//                     is_primary: false,
+//                     ..Default::default()
+//                 }
+//             },
+//             DiagnosticEntry {
+//                 range: Point::new(2, 8)..Point::new(2, 17),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::ERROR,
+//                     message: "error 2".to_string(),
+//                     group_id: 0,
+//                     is_primary: true,
+//                     ..Default::default()
+//                 }
+//             }
+//         ]
+//     );
+
+//     assert_eq!(
+//         buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
+//         &[
+//             DiagnosticEntry {
+//                 range: Point::new(1, 8)..Point::new(1, 9),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::WARNING,
+//                     message: "error 1".to_string(),
+//                     group_id: 1,
+//                     is_primary: true,
+//                     ..Default::default()
+//                 }
+//             },
+//             DiagnosticEntry {
+//                 range: Point::new(1, 8)..Point::new(1, 9),
+//                 diagnostic: Diagnostic {
+//                     severity: DiagnosticSeverity::HINT,
+//                     message: "error 1 hint 1".to_string(),
+//                     group_id: 1,
+//                     is_primary: false,
+//                     ..Default::default()
+//                 }
+//             },
+//         ]
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_rename(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             capabilities: lsp2::ServerCapabilities {
+//                 rename_provider: Some(lsp2::OneOf::Right(lsp2::RenameOptions {
+//                     prepare_provider: Some(true),
+//                     work_done_progress_options: Default::default(),
+//                 })),
+//                 ..Default::default()
+//             },
+//             ..Default::default()
+//         }))
+//         .await;
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "one.rs": "const ONE: usize = 1;",
+//             "two.rs": "const TWO: usize = one::ONE + one::ONE;"
+//         }),
+//     )
+//     .await;
+
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+//     project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+//     let buffer = project
+//         .update(cx, |project, cx| {
+//             project.open_local_buffer("/dir/one.rs", cx)
+//         })
+//         .await
+//         .unwrap();
+
+//     let fake_server = fake_servers.next().await.unwrap();
+
+//     let response = project.update(cx, |project, cx| {
+//         project.prepare_rename(buffer.clone(), 7, cx)
+//     });
+//     fake_server
+//         .handle_request::<lsp2::request::PrepareRenameRequest, _, _>(|params, _| async move {
+//             assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
+//             assert_eq!(params.position, lsp2::Position::new(0, 7));
+//             Ok(Some(lsp2::PrepareRenameResponse::Range(lsp2::Range::new(
+//                 lsp2::Position::new(0, 6),
+//                 lsp2::Position::new(0, 9),
+//             ))))
+//         })
+//         .next()
+//         .await
+//         .unwrap();
+//     let range = response.await.unwrap().unwrap();
+//     let range = buffer.read_with(cx, |buffer, _| range.to_offset(buffer));
+//     assert_eq!(range, 6..9);
+
+//     let response = project.update(cx, |project, cx| {
+//         project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
+//     });
+//     fake_server
+//         .handle_request::<lsp2::request::Rename, _, _>(|params, _| async move {
+//             assert_eq!(
+//                 params.text_document_position.text_document.uri.as_str(),
+//                 "file:///dir/one.rs"
+//             );
+//             assert_eq!(
+//                 params.text_document_position.position,
+//                 lsp2::Position::new(0, 7)
+//             );
+//             assert_eq!(params.new_name, "THREE");
+//             Ok(Some(lsp::WorkspaceEdit {
+//                 changes: Some(
+//                     [
+//                         (
+//                             lsp2::Url::from_file_path("/dir/one.rs").unwrap(),
+//                             vec![lsp2::TextEdit::new(
+//                                 lsp2::Range::new(
+//                                     lsp2::Position::new(0, 6),
+//                                     lsp2::Position::new(0, 9),
+//                                 ),
+//                                 "THREE".to_string(),
+//                             )],
+//                         ),
+//                         (
+//                             lsp2::Url::from_file_path("/dir/two.rs").unwrap(),
+//                             vec![
+//                                 lsp2::TextEdit::new(
+//                                     lsp2::Range::new(
+//                                         lsp2::Position::new(0, 24),
+//                                         lsp2::Position::new(0, 27),
+//                                     ),
+//                                     "THREE".to_string(),
+//                                 ),
+//                                 lsp2::TextEdit::new(
+//                                     lsp2::Range::new(
+//                                         lsp2::Position::new(0, 35),
+//                                         lsp2::Position::new(0, 38),
+//                                     ),
+//                                     "THREE".to_string(),
+//                                 ),
+//                             ],
+//                         ),
+//                     ]
+//                     .into_iter()
+//                     .collect(),
+//                 ),
+//                 ..Default::default()
+//             }))
+//         })
+//         .next()
+//         .await
+//         .unwrap();
+//     let mut transaction = response.await.unwrap().0;
+//     assert_eq!(transaction.len(), 2);
+//     assert_eq!(
+//         transaction
+//             .remove_entry(&buffer)
+//             .unwrap()
+//             .0
+//             .read_with(cx, |buffer, _| buffer.text()),
+//         "const THREE: usize = 1;"
+//     );
+//     assert_eq!(
+//         transaction
+//             .into_keys()
+//             .next()
+//             .unwrap()
+//             .read_with(cx, |buffer, _| buffer.text()),
+//         "const TWO: usize = one::THREE + one::THREE;"
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_search(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "one.rs": "const ONE: usize = 1;",
+//             "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+//             "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+//             "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+//         }),
+//     )
+//     .await;
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+//     assert_eq!(
+//         search(
+//             &project,
+//             SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap(),
+//         HashMap::from_iter([
+//             ("two.rs".to_string(), vec![6..9]),
+//             ("three.rs".to_string(), vec![37..40])
+//         ])
+//     );
+
+//     let buffer_4 = project
+//         .update(cx, |project, cx| {
+//             project.open_local_buffer("/dir/four.rs", cx)
+//         })
+//         .await
+//         .unwrap();
+//     buffer_4.update(cx, |buffer, cx| {
+//         let text = "two::TWO";
+//         buffer.edit([(20..28, text), (31..43, text)], None, cx);
+//     });
+
+//     assert_eq!(
+//         search(
+//             &project,
+//             SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap(),
+//         HashMap::from_iter([
+//             ("two.rs".to_string(), vec![6..9]),
+//             ("three.rs".to_string(), vec![37..40]),
+//             ("four.rs".to_string(), vec![25..28, 36..39])
+//         ])
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let search_query = "file";
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "one.rs": r#"// Rust file one"#,
+//             "one.ts": r#"// TypeScript file one"#,
+//             "two.rs": r#"// Rust file two"#,
+//             "two.ts": r#"// TypeScript file two"#,
+//         }),
+//     )
+//     .await;
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+//     assert!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 vec![PathMatcher::new("*.odd").unwrap()],
+//                 Vec::new()
+//             )
+//             .unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap()
+//         .is_empty(),
+//         "If no inclusions match, no files should be returned"
+//     );
+
+//     assert_eq!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 vec![PathMatcher::new("*.rs").unwrap()],
+//                 Vec::new()
+//             )
+//             .unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap(),
+//         HashMap::from_iter([
+//             ("one.rs".to_string(), vec![8..12]),
+//             ("two.rs".to_string(), vec![8..12]),
+//         ]),
+//         "Rust only search should give only Rust files"
+//     );
+
+//     assert_eq!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 vec![
+//                     PathMatcher::new("*.ts").unwrap(),
+//                     PathMatcher::new("*.odd").unwrap(),
+//                 ],
+//                 Vec::new()
+//             ).unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap(),
+//         HashMap::from_iter([
+//             ("one.ts".to_string(), vec![14..18]),
+//             ("two.ts".to_string(), vec![14..18]),
+//         ]),
+//         "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
+//     );
+
+//     assert_eq!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 vec![
+//                     PathMatcher::new("*.rs").unwrap(),
+//                     PathMatcher::new("*.ts").unwrap(),
+//                     PathMatcher::new("*.odd").unwrap(),
+//                 ],
+//                 Vec::new()
+//             ).unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap(),
+//         HashMap::from_iter([
+//             ("one.rs".to_string(), vec![8..12]),
+//             ("one.ts".to_string(), vec![14..18]),
+//             ("two.rs".to_string(), vec![8..12]),
+//             ("two.ts".to_string(), vec![14..18]),
+//         ]),
+//         "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let search_query = "file";
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "one.rs": r#"// Rust file one"#,
+//             "one.ts": r#"// TypeScript file one"#,
+//             "two.rs": r#"// Rust file two"#,
+//             "two.ts": r#"// TypeScript file two"#,
+//         }),
+//     )
+//     .await;
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+//     assert_eq!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 Vec::new(),
+//                 vec![PathMatcher::new("*.odd").unwrap()],
+//             )
+//             .unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap(),
+//         HashMap::from_iter([
+//             ("one.rs".to_string(), vec![8..12]),
+//             ("one.ts".to_string(), vec![14..18]),
+//             ("two.rs".to_string(), vec![8..12]),
+//             ("two.ts".to_string(), vec![14..18]),
+//         ]),
+//         "If no exclusions match, all files should be returned"
+//     );
+
+//     assert_eq!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 Vec::new(),
+//                 vec![PathMatcher::new("*.rs").unwrap()],
+//             )
+//             .unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap(),
+//         HashMap::from_iter([
+//             ("one.ts".to_string(), vec![14..18]),
+//             ("two.ts".to_string(), vec![14..18]),
+//         ]),
+//         "Rust exclusion search should give only TypeScript files"
+//     );
+
+//     assert_eq!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 Vec::new(),
+//                 vec![
+//                     PathMatcher::new("*.ts").unwrap(),
+//                     PathMatcher::new("*.odd").unwrap(),
+//                 ],
+//             ).unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap(),
+//         HashMap::from_iter([
+//             ("one.rs".to_string(), vec![8..12]),
+//             ("two.rs".to_string(), vec![8..12]),
+//         ]),
+//         "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
+//     );
+
+//     assert!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 Vec::new(),
+//                 vec![
+//                     PathMatcher::new("*.rs").unwrap(),
+//                     PathMatcher::new("*.ts").unwrap(),
+//                     PathMatcher::new("*.odd").unwrap(),
+//                 ],
+//             ).unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap().is_empty(),
+//         "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let search_query = "file";
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/dir",
+//         json!({
+//             "one.rs": r#"// Rust file one"#,
+//             "one.ts": r#"// TypeScript file one"#,
+//             "two.rs": r#"// Rust file two"#,
+//             "two.ts": r#"// TypeScript file two"#,
+//         }),
+//     )
+//     .await;
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+//     assert!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 vec![PathMatcher::new("*.odd").unwrap()],
+//                 vec![PathMatcher::new("*.odd").unwrap()],
+//             )
+//             .unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap()
+//         .is_empty(),
+//         "If both no exclusions and inclusions match, exclusions should win and return nothing"
+//     );
+
+//     assert!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 vec![PathMatcher::new("*.ts").unwrap()],
+//                 vec![PathMatcher::new("*.ts").unwrap()],
+//             ).unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap()
+//         .is_empty(),
+//         "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
+//     );
+
+//     assert!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 vec![
+//                     PathMatcher::new("*.ts").unwrap(),
+//                     PathMatcher::new("*.odd").unwrap()
+//                 ],
+//                 vec![
+//                     PathMatcher::new("*.ts").unwrap(),
+//                     PathMatcher::new("*.odd").unwrap()
+//                 ],
+//             )
+//             .unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap()
+//         .is_empty(),
+//         "Non-matching inclusions and exclusions should not change that."
+//     );
+
+//     assert_eq!(
+//         search(
+//             &project,
+//             SearchQuery::text(
+//                 search_query,
+//                 false,
+//                 true,
+//                 vec![
+//                     PathMatcher::new("*.ts").unwrap(),
+//                     PathMatcher::new("*.odd").unwrap()
+//                 ],
+//                 vec![
+//                     PathMatcher::new("*.rs").unwrap(),
+//                     PathMatcher::new("*.odd").unwrap()
+//                 ],
+//             )
+//             .unwrap(),
+//             cx
+//         )
+//         .await
+//         .unwrap(),
+//         HashMap::from_iter([
+//             ("one.ts".to_string(), vec![14..18]),
+//             ("two.ts".to_string(), vec![14..18]),
+//         ]),
+//         "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
+//     );
+// }
+
+// #[test]
+// fn test_glob_literal_prefix() {
+//     assert_eq!(glob_literal_prefix("**/*.js"), "");
+//     assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules");
+//     assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo");
+//     assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
+// }
+
+// async fn search(
+//     project: &ModelHandle<Project>,
+//     query: SearchQuery,
+//     cx: &mut gpui::TestAppContext,
+// ) -> Result<HashMap<String, Vec<Range<usize>>>> {
+//     let mut search_rx = project.update(cx, |project, cx| project.search(query, cx));
+//     let mut result = HashMap::default();
+//     while let Some((buffer, range)) = search_rx.next().await {
+//         result.entry(buffer).or_insert(range);
+//     }
+//     Ok(result
+//         .into_iter()
+//         .map(|(buffer, ranges)| {
+//             buffer.read_with(cx, |buffer, _| {
+//                 let path = buffer.file().unwrap().path().to_string_lossy().to_string();
+//                 let ranges = ranges
+//                     .into_iter()
+//                     .map(|range| range.to_offset(buffer))
+//                     .collect::<Vec<_>>();
+//                 (path, ranges)
+//             })
+//         })
+//         .collect())
+// }
+
+// fn init_test(cx: &mut gpui::TestAppContext) {
+//     cx.foreground().forbid_parking();
+
+//     cx.update(|cx| {
+//         cx.set_global(SettingsStore::test(cx));
+//         language2::init(cx);
+//         Project::init_settings(cx);
+//     });
+// }

crates/project2/src/search.rs 🔗

@@ -0,0 +1,458 @@
+use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
+use anyhow::{Context, Result};
+use client2::proto;
+use globset::{Glob, GlobMatcher};
+use itertools::Itertools;
+use language2::{char_kind, BufferSnapshot};
+use regex::{Regex, RegexBuilder};
+use smol::future::yield_now;
+use std::{
+    borrow::Cow,
+    io::{BufRead, BufReader, Read},
+    ops::Range,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+
+#[derive(Clone, Debug)]
+pub struct SearchInputs {
+    query: Arc<str>,
+    files_to_include: Vec<PathMatcher>,
+    files_to_exclude: Vec<PathMatcher>,
+}
+
+impl SearchInputs {
+    pub fn as_str(&self) -> &str {
+        self.query.as_ref()
+    }
+    pub fn files_to_include(&self) -> &[PathMatcher] {
+        &self.files_to_include
+    }
+    pub fn files_to_exclude(&self) -> &[PathMatcher] {
+        &self.files_to_exclude
+    }
+}
+#[derive(Clone, Debug)]
+pub enum SearchQuery {
+    Text {
+        search: Arc<AhoCorasick>,
+        replacement: Option<String>,
+        whole_word: bool,
+        case_sensitive: bool,
+        inner: SearchInputs,
+    },
+
+    Regex {
+        regex: Regex,
+        replacement: Option<String>,
+        multiline: bool,
+        whole_word: bool,
+        case_sensitive: bool,
+        inner: SearchInputs,
+    },
+}
+
+#[derive(Clone, Debug)]
+pub struct PathMatcher {
+    maybe_path: PathBuf,
+    glob: GlobMatcher,
+}
+
+impl std::fmt::Display for PathMatcher {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.maybe_path.to_string_lossy().fmt(f)
+    }
+}
+
+impl PathMatcher {
+    pub fn new(maybe_glob: &str) -> Result<Self, globset::Error> {
+        Ok(PathMatcher {
+            glob: Glob::new(&maybe_glob)?.compile_matcher(),
+            maybe_path: PathBuf::from(maybe_glob),
+        })
+    }
+
+    pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
+        other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other)
+    }
+}
+
+impl SearchQuery {
+    pub fn text(
+        query: impl ToString,
+        whole_word: bool,
+        case_sensitive: bool,
+        files_to_include: Vec<PathMatcher>,
+        files_to_exclude: Vec<PathMatcher>,
+    ) -> Result<Self> {
+        let query = query.to_string();
+        let search = AhoCorasickBuilder::new()
+            .ascii_case_insensitive(!case_sensitive)
+            .build(&[&query])?;
+        let inner = SearchInputs {
+            query: query.into(),
+            files_to_exclude,
+            files_to_include,
+        };
+        Ok(Self::Text {
+            search: Arc::new(search),
+            replacement: None,
+            whole_word,
+            case_sensitive,
+            inner,
+        })
+    }
+
+    pub fn regex(
+        query: impl ToString,
+        whole_word: bool,
+        case_sensitive: bool,
+        files_to_include: Vec<PathMatcher>,
+        files_to_exclude: Vec<PathMatcher>,
+    ) -> Result<Self> {
+        let mut query = query.to_string();
+        let initial_query = Arc::from(query.as_str());
+        if whole_word {
+            let mut word_query = String::new();
+            word_query.push_str("\\b");
+            word_query.push_str(&query);
+            word_query.push_str("\\b");
+            query = word_query
+        }
+
+        let multiline = query.contains('\n') || query.contains("\\n");
+        let regex = RegexBuilder::new(&query)
+            .case_insensitive(!case_sensitive)
+            .multi_line(multiline)
+            .build()?;
+        let inner = SearchInputs {
+            query: initial_query,
+            files_to_exclude,
+            files_to_include,
+        };
+        Ok(Self::Regex {
+            regex,
+            replacement: None,
+            multiline,
+            whole_word,
+            case_sensitive,
+            inner,
+        })
+    }
+
+    pub fn from_proto(message: proto::SearchProject) -> Result<Self> {
+        if message.regex {
+            Self::regex(
+                message.query,
+                message.whole_word,
+                message.case_sensitive,
+                deserialize_path_matches(&message.files_to_include)?,
+                deserialize_path_matches(&message.files_to_exclude)?,
+            )
+        } else {
+            Self::text(
+                message.query,
+                message.whole_word,
+                message.case_sensitive,
+                deserialize_path_matches(&message.files_to_include)?,
+                deserialize_path_matches(&message.files_to_exclude)?,
+            )
+        }
+    }
+    pub fn with_replacement(mut self, new_replacement: String) -> Self {
+        match self {
+            Self::Text {
+                ref mut replacement,
+                ..
+            }
+            | Self::Regex {
+                ref mut replacement,
+                ..
+            } => {
+                *replacement = Some(new_replacement);
+                self
+            }
+        }
+    }
+    pub fn to_proto(&self, project_id: u64) -> proto::SearchProject {
+        proto::SearchProject {
+            project_id,
+            query: self.as_str().to_string(),
+            regex: self.is_regex(),
+            whole_word: self.whole_word(),
+            case_sensitive: self.case_sensitive(),
+            files_to_include: self
+                .files_to_include()
+                .iter()
+                .map(|matcher| matcher.to_string())
+                .join(","),
+            files_to_exclude: self
+                .files_to_exclude()
+                .iter()
+                .map(|matcher| matcher.to_string())
+                .join(","),
+        }
+    }
+
+    pub fn detect<T: Read>(&self, stream: T) -> Result<bool> {
+        if self.as_str().is_empty() {
+            return Ok(false);
+        }
+
+        match self {
+            Self::Text { search, .. } => {
+                let mat = search.stream_find_iter(stream).next();
+                match mat {
+                    Some(Ok(_)) => Ok(true),
+                    Some(Err(err)) => Err(err.into()),
+                    None => Ok(false),
+                }
+            }
+            Self::Regex {
+                regex, multiline, ..
+            } => {
+                let mut reader = BufReader::new(stream);
+                if *multiline {
+                    let mut text = String::new();
+                    if let Err(err) = reader.read_to_string(&mut text) {
+                        Err(err.into())
+                    } else {
+                        Ok(regex.find(&text).is_some())
+                    }
+                } else {
+                    for line in reader.lines() {
+                        let line = line?;
+                        if regex.find(&line).is_some() {
+                            return Ok(true);
+                        }
+                    }
+                    Ok(false)
+                }
+            }
+        }
+    }
+    /// Returns the replacement text for this `SearchQuery`.
+    pub fn replacement(&self) -> Option<&str> {
+        match self {
+            SearchQuery::Text { replacement, .. } | SearchQuery::Regex { replacement, .. } => {
+                replacement.as_deref()
+            }
+        }
+    }
+    /// Replaces search hits if replacement is set. `text` is assumed to be a string that matches this `SearchQuery` exactly, without any leftovers on either side.
+    pub fn replacement_for<'a>(&self, text: &'a str) -> Option<Cow<'a, str>> {
+        match self {
+            SearchQuery::Text { replacement, .. } => replacement.clone().map(Cow::from),
+            SearchQuery::Regex {
+                regex, replacement, ..
+            } => {
+                if let Some(replacement) = replacement {
+                    Some(regex.replace(text, replacement))
+                } else {
+                    None
+                }
+            }
+        }
+    }
+    pub async fn search(
+        &self,
+        buffer: &BufferSnapshot,
+        subrange: Option<Range<usize>>,
+    ) -> Vec<Range<usize>> {
+        const YIELD_INTERVAL: usize = 20000;
+
+        if self.as_str().is_empty() {
+            return Default::default();
+        }
+
+        let range_offset = subrange.as_ref().map(|r| r.start).unwrap_or(0);
+        let rope = if let Some(range) = subrange {
+            buffer.as_rope().slice(range)
+        } else {
+            buffer.as_rope().clone()
+        };
+
+        let mut matches = Vec::new();
+        match self {
+            Self::Text {
+                search, whole_word, ..
+            } => {
+                for (ix, mat) in search
+                    .stream_find_iter(rope.bytes_in_range(0..rope.len()))
+                    .enumerate()
+                {
+                    if (ix + 1) % YIELD_INTERVAL == 0 {
+                        yield_now().await;
+                    }
+
+                    let mat = mat.unwrap();
+                    if *whole_word {
+                        let scope = buffer.language_scope_at(range_offset + mat.start());
+                        let kind = |c| char_kind(&scope, c);
+
+                        let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind);
+                        let start_kind = kind(rope.chars_at(mat.start()).next().unwrap());
+                        let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap());
+                        let next_kind = rope.chars_at(mat.end()).next().map(kind);
+                        if Some(start_kind) == prev_kind || Some(end_kind) == next_kind {
+                            continue;
+                        }
+                    }
+                    matches.push(mat.start()..mat.end())
+                }
+            }
+
+            Self::Regex {
+                regex, multiline, ..
+            } => {
+                if *multiline {
+                    let text = rope.to_string();
+                    for (ix, mat) in regex.find_iter(&text).enumerate() {
+                        if (ix + 1) % YIELD_INTERVAL == 0 {
+                            yield_now().await;
+                        }
+
+                        matches.push(mat.start()..mat.end());
+                    }
+                } else {
+                    let mut line = String::new();
+                    let mut line_offset = 0;
+                    for (chunk_ix, chunk) in rope.chunks().chain(["\n"]).enumerate() {
+                        if (chunk_ix + 1) % YIELD_INTERVAL == 0 {
+                            yield_now().await;
+                        }
+
+                        for (newline_ix, text) in chunk.split('\n').enumerate() {
+                            if newline_ix > 0 {
+                                for mat in regex.find_iter(&line) {
+                                    let start = line_offset + mat.start();
+                                    let end = line_offset + mat.end();
+                                    matches.push(start..end);
+                                }
+
+                                line_offset += line.len() + 1;
+                                line.clear();
+                            }
+                            line.push_str(text);
+                        }
+                    }
+                }
+            }
+        }
+
+        matches
+    }
+
+    pub fn as_str(&self) -> &str {
+        self.as_inner().as_str()
+    }
+
+    pub fn whole_word(&self) -> bool {
+        match self {
+            Self::Text { whole_word, .. } => *whole_word,
+            Self::Regex { whole_word, .. } => *whole_word,
+        }
+    }
+
+    pub fn case_sensitive(&self) -> bool {
+        match self {
+            Self::Text { case_sensitive, .. } => *case_sensitive,
+            Self::Regex { case_sensitive, .. } => *case_sensitive,
+        }
+    }
+
+    pub fn is_regex(&self) -> bool {
+        matches!(self, Self::Regex { .. })
+    }
+
+    pub fn files_to_include(&self) -> &[PathMatcher] {
+        self.as_inner().files_to_include()
+    }
+
+    pub fn files_to_exclude(&self) -> &[PathMatcher] {
+        self.as_inner().files_to_exclude()
+    }
+
+    pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
+        match file_path {
+            Some(file_path) => {
+                !self
+                    .files_to_exclude()
+                    .iter()
+                    .any(|exclude_glob| exclude_glob.is_match(file_path))
+                    && (self.files_to_include().is_empty()
+                        || self
+                            .files_to_include()
+                            .iter()
+                            .any(|include_glob| include_glob.is_match(file_path)))
+            }
+            None => self.files_to_include().is_empty(),
+        }
+    }
+    pub fn as_inner(&self) -> &SearchInputs {
+        match self {
+            Self::Regex { inner, .. } | Self::Text { inner, .. } => inner,
+        }
+    }
+}
+
+fn deserialize_path_matches(glob_set: &str) -> anyhow::Result<Vec<PathMatcher>> {
+    glob_set
+        .split(',')
+        .map(str::trim)
+        .filter(|glob_str| !glob_str.is_empty())
+        .map(|glob_str| {
+            PathMatcher::new(glob_str)
+                .with_context(|| format!("deserializing path match glob {glob_str}"))
+        })
+        .collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn path_matcher_creation_for_valid_paths() {
+        for valid_path in [
+            "file",
+            "Cargo.toml",
+            ".DS_Store",
+            "~/dir/another_dir/",
+            "./dir/file",
+            "dir/[a-z].txt",
+            "../dir/filé",
+        ] {
+            let path_matcher = PathMatcher::new(valid_path).unwrap_or_else(|e| {
+                panic!("Valid path {valid_path} should be accepted, but got: {e}")
+            });
+            assert!(
+                path_matcher.is_match(valid_path),
+                "Path matcher for valid path {valid_path} should match itself"
+            )
+        }
+    }
+
+    #[test]
+    fn path_matcher_creation_for_globs() {
+        for invalid_glob in ["dir/[].txt", "dir/[a-z.txt", "dir/{file"] {
+            match PathMatcher::new(invalid_glob) {
+                Ok(_) => panic!("Invalid glob {invalid_glob} should not be accepted"),
+                Err(_expected) => {}
+            }
+        }
+
+        for valid_glob in [
+            "dir/?ile",
+            "dir/*.txt",
+            "dir/**/file",
+            "dir/[a-z].txt",
+            "{dir,file}",
+        ] {
+            match PathMatcher::new(valid_glob) {
+                Ok(_expected) => {}
+                Err(e) => panic!("Valid glob {valid_glob} should be accepted, but got: {e}"),
+            }
+        }
+    }
+}

crates/project2/src/terminals.rs 🔗

@@ -0,0 +1,129 @@
+use crate::Project;
+use gpui2::{AnyWindowHandle, Context, Entity, Model, ModelContext, WeakModel};
+use settings2::Settings;
+use std::path::{Path, PathBuf};
+use terminal2::{
+    terminal_settings::{self, TerminalSettings, VenvSettingsContent},
+    Terminal, TerminalBuilder,
+};
+
+#[cfg(target_os = "macos")]
+use std::os::unix::ffi::OsStrExt;
+
+pub struct Terminals {
+    pub(crate) local_handles: Vec<WeakModel<terminal2::Terminal>>,
+}
+
+impl Project {
+    pub fn create_terminal(
+        &mut self,
+        working_directory: Option<PathBuf>,
+        window: AnyWindowHandle,
+        cx: &mut ModelContext<Self>,
+    ) -> anyhow::Result<Model<Terminal>> {
+        if self.is_remote() {
+            return Err(anyhow::anyhow!(
+                "creating terminals as a guest is not supported yet"
+            ));
+        } else {
+            let settings = TerminalSettings::get_global(cx);
+            let python_settings = settings.detect_venv.clone();
+            let shell = settings.shell.clone();
+
+            let terminal = TerminalBuilder::new(
+                working_directory.clone(),
+                shell.clone(),
+                settings.env.clone(),
+                Some(settings.blinking.clone()),
+                settings.alternate_scroll,
+                window,
+                |_, _| todo!("color_for_index"),
+            )
+            .map(|builder| {
+                let terminal_handle = cx.build_model(|cx| builder.subscribe(cx));
+
+                self.terminals
+                    .local_handles
+                    .push(terminal_handle.downgrade());
+
+                let id = terminal_handle.entity_id();
+                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
+                    let handles = &mut project.terminals.local_handles;
+
+                    if let Some(index) = handles
+                        .iter()
+                        .position(|terminal| terminal.entity_id() == id)
+                    {
+                        handles.remove(index);
+                        cx.notify();
+                    }
+                })
+                .detach();
+
+                if let Some(python_settings) = &python_settings.as_option() {
+                    let activate_script_path =
+                        self.find_activate_script_path(&python_settings, working_directory);
+                    self.activate_python_virtual_environment(
+                        activate_script_path,
+                        &terminal_handle,
+                        cx,
+                    );
+                }
+                terminal_handle
+            });
+
+            terminal
+        }
+    }
+
+    pub fn find_activate_script_path(
+        &mut self,
+        settings: &VenvSettingsContent,
+        working_directory: Option<PathBuf>,
+    ) -> Option<PathBuf> {
+        // When we are unable to resolve the working directory, the terminal builder
+        // defaults to '/'. We should probably encode this directly somewhere, but for
+        // now, let's just hard code it here.
+        let working_directory = working_directory.unwrap_or_else(|| Path::new("/").to_path_buf());
+        let activate_script_name = match settings.activate_script {
+            terminal_settings::ActivateScript::Default => "activate",
+            terminal_settings::ActivateScript::Csh => "activate.csh",
+            terminal_settings::ActivateScript::Fish => "activate.fish",
+            terminal_settings::ActivateScript::Nushell => "activate.nu",
+        };
+
+        for virtual_environment_name in settings.directories {
+            let mut path = working_directory.join(virtual_environment_name);
+            path.push("bin/");
+            path.push(activate_script_name);
+
+            if path.exists() {
+                return Some(path);
+            }
+        }
+
+        None
+    }
+
+    fn activate_python_virtual_environment(
+        &mut self,
+        activate_script: Option<PathBuf>,
+        terminal_handle: &Model<Terminal>,
+        cx: &mut ModelContext<Project>,
+    ) {
+        if let Some(activate_script) = activate_script {
+            // Paths are not strings so we need to jump through some hoops to format the command without `format!`
+            let mut command = Vec::from("source ".as_bytes());
+            command.extend_from_slice(activate_script.as_os_str().as_bytes());
+            command.push(b'\n');
+
+            terminal_handle.update(cx, |this, _| this.input_bytes(command));
+        }
+    }
+
+    pub fn local_terminal_handles(&self) -> &Vec<WeakModel<terminal2::Terminal>> {
+        &self.terminals.local_handles
+    }
+}
+
+// TODO: Add a few tests for adding and removing terminal tabs

crates/project2/src/worktree.rs 🔗

@@ -0,0 +1,4387 @@
+use crate::{
+    copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
+};
+use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
+use anyhow::{anyhow, Context as _, Result};
+use client2::{proto, Client};
+use clock::ReplicaId;
+use collections::{HashMap, HashSet, VecDeque};
+use fs2::{
+    repository::{GitFileStatus, GitRepository, RepoPath},
+    Fs,
+};
+use futures::{
+    channel::{
+        mpsc::{self, UnboundedSender},
+        oneshot,
+    },
+    select_biased,
+    task::Poll,
+    FutureExt, Stream, StreamExt,
+};
+use fuzzy2::CharBag;
+use git::{DOT_GIT, GITIGNORE};
+use gpui2::{
+    AppContext, AsyncAppContext, Context, EventEmitter, Executor, Model, ModelContext, Task,
+};
+use language2::{
+    proto::{
+        deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
+        serialize_version,
+    },
+    Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped,
+};
+use lsp2::LanguageServerId;
+use parking_lot::Mutex;
+use postage::{
+    barrier,
+    prelude::{Sink as _, Stream as _},
+    watch,
+};
+use smol::channel::{self, Sender};
+use std::{
+    any::Any,
+    cmp::{self, Ordering},
+    convert::TryFrom,
+    ffi::OsStr,
+    fmt,
+    future::Future,
+    mem,
+    ops::{AddAssign, Deref, DerefMut, Sub},
+    path::{Path, PathBuf},
+    pin::Pin,
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+    time::{Duration, SystemTime},
+};
+use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
+use util::{paths::HOME, ResultExt};
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
+pub struct WorktreeId(usize);
+
+pub enum Worktree {
+    Local(LocalWorktree),
+    Remote(RemoteWorktree),
+}
+
+pub struct LocalWorktree {
+    snapshot: LocalSnapshot,
+    scan_requests_tx: channel::Sender<ScanRequest>,
+    path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
+    is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
+    _background_scanner_task: Task<()>,
+    share: Option<ShareState>,
+    diagnostics: HashMap<
+        Arc<Path>,
+        Vec<(
+            LanguageServerId,
+            Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
+        )>,
+    >,
+    diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>,
+    client: Arc<Client>,
+    fs: Arc<dyn Fs>,
+    visible: bool,
+}
+
+struct ScanRequest {
+    relative_paths: Vec<Arc<Path>>,
+    done: barrier::Sender,
+}
+
+pub struct RemoteWorktree {
+    snapshot: Snapshot,
+    background_snapshot: Arc<Mutex<Snapshot>>,
+    project_id: u64,
+    client: Arc<Client>,
+    updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
+    snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
+    replica_id: ReplicaId,
+    diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>,
+    visible: bool,
+    disconnected: bool,
+}
+
+#[derive(Clone)]
+pub struct Snapshot {
+    id: WorktreeId,
+    abs_path: Arc<Path>,
+    root_name: String,
+    root_char_bag: CharBag,
+    entries_by_path: SumTree<Entry>,
+    entries_by_id: SumTree<PathEntry>,
+    repository_entries: TreeMap<RepositoryWorkDirectory, RepositoryEntry>,
+
+    /// A number that increases every time the worktree begins scanning
+    /// a set of paths from the filesystem. This scanning could be caused
+    /// by some operation performed on the worktree, such as reading or
+    /// writing a file, or by an event reported by the filesystem.
+    scan_id: usize,
+
+    /// The latest scan id that has completed, and whose preceding scans
+    /// have all completed. The current `scan_id` could be more than one
+    /// greater than the `completed_scan_id` if operations are performed
+    /// on the worktree while it is processing a file-system event.
+    completed_scan_id: usize,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RepositoryEntry {
+    pub(crate) work_directory: WorkDirectoryEntry,
+    pub(crate) branch: Option<Arc<str>>,
+}
+
+impl RepositoryEntry {
+    pub fn branch(&self) -> Option<Arc<str>> {
+        self.branch.clone()
+    }
+
+    pub fn work_directory_id(&self) -> ProjectEntryId {
+        *self.work_directory
+    }
+
+    pub fn work_directory(&self, snapshot: &Snapshot) -> Option<RepositoryWorkDirectory> {
+        snapshot
+            .entry_for_id(self.work_directory_id())
+            .map(|entry| RepositoryWorkDirectory(entry.path.clone()))
+    }
+
+    pub fn build_update(&self, _: &Self) -> proto::RepositoryEntry {
+        proto::RepositoryEntry {
+            work_directory_id: self.work_directory_id().to_proto(),
+            branch: self.branch.as_ref().map(|str| str.to_string()),
+        }
+    }
+}
+
+impl From<&RepositoryEntry> for proto::RepositoryEntry {
+    fn from(value: &RepositoryEntry) -> Self {
+        proto::RepositoryEntry {
+            work_directory_id: value.work_directory.to_proto(),
+            branch: value.branch.as_ref().map(|str| str.to_string()),
+        }
+    }
+}
+
+/// This path corresponds to the 'content path' (the folder that contains the .git)
+#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
+pub struct RepositoryWorkDirectory(pub(crate) Arc<Path>);
+
+impl Default for RepositoryWorkDirectory {
+    fn default() -> Self {
+        RepositoryWorkDirectory(Arc::from(Path::new("")))
+    }
+}
+
+impl AsRef<Path> for RepositoryWorkDirectory {
+    fn as_ref(&self) -> &Path {
+        self.0.as_ref()
+    }
+}
+
+#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
+pub struct WorkDirectoryEntry(ProjectEntryId);
+
+impl WorkDirectoryEntry {
+    pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option<RepoPath> {
+        worktree.entry_for_id(self.0).and_then(|entry| {
+            path.strip_prefix(&entry.path)
+                .ok()
+                .map(move |path| path.into())
+        })
+    }
+}
+
+impl Deref for WorkDirectoryEntry {
+    type Target = ProjectEntryId;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl<'a> From<ProjectEntryId> for WorkDirectoryEntry {
+    fn from(value: ProjectEntryId) -> Self {
+        WorkDirectoryEntry(value)
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct LocalSnapshot {
+    snapshot: Snapshot,
+    /// All of the gitignore files in the worktree, indexed by their relative path.
+    /// The boolean indicates whether the gitignore needs to be updated.
+    ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
+    /// All of the git repositories in the worktree, indexed by the project entry
+    /// id of their parent directory.
+    git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
+}
+
+struct BackgroundScannerState {
+    snapshot: LocalSnapshot,
+    scanned_dirs: HashSet<ProjectEntryId>,
+    path_prefixes_to_scan: HashSet<Arc<Path>>,
+    paths_to_scan: HashSet<Arc<Path>>,
+    /// The ids of all of the entries that were removed from the snapshot
+    /// as part of the current update. These entry ids may be re-used
+    /// if the same inode is discovered at a new path, or if the given
+    /// path is re-created after being deleted.
+    removed_entry_ids: HashMap<u64, ProjectEntryId>,
+    changed_paths: Vec<Arc<Path>>,
+    prev_snapshot: Snapshot,
+}
+
+#[derive(Debug, Clone)]
+pub struct LocalRepositoryEntry {
+    pub(crate) git_dir_scan_id: usize,
+    pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>,
+    /// Path to the actual .git folder.
+    /// Note: if .git is a file, this points to the folder indicated by the .git file
+    pub(crate) git_dir_path: Arc<Path>,
+}
+
+impl Deref for LocalSnapshot {
+    type Target = Snapshot;
+
+    fn deref(&self) -> &Self::Target {
+        &self.snapshot
+    }
+}
+
+impl DerefMut for LocalSnapshot {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.snapshot
+    }
+}
+
+enum ScanState {
+    Started,
+    Updated {
+        snapshot: LocalSnapshot,
+        changes: UpdatedEntriesSet,
+        barrier: Option<barrier::Sender>,
+        scanning: bool,
+    },
+}
+
+struct ShareState {
+    project_id: u64,
+    snapshots_tx:
+        mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>,
+    resume_updates: watch::Sender<()>,
+    _maintain_remote_snapshot: Task<Option<()>>,
+}
+
+pub enum Event {
+    UpdatedEntries(UpdatedEntriesSet),
+    UpdatedGitRepositories(UpdatedGitRepositoriesSet),
+}
+
+impl EventEmitter for Worktree {
+    type Event = Event;
+}
+
+impl Worktree {
+    pub async fn local(
+        client: Arc<Client>,
+        path: impl Into<Arc<Path>>,
+        visible: bool,
+        fs: Arc<dyn Fs>,
+        next_entry_id: Arc<AtomicUsize>,
+        cx: &mut AsyncAppContext,
+    ) -> 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();
+        let metadata = fs
+            .metadata(&abs_path)
+            .await
+            .context("failed to stat worktree path")?;
+
+        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());
+
+            let mut snapshot = LocalSnapshot {
+                ignores_by_parent_abs_path: Default::default(),
+                git_repositories: Default::default(),
+                snapshot: Snapshot {
+                    id: WorktreeId::from_usize(cx.entity_id().as_u64() as usize),
+                    abs_path: abs_path.clone(),
+                    root_name: root_name.clone(),
+                    root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
+                    entries_by_path: Default::default(),
+                    entries_by_id: Default::default(),
+                    repository_entries: Default::default(),
+                    scan_id: 1,
+                    completed_scan_id: 0,
+                },
+            };
+
+            if let Some(metadata) = metadata {
+                snapshot.insert_entry(
+                    Entry::new(
+                        Arc::from(Path::new("")),
+                        &metadata,
+                        &next_entry_id,
+                        snapshot.root_char_bag,
+                    ),
+                    fs.as_ref(),
+                );
+            }
+
+            let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
+            let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
+            let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
+
+            cx.spawn(|this, mut cx| async move {
+                while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
+                    this.update(&mut cx, |this, cx| {
+                        let this = this.as_local_mut().unwrap();
+                        match state {
+                            ScanState::Started => {
+                                *this.is_scanning.0.borrow_mut() = true;
+                            }
+                            ScanState::Updated {
+                                snapshot,
+                                changes,
+                                barrier,
+                                scanning,
+                            } => {
+                                *this.is_scanning.0.borrow_mut() = scanning;
+                                this.set_snapshot(snapshot, changes, cx);
+                                drop(barrier);
+                            }
+                        }
+                        cx.notify();
+                    })
+                    .ok();
+                }
+            })
+            .detach();
+
+            let background_scanner_task = cx.executor().spawn({
+                let fs = fs.clone();
+                let snapshot = snapshot.clone();
+                let background = cx.executor().clone();
+                async move {
+                    let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
+                    BackgroundScanner::new(
+                        snapshot,
+                        next_entry_id,
+                        fs,
+                        scan_states_tx,
+                        background,
+                        scan_requests_rx,
+                        path_prefixes_to_scan_rx,
+                    )
+                    .run(events)
+                    .await;
+                }
+            });
+
+            Worktree::Local(LocalWorktree {
+                snapshot,
+                is_scanning: watch::channel_with(true),
+                share: None,
+                scan_requests_tx,
+                path_prefixes_to_scan_tx,
+                _background_scanner_task: background_scanner_task,
+                diagnostics: Default::default(),
+                diagnostic_summaries: Default::default(),
+                client,
+                fs,
+                visible,
+            })
+        })
+    }
+
+    pub fn remote(
+        project_remote_id: u64,
+        replica_id: ReplicaId,
+        worktree: proto::WorktreeMetadata,
+        client: Arc<Client>,
+        cx: &mut AppContext,
+    ) -> 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)),
+                root_name: worktree.root_name.clone(),
+                root_char_bag: worktree
+                    .root_name
+                    .chars()
+                    .map(|c| c.to_ascii_lowercase())
+                    .collect(),
+                entries_by_path: Default::default(),
+                entries_by_id: Default::default(),
+                repository_entries: Default::default(),
+                scan_id: 1,
+                completed_scan_id: 0,
+            };
+
+            let (updates_tx, mut updates_rx) = mpsc::unbounded();
+            let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
+            let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
+
+            cx.executor()
+                .spawn({
+                    let background_snapshot = background_snapshot.clone();
+                    async move {
+                        while let Some(update) = updates_rx.next().await {
+                            if let Err(error) =
+                                background_snapshot.lock().apply_remote_update(update)
+                            {
+                                log::error!("error applying worktree update: {}", error);
+                            }
+                            snapshot_updated_tx.send(()).await.ok();
+                        }
+                    }
+                })
+                .detach();
+
+            cx.spawn(|this, mut cx| async move {
+                while (snapshot_updated_rx.recv().await).is_some() {
+                    this.update(&mut cx, |this, cx| {
+                        let this = this.as_remote_mut().unwrap();
+                        this.snapshot = this.background_snapshot.lock().clone();
+                        cx.emit(Event::UpdatedEntries(Arc::from([])));
+                        cx.notify();
+                        while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
+                            if this.observed_snapshot(*scan_id) {
+                                let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap();
+                                let _ = tx.send(());
+                            } else {
+                                break;
+                            }
+                        }
+                    })?;
+                }
+                anyhow::Ok(())
+            })
+            .detach();
+
+            Worktree::Remote(RemoteWorktree {
+                project_id: project_remote_id,
+                replica_id,
+                snapshot: snapshot.clone(),
+                background_snapshot,
+                updates_tx: Some(updates_tx),
+                snapshot_subscriptions: Default::default(),
+                client: client.clone(),
+                diagnostic_summaries: Default::default(),
+                visible: worktree.visible,
+                disconnected: false,
+            })
+        })
+    }
+
+    pub fn as_local(&self) -> Option<&LocalWorktree> {
+        if let Worktree::Local(worktree) = self {
+            Some(worktree)
+        } else {
+            None
+        }
+    }
+
+    pub fn as_remote(&self) -> Option<&RemoteWorktree> {
+        if let Worktree::Remote(worktree) = self {
+            Some(worktree)
+        } else {
+            None
+        }
+    }
+
+    pub fn as_local_mut(&mut self) -> Option<&mut LocalWorktree> {
+        if let Worktree::Local(worktree) = self {
+            Some(worktree)
+        } else {
+            None
+        }
+    }
+
+    pub fn as_remote_mut(&mut self) -> Option<&mut RemoteWorktree> {
+        if let Worktree::Remote(worktree) = self {
+            Some(worktree)
+        } else {
+            None
+        }
+    }
+
+    pub fn is_local(&self) -> bool {
+        matches!(self, Worktree::Local(_))
+    }
+
+    pub fn is_remote(&self) -> bool {
+        !self.is_local()
+    }
+
+    pub fn snapshot(&self) -> Snapshot {
+        match self {
+            Worktree::Local(worktree) => worktree.snapshot().snapshot,
+            Worktree::Remote(worktree) => worktree.snapshot(),
+        }
+    }
+
+    pub fn scan_id(&self) -> usize {
+        match self {
+            Worktree::Local(worktree) => worktree.snapshot.scan_id,
+            Worktree::Remote(worktree) => worktree.snapshot.scan_id,
+        }
+    }
+
+    pub fn completed_scan_id(&self) -> usize {
+        match self {
+            Worktree::Local(worktree) => worktree.snapshot.completed_scan_id,
+            Worktree::Remote(worktree) => worktree.snapshot.completed_scan_id,
+        }
+    }
+
+    pub fn is_visible(&self) -> bool {
+        match self {
+            Worktree::Local(worktree) => worktree.visible,
+            Worktree::Remote(worktree) => worktree.visible,
+        }
+    }
+
+    pub fn replica_id(&self) -> ReplicaId {
+        match self {
+            Worktree::Local(_) => 0,
+            Worktree::Remote(worktree) => worktree.replica_id,
+        }
+    }
+
+    pub fn diagnostic_summaries(
+        &self,
+    ) -> impl Iterator<Item = (Arc<Path>, LanguageServerId, DiagnosticSummary)> + '_ {
+        match self {
+            Worktree::Local(worktree) => &worktree.diagnostic_summaries,
+            Worktree::Remote(worktree) => &worktree.diagnostic_summaries,
+        }
+        .iter()
+        .flat_map(|(path, summaries)| {
+            summaries
+                .iter()
+                .map(move |(&server_id, &summary)| (path.clone(), server_id, summary))
+        })
+    }
+
+    pub fn abs_path(&self) -> Arc<Path> {
+        match self {
+            Worktree::Local(worktree) => worktree.abs_path.clone(),
+            Worktree::Remote(worktree) => worktree.abs_path.clone(),
+        }
+    }
+
+    pub fn root_file(&self, cx: &mut ModelContext<Self>) -> Option<Arc<File>> {
+        let entry = self.root_entry()?;
+        Some(File::for_entry(entry.clone(), cx.handle()))
+    }
+}
+
+impl LocalWorktree {
+    pub fn contains_abs_path(&self, path: &Path) -> bool {
+        path.starts_with(&self.abs_path)
+    }
+
+    pub(crate) fn load_buffer(
+        &mut self,
+        id: u64,
+        path: &Path,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<Model<Buffer>>> {
+        let path = Arc::from(path);
+        cx.spawn(move |this, mut cx| async move {
+            let (file, contents, diff_base) = this
+                .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))?
+                .await?;
+            let text_buffer = cx
+                .executor()
+                .spawn(async move { text::Buffer::new(0, id, contents) })
+                .await;
+            cx.build_model(|_| Buffer::build(text_buffer, diff_base, Some(Arc::new(file))))
+        })
+    }
+
+    pub fn diagnostics_for_path(
+        &self,
+        path: &Path,
+    ) -> Vec<(
+        LanguageServerId,
+        Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
+    )> {
+        self.diagnostics.get(path).cloned().unwrap_or_default()
+    }
+
+    pub fn clear_diagnostics_for_language_server(
+        &mut self,
+        server_id: LanguageServerId,
+        _: &mut ModelContext<Worktree>,
+    ) {
+        let worktree_id = self.id().to_proto();
+        self.diagnostic_summaries
+            .retain(|path, summaries_by_server_id| {
+                if summaries_by_server_id.remove(&server_id).is_some() {
+                    if let Some(share) = self.share.as_ref() {
+                        self.client
+                            .send(proto::UpdateDiagnosticSummary {
+                                project_id: share.project_id,
+                                worktree_id,
+                                summary: Some(proto::DiagnosticSummary {
+                                    path: path.to_string_lossy().to_string(),
+                                    language_server_id: server_id.0 as u64,
+                                    error_count: 0,
+                                    warning_count: 0,
+                                }),
+                            })
+                            .log_err();
+                    }
+                    !summaries_by_server_id.is_empty()
+                } else {
+                    true
+                }
+            });
+
+        self.diagnostics.retain(|_, diagnostics_by_server_id| {
+            if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
+                diagnostics_by_server_id.remove(ix);
+                !diagnostics_by_server_id.is_empty()
+            } else {
+                true
+            }
+        });
+    }
+
+    pub fn update_diagnostics(
+        &mut self,
+        server_id: LanguageServerId,
+        worktree_path: Arc<Path>,
+        diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
+        _: &mut ModelContext<Worktree>,
+    ) -> Result<bool> {
+        let summaries_by_server_id = self
+            .diagnostic_summaries
+            .entry(worktree_path.clone())
+            .or_default();
+
+        let old_summary = summaries_by_server_id
+            .remove(&server_id)
+            .unwrap_or_default();
+
+        let new_summary = DiagnosticSummary::new(&diagnostics);
+        if new_summary.is_empty() {
+            if let Some(diagnostics_by_server_id) = self.diagnostics.get_mut(&worktree_path) {
+                if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
+                    diagnostics_by_server_id.remove(ix);
+                }
+                if diagnostics_by_server_id.is_empty() {
+                    self.diagnostics.remove(&worktree_path);
+                }
+            }
+        } else {
+            summaries_by_server_id.insert(server_id, new_summary);
+            let diagnostics_by_server_id =
+                self.diagnostics.entry(worktree_path.clone()).or_default();
+            match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
+                Ok(ix) => {
+                    diagnostics_by_server_id[ix] = (server_id, diagnostics);
+                }
+                Err(ix) => {
+                    diagnostics_by_server_id.insert(ix, (server_id, diagnostics));
+                }
+            }
+        }
+
+        if !old_summary.is_empty() || !new_summary.is_empty() {
+            if let Some(share) = self.share.as_ref() {
+                self.client
+                    .send(proto::UpdateDiagnosticSummary {
+                        project_id: share.project_id,
+                        worktree_id: self.id().to_proto(),
+                        summary: Some(proto::DiagnosticSummary {
+                            path: worktree_path.to_string_lossy().to_string(),
+                            language_server_id: server_id.0 as u64,
+                            error_count: new_summary.error_count as u32,
+                            warning_count: new_summary.warning_count as u32,
+                        }),
+                    })
+                    .log_err();
+            }
+        }
+
+        Ok(!old_summary.is_empty() || !new_summary.is_empty())
+    }
+
+    fn set_snapshot(
+        &mut self,
+        new_snapshot: LocalSnapshot,
+        entry_changes: UpdatedEntriesSet,
+        cx: &mut ModelContext<Worktree>,
+    ) {
+        let repo_changes = self.changed_repos(&self.snapshot, &new_snapshot);
+
+        self.snapshot = new_snapshot;
+
+        if let Some(share) = self.share.as_mut() {
+            share
+                .snapshots_tx
+                .unbounded_send((
+                    self.snapshot.clone(),
+                    entry_changes.clone(),
+                    repo_changes.clone(),
+                ))
+                .ok();
+        }
+
+        if !entry_changes.is_empty() {
+            cx.emit(Event::UpdatedEntries(entry_changes));
+        }
+        if !repo_changes.is_empty() {
+            cx.emit(Event::UpdatedGitRepositories(repo_changes));
+        }
+    }
+
+    fn changed_repos(
+        &self,
+        old_snapshot: &LocalSnapshot,
+        new_snapshot: &LocalSnapshot,
+    ) -> UpdatedGitRepositoriesSet {
+        let mut changes = Vec::new();
+        let mut old_repos = old_snapshot.git_repositories.iter().peekable();
+        let mut new_repos = new_snapshot.git_repositories.iter().peekable();
+        loop {
+            match (new_repos.peek().map(clone), old_repos.peek().map(clone)) {
+                (Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => {
+                    match Ord::cmp(&new_entry_id, &old_entry_id) {
+                        Ordering::Less => {
+                            if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) {
+                                changes.push((
+                                    entry.path.clone(),
+                                    GitRepositoryChange {
+                                        old_repository: None,
+                                    },
+                                ));
+                            }
+                            new_repos.next();
+                        }
+                        Ordering::Equal => {
+                            if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id {
+                                if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) {
+                                    let old_repo = old_snapshot
+                                        .repository_entries
+                                        .get(&RepositoryWorkDirectory(entry.path.clone()))
+                                        .cloned();
+                                    changes.push((
+                                        entry.path.clone(),
+                                        GitRepositoryChange {
+                                            old_repository: old_repo,
+                                        },
+                                    ));
+                                }
+                            }
+                            new_repos.next();
+                            old_repos.next();
+                        }
+                        Ordering::Greater => {
+                            if let Some(entry) = old_snapshot.entry_for_id(old_entry_id) {
+                                let old_repo = old_snapshot
+                                    .repository_entries
+                                    .get(&RepositoryWorkDirectory(entry.path.clone()))
+                                    .cloned();
+                                changes.push((
+                                    entry.path.clone(),
+                                    GitRepositoryChange {
+                                        old_repository: old_repo,
+                                    },
+                                ));
+                            }
+                            old_repos.next();
+                        }
+                    }
+                }
+                (Some((entry_id, _)), None) => {
+                    if let Some(entry) = new_snapshot.entry_for_id(entry_id) {
+                        changes.push((
+                            entry.path.clone(),
+                            GitRepositoryChange {
+                                old_repository: None,
+                            },
+                        ));
+                    }
+                    new_repos.next();
+                }
+                (None, Some((entry_id, _))) => {
+                    if let Some(entry) = old_snapshot.entry_for_id(entry_id) {
+                        let old_repo = old_snapshot
+                            .repository_entries
+                            .get(&RepositoryWorkDirectory(entry.path.clone()))
+                            .cloned();
+                        changes.push((
+                            entry.path.clone(),
+                            GitRepositoryChange {
+                                old_repository: old_repo,
+                            },
+                        ));
+                    }
+                    old_repos.next();
+                }
+                (None, None) => break,
+            }
+        }
+
+        fn clone<T: Clone, U: Clone>(value: &(&T, &U)) -> (T, U) {
+            (value.0.clone(), value.1.clone())
+        }
+
+        changes.into()
+    }
+
+    pub fn scan_complete(&self) -> impl Future<Output = ()> {
+        let mut is_scanning_rx = self.is_scanning.1.clone();
+        async move {
+            let mut is_scanning = is_scanning_rx.borrow().clone();
+            while is_scanning {
+                if let Some(value) = is_scanning_rx.recv().await {
+                    is_scanning = value;
+                } else {
+                    break;
+                }
+            }
+        }
+    }
+
+    pub fn snapshot(&self) -> LocalSnapshot {
+        self.snapshot.clone()
+    }
+
+    pub fn metadata_proto(&self) -> proto::WorktreeMetadata {
+        proto::WorktreeMetadata {
+            id: self.id().to_proto(),
+            root_name: self.root_name().to_string(),
+            visible: self.visible,
+            abs_path: self.abs_path().as_os_str().to_string_lossy().into(),
+        }
+    }
+
+    fn load(
+        &self,
+        path: &Path,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<(File, String, Option<String>)>> {
+        let path = Arc::from(path);
+        let abs_path = self.absolutize(&path);
+        let fs = self.fs.clone();
+        let entry = self.refresh_entry(path.clone(), None, cx);
+
+        cx.spawn(|this, mut cx| async move {
+            let text = fs.load(&abs_path).await?;
+            let entry = entry.await?;
+
+            let mut index_task = None;
+            let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?;
+            if let Some(repo) = snapshot.repository_for_path(&path) {
+                let repo_path = repo.work_directory.relativize(&snapshot, &path).unwrap();
+                if let Some(repo) = snapshot.git_repositories.get(&*repo.work_directory) {
+                    let repo = repo.repo_ptr.clone();
+                    index_task = Some(
+                        cx.executor()
+                            .spawn(async move { repo.lock().load_index_text(&repo_path) }),
+                    );
+                }
+            }
+
+            let diff_base = if let Some(index_task) = index_task {
+                index_task.await
+            } else {
+                None
+            };
+
+            let worktree = this
+                .upgrade()
+                .ok_or_else(|| anyhow!("worktree was dropped"))?;
+            Ok((
+                File {
+                    entry_id: entry.id,
+                    worktree,
+                    path: entry.path,
+                    mtime: entry.mtime,
+                    is_local: true,
+                    is_deleted: false,
+                },
+                text,
+                diff_base,
+            ))
+        })
+    }
+
+    pub fn save_buffer(
+        &self,
+        buffer_handle: Model<Buffer>,
+        path: Arc<Path>,
+        has_changed_file: bool,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<()>> {
+        let buffer = buffer_handle.read(cx);
+
+        let rpc = self.client.clone();
+        let buffer_id = buffer.remote_id();
+        let project_id = self.share.as_ref().map(|share| share.project_id);
+
+        let text = buffer.as_rope().clone();
+        let fingerprint = text.fingerprint();
+        let version = buffer.version();
+        let save = self.write_file(path, text, buffer.line_ending(), cx);
+
+        cx.spawn(move |this, mut cx| async move {
+            let entry = save.await?;
+            let this = this.upgrade().context("worktree dropped")?;
+
+            if has_changed_file {
+                let new_file = Arc::new(File {
+                    entry_id: entry.id,
+                    worktree: this,
+                    path: entry.path,
+                    mtime: entry.mtime,
+                    is_local: true,
+                    is_deleted: false,
+                });
+
+                if let Some(project_id) = project_id {
+                    rpc.send(proto::UpdateBufferFile {
+                        project_id,
+                        buffer_id,
+                        file: Some(new_file.to_proto()),
+                    })
+                    .log_err();
+                }
+
+                buffer_handle.update(&mut cx, |buffer, cx| {
+                    if has_changed_file {
+                        buffer.file_updated(new_file, cx).detach();
+                    }
+                })?;
+            }
+
+            if let Some(project_id) = project_id {
+                rpc.send(proto::BufferSaved {
+                    project_id,
+                    buffer_id,
+                    version: serialize_version(&version),
+                    mtime: Some(entry.mtime.into()),
+                    fingerprint: serialize_fingerprint(fingerprint),
+                })?;
+            }
+
+            buffer_handle.update(&mut cx, |buffer, cx| {
+                buffer.did_save(version.clone(), fingerprint, entry.mtime, cx);
+            })?;
+
+            Ok(())
+        })
+    }
+
+    /// Find the lowest path in the worktree's datastructures that is an ancestor
+    fn lowest_ancestor(&self, path: &Path) -> PathBuf {
+        let mut lowest_ancestor = None;
+        for path in path.ancestors() {
+            if self.entry_for_path(path).is_some() {
+                lowest_ancestor = Some(path.to_path_buf());
+                break;
+            }
+        }
+
+        lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
+    }
+
+    pub fn create_entry(
+        &self,
+        path: impl Into<Arc<Path>>,
+        is_dir: bool,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<Entry>> {
+        let path = path.into();
+        let lowest_ancestor = self.lowest_ancestor(&path);
+        let abs_path = self.absolutize(&path);
+        let fs = self.fs.clone();
+        let write = cx.executor().spawn(async move {
+            if is_dir {
+                fs.create_dir(&abs_path).await
+            } else {
+                fs.save(&abs_path, &Default::default(), Default::default())
+                    .await
+            }
+        });
+
+        cx.spawn(|this, mut cx| async move {
+            write.await?;
+            let (result, refreshes) = this.update(&mut cx, |this, cx| {
+                let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+                let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
+                for refresh_path in refresh_paths.ancestors() {
+                    if refresh_path == Path::new("") {
+                        continue;
+                    }
+                    let refresh_full_path = lowest_ancestor.join(refresh_path);
+
+                    refreshes.push(this.as_local_mut().unwrap().refresh_entry(
+                        refresh_full_path.into(),
+                        None,
+                        cx,
+                    ));
+                }
+                (
+                    this.as_local_mut().unwrap().refresh_entry(path, None, cx),
+                    refreshes,
+                )
+            })?;
+            for refresh in refreshes {
+                refresh.await.log_err();
+            }
+
+            result.await
+        })
+    }
+
+    pub fn write_file(
+        &self,
+        path: impl Into<Arc<Path>>,
+        text: Rope,
+        line_ending: LineEnding,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<Entry>> {
+        let path = path.into();
+        let abs_path = self.absolutize(&path);
+        let fs = self.fs.clone();
+        let write = cx
+            .executor()
+            .spawn(async move { fs.save(&abs_path, &text, line_ending).await });
+
+        cx.spawn(|this, mut cx| async move {
+            write.await?;
+            this.update(&mut cx, |this, cx| {
+                this.as_local_mut().unwrap().refresh_entry(path, None, cx)
+            })?
+            .await
+        })
+    }
+
+    pub fn delete_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Option<Task<Result<()>>> {
+        let entry = self.entry_for_id(entry_id)?.clone();
+        let abs_path = self.absolutize(&entry.path);
+        let fs = self.fs.clone();
+
+        let delete = cx.executor().spawn(async move {
+            if entry.is_file() {
+                fs.remove_file(&abs_path, Default::default()).await?;
+            } else {
+                fs.remove_dir(
+                    &abs_path,
+                    RemoveOptions {
+                        recursive: true,
+                        ignore_if_not_exists: false,
+                    },
+                )
+                .await?;
+            }
+            anyhow::Ok(entry.path)
+        });
+
+        Some(cx.spawn(|this, mut cx| async move {
+            let path = delete.await?;
+            this.update(&mut cx, |this, _| {
+                this.as_local_mut()
+                    .unwrap()
+                    .refresh_entries_for_paths(vec![path])
+            })?
+            .recv()
+            .await;
+            Ok(())
+        }))
+    }
+
+    pub fn rename_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        new_path: impl Into<Arc<Path>>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Option<Task<Result<Entry>>> {
+        let old_path = self.entry_for_id(entry_id)?.path.clone();
+        let new_path = new_path.into();
+        let abs_old_path = self.absolutize(&old_path);
+        let abs_new_path = self.absolutize(&new_path);
+        let fs = self.fs.clone();
+        let rename = cx.executor().spawn(async move {
+            fs.rename(&abs_old_path, &abs_new_path, Default::default())
+                .await
+        });
+
+        Some(cx.spawn(|this, mut cx| async move {
+            rename.await?;
+            this.update(&mut cx, |this, cx| {
+                this.as_local_mut()
+                    .unwrap()
+                    .refresh_entry(new_path.clone(), Some(old_path), cx)
+            })?
+            .await
+        }))
+    }
+
+    pub fn copy_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        new_path: impl Into<Arc<Path>>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Option<Task<Result<Entry>>> {
+        let old_path = self.entry_for_id(entry_id)?.path.clone();
+        let new_path = new_path.into();
+        let abs_old_path = self.absolutize(&old_path);
+        let abs_new_path = self.absolutize(&new_path);
+        let fs = self.fs.clone();
+        let copy = cx.executor().spawn(async move {
+            copy_recursive(
+                fs.as_ref(),
+                &abs_old_path,
+                &abs_new_path,
+                Default::default(),
+            )
+            .await
+        });
+
+        Some(cx.spawn(|this, mut cx| async move {
+            copy.await?;
+            this.update(&mut cx, |this, cx| {
+                this.as_local_mut()
+                    .unwrap()
+                    .refresh_entry(new_path.clone(), None, cx)
+            })?
+            .await
+        }))
+    }
+
+    pub fn expand_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Option<Task<Result<()>>> {
+        let path = self.entry_for_id(entry_id)?.path.clone();
+        let mut refresh = self.refresh_entries_for_paths(vec![path]);
+        Some(cx.executor().spawn(async move {
+            refresh.next().await;
+            Ok(())
+        }))
+    }
+
+    pub fn refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+        let (tx, rx) = barrier::channel();
+        self.scan_requests_tx
+            .try_send(ScanRequest {
+                relative_paths: paths,
+                done: tx,
+            })
+            .ok();
+        rx
+    }
+
+    pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<Path>) {
+        self.path_prefixes_to_scan_tx.try_send(path_prefix).ok();
+    }
+
+    fn refresh_entry(
+        &self,
+        path: Arc<Path>,
+        old_path: Option<Arc<Path>>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<Entry>> {
+        let paths = if let Some(old_path) = old_path.as_ref() {
+            vec![old_path.clone(), path.clone()]
+        } else {
+            vec![path.clone()]
+        };
+        let mut refresh = self.refresh_entries_for_paths(paths);
+        cx.spawn(move |this, mut cx| async move {
+            refresh.recv().await;
+            this.update(&mut cx, |this, _| {
+                this.entry_for_path(path)
+                    .cloned()
+                    .ok_or_else(|| anyhow!("failed to read path after update"))
+            })?
+        })
+    }
+
+    pub fn observe_updates<F, Fut>(
+        &mut self,
+        project_id: u64,
+        cx: &mut ModelContext<Worktree>,
+        callback: F,
+    ) -> oneshot::Receiver<()>
+    where
+        F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
+        Fut: Send + Future<Output = bool>,
+    {
+        #[cfg(any(test, feature = "test-support"))]
+        const MAX_CHUNK_SIZE: usize = 2;
+        #[cfg(not(any(test, feature = "test-support")))]
+        const MAX_CHUNK_SIZE: usize = 256;
+
+        let (share_tx, share_rx) = oneshot::channel();
+
+        if let Some(share) = self.share.as_mut() {
+            share_tx.send(()).ok();
+            *share.resume_updates.borrow_mut() = ();
+            return share_rx;
+        }
+
+        let (resume_updates_tx, mut resume_updates_rx) = watch::channel::<()>();
+        let (snapshots_tx, mut snapshots_rx) =
+            mpsc::unbounded::<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>();
+        snapshots_tx
+            .unbounded_send((self.snapshot(), Arc::from([]), Arc::from([])))
+            .ok();
+
+        let worktree_id = cx.entity_id().as_u64();
+        let _maintain_remote_snapshot = cx.executor().spawn(async move {
+            let mut is_first = true;
+            while let Some((snapshot, entry_changes, repo_changes)) = snapshots_rx.next().await {
+                let update;
+                if is_first {
+                    update = snapshot.build_initial_update(project_id, worktree_id);
+                    is_first = false;
+                } else {
+                    update =
+                        snapshot.build_update(project_id, worktree_id, entry_changes, repo_changes);
+                }
+
+                for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) {
+                    let _ = resume_updates_rx.try_recv();
+                    loop {
+                        let result = callback(update.clone());
+                        if result.await {
+                            break;
+                        } else {
+                            log::info!("waiting to resume updates");
+                            if resume_updates_rx.next().await.is_none() {
+                                return Some(());
+                            }
+                        }
+                    }
+                }
+            }
+            share_tx.send(()).ok();
+            Some(())
+        });
+
+        self.share = Some(ShareState {
+            project_id,
+            snapshots_tx,
+            resume_updates: resume_updates_tx,
+            _maintain_remote_snapshot,
+        });
+        share_rx
+    }
+
+    pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
+        let client = self.client.clone();
+
+        for (path, summaries) in &self.diagnostic_summaries {
+            for (&server_id, summary) in summaries {
+                if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary {
+                    project_id,
+                    worktree_id: cx.entity_id().as_u64(),
+                    summary: Some(summary.to_proto(server_id, &path)),
+                }) {
+                    return Task::ready(Err(e));
+                }
+            }
+        }
+
+        let rx = self.observe_updates(project_id, cx, move |update| {
+            client.request(update).map(|result| result.is_ok())
+        });
+        cx.executor()
+            .spawn(async move { rx.await.map_err(|_| anyhow!("share ended")) })
+    }
+
+    pub fn unshare(&mut self) {
+        self.share.take();
+    }
+
+    pub fn is_shared(&self) -> bool {
+        self.share.is_some()
+    }
+}
+
+impl RemoteWorktree {
+    fn snapshot(&self) -> Snapshot {
+        self.snapshot.clone()
+    }
+
+    pub fn disconnected_from_host(&mut self) {
+        self.updates_tx.take();
+        self.snapshot_subscriptions.clear();
+        self.disconnected = true;
+    }
+
+    pub fn save_buffer(
+        &self,
+        buffer_handle: Model<Buffer>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<()>> {
+        let buffer = buffer_handle.read(cx);
+        let buffer_id = buffer.remote_id();
+        let version = buffer.version();
+        let rpc = self.client.clone();
+        let project_id = self.project_id;
+        cx.spawn(move |_, mut cx| async move {
+            let response = rpc
+                .request(proto::SaveBuffer {
+                    project_id,
+                    buffer_id,
+                    version: serialize_version(&version),
+                })
+                .await?;
+            let version = deserialize_version(&response.version);
+            let fingerprint = deserialize_fingerprint(&response.fingerprint)?;
+            let mtime = response
+                .mtime
+                .ok_or_else(|| anyhow!("missing mtime"))?
+                .into();
+
+            buffer_handle.update(&mut cx, |buffer, cx| {
+                buffer.did_save(version.clone(), fingerprint, mtime, cx);
+            })?;
+
+            Ok(())
+        })
+    }
+
+    pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) {
+        if let Some(updates_tx) = &self.updates_tx {
+            updates_tx
+                .unbounded_send(update)
+                .expect("consumer runs to completion");
+        }
+    }
+
+    fn observed_snapshot(&self, scan_id: usize) -> bool {
+        self.completed_scan_id >= scan_id
+    }
+
+    pub(crate) fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future<Output = Result<()>> {
+        let (tx, rx) = oneshot::channel();
+        if self.observed_snapshot(scan_id) {
+            let _ = tx.send(());
+        } else if self.disconnected {
+            drop(tx);
+        } else {
+            match self
+                .snapshot_subscriptions
+                .binary_search_by_key(&scan_id, |probe| probe.0)
+            {
+                Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)),
+            }
+        }
+
+        async move {
+            rx.await?;
+            Ok(())
+        }
+    }
+
+    pub fn update_diagnostic_summary(
+        &mut self,
+        path: Arc<Path>,
+        summary: &proto::DiagnosticSummary,
+    ) {
+        let server_id = LanguageServerId(summary.language_server_id as usize);
+        let summary = DiagnosticSummary {
+            error_count: summary.error_count as usize,
+            warning_count: summary.warning_count as usize,
+        };
+
+        if summary.is_empty() {
+            if let Some(summaries) = self.diagnostic_summaries.get_mut(&path) {
+                summaries.remove(&server_id);
+                if summaries.is_empty() {
+                    self.diagnostic_summaries.remove(&path);
+                }
+            }
+        } else {
+            self.diagnostic_summaries
+                .entry(path)
+                .or_default()
+                .insert(server_id, summary);
+        }
+    }
+
+    pub fn insert_entry(
+        &mut self,
+        entry: proto::Entry,
+        scan_id: usize,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<Entry>> {
+        let wait_for_snapshot = self.wait_for_snapshot(scan_id);
+        cx.spawn(|this, mut cx| async move {
+            wait_for_snapshot.await?;
+            this.update(&mut cx, |worktree, _| {
+                let worktree = worktree.as_remote_mut().unwrap();
+                let mut snapshot = worktree.background_snapshot.lock();
+                let entry = snapshot.insert_entry(entry);
+                worktree.snapshot = snapshot.clone();
+                entry
+            })?
+        })
+    }
+
+    pub(crate) fn delete_entry(
+        &mut self,
+        id: ProjectEntryId,
+        scan_id: usize,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Task<Result<()>> {
+        let wait_for_snapshot = self.wait_for_snapshot(scan_id);
+        cx.spawn(move |this, mut cx| async move {
+            wait_for_snapshot.await?;
+            this.update(&mut cx, |worktree, _| {
+                let worktree = worktree.as_remote_mut().unwrap();
+                let mut snapshot = worktree.background_snapshot.lock();
+                snapshot.delete_entry(id);
+                worktree.snapshot = snapshot.clone();
+            })?;
+            Ok(())
+        })
+    }
+}
+
+impl Snapshot {
+    pub fn id(&self) -> WorktreeId {
+        self.id
+    }
+
+    pub fn abs_path(&self) -> &Arc<Path> {
+        &self.abs_path
+    }
+
+    pub fn absolutize(&self, path: &Path) -> PathBuf {
+        if path.file_name().is_some() {
+            self.abs_path.join(path)
+        } else {
+            self.abs_path.to_path_buf()
+        }
+    }
+
+    pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
+        self.entries_by_id.get(&entry_id, &()).is_some()
+    }
+
+    pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
+        let entry = Entry::try_from((&self.root_char_bag, entry))?;
+        let old_entry = self.entries_by_id.insert_or_replace(
+            PathEntry {
+                id: entry.id,
+                path: entry.path.clone(),
+                is_ignored: entry.is_ignored,
+                scan_id: 0,
+            },
+            &(),
+        );
+        if let Some(old_entry) = old_entry {
+            self.entries_by_path.remove(&PathKey(old_entry.path), &());
+        }
+        self.entries_by_path.insert_or_replace(entry.clone(), &());
+        Ok(entry)
+    }
+
+    fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option<Arc<Path>> {
+        let removed_entry = self.entries_by_id.remove(&entry_id, &())?;
+        self.entries_by_path = {
+            let mut cursor = self.entries_by_path.cursor::<TraversalProgress>();
+            let mut new_entries_by_path =
+                cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &());
+            while let Some(entry) = cursor.item() {
+                if entry.path.starts_with(&removed_entry.path) {
+                    self.entries_by_id.remove(&entry.id, &());
+                    cursor.next(&());
+                } else {
+                    break;
+                }
+            }
+            new_entries_by_path.append(cursor.suffix(&()), &());
+            new_entries_by_path
+        };
+
+        Some(removed_entry.path)
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn status_for_file(&self, path: impl Into<PathBuf>) -> Option<GitFileStatus> {
+        let path = path.into();
+        self.entries_by_path
+            .get(&PathKey(Arc::from(path)), &())
+            .and_then(|entry| entry.git_status)
+    }
+
+    pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> {
+        let mut entries_by_path_edits = Vec::new();
+        let mut entries_by_id_edits = Vec::new();
+
+        for entry_id in update.removed_entries {
+            let entry_id = ProjectEntryId::from_proto(entry_id);
+            entries_by_id_edits.push(Edit::Remove(entry_id));
+            if let Some(entry) = self.entry_for_id(entry_id) {
+                entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
+            }
+        }
+
+        for entry in update.updated_entries {
+            let entry = Entry::try_from((&self.root_char_bag, entry))?;
+            if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, &()) {
+                entries_by_path_edits.push(Edit::Remove(PathKey(path.clone())));
+            }
+            if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) {
+                if old_entry.id != entry.id {
+                    entries_by_id_edits.push(Edit::Remove(old_entry.id));
+                }
+            }
+            entries_by_id_edits.push(Edit::Insert(PathEntry {
+                id: entry.id,
+                path: entry.path.clone(),
+                is_ignored: entry.is_ignored,
+                scan_id: 0,
+            }));
+            entries_by_path_edits.push(Edit::Insert(entry));
+        }
+
+        self.entries_by_path.edit(entries_by_path_edits, &());
+        self.entries_by_id.edit(entries_by_id_edits, &());
+
+        update.removed_repositories.sort_unstable();
+        self.repository_entries.retain(|_, entry| {
+            if let Ok(_) = update
+                .removed_repositories
+                .binary_search(&entry.work_directory.to_proto())
+            {
+                false
+            } else {
+                true
+            }
+        });
+
+        for repository in update.updated_repositories {
+            let work_directory_entry: WorkDirectoryEntry =
+                ProjectEntryId::from_proto(repository.work_directory_id).into();
+
+            if let Some(entry) = self.entry_for_id(*work_directory_entry) {
+                let work_directory = RepositoryWorkDirectory(entry.path.clone());
+                if self.repository_entries.get(&work_directory).is_some() {
+                    self.repository_entries.update(&work_directory, |repo| {
+                        repo.branch = repository.branch.map(Into::into);
+                    });
+                } else {
+                    self.repository_entries.insert(
+                        work_directory,
+                        RepositoryEntry {
+                            work_directory: work_directory_entry,
+                            branch: repository.branch.map(Into::into),
+                        },
+                    )
+                }
+            } else {
+                log::error!("no work directory entry for repository {:?}", repository)
+            }
+        }
+
+        self.scan_id = update.scan_id as usize;
+        if update.is_last_update {
+            self.completed_scan_id = update.scan_id as usize;
+        }
+
+        Ok(())
+    }
+
+    pub fn file_count(&self) -> usize {
+        self.entries_by_path.summary().file_count
+    }
+
+    pub fn visible_file_count(&self) -> usize {
+        self.entries_by_path.summary().non_ignored_file_count
+    }
+
+    fn traverse_from_offset(
+        &self,
+        include_dirs: bool,
+        include_ignored: bool,
+        start_offset: usize,
+    ) -> Traversal {
+        let mut cursor = self.entries_by_path.cursor();
+        cursor.seek(
+            &TraversalTarget::Count {
+                count: start_offset,
+                include_dirs,
+                include_ignored,
+            },
+            Bias::Right,
+            &(),
+        );
+        Traversal {
+            cursor,
+            include_dirs,
+            include_ignored,
+        }
+    }
+
+    fn traverse_from_path(
+        &self,
+        include_dirs: bool,
+        include_ignored: bool,
+        path: &Path,
+    ) -> Traversal {
+        let mut cursor = self.entries_by_path.cursor();
+        cursor.seek(&TraversalTarget::Path(path), Bias::Left, &());
+        Traversal {
+            cursor,
+            include_dirs,
+            include_ignored,
+        }
+    }
+
+    pub fn files(&self, include_ignored: bool, start: usize) -> Traversal {
+        self.traverse_from_offset(false, include_ignored, start)
+    }
+
+    pub fn entries(&self, include_ignored: bool) -> Traversal {
+        self.traverse_from_offset(true, include_ignored, 0)
+    }
+
+    pub fn repositories(&self) -> impl Iterator<Item = (&Arc<Path>, &RepositoryEntry)> {
+        self.repository_entries
+            .iter()
+            .map(|(path, entry)| (&path.0, entry))
+    }
+
+    /// Get the repository whose work directory contains the given path.
+    pub fn repository_for_work_directory(&self, path: &Path) -> Option<RepositoryEntry> {
+        self.repository_entries
+            .get(&RepositoryWorkDirectory(path.into()))
+            .cloned()
+    }
+
+    /// Get the repository whose work directory contains the given path.
+    pub fn repository_for_path(&self, path: &Path) -> Option<RepositoryEntry> {
+        self.repository_and_work_directory_for_path(path)
+            .map(|e| e.1)
+    }
+
+    pub fn repository_and_work_directory_for_path(
+        &self,
+        path: &Path,
+    ) -> Option<(RepositoryWorkDirectory, RepositoryEntry)> {
+        self.repository_entries
+            .iter()
+            .filter(|(workdir_path, _)| path.starts_with(workdir_path))
+            .last()
+            .map(|(path, repo)| (path.clone(), repo.clone()))
+    }
+
+    /// Given an ordered iterator of entries, returns an iterator of those entries,
+    /// along with their containing git repository.
+    pub fn entries_with_repositories<'a>(
+        &'a self,
+        entries: impl 'a + Iterator<Item = &'a Entry>,
+    ) -> impl 'a + Iterator<Item = (&'a Entry, Option<&'a RepositoryEntry>)> {
+        let mut containing_repos = Vec::<(&Arc<Path>, &RepositoryEntry)>::new();
+        let mut repositories = self.repositories().peekable();
+        entries.map(move |entry| {
+            while let Some((repo_path, _)) = containing_repos.last() {
+                if !entry.path.starts_with(repo_path) {
+                    containing_repos.pop();
+                } else {
+                    break;
+                }
+            }
+            while let Some((repo_path, _)) = repositories.peek() {
+                if entry.path.starts_with(repo_path) {
+                    containing_repos.push(repositories.next().unwrap());
+                } else {
+                    break;
+                }
+            }
+            let repo = containing_repos.last().map(|(_, repo)| *repo);
+            (entry, repo)
+        })
+    }
+
+    /// Update the `git_status` of the given entries such that files'
+    /// statuses bubble up to their ancestor directories.
+    pub fn propagate_git_statuses(&self, result: &mut [Entry]) {
+        let mut cursor = self
+            .entries_by_path
+            .cursor::<(TraversalProgress, GitStatuses)>();
+        let mut entry_stack = Vec::<(usize, GitStatuses)>::new();
+
+        let mut result_ix = 0;
+        loop {
+            let next_entry = result.get(result_ix);
+            let containing_entry = entry_stack.last().map(|(ix, _)| &result[*ix]);
+
+            let entry_to_finish = match (containing_entry, next_entry) {
+                (Some(_), None) => entry_stack.pop(),
+                (Some(containing_entry), Some(next_path)) => {
+                    if !next_path.path.starts_with(&containing_entry.path) {
+                        entry_stack.pop()
+                    } else {
+                        None
+                    }
+                }
+                (None, Some(_)) => None,
+                (None, None) => break,
+            };
+
+            if let Some((entry_ix, prev_statuses)) = entry_to_finish {
+                cursor.seek_forward(
+                    &TraversalTarget::PathSuccessor(&result[entry_ix].path),
+                    Bias::Left,
+                    &(),
+                );
+
+                let statuses = cursor.start().1 - prev_statuses;
+
+                result[entry_ix].git_status = if statuses.conflict > 0 {
+                    Some(GitFileStatus::Conflict)
+                } else if statuses.modified > 0 {
+                    Some(GitFileStatus::Modified)
+                } else if statuses.added > 0 {
+                    Some(GitFileStatus::Added)
+                } else {
+                    None
+                };
+            } else {
+                if result[result_ix].is_dir() {
+                    cursor.seek_forward(
+                        &TraversalTarget::Path(&result[result_ix].path),
+                        Bias::Left,
+                        &(),
+                    );
+                    entry_stack.push((result_ix, cursor.start().1));
+                }
+                result_ix += 1;
+            }
+        }
+    }
+
+    pub fn paths(&self) -> impl Iterator<Item = &Arc<Path>> {
+        let empty_path = Path::new("");
+        self.entries_by_path
+            .cursor::<()>()
+            .filter(move |entry| entry.path.as_ref() != empty_path)
+            .map(|entry| &entry.path)
+    }
+
+    fn child_entries<'a>(&'a self, parent_path: &'a Path) -> ChildEntriesIter<'a> {
+        let mut cursor = self.entries_by_path.cursor();
+        cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &());
+        let traversal = Traversal {
+            cursor,
+            include_dirs: true,
+            include_ignored: true,
+        };
+        ChildEntriesIter {
+            traversal,
+            parent_path,
+        }
+    }
+
+    pub fn descendent_entries<'a>(
+        &'a self,
+        include_dirs: bool,
+        include_ignored: bool,
+        parent_path: &'a Path,
+    ) -> DescendentEntriesIter<'a> {
+        let mut cursor = self.entries_by_path.cursor();
+        cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &());
+        let mut traversal = Traversal {
+            cursor,
+            include_dirs,
+            include_ignored,
+        };
+
+        if traversal.end_offset() == traversal.start_offset() {
+            traversal.advance();
+        }
+
+        DescendentEntriesIter {
+            traversal,
+            parent_path,
+        }
+    }
+
+    pub fn root_entry(&self) -> Option<&Entry> {
+        self.entry_for_path("")
+    }
+
+    pub fn root_name(&self) -> &str {
+        &self.root_name
+    }
+
+    pub fn root_git_entry(&self) -> Option<RepositoryEntry> {
+        self.repository_entries
+            .get(&RepositoryWorkDirectory(Path::new("").into()))
+            .map(|entry| entry.to_owned())
+    }
+
+    pub fn git_entries(&self) -> impl Iterator<Item = &RepositoryEntry> {
+        self.repository_entries.values()
+    }
+
+    pub fn scan_id(&self) -> usize {
+        self.scan_id
+    }
+
+    pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
+        let path = path.as_ref();
+        self.traverse_from_path(true, true, path)
+            .entry()
+            .and_then(|entry| {
+                if entry.path.as_ref() == path {
+                    Some(entry)
+                } else {
+                    None
+                }
+            })
+    }
+
+    pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> {
+        let entry = self.entries_by_id.get(&id, &())?;
+        self.entry_for_path(&entry.path)
+    }
+
+    pub fn inode_for_path(&self, path: impl AsRef<Path>) -> Option<u64> {
+        self.entry_for_path(path.as_ref()).map(|e| e.inode)
+    }
+}
+
+impl LocalSnapshot {
+    pub(crate) fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> {
+        self.git_repositories.get(&repo.work_directory.0)
+    }
+
+    pub(crate) fn local_repo_for_path(
+        &self,
+        path: &Path,
+    ) -> Option<(RepositoryWorkDirectory, &LocalRepositoryEntry)> {
+        let (path, repo) = self.repository_and_work_directory_for_path(path)?;
+        Some((path, self.git_repositories.get(&repo.work_directory_id())?))
+    }
+
+    fn build_update(
+        &self,
+        project_id: u64,
+        worktree_id: u64,
+        entry_changes: UpdatedEntriesSet,
+        repo_changes: UpdatedGitRepositoriesSet,
+    ) -> proto::UpdateWorktree {
+        let mut updated_entries = Vec::new();
+        let mut removed_entries = Vec::new();
+        let mut updated_repositories = Vec::new();
+        let mut removed_repositories = Vec::new();
+
+        for (_, entry_id, path_change) in entry_changes.iter() {
+            if let PathChange::Removed = path_change {
+                removed_entries.push(entry_id.0 as u64);
+            } else if let Some(entry) = self.entry_for_id(*entry_id) {
+                updated_entries.push(proto::Entry::from(entry));
+            }
+        }
+
+        for (work_dir_path, change) in repo_changes.iter() {
+            let new_repo = self
+                .repository_entries
+                .get(&RepositoryWorkDirectory(work_dir_path.clone()));
+            match (&change.old_repository, new_repo) {
+                (Some(old_repo), Some(new_repo)) => {
+                    updated_repositories.push(new_repo.build_update(old_repo));
+                }
+                (None, Some(new_repo)) => {
+                    updated_repositories.push(proto::RepositoryEntry::from(new_repo));
+                }
+                (Some(old_repo), None) => {
+                    removed_repositories.push(old_repo.work_directory.0.to_proto());
+                }
+                _ => {}
+            }
+        }
+
+        removed_entries.sort_unstable();
+        updated_entries.sort_unstable_by_key(|e| e.id);
+        removed_repositories.sort_unstable();
+        updated_repositories.sort_unstable_by_key(|e| e.work_directory_id);
+
+        // TODO - optimize, knowing that removed_entries are sorted.
+        removed_entries.retain(|id| updated_entries.binary_search_by_key(id, |e| e.id).is_err());
+
+        proto::UpdateWorktree {
+            project_id,
+            worktree_id,
+            abs_path: self.abs_path().to_string_lossy().into(),
+            root_name: self.root_name().to_string(),
+            updated_entries,
+            removed_entries,
+            scan_id: self.scan_id as u64,
+            is_last_update: self.completed_scan_id == self.scan_id,
+            updated_repositories,
+            removed_repositories,
+        }
+    }
+
+    fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
+        let mut updated_entries = self
+            .entries_by_path
+            .iter()
+            .map(proto::Entry::from)
+            .collect::<Vec<_>>();
+        updated_entries.sort_unstable_by_key(|e| e.id);
+
+        let mut updated_repositories = self
+            .repository_entries
+            .values()
+            .map(proto::RepositoryEntry::from)
+            .collect::<Vec<_>>();
+        updated_repositories.sort_unstable_by_key(|e| e.work_directory_id);
+
+        proto::UpdateWorktree {
+            project_id,
+            worktree_id,
+            abs_path: self.abs_path().to_string_lossy().into(),
+            root_name: self.root_name().to_string(),
+            updated_entries,
+            removed_entries: Vec::new(),
+            scan_id: self.scan_id as u64,
+            is_last_update: self.completed_scan_id == self.scan_id,
+            updated_repositories,
+            removed_repositories: Vec::new(),
+        }
+    }
+
+    fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
+        if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
+            let abs_path = self.abs_path.join(&entry.path);
+            match smol::block_on(build_gitignore(&abs_path, fs)) {
+                Ok(ignore) => {
+                    self.ignores_by_parent_abs_path
+                        .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true));
+                }
+                Err(error) => {
+                    log::error!(
+                        "error loading .gitignore file {:?} - {:?}",
+                        &entry.path,
+                        error
+                    );
+                }
+            }
+        }
+
+        if entry.kind == EntryKind::PendingDir {
+            if let Some(existing_entry) =
+                self.entries_by_path.get(&PathKey(entry.path.clone()), &())
+            {
+                entry.kind = existing_entry.kind;
+            }
+        }
+
+        let scan_id = self.scan_id;
+        let removed = self.entries_by_path.insert_or_replace(entry.clone(), &());
+        if let Some(removed) = removed {
+            if removed.id != entry.id {
+                self.entries_by_id.remove(&removed.id, &());
+            }
+        }
+        self.entries_by_id.insert_or_replace(
+            PathEntry {
+                id: entry.id,
+                path: entry.path.clone(),
+                is_ignored: entry.is_ignored,
+                scan_id,
+            },
+            &(),
+        );
+
+        entry
+    }
+
+    fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet<u64> {
+        let mut inodes = TreeSet::default();
+        for ancestor in path.ancestors().skip(1) {
+            if let Some(entry) = self.entry_for_path(ancestor) {
+                inodes.insert(entry.inode);
+            }
+        }
+        inodes
+    }
+
+    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));
+            }
+        }
+
+        let mut ignore_stack = IgnoreStack::none();
+        for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
+            if ignore_stack.is_abs_path_ignored(parent_abs_path, true) {
+                ignore_stack = IgnoreStack::all();
+                break;
+            } else if let Some(ignore) = ignore {
+                ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore);
+            }
+        }
+
+        if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
+            ignore_stack = IgnoreStack::all();
+        }
+
+        ignore_stack
+    }
+
+    #[allow(dead_code)] // todo!("remove this when we use it")
+    #[cfg(test)]
+    pub(crate) fn expanded_entries(&self) -> impl Iterator<Item = &Entry> {
+        self.entries_by_path
+            .cursor::<()>()
+            .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored))
+    }
+
+    #[cfg(test)]
+    pub fn check_invariants(&self, git_state: bool) {
+        use pretty_assertions::assert_eq;
+
+        assert_eq!(
+            self.entries_by_path
+                .cursor::<()>()
+                .map(|e| (&e.path, e.id))
+                .collect::<Vec<_>>(),
+            self.entries_by_id
+                .cursor::<()>()
+                .map(|e| (&e.path, e.id))
+                .collect::<collections::BTreeSet<_>>()
+                .into_iter()
+                .collect::<Vec<_>>(),
+            "entries_by_path and entries_by_id are inconsistent"
+        );
+
+        let mut files = self.files(true, 0);
+        let mut visible_files = self.files(false, 0);
+        for entry in self.entries_by_path.cursor::<()>() {
+            if entry.is_file() {
+                assert_eq!(files.next().unwrap().inode, entry.inode);
+                if !entry.is_ignored && !entry.is_external {
+                    assert_eq!(visible_files.next().unwrap().inode, entry.inode);
+                }
+            }
+        }
+
+        assert!(files.next().is_none());
+        assert!(visible_files.next().is_none());
+
+        let mut bfs_paths = Vec::new();
+        let mut stack = self
+            .root_entry()
+            .map(|e| e.path.as_ref())
+            .into_iter()
+            .collect::<Vec<_>>();
+        while let Some(path) = stack.pop() {
+            bfs_paths.push(path);
+            let ix = stack.len();
+            for child_entry in self.child_entries(path) {
+                stack.insert(ix, &child_entry.path);
+            }
+        }
+
+        let dfs_paths_via_iter = self
+            .entries_by_path
+            .cursor::<()>()
+            .map(|e| e.path.as_ref())
+            .collect::<Vec<_>>();
+        assert_eq!(bfs_paths, dfs_paths_via_iter);
+
+        let dfs_paths_via_traversal = self
+            .entries(true)
+            .map(|e| e.path.as_ref())
+            .collect::<Vec<_>>();
+        assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter);
+
+        if git_state {
+            for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() {
+                let ignore_parent_path =
+                    ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap();
+                assert!(self.entry_for_path(&ignore_parent_path).is_some());
+                assert!(self
+                    .entry_for_path(ignore_parent_path.join(&*GITIGNORE))
+                    .is_some());
+            }
+        }
+    }
+
+    #[cfg(test)]
+    pub fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&Path, u64, bool)> {
+        let mut paths = Vec::new();
+        for entry in self.entries_by_path.cursor::<()>() {
+            if include_ignored || !entry.is_ignored {
+                paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored));
+            }
+        }
+        paths.sort_by(|a, b| a.0.cmp(b.0));
+        paths
+    }
+}
+
+impl BackgroundScannerState {
+    fn should_scan_directory(&self, entry: &Entry) -> bool {
+        (!entry.is_external && !entry.is_ignored)
+            || entry.path.file_name() == Some(&*DOT_GIT)
+            || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
+            || self
+                .paths_to_scan
+                .iter()
+                .any(|p| p.starts_with(&entry.path))
+            || self
+                .path_prefixes_to_scan
+                .iter()
+                .any(|p| entry.path.starts_with(p))
+    }
+
+    fn enqueue_scan_dir(&self, abs_path: Arc<Path>, entry: &Entry, scan_job_tx: &Sender<ScanJob>) {
+        let path = entry.path.clone();
+        let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
+        let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
+        let mut containing_repository = None;
+        if !ignore_stack.is_all() {
+            if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
+                if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
+                    containing_repository = Some((
+                        workdir_path,
+                        repo.repo_ptr.clone(),
+                        repo.repo_ptr.lock().staged_statuses(repo_path),
+                    ));
+                }
+            }
+        }
+        if !ancestor_inodes.contains(&entry.inode) {
+            ancestor_inodes.insert(entry.inode);
+            scan_job_tx
+                .try_send(ScanJob {
+                    abs_path,
+                    path,
+                    ignore_stack,
+                    scan_queue: scan_job_tx.clone(),
+                    ancestor_inodes,
+                    is_external: entry.is_external,
+                    containing_repository,
+                })
+                .unwrap();
+        }
+    }
+
+    fn reuse_entry_id(&mut self, entry: &mut Entry) {
+        if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) {
+            entry.id = removed_entry_id;
+        } else if let Some(existing_entry) = self.snapshot.entry_for_path(&entry.path) {
+            entry.id = existing_entry.id;
+        }
+    }
+
+    fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
+        self.reuse_entry_id(&mut entry);
+        let entry = self.snapshot.insert_entry(entry, fs);
+        if entry.path.file_name() == Some(&DOT_GIT) {
+            self.build_git_repository(entry.path.clone(), fs);
+        }
+
+        #[cfg(test)]
+        self.snapshot.check_invariants(false);
+
+        entry
+    }
+
+    fn populate_dir(
+        &mut self,
+        parent_path: &Arc<Path>,
+        entries: impl IntoIterator<Item = Entry>,
+        ignore: Option<Arc<Gitignore>>,
+    ) {
+        let mut parent_entry = if let Some(parent_entry) = self
+            .snapshot
+            .entries_by_path
+            .get(&PathKey(parent_path.clone()), &())
+        {
+            parent_entry.clone()
+        } else {
+            log::warn!(
+                "populating a directory {:?} that has been removed",
+                parent_path
+            );
+            return;
+        };
+
+        match parent_entry.kind {
+            EntryKind::PendingDir | EntryKind::UnloadedDir => parent_entry.kind = EntryKind::Dir,
+            EntryKind::Dir => {}
+            _ => return,
+        }
+
+        if let Some(ignore) = ignore {
+            let abs_parent_path = self.snapshot.abs_path.join(&parent_path).into();
+            self.snapshot
+                .ignores_by_parent_abs_path
+                .insert(abs_parent_path, (ignore, false));
+        }
+
+        let parent_entry_id = parent_entry.id;
+        self.scanned_dirs.insert(parent_entry_id);
+        let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
+        let mut entries_by_id_edits = Vec::new();
+
+        for entry in entries {
+            entries_by_id_edits.push(Edit::Insert(PathEntry {
+                id: entry.id,
+                path: entry.path.clone(),
+                is_ignored: entry.is_ignored,
+                scan_id: self.snapshot.scan_id,
+            }));
+            entries_by_path_edits.push(Edit::Insert(entry));
+        }
+
+        self.snapshot
+            .entries_by_path
+            .edit(entries_by_path_edits, &());
+        self.snapshot.entries_by_id.edit(entries_by_id_edits, &());
+
+        if let Err(ix) = self.changed_paths.binary_search(parent_path) {
+            self.changed_paths.insert(ix, parent_path.clone());
+        }
+
+        #[cfg(test)]
+        self.snapshot.check_invariants(false);
+    }
+
+    fn remove_path(&mut self, path: &Path) {
+        let mut new_entries;
+        let removed_entries;
+        {
+            let mut cursor = self.snapshot.entries_by_path.cursor::<TraversalProgress>();
+            new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &());
+            removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &());
+            new_entries.append(cursor.suffix(&()), &());
+        }
+        self.snapshot.entries_by_path = new_entries;
+
+        let mut entries_by_id_edits = Vec::new();
+        for entry in removed_entries.cursor::<()>() {
+            let removed_entry_id = self
+                .removed_entry_ids
+                .entry(entry.inode)
+                .or_insert(entry.id);
+            *removed_entry_id = cmp::max(*removed_entry_id, entry.id);
+            entries_by_id_edits.push(Edit::Remove(entry.id));
+        }
+        self.snapshot.entries_by_id.edit(entries_by_id_edits, &());
+
+        if path.file_name() == Some(&GITIGNORE) {
+            let abs_parent_path = self.snapshot.abs_path.join(path.parent().unwrap());
+            if let Some((_, needs_update)) = self
+                .snapshot
+                .ignores_by_parent_abs_path
+                .get_mut(abs_parent_path.as_path())
+            {
+                *needs_update = true;
+            }
+        }
+
+        #[cfg(test)]
+        self.snapshot.check_invariants(false);
+    }
+
+    fn reload_repositories(&mut self, changed_paths: &[Arc<Path>], fs: &dyn Fs) {
+        let scan_id = self.snapshot.scan_id;
+
+        // Find each of the .git directories that contain any of the given paths.
+        let mut prev_dot_git_dir = None;
+        for changed_path in changed_paths {
+            let Some(dot_git_dir) = changed_path
+                .ancestors()
+                .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))
+            else {
+                continue;
+            };
+
+            // Avoid processing the same repository multiple times, if multiple paths
+            // within it have changed.
+            if prev_dot_git_dir == Some(dot_git_dir) {
+                continue;
+            }
+            prev_dot_git_dir = Some(dot_git_dir);
+
+            // If there is already a repository for this .git directory, reload
+            // the status for all of its files.
+            let repository = self
+                .snapshot
+                .git_repositories
+                .iter()
+                .find_map(|(entry_id, repo)| {
+                    (repo.git_dir_path.as_ref() == dot_git_dir).then(|| (*entry_id, repo.clone()))
+                });
+            match repository {
+                None => {
+                    self.build_git_repository(dot_git_dir.into(), fs);
+                }
+                Some((entry_id, repository)) => {
+                    if repository.git_dir_scan_id == scan_id {
+                        continue;
+                    }
+                    let Some(work_dir) = self
+                        .snapshot
+                        .entry_for_id(entry_id)
+                        .map(|entry| RepositoryWorkDirectory(entry.path.clone()))
+                    else {
+                        continue;
+                    };
+
+                    log::info!("reload git repository {:?}", dot_git_dir);
+                    let repository = repository.repo_ptr.lock();
+                    let branch = repository.branch_name();
+                    repository.reload_index();
+
+                    self.snapshot
+                        .git_repositories
+                        .update(&entry_id, |entry| entry.git_dir_scan_id = scan_id);
+                    self.snapshot
+                        .snapshot
+                        .repository_entries
+                        .update(&work_dir, |entry| entry.branch = branch.map(Into::into));
+
+                    self.update_git_statuses(&work_dir, &*repository);
+                }
+            }
+        }
+
+        // Remove any git repositories whose .git entry no longer exists.
+        let snapshot = &mut self.snapshot;
+        let mut repositories = mem::take(&mut snapshot.git_repositories);
+        let mut repository_entries = mem::take(&mut snapshot.repository_entries);
+        repositories.retain(|work_directory_id, _| {
+            snapshot
+                .entry_for_id(*work_directory_id)
+                .map_or(false, |entry| {
+                    snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
+                })
+        });
+        repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some());
+        snapshot.git_repositories = repositories;
+        snapshot.repository_entries = repository_entries;
+    }
+
+    fn build_git_repository(
+        &mut self,
+        dot_git_path: Arc<Path>,
+        fs: &dyn Fs,
+    ) -> Option<(
+        RepositoryWorkDirectory,
+        Arc<Mutex<dyn GitRepository>>,
+        TreeMap<RepoPath, GitFileStatus>,
+    )> {
+        log::info!("build git repository {:?}", dot_git_path);
+
+        let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
+
+        // Guard against repositories inside the repository metadata
+        if work_dir_path.iter().any(|component| component == *DOT_GIT) {
+            return None;
+        };
+
+        let work_dir_id = self
+            .snapshot
+            .entry_for_path(work_dir_path.clone())
+            .map(|entry| entry.id)?;
+
+        if self.snapshot.git_repositories.get(&work_dir_id).is_some() {
+            return None;
+        }
+
+        let abs_path = self.snapshot.abs_path.join(&dot_git_path);
+        let repository = fs.open_repo(abs_path.as_path())?;
+        let work_directory = RepositoryWorkDirectory(work_dir_path.clone());
+
+        let repo_lock = repository.lock();
+        self.snapshot.repository_entries.insert(
+            work_directory.clone(),
+            RepositoryEntry {
+                work_directory: work_dir_id.into(),
+                branch: repo_lock.branch_name().map(Into::into),
+            },
+        );
+
+        let staged_statuses = self.update_git_statuses(&work_directory, &*repo_lock);
+        drop(repo_lock);
+
+        self.snapshot.git_repositories.insert(
+            work_dir_id,
+            LocalRepositoryEntry {
+                git_dir_scan_id: 0,
+                repo_ptr: repository.clone(),
+                git_dir_path: dot_git_path.clone(),
+            },
+        );
+
+        Some((work_directory, repository, staged_statuses))
+    }
+
+    fn update_git_statuses(
+        &mut self,
+        work_directory: &RepositoryWorkDirectory,
+        repo: &dyn GitRepository,
+    ) -> TreeMap<RepoPath, GitFileStatus> {
+        let staged_statuses = repo.staged_statuses(Path::new(""));
+
+        let mut changes = vec![];
+        let mut edits = vec![];
+
+        for mut entry in self
+            .snapshot
+            .descendent_entries(false, false, &work_directory.0)
+            .cloned()
+        {
+            let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else {
+                continue;
+            };
+            let repo_path = RepoPath(repo_path.to_path_buf());
+            let git_file_status = combine_git_statuses(
+                staged_statuses.get(&repo_path).copied(),
+                repo.unstaged_status(&repo_path, entry.mtime),
+            );
+            if entry.git_status != git_file_status {
+                entry.git_status = git_file_status;
+                changes.push(entry.path.clone());
+                edits.push(Edit::Insert(entry));
+            }
+        }
+
+        self.snapshot.entries_by_path.edit(edits, &());
+        util::extend_sorted(&mut self.changed_paths, changes, usize::MAX, Ord::cmp);
+        staged_statuses
+    }
+}
+
+async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
+    let contents = fs.load(abs_path).await?;
+    let parent = abs_path.parent().unwrap_or_else(|| Path::new("/"));
+    let mut builder = GitignoreBuilder::new(parent);
+    for line in contents.lines() {
+        builder.add_line(Some(abs_path.into()), line)?;
+    }
+    Ok(builder.build()?)
+}
+
+impl WorktreeId {
+    pub fn from_usize(handle_id: usize) -> Self {
+        Self(handle_id)
+    }
+
+    pub(crate) fn from_proto(id: u64) -> Self {
+        Self(id as usize)
+    }
+
+    pub fn to_proto(&self) -> u64 {
+        self.0 as u64
+    }
+
+    pub fn to_usize(&self) -> usize {
+        self.0
+    }
+}
+
+impl fmt::Display for WorktreeId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl Deref for Worktree {
+    type Target = Snapshot;
+
+    fn deref(&self) -> &Self::Target {
+        match self {
+            Worktree::Local(worktree) => &worktree.snapshot,
+            Worktree::Remote(worktree) => &worktree.snapshot,
+        }
+    }
+}
+
+impl Deref for LocalWorktree {
+    type Target = LocalSnapshot;
+
+    fn deref(&self) -> &Self::Target {
+        &self.snapshot
+    }
+}
+
+impl Deref for RemoteWorktree {
+    type Target = Snapshot;
+
+    fn deref(&self) -> &Self::Target {
+        &self.snapshot
+    }
+}
+
+impl fmt::Debug for LocalWorktree {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.snapshot.fmt(f)
+    }
+}
+
+impl fmt::Debug for Snapshot {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        struct EntriesById<'a>(&'a SumTree<PathEntry>);
+        struct EntriesByPath<'a>(&'a SumTree<Entry>);
+
+        impl<'a> fmt::Debug for EntriesByPath<'a> {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                f.debug_map()
+                    .entries(self.0.iter().map(|entry| (&entry.path, entry.id)))
+                    .finish()
+            }
+        }
+
+        impl<'a> fmt::Debug for EntriesById<'a> {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                f.debug_list().entries(self.0.iter()).finish()
+            }
+        }
+
+        f.debug_struct("Snapshot")
+            .field("id", &self.id)
+            .field("root_name", &self.root_name)
+            .field("entries_by_path", &EntriesByPath(&self.entries_by_path))
+            .field("entries_by_id", &EntriesById(&self.entries_by_id))
+            .finish()
+    }
+}
+
+#[derive(Clone, PartialEq)]
+pub struct File {
+    pub worktree: Model<Worktree>,
+    pub path: Arc<Path>,
+    pub mtime: SystemTime,
+    pub(crate) entry_id: ProjectEntryId,
+    pub(crate) is_local: bool,
+    pub(crate) is_deleted: bool,
+}
+
+impl language2::File for File {
+    fn as_local(&self) -> Option<&dyn language2::LocalFile> {
+        if self.is_local {
+            Some(self)
+        } else {
+            None
+        }
+    }
+
+    fn mtime(&self) -> SystemTime {
+        self.mtime
+    }
+
+    fn path(&self) -> &Arc<Path> {
+        &self.path
+    }
+
+    fn full_path(&self, cx: &AppContext) -> PathBuf {
+        let mut full_path = PathBuf::new();
+        let worktree = self.worktree.read(cx);
+
+        if worktree.is_visible() {
+            full_path.push(worktree.root_name());
+        } else {
+            let path = worktree.abs_path();
+
+            if worktree.is_local() && path.starts_with(HOME.as_path()) {
+                full_path.push("~");
+                full_path.push(path.strip_prefix(HOME.as_path()).unwrap());
+            } else {
+                full_path.push(path)
+            }
+        }
+
+        if self.path.components().next().is_some() {
+            full_path.push(&self.path);
+        }
+
+        full_path
+    }
+
+    /// Returns the last component of this handle's absolute path. If this handle refers to the root
+    /// of its worktree, then this method will return the name of the worktree itself.
+    fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr {
+        self.path
+            .file_name()
+            .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
+    }
+
+    fn worktree_id(&self) -> usize {
+        self.worktree.entity_id().as_u64() as usize
+    }
+
+    fn is_deleted(&self) -> bool {
+        self.is_deleted
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn to_proto(&self) -> rpc2::proto::File {
+        rpc2::proto::File {
+            worktree_id: self.worktree.entity_id().as_u64(),
+            entry_id: self.entry_id.to_proto(),
+            path: self.path.to_string_lossy().into(),
+            mtime: Some(self.mtime.into()),
+            is_deleted: self.is_deleted,
+        }
+    }
+}
+
+impl language2::LocalFile for File {
+    fn abs_path(&self, cx: &AppContext) -> PathBuf {
+        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>> {
+        let worktree = self.worktree.read(cx).as_local().unwrap();
+        let abs_path = worktree.absolutize(&self.path);
+        let fs = worktree.fs.clone();
+        cx.executor().spawn(async move { fs.load(&abs_path).await })
+    }
+
+    fn buffer_reloaded(
+        &self,
+        buffer_id: u64,
+        version: &clock::Global,
+        fingerprint: RopeFingerprint,
+        line_ending: LineEnding,
+        mtime: SystemTime,
+        cx: &mut AppContext,
+    ) {
+        let worktree = self.worktree.read(cx).as_local().unwrap();
+        if let Some(project_id) = worktree.share.as_ref().map(|share| share.project_id) {
+            worktree
+                .client
+                .send(proto::BufferReloaded {
+                    project_id,
+                    buffer_id,
+                    version: serialize_version(version),
+                    mtime: Some(mtime.into()),
+                    fingerprint: serialize_fingerprint(fingerprint),
+                    line_ending: serialize_line_ending(line_ending) as i32,
+                })
+                .log_err();
+        }
+    }
+}
+
+impl File {
+    pub fn for_entry(entry: Entry, worktree: Model<Worktree>) -> Arc<Self> {
+        Arc::new(Self {
+            worktree,
+            path: entry.path.clone(),
+            mtime: entry.mtime,
+            entry_id: entry.id,
+            is_local: true,
+            is_deleted: false,
+        })
+    }
+
+    pub fn from_proto(
+        proto: rpc2::proto::File,
+        worktree: Model<Worktree>,
+        cx: &AppContext,
+    ) -> Result<Self> {
+        let worktree_id = worktree
+            .read(cx)
+            .as_remote()
+            .ok_or_else(|| anyhow!("not remote"))?
+            .id();
+
+        if worktree_id.to_proto() != proto.worktree_id {
+            return Err(anyhow!("worktree id does not match file"));
+        }
+
+        Ok(Self {
+            worktree,
+            path: Path::new(&proto.path).into(),
+            mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
+            entry_id: ProjectEntryId::from_proto(proto.entry_id),
+            is_local: false,
+            is_deleted: proto.is_deleted,
+        })
+    }
+
+    pub fn from_dyn(file: Option<&Arc<dyn language2::File>>) -> Option<&Self> {
+        file.and_then(|f| f.as_any().downcast_ref())
+    }
+
+    pub fn worktree_id(&self, cx: &AppContext) -> WorktreeId {
+        self.worktree.read(cx).id()
+    }
+
+    pub fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
+        if self.is_deleted {
+            None
+        } else {
+            Some(self.entry_id)
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Entry {
+    pub id: ProjectEntryId,
+    pub kind: EntryKind,
+    pub path: Arc<Path>,
+    pub inode: u64,
+    pub mtime: SystemTime,
+    pub is_symlink: bool,
+
+    /// Whether this entry is ignored by Git.
+    ///
+    /// We only scan ignored entries once the directory is expanded and
+    /// exclude them from searches.
+    pub is_ignored: bool,
+
+    /// Whether this entry's canonical path is outside of the worktree.
+    /// This means the entry is only accessible from the worktree root via a
+    /// symlink.
+    ///
+    /// We only scan entries outside of the worktree once the symlinked
+    /// directory is expanded. External entries are treated like gitignored
+    /// entries in that they are not included in searches.
+    pub is_external: bool,
+    pub git_status: Option<GitFileStatus>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum EntryKind {
+    UnloadedDir,
+    PendingDir,
+    Dir,
+    File(CharBag),
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum PathChange {
+    /// A filesystem entry was was created.
+    Added,
+    /// A filesystem entry was removed.
+    Removed,
+    /// A filesystem entry was updated.
+    Updated,
+    /// A filesystem entry was either updated or added. We don't know
+    /// whether or not it already existed, because the path had not
+    /// been loaded before the event.
+    AddedOrUpdated,
+    /// A filesystem entry was found during the initial scan of the worktree.
+    Loaded,
+}
+
+pub struct GitRepositoryChange {
+    /// The previous state of the repository, if it already existed.
+    pub old_repository: Option<RepositoryEntry>,
+}
+
+pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
+pub type UpdatedGitRepositoriesSet = Arc<[(Arc<Path>, GitRepositoryChange)]>;
+
+impl Entry {
+    fn new(
+        path: Arc<Path>,
+        metadata: &fs2::Metadata,
+        next_entry_id: &AtomicUsize,
+        root_char_bag: CharBag,
+    ) -> Self {
+        Self {
+            id: ProjectEntryId::new(next_entry_id),
+            kind: if metadata.is_dir {
+                EntryKind::PendingDir
+            } else {
+                EntryKind::File(char_bag_for_path(root_char_bag, &path))
+            },
+            path,
+            inode: metadata.inode,
+            mtime: metadata.mtime,
+            is_symlink: metadata.is_symlink,
+            is_ignored: false,
+            is_external: false,
+            git_status: None,
+        }
+    }
+
+    pub fn is_dir(&self) -> bool {
+        self.kind.is_dir()
+    }
+
+    pub fn is_file(&self) -> bool {
+        self.kind.is_file()
+    }
+
+    pub fn git_status(&self) -> Option<GitFileStatus> {
+        self.git_status
+    }
+}
+
+impl EntryKind {
+    pub fn is_dir(&self) -> bool {
+        matches!(
+            self,
+            EntryKind::Dir | EntryKind::PendingDir | EntryKind::UnloadedDir
+        )
+    }
+
+    pub fn is_unloaded(&self) -> bool {
+        matches!(self, EntryKind::UnloadedDir)
+    }
+
+    pub fn is_file(&self) -> bool {
+        matches!(self, EntryKind::File(_))
+    }
+}
+
+impl sum_tree::Item for Entry {
+    type Summary = EntrySummary;
+
+    fn summary(&self) -> Self::Summary {
+        let non_ignored_count = if self.is_ignored || self.is_external {
+            0
+        } else {
+            1
+        };
+        let file_count;
+        let non_ignored_file_count;
+        if self.is_file() {
+            file_count = 1;
+            non_ignored_file_count = non_ignored_count;
+        } else {
+            file_count = 0;
+            non_ignored_file_count = 0;
+        }
+
+        let mut statuses = GitStatuses::default();
+        match self.git_status {
+            Some(status) => match status {
+                GitFileStatus::Added => statuses.added = 1,
+                GitFileStatus::Modified => statuses.modified = 1,
+                GitFileStatus::Conflict => statuses.conflict = 1,
+            },
+            None => {}
+        }
+
+        EntrySummary {
+            max_path: self.path.clone(),
+            count: 1,
+            non_ignored_count,
+            file_count,
+            non_ignored_file_count,
+            statuses,
+        }
+    }
+}
+
+impl sum_tree::KeyedItem for Entry {
+    type Key = PathKey;
+
+    fn key(&self) -> Self::Key {
+        PathKey(self.path.clone())
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct EntrySummary {
+    max_path: Arc<Path>,
+    count: usize,
+    non_ignored_count: usize,
+    file_count: usize,
+    non_ignored_file_count: usize,
+    statuses: GitStatuses,
+}
+
+impl Default for EntrySummary {
+    fn default() -> Self {
+        Self {
+            max_path: Arc::from(Path::new("")),
+            count: 0,
+            non_ignored_count: 0,
+            file_count: 0,
+            non_ignored_file_count: 0,
+            statuses: Default::default(),
+        }
+    }
+}
+
+impl sum_tree::Summary for EntrySummary {
+    type Context = ();
+
+    fn add_summary(&mut self, rhs: &Self, _: &()) {
+        self.max_path = rhs.max_path.clone();
+        self.count += rhs.count;
+        self.non_ignored_count += rhs.non_ignored_count;
+        self.file_count += rhs.file_count;
+        self.non_ignored_file_count += rhs.non_ignored_file_count;
+        self.statuses += rhs.statuses;
+    }
+}
+
+#[derive(Clone, Debug)]
+struct PathEntry {
+    id: ProjectEntryId,
+    path: Arc<Path>,
+    is_ignored: bool,
+    scan_id: usize,
+}
+
+impl sum_tree::Item for PathEntry {
+    type Summary = PathEntrySummary;
+
+    fn summary(&self) -> Self::Summary {
+        PathEntrySummary { max_id: self.id }
+    }
+}
+
+impl sum_tree::KeyedItem for PathEntry {
+    type Key = ProjectEntryId;
+
+    fn key(&self) -> Self::Key {
+        self.id
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+struct PathEntrySummary {
+    max_id: ProjectEntryId,
+}
+
+impl sum_tree::Summary for PathEntrySummary {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &Self::Context) {
+        self.max_id = summary.max_id;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
+    fn add_summary(&mut self, summary: &'a PathEntrySummary, _: &()) {
+        *self = summary.max_id;
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
+pub struct PathKey(Arc<Path>);
+
+impl Default for PathKey {
+    fn default() -> Self {
+        Self(Path::new("").into())
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
+    fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
+        self.0 = summary.max_path.clone();
+    }
+}
+
+struct BackgroundScanner {
+    state: Mutex<BackgroundScannerState>,
+    fs: Arc<dyn Fs>,
+    status_updates_tx: UnboundedSender<ScanState>,
+    executor: Executor,
+    scan_requests_rx: channel::Receiver<ScanRequest>,
+    path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
+    next_entry_id: Arc<AtomicUsize>,
+    phase: BackgroundScannerPhase,
+}
+
+#[derive(PartialEq)]
+enum BackgroundScannerPhase {
+    InitialScan,
+    EventsReceivedDuringInitialScan,
+    Events,
+}
+
+impl BackgroundScanner {
+    fn new(
+        snapshot: LocalSnapshot,
+        next_entry_id: Arc<AtomicUsize>,
+        fs: Arc<dyn Fs>,
+        status_updates_tx: UnboundedSender<ScanState>,
+        executor: Executor,
+        scan_requests_rx: channel::Receiver<ScanRequest>,
+        path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
+    ) -> Self {
+        Self {
+            fs,
+            status_updates_tx,
+            executor,
+            scan_requests_rx,
+            path_prefixes_to_scan_rx,
+            next_entry_id,
+            state: Mutex::new(BackgroundScannerState {
+                prev_snapshot: snapshot.snapshot.clone(),
+                snapshot,
+                scanned_dirs: Default::default(),
+                path_prefixes_to_scan: Default::default(),
+                paths_to_scan: Default::default(),
+                removed_entry_ids: Default::default(),
+                changed_paths: Default::default(),
+            }),
+            phase: BackgroundScannerPhase::InitialScan,
+        }
+    }
+
+    async fn run(
+        &mut self,
+        mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>,
+    ) {
+        use futures::FutureExt as _;
+
+        // 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));
+            }
+        }
+
+        let (scan_job_tx, scan_job_rx) = channel::unbounded();
+        {
+            let mut state = self.state.lock();
+            state.snapshot.scan_id += 1;
+            if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
+                let ignore_stack = state
+                    .snapshot
+                    .ignore_stack_for_abs_path(&root_abs_path, true);
+                if ignore_stack.is_all() {
+                    root_entry.is_ignored = true;
+                    state.insert_entry(root_entry.clone(), self.fs.as_ref());
+                }
+                state.enqueue_scan_dir(root_abs_path, &root_entry, &scan_job_tx);
+            }
+        };
+
+        // Perform an initial scan of the directory.
+        drop(scan_job_tx);
+        self.scan_dirs(true, scan_job_rx).await;
+        {
+            let mut state = self.state.lock();
+            state.snapshot.completed_scan_id = state.snapshot.scan_id;
+        }
+
+        self.send_status_update(false, None);
+
+        // Process any any FS events that occurred while performing the initial scan.
+        // For these events, update events cannot be as precise, because we didn't
+        // have the previous state loaded yet.
+        self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan;
+        if let Poll::Ready(Some(events)) = futures::poll!(fs_events_rx.next()) {
+            let mut paths = events.into_iter().map(|e| e.path).collect::<Vec<_>>();
+            while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
+                paths.extend(more_events.into_iter().map(|e| e.path));
+            }
+            self.process_events(paths).await;
+        }
+
+        // Continue processing events until the worktree is dropped.
+        self.phase = BackgroundScannerPhase::Events;
+        loop {
+            select_biased! {
+                // Process any path refresh requests from the worktree. Prioritize
+                // these before handling changes reported by the filesystem.
+                request = self.scan_requests_rx.recv().fuse() => {
+                    let Ok(request) = request else { break };
+                    if !self.process_scan_request(request, false).await {
+                        return;
+                    }
+                }
+
+                path_prefix = self.path_prefixes_to_scan_rx.recv().fuse() => {
+                    let Ok(path_prefix) = path_prefix else { break };
+                    log::trace!("adding path prefix {:?}", path_prefix);
+
+                    let did_scan = self.forcibly_load_paths(&[path_prefix.clone()]).await;
+                    if did_scan {
+                        let abs_path =
+                        {
+                            let mut state = self.state.lock();
+                            state.path_prefixes_to_scan.insert(path_prefix.clone());
+                            state.snapshot.abs_path.join(&path_prefix)
+                        };
+
+                        if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
+                            self.process_events(vec![abs_path]).await;
+                        }
+                    }
+                }
+
+                events = fs_events_rx.next().fuse() => {
+                    let Some(events) = events else { break };
+                    let mut paths = events.into_iter().map(|e| e.path).collect::<Vec<_>>();
+                    while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
+                        paths.extend(more_events.into_iter().map(|e| e.path));
+                    }
+                    self.process_events(paths.clone()).await;
+                }
+            }
+        }
+    }
+
+    async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool {
+        log::debug!("rescanning paths {:?}", request.relative_paths);
+
+        request.relative_paths.sort_unstable();
+        self.forcibly_load_paths(&request.relative_paths).await;
+
+        let root_path = self.state.lock().snapshot.abs_path.clone();
+        let root_canonical_path = match self.fs.canonicalize(&root_path).await {
+            Ok(path) => path,
+            Err(err) => {
+                log::error!("failed to canonicalize root path: {}", err);
+                return false;
+            }
+        };
+        let abs_paths = request
+            .relative_paths
+            .iter()
+            .map(|path| {
+                if path.file_name().is_some() {
+                    root_canonical_path.join(path)
+                } else {
+                    root_canonical_path.clone()
+                }
+            })
+            .collect::<Vec<_>>();
+
+        self.reload_entries_for_paths(
+            root_path,
+            root_canonical_path,
+            &request.relative_paths,
+            abs_paths,
+            None,
+        )
+        .await;
+        self.send_status_update(scanning, Some(request.done))
+    }
+
+    async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
+        let root_path = self.state.lock().snapshot.abs_path.clone();
+        let root_canonical_path = match self.fs.canonicalize(&root_path).await {
+            Ok(path) => path,
+            Err(err) => {
+                log::error!("failed to canonicalize root path: {}", err);
+                return;
+            }
+        };
+
+        let mut relative_paths = Vec::with_capacity(abs_paths.len());
+        abs_paths.sort_unstable();
+        abs_paths.dedup_by(|a, b| a.starts_with(&b));
+        abs_paths.retain(|abs_path| {
+            let snapshot = &self.state.lock().snapshot;
+            {
+                let relative_path: Arc<Path> =
+                    if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
+                        path.into()
+                    } else {
+                        log::error!(
+                        "ignoring event {abs_path:?} outside of root path {root_canonical_path:?}",
+                    );
+                        return false;
+                    };
+
+                let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
+                    snapshot
+                        .entry_for_path(parent)
+                        .map_or(false, |entry| entry.kind == EntryKind::Dir)
+                });
+                if !parent_dir_is_loaded {
+                    log::debug!("ignoring event {relative_path:?} within unloaded directory");
+                    return false;
+                }
+
+                relative_paths.push(relative_path);
+                true
+            }
+        });
+
+        if relative_paths.is_empty() {
+            return;
+        }
+
+        log::debug!("received fs events {:?}", relative_paths);
+
+        let (scan_job_tx, scan_job_rx) = channel::unbounded();
+        self.reload_entries_for_paths(
+            root_path,
+            root_canonical_path,
+            &relative_paths,
+            abs_paths,
+            Some(scan_job_tx.clone()),
+        )
+        .await;
+        drop(scan_job_tx);
+        self.scan_dirs(false, scan_job_rx).await;
+
+        let (scan_job_tx, scan_job_rx) = channel::unbounded();
+        self.update_ignore_statuses(scan_job_tx).await;
+        self.scan_dirs(false, scan_job_rx).await;
+
+        {
+            let mut state = self.state.lock();
+            state.reload_repositories(&relative_paths, self.fs.as_ref());
+            state.snapshot.completed_scan_id = state.snapshot.scan_id;
+            for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
+                state.scanned_dirs.remove(&entry_id);
+            }
+        }
+
+        self.send_status_update(false, None);
+    }
+
+    async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> bool {
+        let (scan_job_tx, mut scan_job_rx) = channel::unbounded();
+        {
+            let mut state = self.state.lock();
+            let root_path = state.snapshot.abs_path.clone();
+            for path in paths {
+                for ancestor in path.ancestors() {
+                    if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
+                        if entry.kind == EntryKind::UnloadedDir {
+                            let abs_path = root_path.join(ancestor);
+                            state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx);
+                            state.paths_to_scan.insert(path.clone());
+                            break;
+                        }
+                    }
+                }
+            }
+            drop(scan_job_tx);
+        }
+        while let Some(job) = scan_job_rx.next().await {
+            self.scan_dir(&job).await.log_err();
+        }
+
+        mem::take(&mut self.state.lock().paths_to_scan).len() > 0
+    }
+
+    async fn scan_dirs(
+        &self,
+        enable_progress_updates: bool,
+        scan_jobs_rx: channel::Receiver<ScanJob>,
+    ) {
+        use futures::FutureExt as _;
+
+        if self
+            .status_updates_tx
+            .unbounded_send(ScanState::Started)
+            .is_err()
+        {
+            return;
+        }
+
+        let progress_update_count = AtomicUsize::new(0);
+        self.executor
+            .scoped(|scope| {
+                for _ in 0..self.executor.num_cpus() {
+                    scope.spawn(async {
+                        let mut last_progress_update_count = 0;
+                        let progress_update_timer = self.progress_timer(enable_progress_updates).fuse();
+                        futures::pin_mut!(progress_update_timer);
+
+                        loop {
+                            select_biased! {
+                                // Process any path refresh requests before moving on to process
+                                // the scan queue, so that user operations are prioritized.
+                                request = self.scan_requests_rx.recv().fuse() => {
+                                    let Ok(request) = request else { break };
+                                    if !self.process_scan_request(request, true).await {
+                                        return;
+                                    }
+                                }
+
+                                // Send periodic progress updates to the worktree. Use an atomic counter
+                                // to ensure that only one of the workers sends a progress update after
+                                // the update interval elapses.
+                                _ = progress_update_timer => {
+                                    match progress_update_count.compare_exchange(
+                                        last_progress_update_count,
+                                        last_progress_update_count + 1,
+                                        SeqCst,
+                                        SeqCst
+                                    ) {
+                                        Ok(_) => {
+                                            last_progress_update_count += 1;
+                                            self.send_status_update(true, None);
+                                        }
+                                        Err(count) => {
+                                            last_progress_update_count = count;
+                                        }
+                                    }
+                                    progress_update_timer.set(self.progress_timer(enable_progress_updates).fuse());
+                                }
+
+                                // Recursively load directories from the file system.
+                                job = scan_jobs_rx.recv().fuse() => {
+                                    let Ok(job) = job else { break };
+                                    if let Err(err) = self.scan_dir(&job).await {
+                                        if job.path.as_ref() != Path::new("") {
+                                            log::error!("error scanning directory {:?}: {}", job.abs_path, err);
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    })
+                }
+            })
+            .await;
+    }
+
+    fn send_status_update(&self, scanning: bool, barrier: Option<barrier::Sender>) -> bool {
+        let mut state = self.state.lock();
+        if state.changed_paths.is_empty() && scanning {
+            return true;
+        }
+
+        let new_snapshot = state.snapshot.clone();
+        let old_snapshot = mem::replace(&mut state.prev_snapshot, new_snapshot.snapshot.clone());
+        let changes = self.build_change_set(&old_snapshot, &new_snapshot, &state.changed_paths);
+        state.changed_paths.clear();
+
+        self.status_updates_tx
+            .unbounded_send(ScanState::Updated {
+                snapshot: new_snapshot,
+                changes,
+                scanning,
+                barrier,
+            })
+            .is_ok()
+    }
+
+    async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
+        log::debug!("scan directory {:?}", job.path);
+
+        let mut ignore_stack = job.ignore_stack.clone();
+        let mut new_ignore = None;
+        let (root_abs_path, root_char_bag, next_entry_id) = {
+            let snapshot = &self.state.lock().snapshot;
+            (
+                snapshot.abs_path().clone(),
+                snapshot.root_char_bag,
+                self.next_entry_id.clone(),
+            )
+        };
+
+        let mut dotgit_path = None;
+        let mut root_canonical_path = None;
+        let mut new_entries: Vec<Entry> = Vec::new();
+        let mut new_jobs: Vec<Option<ScanJob>> = Vec::new();
+        let mut child_paths = self.fs.read_dir(&job.abs_path).await?;
+        while let Some(child_abs_path) = child_paths.next().await {
+            let child_abs_path: Arc<Path> = match child_abs_path {
+                Ok(child_abs_path) => child_abs_path.into(),
+                Err(error) => {
+                    log::error!("error processing entry {:?}", error);
+                    continue;
+                }
+            };
+
+            let child_name = child_abs_path.file_name().unwrap();
+            let child_path: Arc<Path> = job.path.join(child_name).into();
+            let child_metadata = match self.fs.metadata(&child_abs_path).await {
+                Ok(Some(metadata)) => metadata,
+                Ok(None) => continue,
+                Err(err) => {
+                    log::error!("error processing {:?}: {:?}", child_abs_path, err);
+                    continue;
+                }
+            };
+
+            // If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
+            if child_name == *GITIGNORE {
+                match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
+                    Ok(ignore) => {
+                        let ignore = Arc::new(ignore);
+                        ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
+                        new_ignore = Some(ignore);
+                    }
+                    Err(error) => {
+                        log::error!(
+                            "error loading .gitignore file {:?} - {:?}",
+                            child_name,
+                            error
+                        );
+                    }
+                }
+
+                // Update ignore status of any child entries we've already processed to reflect the
+                // ignore file in the current directory. Because `.gitignore` starts with a `.`,
+                // there should rarely be too numerous. Update the ignore stack associated with any
+                // new jobs as well.
+                let mut new_jobs = new_jobs.iter_mut();
+                for entry in &mut new_entries {
+                    let entry_abs_path = root_abs_path.join(&entry.path);
+                    entry.is_ignored =
+                        ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir());
+
+                    if entry.is_dir() {
+                        if let Some(job) = new_jobs.next().expect("missing scan job for entry") {
+                            job.ignore_stack = if entry.is_ignored {
+                                IgnoreStack::all()
+                            } else {
+                                ignore_stack.clone()
+                            };
+                        }
+                    }
+                }
+            }
+            // If we find a .git, we'll need to load the repository.
+            else if child_name == *DOT_GIT {
+                dotgit_path = Some(child_path.clone());
+            }
+
+            let mut child_entry = Entry::new(
+                child_path.clone(),
+                &child_metadata,
+                &next_entry_id,
+                root_char_bag,
+            );
+
+            if job.is_external {
+                child_entry.is_external = true;
+            } else if child_metadata.is_symlink {
+                let canonical_path = match self.fs.canonicalize(&child_abs_path).await {
+                    Ok(path) => path,
+                    Err(err) => {
+                        log::error!(
+                            "error reading target of symlink {:?}: {:?}",
+                            child_abs_path,
+                            err
+                        );
+                        continue;
+                    }
+                };
+
+                // lazily canonicalize the root path in order to determine if
+                // symlinks point outside of the worktree.
+                let root_canonical_path = match &root_canonical_path {
+                    Some(path) => path,
+                    None => match self.fs.canonicalize(&root_abs_path).await {
+                        Ok(path) => root_canonical_path.insert(path),
+                        Err(err) => {
+                            log::error!("error canonicalizing root {:?}: {:?}", root_abs_path, err);
+                            continue;
+                        }
+                    },
+                };
+
+                if !canonical_path.starts_with(root_canonical_path) {
+                    child_entry.is_external = true;
+                }
+            }
+
+            if child_entry.is_dir() {
+                child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
+
+                // Avoid recursing until crash in the case of a recursive symlink
+                if !job.ancestor_inodes.contains(&child_entry.inode) {
+                    let mut ancestor_inodes = job.ancestor_inodes.clone();
+                    ancestor_inodes.insert(child_entry.inode);
+
+                    new_jobs.push(Some(ScanJob {
+                        abs_path: child_abs_path,
+                        path: child_path,
+                        is_external: child_entry.is_external,
+                        ignore_stack: if child_entry.is_ignored {
+                            IgnoreStack::all()
+                        } else {
+                            ignore_stack.clone()
+                        },
+                        ancestor_inodes,
+                        scan_queue: job.scan_queue.clone(),
+                        containing_repository: job.containing_repository.clone(),
+                    }));
+                } else {
+                    new_jobs.push(None);
+                }
+            } else {
+                child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
+                if !child_entry.is_ignored {
+                    if let Some((repository_dir, repository, staged_statuses)) =
+                        &job.containing_repository
+                    {
+                        if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) {
+                            let repo_path = RepoPath(repo_path.into());
+                            child_entry.git_status = combine_git_statuses(
+                                staged_statuses.get(&repo_path).copied(),
+                                repository
+                                    .lock()
+                                    .unstaged_status(&repo_path, child_entry.mtime),
+                            );
+                        }
+                    }
+                }
+            }
+
+            new_entries.push(child_entry);
+        }
+
+        let mut state = self.state.lock();
+
+        // Identify any subdirectories that should not be scanned.
+        let mut job_ix = 0;
+        for entry in &mut new_entries {
+            state.reuse_entry_id(entry);
+            if entry.is_dir() {
+                if state.should_scan_directory(&entry) {
+                    job_ix += 1;
+                } else {
+                    log::debug!("defer scanning directory {:?}", entry.path);
+                    entry.kind = EntryKind::UnloadedDir;
+                    new_jobs.remove(job_ix);
+                }
+            }
+        }
+
+        state.populate_dir(&job.path, new_entries, new_ignore);
+
+        let repository =
+            dotgit_path.and_then(|path| state.build_git_repository(path, self.fs.as_ref()));
+
+        for new_job in new_jobs {
+            if let Some(mut new_job) = new_job {
+                if let Some(containing_repository) = &repository {
+                    new_job.containing_repository = Some(containing_repository.clone());
+                }
+
+                job.scan_queue
+                    .try_send(new_job)
+                    .expect("channel is unbounded");
+            }
+        }
+
+        Ok(())
+    }
+
+    async fn reload_entries_for_paths(
+        &self,
+        root_abs_path: Arc<Path>,
+        root_canonical_path: PathBuf,
+        relative_paths: &[Arc<Path>],
+        abs_paths: Vec<PathBuf>,
+        scan_queue_tx: Option<Sender<ScanJob>>,
+    ) {
+        let metadata = futures::future::join_all(
+            abs_paths
+                .iter()
+                .map(|abs_path| async move {
+                    let metadata = self.fs.metadata(&abs_path).await?;
+                    if let Some(metadata) = metadata {
+                        let canonical_path = self.fs.canonicalize(&abs_path).await?;
+                        anyhow::Ok(Some((metadata, canonical_path)))
+                    } else {
+                        Ok(None)
+                    }
+                })
+                .collect::<Vec<_>>(),
+        )
+        .await;
+
+        let mut state = self.state.lock();
+        let snapshot = &mut state.snapshot;
+        let is_idle = snapshot.completed_scan_id == snapshot.scan_id;
+        let doing_recursive_update = scan_queue_tx.is_some();
+        snapshot.scan_id += 1;
+        if is_idle && !doing_recursive_update {
+            snapshot.completed_scan_id = snapshot.scan_id;
+        }
+
+        // Remove any entries for paths that no longer exist or are being recursively
+        // refreshed. Do this before adding any new entries, so that renames can be
+        // detected regardless of the order of the paths.
+        for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
+            if matches!(metadata, Ok(None)) || doing_recursive_update {
+                log::trace!("remove path {:?}", path);
+                state.remove_path(path);
+            }
+        }
+
+        for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
+            let abs_path: Arc<Path> = root_abs_path.join(&path).into();
+            match metadata {
+                Ok(Some((metadata, canonical_path))) => {
+                    let ignore_stack = state
+                        .snapshot
+                        .ignore_stack_for_abs_path(&abs_path, metadata.is_dir);
+
+                    let mut fs_entry = Entry::new(
+                        path.clone(),
+                        metadata,
+                        self.next_entry_id.as_ref(),
+                        state.snapshot.root_char_bag,
+                    );
+                    fs_entry.is_ignored = ignore_stack.is_all();
+                    fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
+
+                    if !fs_entry.is_ignored {
+                        if !fs_entry.is_dir() {
+                            if let Some((work_dir, repo)) =
+                                state.snapshot.local_repo_for_path(&path)
+                            {
+                                if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
+                                    let repo_path = RepoPath(repo_path.into());
+                                    let repo = repo.repo_ptr.lock();
+                                    fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
+                                }
+                            }
+                        }
+                    }
+
+                    if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
+                        if state.should_scan_directory(&fs_entry) {
+                            state.enqueue_scan_dir(abs_path, &fs_entry, scan_queue_tx);
+                        } else {
+                            fs_entry.kind = EntryKind::UnloadedDir;
+                        }
+                    }
+
+                    state.insert_entry(fs_entry, self.fs.as_ref());
+                }
+                Ok(None) => {
+                    self.remove_repo_path(&path, &mut state.snapshot);
+                }
+                Err(err) => {
+                    // TODO - create a special 'error' entry in the entries tree to mark this
+                    log::error!("error reading file on event {:?}", err);
+                }
+            }
+        }
+
+        util::extend_sorted(
+            &mut state.changed_paths,
+            relative_paths.iter().cloned(),
+            usize::MAX,
+            Ord::cmp,
+        );
+    }
+
+    fn remove_repo_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> {
+        if !path
+            .components()
+            .any(|component| component.as_os_str() == *DOT_GIT)
+        {
+            if let Some(repository) = snapshot.repository_for_work_directory(path) {
+                let entry = repository.work_directory.0;
+                snapshot.git_repositories.remove(&entry);
+                snapshot
+                    .snapshot
+                    .repository_entries
+                    .remove(&RepositoryWorkDirectory(path.into()));
+                return Some(());
+            }
+        }
+
+        // TODO statuses
+        // Track when a .git is removed and iterate over the file system there
+
+        Some(())
+    }
+
+    async fn update_ignore_statuses(&self, scan_job_tx: Sender<ScanJob>) {
+        use futures::FutureExt as _;
+
+        let mut snapshot = self.state.lock().snapshot.clone();
+        let mut ignores_to_update = Vec::new();
+        let mut ignores_to_delete = Vec::new();
+        let abs_path = snapshot.abs_path.clone();
+        for (parent_abs_path, (_, needs_update)) in &mut snapshot.ignores_by_parent_abs_path {
+            if let Ok(parent_path) = parent_abs_path.strip_prefix(&abs_path) {
+                if *needs_update {
+                    *needs_update = false;
+                    if snapshot.snapshot.entry_for_path(parent_path).is_some() {
+                        ignores_to_update.push(parent_abs_path.clone());
+                    }
+                }
+
+                let ignore_path = parent_path.join(&*GITIGNORE);
+                if snapshot.snapshot.entry_for_path(ignore_path).is_none() {
+                    ignores_to_delete.push(parent_abs_path.clone());
+                }
+            }
+        }
+
+        for parent_abs_path in ignores_to_delete {
+            snapshot.ignores_by_parent_abs_path.remove(&parent_abs_path);
+            self.state
+                .lock()
+                .snapshot
+                .ignores_by_parent_abs_path
+                .remove(&parent_abs_path);
+        }
+
+        let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
+        ignores_to_update.sort_unstable();
+        let mut ignores_to_update = ignores_to_update.into_iter().peekable();
+        while let Some(parent_abs_path) = ignores_to_update.next() {
+            while ignores_to_update
+                .peek()
+                .map_or(false, |p| p.starts_with(&parent_abs_path))
+            {
+                ignores_to_update.next().unwrap();
+            }
+
+            let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true);
+            smol::block_on(ignore_queue_tx.send(UpdateIgnoreStatusJob {
+                abs_path: parent_abs_path,
+                ignore_stack,
+                ignore_queue: ignore_queue_tx.clone(),
+                scan_queue: scan_job_tx.clone(),
+            }))
+            .unwrap();
+        }
+        drop(ignore_queue_tx);
+
+        self.executor
+            .scoped(|scope| {
+                for _ in 0..self.executor.num_cpus() {
+                    scope.spawn(async {
+                        loop {
+                            select_biased! {
+                                // Process any path refresh requests before moving on to process
+                                // the queue of ignore statuses.
+                                request = self.scan_requests_rx.recv().fuse() => {
+                                    let Ok(request) = request else { break };
+                                    if !self.process_scan_request(request, true).await {
+                                        return;
+                                    }
+                                }
+
+                                // Recursively process directories whose ignores have changed.
+                                job = ignore_queue_rx.recv().fuse() => {
+                                    let Ok(job) = job else { break };
+                                    self.update_ignore_status(job, &snapshot).await;
+                                }
+                            }
+                        }
+                    });
+                }
+            })
+            .await;
+    }
+
+    async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
+        log::trace!("update ignore status {:?}", job.abs_path);
+
+        let mut ignore_stack = job.ignore_stack;
+        if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
+            ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
+        }
+
+        let mut entries_by_id_edits = Vec::new();
+        let mut entries_by_path_edits = Vec::new();
+        let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap();
+        for mut entry in snapshot.child_entries(path).cloned() {
+            let was_ignored = entry.is_ignored;
+            let abs_path: Arc<Path> = snapshot.abs_path().join(&entry.path).into();
+            entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir());
+            if entry.is_dir() {
+                let child_ignore_stack = if entry.is_ignored {
+                    IgnoreStack::all()
+                } else {
+                    ignore_stack.clone()
+                };
+
+                // Scan any directories that were previously ignored and weren't
+                // previously scanned.
+                if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
+                    let state = self.state.lock();
+                    if state.should_scan_directory(&entry) {
+                        state.enqueue_scan_dir(abs_path.clone(), &entry, &job.scan_queue);
+                    }
+                }
+
+                job.ignore_queue
+                    .send(UpdateIgnoreStatusJob {
+                        abs_path: abs_path.clone(),
+                        ignore_stack: child_ignore_stack,
+                        ignore_queue: job.ignore_queue.clone(),
+                        scan_queue: job.scan_queue.clone(),
+                    })
+                    .await
+                    .unwrap();
+            }
+
+            if entry.is_ignored != was_ignored {
+                let mut path_entry = snapshot.entries_by_id.get(&entry.id, &()).unwrap().clone();
+                path_entry.scan_id = snapshot.scan_id;
+                path_entry.is_ignored = entry.is_ignored;
+                entries_by_id_edits.push(Edit::Insert(path_entry));
+                entries_by_path_edits.push(Edit::Insert(entry));
+            }
+        }
+
+        let state = &mut self.state.lock();
+        for edit in &entries_by_path_edits {
+            if let Edit::Insert(entry) = edit {
+                if let Err(ix) = state.changed_paths.binary_search(&entry.path) {
+                    state.changed_paths.insert(ix, entry.path.clone());
+                }
+            }
+        }
+
+        state
+            .snapshot
+            .entries_by_path
+            .edit(entries_by_path_edits, &());
+        state.snapshot.entries_by_id.edit(entries_by_id_edits, &());
+    }
+
+    fn build_change_set(
+        &self,
+        old_snapshot: &Snapshot,
+        new_snapshot: &Snapshot,
+        event_paths: &[Arc<Path>],
+    ) -> UpdatedEntriesSet {
+        use BackgroundScannerPhase::*;
+        use PathChange::{Added, AddedOrUpdated, Loaded, Removed, Updated};
+
+        // Identify which paths have changed. Use the known set of changed
+        // parent paths to optimize the search.
+        let mut changes = Vec::new();
+        let mut old_paths = old_snapshot.entries_by_path.cursor::<PathKey>();
+        let mut new_paths = new_snapshot.entries_by_path.cursor::<PathKey>();
+        let mut last_newly_loaded_dir_path = None;
+        old_paths.next(&());
+        new_paths.next(&());
+        for path in event_paths {
+            let path = PathKey(path.clone());
+            if old_paths.item().map_or(false, |e| e.path < path.0) {
+                old_paths.seek_forward(&path, Bias::Left, &());
+            }
+            if new_paths.item().map_or(false, |e| e.path < path.0) {
+                new_paths.seek_forward(&path, Bias::Left, &());
+            }
+            loop {
+                match (old_paths.item(), new_paths.item()) {
+                    (Some(old_entry), Some(new_entry)) => {
+                        if old_entry.path > path.0
+                            && new_entry.path > path.0
+                            && !old_entry.path.starts_with(&path.0)
+                            && !new_entry.path.starts_with(&path.0)
+                        {
+                            break;
+                        }
+
+                        match Ord::cmp(&old_entry.path, &new_entry.path) {
+                            Ordering::Less => {
+                                changes.push((old_entry.path.clone(), old_entry.id, Removed));
+                                old_paths.next(&());
+                            }
+                            Ordering::Equal => {
+                                if self.phase == EventsReceivedDuringInitialScan {
+                                    if old_entry.id != new_entry.id {
+                                        changes.push((
+                                            old_entry.path.clone(),
+                                            old_entry.id,
+                                            Removed,
+                                        ));
+                                    }
+                                    // If the worktree was not fully initialized when this event was generated,
+                                    // we can't know whether this entry was added during the scan or whether
+                                    // it was merely updated.
+                                    changes.push((
+                                        new_entry.path.clone(),
+                                        new_entry.id,
+                                        AddedOrUpdated,
+                                    ));
+                                } else if old_entry.id != new_entry.id {
+                                    changes.push((old_entry.path.clone(), old_entry.id, Removed));
+                                    changes.push((new_entry.path.clone(), new_entry.id, Added));
+                                } else if old_entry != new_entry {
+                                    if old_entry.kind.is_unloaded() {
+                                        last_newly_loaded_dir_path = Some(&new_entry.path);
+                                        changes.push((
+                                            new_entry.path.clone(),
+                                            new_entry.id,
+                                            Loaded,
+                                        ));
+                                    } else {
+                                        changes.push((
+                                            new_entry.path.clone(),
+                                            new_entry.id,
+                                            Updated,
+                                        ));
+                                    }
+                                }
+                                old_paths.next(&());
+                                new_paths.next(&());
+                            }
+                            Ordering::Greater => {
+                                let is_newly_loaded = self.phase == InitialScan
+                                    || last_newly_loaded_dir_path
+                                        .as_ref()
+                                        .map_or(false, |dir| new_entry.path.starts_with(&dir));
+                                changes.push((
+                                    new_entry.path.clone(),
+                                    new_entry.id,
+                                    if is_newly_loaded { Loaded } else { Added },
+                                ));
+                                new_paths.next(&());
+                            }
+                        }
+                    }
+                    (Some(old_entry), None) => {
+                        changes.push((old_entry.path.clone(), old_entry.id, Removed));
+                        old_paths.next(&());
+                    }
+                    (None, Some(new_entry)) => {
+                        let is_newly_loaded = self.phase == InitialScan
+                            || last_newly_loaded_dir_path
+                                .as_ref()
+                                .map_or(false, |dir| new_entry.path.starts_with(&dir));
+                        changes.push((
+                            new_entry.path.clone(),
+                            new_entry.id,
+                            if is_newly_loaded { Loaded } else { Added },
+                        ));
+                        new_paths.next(&());
+                    }
+                    (None, None) => break,
+                }
+            }
+        }
+
+        changes.into()
+    }
+
+    async fn progress_timer(&self, running: bool) {
+        if !running {
+            return futures::future::pending().await;
+        }
+
+        #[cfg(any(test, feature = "test-support"))]
+        if self.fs.is_fake() {
+            return self.executor.simulate_random_delay().await;
+        }
+
+        smol::Timer::after(Duration::from_millis(100)).await;
+    }
+}
+
+fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
+    let mut result = root_char_bag;
+    result.extend(
+        path.to_string_lossy()
+            .chars()
+            .map(|c| c.to_ascii_lowercase()),
+    );
+    result
+}
+
+struct ScanJob {
+    abs_path: Arc<Path>,
+    path: Arc<Path>,
+    ignore_stack: Arc<IgnoreStack>,
+    scan_queue: Sender<ScanJob>,
+    ancestor_inodes: TreeSet<u64>,
+    is_external: bool,
+    containing_repository: Option<(
+        RepositoryWorkDirectory,
+        Arc<Mutex<dyn GitRepository>>,
+        TreeMap<RepoPath, GitFileStatus>,
+    )>,
+}
+
+struct UpdateIgnoreStatusJob {
+    abs_path: Arc<Path>,
+    ignore_stack: Arc<IgnoreStack>,
+    ignore_queue: Sender<UpdateIgnoreStatusJob>,
+    scan_queue: Sender<ScanJob>,
+}
+
+// todo!("re-enable when we have tests")
+// pub trait WorktreeModelHandle {
+// #[cfg(any(test, feature = "test-support"))]
+// fn flush_fs_events<'a>(
+//     &self,
+//     cx: &'a gpui::TestAppContext,
+// ) -> futures::future::LocalBoxFuture<'a, ()>;
+// }
+
+// impl WorktreeModelHandle for Handle<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.
+//     //
+//     // This function mutates the worktree's directory and waits for those mutations to be picked up,
+//     // to ensure that all redundant FS events have already been processed.
+//     #[cfg(any(test, feature = "test-support"))]
+//     fn flush_fs_events<'a>(
+//         &self,
+//         cx: &'a gpui::TestAppContext,
+//     ) -> futures::future::LocalBoxFuture<'a, ()> {
+//         let filename = "fs-event-sentinel";
+//         let tree = self.clone();
+//         let (fs, root_path) = self.read_with(cx, |tree, _| {
+//             let tree = tree.as_local().unwrap();
+//             (tree.fs.clone(), tree.abs_path().clone())
+//         });
+
+//         async move {
+//             fs.create_file(&root_path.join(filename), Default::default())
+//                 .await
+//                 .unwrap();
+//             tree.condition(cx, |tree, _| tree.entry_for_path(filename).is_some())
+//                 .await;
+
+//             fs.remove_file(&root_path.join(filename), Default::default())
+//                 .await
+//                 .unwrap();
+//             tree.condition(cx, |tree, _| tree.entry_for_path(filename).is_none())
+//                 .await;
+
+//             cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//                 .await;
+//         }
+//         .boxed_local()
+//     }
+// }
+
+#[derive(Clone, Debug)]
+struct TraversalProgress<'a> {
+    max_path: &'a Path,
+    count: usize,
+    non_ignored_count: usize,
+    file_count: usize,
+    non_ignored_file_count: usize,
+}
+
+impl<'a> TraversalProgress<'a> {
+    fn count(&self, include_dirs: bool, include_ignored: bool) -> usize {
+        match (include_ignored, include_dirs) {
+            (true, true) => self.count,
+            (true, false) => self.file_count,
+            (false, true) => self.non_ignored_count,
+            (false, false) => self.non_ignored_file_count,
+        }
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, EntrySummary> for TraversalProgress<'a> {
+    fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
+        self.max_path = summary.max_path.as_ref();
+        self.count += summary.count;
+        self.non_ignored_count += summary.non_ignored_count;
+        self.file_count += summary.file_count;
+        self.non_ignored_file_count += summary.non_ignored_file_count;
+    }
+}
+
+impl<'a> Default for TraversalProgress<'a> {
+    fn default() -> Self {
+        Self {
+            max_path: Path::new(""),
+            count: 0,
+            non_ignored_count: 0,
+            file_count: 0,
+            non_ignored_file_count: 0,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default, Copy)]
+struct GitStatuses {
+    added: usize,
+    modified: usize,
+    conflict: usize,
+}
+
+impl AddAssign for GitStatuses {
+    fn add_assign(&mut self, rhs: Self) {
+        self.added += rhs.added;
+        self.modified += rhs.modified;
+        self.conflict += rhs.conflict;
+    }
+}
+
+impl Sub for GitStatuses {
+    type Output = GitStatuses;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        GitStatuses {
+            added: self.added - rhs.added,
+            modified: self.modified - rhs.modified,
+            conflict: self.conflict - rhs.conflict,
+        }
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, EntrySummary> for GitStatuses {
+    fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
+        *self += summary.statuses
+    }
+}
+
+pub struct Traversal<'a> {
+    cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>,
+    include_ignored: bool,
+    include_dirs: bool,
+}
+
+impl<'a> Traversal<'a> {
+    pub fn advance(&mut self) -> bool {
+        self.cursor.seek_forward(
+            &TraversalTarget::Count {
+                count: self.end_offset() + 1,
+                include_dirs: self.include_dirs,
+                include_ignored: self.include_ignored,
+            },
+            Bias::Left,
+            &(),
+        )
+    }
+
+    pub fn advance_to_sibling(&mut self) -> bool {
+        while let Some(entry) = self.cursor.item() {
+            self.cursor.seek_forward(
+                &TraversalTarget::PathSuccessor(&entry.path),
+                Bias::Left,
+                &(),
+            );
+            if let Some(entry) = self.cursor.item() {
+                if (self.include_dirs || !entry.is_dir())
+                    && (self.include_ignored || !entry.is_ignored)
+                {
+                    return true;
+                }
+            }
+        }
+        false
+    }
+
+    pub fn entry(&self) -> Option<&'a Entry> {
+        self.cursor.item()
+    }
+
+    pub fn start_offset(&self) -> usize {
+        self.cursor
+            .start()
+            .count(self.include_dirs, self.include_ignored)
+    }
+
+    pub fn end_offset(&self) -> usize {
+        self.cursor
+            .end(&())
+            .count(self.include_dirs, self.include_ignored)
+    }
+}
+
+impl<'a> Iterator for Traversal<'a> {
+    type Item = &'a Entry;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(item) = self.entry() {
+            self.advance();
+            Some(item)
+        } else {
+            None
+        }
+    }
+}
+
+#[derive(Debug)]
+enum TraversalTarget<'a> {
+    Path(&'a Path),
+    PathSuccessor(&'a Path),
+    Count {
+        count: usize,
+        include_ignored: bool,
+        include_dirs: bool,
+    },
+}
+
+impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget<'b> {
+    fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering {
+        match self {
+            TraversalTarget::Path(path) => path.cmp(&cursor_location.max_path),
+            TraversalTarget::PathSuccessor(path) => {
+                if !cursor_location.max_path.starts_with(path) {
+                    Ordering::Equal
+                } else {
+                    Ordering::Greater
+                }
+            }
+            TraversalTarget::Count {
+                count,
+                include_dirs,
+                include_ignored,
+            } => Ord::cmp(
+                count,
+                &cursor_location.count(*include_dirs, *include_ignored),
+            ),
+        }
+    }
+}
+
+impl<'a, 'b> SeekTarget<'a, EntrySummary, (TraversalProgress<'a>, GitStatuses)>
+    for TraversalTarget<'b>
+{
+    fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitStatuses), _: &()) -> Ordering {
+        self.cmp(&cursor_location.0, &())
+    }
+}
+
+struct ChildEntriesIter<'a> {
+    parent_path: &'a Path,
+    traversal: Traversal<'a>,
+}
+
+impl<'a> Iterator for ChildEntriesIter<'a> {
+    type Item = &'a Entry;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(item) = self.traversal.entry() {
+            if item.path.starts_with(&self.parent_path) {
+                self.traversal.advance_to_sibling();
+                return Some(item);
+            }
+        }
+        None
+    }
+}
+
+pub struct DescendentEntriesIter<'a> {
+    parent_path: &'a Path,
+    traversal: Traversal<'a>,
+}
+
+impl<'a> Iterator for DescendentEntriesIter<'a> {
+    type Item = &'a Entry;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(item) = self.traversal.entry() {
+            if item.path.starts_with(&self.parent_path) {
+                self.traversal.advance();
+                return Some(item);
+            }
+        }
+        None
+    }
+}
+
+impl<'a> From<&'a Entry> for proto::Entry {
+    fn from(entry: &'a Entry) -> Self {
+        Self {
+            id: entry.id.to_proto(),
+            is_dir: entry.is_dir(),
+            path: entry.path.to_string_lossy().into(),
+            inode: entry.inode,
+            mtime: Some(entry.mtime.into()),
+            is_symlink: entry.is_symlink,
+            is_ignored: entry.is_ignored,
+            is_external: entry.is_external,
+            git_status: entry.git_status.map(git_status_to_proto),
+        }
+    }
+}
+
+impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
+    type Error = anyhow::Error;
+
+    fn try_from((root_char_bag, entry): (&'a CharBag, proto::Entry)) -> Result<Self> {
+        if let Some(mtime) = entry.mtime {
+            let kind = if entry.is_dir {
+                EntryKind::Dir
+            } else {
+                let mut char_bag = *root_char_bag;
+                char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase()));
+                EntryKind::File(char_bag)
+            };
+            let path: Arc<Path> = PathBuf::from(entry.path).into();
+            Ok(Entry {
+                id: ProjectEntryId::from_proto(entry.id),
+                kind,
+                path,
+                inode: entry.inode,
+                mtime: mtime.into(),
+                is_symlink: entry.is_symlink,
+                is_ignored: entry.is_ignored,
+                is_external: entry.is_external,
+                git_status: git_status_from_proto(entry.git_status),
+            })
+        } else {
+            Err(anyhow!(
+                "missing mtime in remote worktree entry {:?}",
+                entry.path
+            ))
+        }
+    }
+}
+
+fn combine_git_statuses(
+    staged: Option<GitFileStatus>,
+    unstaged: Option<GitFileStatus>,
+) -> Option<GitFileStatus> {
+    if let Some(staged) = staged {
+        if let Some(unstaged) = unstaged {
+            if unstaged != staged {
+                Some(GitFileStatus::Modified)
+            } else {
+                Some(staged)
+            }
+        } else {
+            Some(staged)
+        }
+    } else {
+        unstaged
+    }
+}
+
+fn git_status_from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
+    git_status.and_then(|status| {
+        proto::GitStatus::from_i32(status).map(|status| match status {
+            proto::GitStatus::Added => GitFileStatus::Added,
+            proto::GitStatus::Modified => GitFileStatus::Modified,
+            proto::GitStatus::Conflict => GitFileStatus::Conflict,
+        })
+    })
+}
+
+fn git_status_to_proto(status: GitFileStatus) -> i32 {
+    match status {
+        GitFileStatus::Added => proto::GitStatus::Added as i32,
+        GitFileStatus::Modified => proto::GitStatus::Modified as i32,
+        GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
+    }
+}

crates/project2/src/worktree_tests.rs 🔗

@@ -0,0 +1,2141 @@
+// use crate::{
+//     worktree::{Event, Snapshot, WorktreeModelHandle},
+//     Entry, EntryKind, PathChange, Worktree,
+// };
+// use anyhow::Result;
+// use client2::Client;
+// use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
+// use git::GITIGNORE;
+// use gpui::{executor::Deterministic, ModelContext, Task, TestAppContext};
+// use parking_lot::Mutex;
+// use postage::stream::Stream;
+// use pretty_assertions::assert_eq;
+// use rand::prelude::*;
+// use serde_json::json;
+// use std::{
+//     env,
+//     fmt::Write,
+//     mem,
+//     path::{Path, PathBuf},
+//     sync::Arc,
+// };
+// use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
+
+// #[gpui::test]
+// async fn test_traversal(cx: &mut TestAppContext) {
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//            ".gitignore": "a/b\n",
+//            "a": {
+//                "b": "",
+//                "c": "",
+//            }
+//         }),
+//     )
+//     .await;
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         Path::new("/root"),
+//         true,
+//         fs,
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.entries(false)
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 Path::new(""),
+//                 Path::new(".gitignore"),
+//                 Path::new("a"),
+//                 Path::new("a/c"),
+//             ]
+//         );
+//         assert_eq!(
+//             tree.entries(true)
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 Path::new(""),
+//                 Path::new(".gitignore"),
+//                 Path::new("a"),
+//                 Path::new("a/b"),
+//                 Path::new("a/c"),
+//             ]
+//         );
+//     })
+// }
+
+// #[gpui::test]
+// async fn test_descendent_entries(cx: &mut TestAppContext) {
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//             "a": "",
+//             "b": {
+//                "c": {
+//                    "d": ""
+//                },
+//                "e": {}
+//             },
+//             "f": "",
+//             "g": {
+//                 "h": {}
+//             },
+//             "i": {
+//                 "j": {
+//                     "k": ""
+//                 },
+//                 "l": {
+
+//                 }
+//             },
+//             ".gitignore": "i/j\n",
+//         }),
+//     )
+//     .await;
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         Path::new("/root"),
+//         true,
+//         fs,
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.descendent_entries(false, false, Path::new("b"))
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             vec![Path::new("b/c/d"),]
+//         );
+//         assert_eq!(
+//             tree.descendent_entries(true, false, Path::new("b"))
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 Path::new("b"),
+//                 Path::new("b/c"),
+//                 Path::new("b/c/d"),
+//                 Path::new("b/e"),
+//             ]
+//         );
+
+//         assert_eq!(
+//             tree.descendent_entries(false, false, Path::new("g"))
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             Vec::<PathBuf>::new()
+//         );
+//         assert_eq!(
+//             tree.descendent_entries(true, false, Path::new("g"))
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             vec![Path::new("g"), Path::new("g/h"),]
+//         );
+//     });
+
+//     // Expand gitignored directory.
+//     tree.read_with(cx, |tree, _| {
+//         tree.as_local()
+//             .unwrap()
+//             .refresh_entries_for_paths(vec![Path::new("i/j").into()])
+//     })
+//     .recv()
+//     .await;
+
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.descendent_entries(false, false, Path::new("i"))
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             Vec::<PathBuf>::new()
+//         );
+//         assert_eq!(
+//             tree.descendent_entries(false, true, Path::new("i"))
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             vec![Path::new("i/j/k")]
+//         );
+//         assert_eq!(
+//             tree.descendent_entries(true, false, Path::new("i"))
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             vec![Path::new("i"), Path::new("i/l"),]
+//         );
+//     })
+// }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//             "lib": {
+//                 "a": {
+//                     "a.txt": ""
+//                 },
+//                 "b": {
+//                     "b.txt": ""
+//                 }
+//             }
+//         }),
+//     )
+//     .await;
+//     fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
+//     fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         Path::new("/root"),
+//         true,
+//         fs.clone(),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.entries(false)
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 Path::new(""),
+//                 Path::new("lib"),
+//                 Path::new("lib/a"),
+//                 Path::new("lib/a/a.txt"),
+//                 Path::new("lib/a/lib"),
+//                 Path::new("lib/b"),
+//                 Path::new("lib/b/b.txt"),
+//                 Path::new("lib/b/lib"),
+//             ]
+//         );
+//     });
+
+//     fs.rename(
+//         Path::new("/root/lib/a/lib"),
+//         Path::new("/root/lib/a/lib-2"),
+//         Default::default(),
+//     )
+//     .await
+//     .unwrap();
+//     executor.run_until_parked();
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.entries(false)
+//                 .map(|entry| entry.path.as_ref())
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 Path::new(""),
+//                 Path::new("lib"),
+//                 Path::new("lib/a"),
+//                 Path::new("lib/a/a.txt"),
+//                 Path::new("lib/a/lib-2"),
+//                 Path::new("lib/b"),
+//                 Path::new("lib/b/b.txt"),
+//                 Path::new("lib/b/lib"),
+//             ]
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//             "dir1": {
+//                 "deps": {
+//                     // symlinks here
+//                 },
+//                 "src": {
+//                     "a.rs": "",
+//                     "b.rs": "",
+//                 },
+//             },
+//             "dir2": {
+//                 "src": {
+//                     "c.rs": "",
+//                     "d.rs": "",
+//                 }
+//             },
+//             "dir3": {
+//                 "deps": {},
+//                 "src": {
+//                     "e.rs": "",
+//                     "f.rs": "",
+//                 },
+//             }
+//         }),
+//     )
+//     .await;
+
+//     // These symlinks point to directories outside of the worktree's root, dir1.
+//     fs.insert_symlink("/root/dir1/deps/dep-dir2", "../../dir2".into())
+//         .await;
+//     fs.insert_symlink("/root/dir1/deps/dep-dir3", "../../dir3".into())
+//         .await;
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         Path::new("/root/dir1"),
+//         true,
+//         fs.clone(),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+
+//     let tree_updates = Arc::new(Mutex::new(Vec::new()));
+//     tree.update(cx, |_, cx| {
+//         let tree_updates = tree_updates.clone();
+//         cx.subscribe(&tree, move |_, _, event, _| {
+//             if let Event::UpdatedEntries(update) = event {
+//                 tree_updates.lock().extend(
+//                     update
+//                         .iter()
+//                         .map(|(path, _, change)| (path.clone(), *change)),
+//                 );
+//             }
+//         })
+//         .detach();
+//     });
+
+//     // The symlinked directories are not scanned by default.
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.entries(true)
+//                 .map(|entry| (entry.path.as_ref(), entry.is_external))
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 (Path::new(""), false),
+//                 (Path::new("deps"), false),
+//                 (Path::new("deps/dep-dir2"), true),
+//                 (Path::new("deps/dep-dir3"), true),
+//                 (Path::new("src"), false),
+//                 (Path::new("src/a.rs"), false),
+//                 (Path::new("src/b.rs"), false),
+//             ]
+//         );
+
+//         assert_eq!(
+//             tree.entry_for_path("deps/dep-dir2").unwrap().kind,
+//             EntryKind::UnloadedDir
+//         );
+//     });
+
+//     // Expand one of the symlinked directories.
+//     tree.read_with(cx, |tree, _| {
+//         tree.as_local()
+//             .unwrap()
+//             .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
+//     })
+//     .recv()
+//     .await;
+
+//     // The expanded directory's contents are loaded. Subdirectories are
+//     // not scanned yet.
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.entries(true)
+//                 .map(|entry| (entry.path.as_ref(), entry.is_external))
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 (Path::new(""), false),
+//                 (Path::new("deps"), false),
+//                 (Path::new("deps/dep-dir2"), true),
+//                 (Path::new("deps/dep-dir3"), true),
+//                 (Path::new("deps/dep-dir3/deps"), true),
+//                 (Path::new("deps/dep-dir3/src"), true),
+//                 (Path::new("src"), false),
+//                 (Path::new("src/a.rs"), false),
+//                 (Path::new("src/b.rs"), false),
+//             ]
+//         );
+//     });
+//     assert_eq!(
+//         mem::take(&mut *tree_updates.lock()),
+//         &[
+//             (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
+//             (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
+//             (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
+//         ]
+//     );
+
+//     // Expand a subdirectory of one of the symlinked directories.
+//     tree.read_with(cx, |tree, _| {
+//         tree.as_local()
+//             .unwrap()
+//             .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
+//     })
+//     .recv()
+//     .await;
+
+//     // The expanded subdirectory's contents are loaded.
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.entries(true)
+//                 .map(|entry| (entry.path.as_ref(), entry.is_external))
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 (Path::new(""), false),
+//                 (Path::new("deps"), false),
+//                 (Path::new("deps/dep-dir2"), true),
+//                 (Path::new("deps/dep-dir3"), true),
+//                 (Path::new("deps/dep-dir3/deps"), true),
+//                 (Path::new("deps/dep-dir3/src"), true),
+//                 (Path::new("deps/dep-dir3/src/e.rs"), true),
+//                 (Path::new("deps/dep-dir3/src/f.rs"), true),
+//                 (Path::new("src"), false),
+//                 (Path::new("src/a.rs"), false),
+//                 (Path::new("src/b.rs"), false),
+//             ]
+//         );
+//     });
+
+//     assert_eq!(
+//         mem::take(&mut *tree_updates.lock()),
+//         &[
+//             (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
+//             (
+//                 Path::new("deps/dep-dir3/src/e.rs").into(),
+//                 PathChange::Loaded
+//             ),
+//             (
+//                 Path::new("deps/dep-dir3/src/f.rs").into(),
+//                 PathChange::Loaded
+//             )
+//         ]
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_open_gitignored_files(cx: &mut TestAppContext) {
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//             ".gitignore": "node_modules\n",
+//             "one": {
+//                 "node_modules": {
+//                     "a": {
+//                         "a1.js": "a1",
+//                         "a2.js": "a2",
+//                     },
+//                     "b": {
+//                         "b1.js": "b1",
+//                         "b2.js": "b2",
+//                     },
+//                     "c": {
+//                         "c1.js": "c1",
+//                         "c2.js": "c2",
+//                     }
+//                 },
+//             },
+//             "two": {
+//                 "x.js": "",
+//                 "y.js": "",
+//             },
+//         }),
+//     )
+//     .await;
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         Path::new("/root"),
+//         true,
+//         fs.clone(),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.entries(true)
+//                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 (Path::new(""), false),
+//                 (Path::new(".gitignore"), false),
+//                 (Path::new("one"), false),
+//                 (Path::new("one/node_modules"), true),
+//                 (Path::new("two"), false),
+//                 (Path::new("two/x.js"), false),
+//                 (Path::new("two/y.js"), false),
+//             ]
+//         );
+//     });
+
+//     // Open a file that is nested inside of a gitignored directory that
+//     // has not yet been expanded.
+//     let prev_read_dir_count = fs.read_dir_call_count();
+//     let buffer = tree
+//         .update(cx, |tree, cx| {
+//             tree.as_local_mut()
+//                 .unwrap()
+//                 .load_buffer(0, "one/node_modules/b/b1.js".as_ref(), cx)
+//         })
+//         .await
+//         .unwrap();
+
+//     tree.read_with(cx, |tree, cx| {
+//         assert_eq!(
+//             tree.entries(true)
+//                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 (Path::new(""), false),
+//                 (Path::new(".gitignore"), false),
+//                 (Path::new("one"), false),
+//                 (Path::new("one/node_modules"), true),
+//                 (Path::new("one/node_modules/a"), true),
+//                 (Path::new("one/node_modules/b"), true),
+//                 (Path::new("one/node_modules/b/b1.js"), true),
+//                 (Path::new("one/node_modules/b/b2.js"), true),
+//                 (Path::new("one/node_modules/c"), true),
+//                 (Path::new("two"), false),
+//                 (Path::new("two/x.js"), false),
+//                 (Path::new("two/y.js"), false),
+//             ]
+//         );
+
+//         assert_eq!(
+//             buffer.read(cx).file().unwrap().path().as_ref(),
+//             Path::new("one/node_modules/b/b1.js")
+//         );
+
+//         // Only the newly-expanded directories are scanned.
+//         assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
+//     });
+
+//     // Open another file in a different subdirectory of the same
+//     // gitignored directory.
+//     let prev_read_dir_count = fs.read_dir_call_count();
+//     let buffer = tree
+//         .update(cx, |tree, cx| {
+//             tree.as_local_mut()
+//                 .unwrap()
+//                 .load_buffer(0, "one/node_modules/a/a2.js".as_ref(), cx)
+//         })
+//         .await
+//         .unwrap();
+
+//     tree.read_with(cx, |tree, cx| {
+//         assert_eq!(
+//             tree.entries(true)
+//                 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+//                 .collect::<Vec<_>>(),
+//             vec![
+//                 (Path::new(""), false),
+//                 (Path::new(".gitignore"), false),
+//                 (Path::new("one"), false),
+//                 (Path::new("one/node_modules"), true),
+//                 (Path::new("one/node_modules/a"), true),
+//                 (Path::new("one/node_modules/a/a1.js"), true),
+//                 (Path::new("one/node_modules/a/a2.js"), true),
+//                 (Path::new("one/node_modules/b"), true),
+//                 (Path::new("one/node_modules/b/b1.js"), true),
+//                 (Path::new("one/node_modules/b/b2.js"), true),
+//                 (Path::new("one/node_modules/c"), true),
+//                 (Path::new("two"), false),
+//                 (Path::new("two/x.js"), false),
+//                 (Path::new("two/y.js"), false),
+//             ]
+//         );
+
+//         assert_eq!(
+//             buffer.read(cx).file().unwrap().path().as_ref(),
+//             Path::new("one/node_modules/a/a2.js")
+//         );
+
+//         // Only the newly-expanded directory is scanned.
+//         assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
+//     });
+
+//     // No work happens when files and directories change within an unloaded directory.
+//     let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
+//     fs.create_dir("/root/one/node_modules/c/lib".as_ref())
+//         .await
+//         .unwrap();
+//     cx.foreground().run_until_parked();
+//     assert_eq!(
+//         fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count,
+//         0
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//             ".gitignore": "node_modules\n",
+//             "a": {
+//                 "a.js": "",
+//             },
+//             "b": {
+//                 "b.js": "",
+//             },
+//             "node_modules": {
+//                 "c": {
+//                     "c.js": "",
+//                 },
+//                 "d": {
+//                     "d.js": "",
+//                     "e": {
+//                         "e1.js": "",
+//                         "e2.js": "",
+//                     },
+//                     "f": {
+//                         "f1.js": "",
+//                         "f2.js": "",
+//                     }
+//                 },
+//             },
+//         }),
+//     )
+//     .await;
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         Path::new("/root"),
+//         true,
+//         fs.clone(),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+
+//     // Open a file within the gitignored directory, forcing some of its
+//     // subdirectories to be read, but not all.
+//     let read_dir_count_1 = fs.read_dir_call_count();
+//     tree.read_with(cx, |tree, _| {
+//         tree.as_local()
+//             .unwrap()
+//             .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
+//     })
+//     .recv()
+//     .await;
+
+//     // Those subdirectories are now loaded.
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.entries(true)
+//                 .map(|e| (e.path.as_ref(), e.is_ignored))
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 (Path::new(""), false),
+//                 (Path::new(".gitignore"), false),
+//                 (Path::new("a"), false),
+//                 (Path::new("a/a.js"), false),
+//                 (Path::new("b"), false),
+//                 (Path::new("b/b.js"), false),
+//                 (Path::new("node_modules"), true),
+//                 (Path::new("node_modules/c"), true),
+//                 (Path::new("node_modules/d"), true),
+//                 (Path::new("node_modules/d/d.js"), true),
+//                 (Path::new("node_modules/d/e"), true),
+//                 (Path::new("node_modules/d/f"), true),
+//             ]
+//         );
+//     });
+//     let read_dir_count_2 = fs.read_dir_call_count();
+//     assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
+
+//     // Update the gitignore so that node_modules is no longer ignored,
+//     // but a subdirectory is ignored
+//     fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
+//         .await
+//         .unwrap();
+//     cx.foreground().run_until_parked();
+
+//     // All of the directories that are no longer ignored are now loaded.
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(
+//             tree.entries(true)
+//                 .map(|e| (e.path.as_ref(), e.is_ignored))
+//                 .collect::<Vec<_>>(),
+//             &[
+//                 (Path::new(""), false),
+//                 (Path::new(".gitignore"), false),
+//                 (Path::new("a"), false),
+//                 (Path::new("a/a.js"), false),
+//                 (Path::new("b"), false),
+//                 (Path::new("b/b.js"), false),
+//                 // This directory is no longer ignored
+//                 (Path::new("node_modules"), false),
+//                 (Path::new("node_modules/c"), false),
+//                 (Path::new("node_modules/c/c.js"), false),
+//                 (Path::new("node_modules/d"), false),
+//                 (Path::new("node_modules/d/d.js"), false),
+//                 // This subdirectory is now ignored
+//                 (Path::new("node_modules/d/e"), true),
+//                 (Path::new("node_modules/d/f"), false),
+//                 (Path::new("node_modules/d/f/f1.js"), false),
+//                 (Path::new("node_modules/d/f/f2.js"), false),
+//             ]
+//         );
+//     });
+
+//     // Each of the newly-loaded directories is scanned only once.
+//     let read_dir_count_3 = fs.read_dir_call_count();
+//     assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
+// }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//             ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
+//             "tree": {
+//                 ".git": {},
+//                 ".gitignore": "ignored-dir\n",
+//                 "tracked-dir": {
+//                     "tracked-file1": "",
+//                     "ancestor-ignored-file1": "",
+//                 },
+//                 "ignored-dir": {
+//                     "ignored-file1": ""
+//                 }
+//             }
+//         }),
+//     )
+//     .await;
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         "/root/tree".as_ref(),
+//         true,
+//         fs.clone(),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+
+//     tree.read_with(cx, |tree, _| {
+//         tree.as_local()
+//             .unwrap()
+//             .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
+//     })
+//     .recv()
+//     .await;
+
+//     cx.read(|cx| {
+//         let tree = tree.read(cx);
+//         assert!(
+//             !tree
+//                 .entry_for_path("tracked-dir/tracked-file1")
+//                 .unwrap()
+//                 .is_ignored
+//         );
+//         assert!(
+//             tree.entry_for_path("tracked-dir/ancestor-ignored-file1")
+//                 .unwrap()
+//                 .is_ignored
+//         );
+//         assert!(
+//             tree.entry_for_path("ignored-dir/ignored-file1")
+//                 .unwrap()
+//                 .is_ignored
+//         );
+//     });
+
+//     fs.create_file(
+//         "/root/tree/tracked-dir/tracked-file2".as_ref(),
+//         Default::default(),
+//     )
+//     .await
+//     .unwrap();
+//     fs.create_file(
+//         "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(),
+//         Default::default(),
+//     )
+//     .await
+//     .unwrap();
+//     fs.create_file(
+//         "/root/tree/ignored-dir/ignored-file2".as_ref(),
+//         Default::default(),
+//     )
+//     .await
+//     .unwrap();
+
+//     cx.foreground().run_until_parked();
+//     cx.read(|cx| {
+//         let tree = tree.read(cx);
+//         assert!(
+//             !tree
+//                 .entry_for_path("tracked-dir/tracked-file2")
+//                 .unwrap()
+//                 .is_ignored
+//         );
+//         assert!(
+//             tree.entry_for_path("tracked-dir/ancestor-ignored-file2")
+//                 .unwrap()
+//                 .is_ignored
+//         );
+//         assert!(
+//             tree.entry_for_path("ignored-dir/ignored-file2")
+//                 .unwrap()
+//                 .is_ignored
+//         );
+//         assert!(tree.entry_for_path(".git").unwrap().is_ignored);
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_write_file(cx: &mut TestAppContext) {
+//     let dir = temp_tree(json!({
+//         ".git": {},
+//         ".gitignore": "ignored-dir\n",
+//         "tracked-dir": {},
+//         "ignored-dir": {}
+//     }));
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         dir.path(),
+//         true,
+//         Arc::new(RealFs),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+//     tree.flush_fs_events(cx).await;
+
+//     tree.update(cx, |tree, cx| {
+//         tree.as_local().unwrap().write_file(
+//             Path::new("tracked-dir/file.txt"),
+//             "hello".into(),
+//             Default::default(),
+//             cx,
+//         )
+//     })
+//     .await
+//     .unwrap();
+//     tree.update(cx, |tree, cx| {
+//         tree.as_local().unwrap().write_file(
+//             Path::new("ignored-dir/file.txt"),
+//             "world".into(),
+//             Default::default(),
+//             cx,
+//         )
+//     })
+//     .await
+//     .unwrap();
+
+//     tree.read_with(cx, |tree, _| {
+//         let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
+//         let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
+//         assert!(!tracked.is_ignored);
+//         assert!(ignored.is_ignored);
+//     });
+// }
+
+// #[gpui::test(iterations = 30)]
+// async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//             "b": {},
+//             "c": {},
+//             "d": {},
+//         }),
+//     )
+//     .await;
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         "/root".as_ref(),
+//         true,
+//         fs,
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     let snapshot1 = tree.update(cx, |tree, cx| {
+//         let tree = tree.as_local_mut().unwrap();
+//         let snapshot = Arc::new(Mutex::new(tree.snapshot()));
+//         let _ = tree.observe_updates(0, cx, {
+//             let snapshot = snapshot.clone();
+//             move |update| {
+//                 snapshot.lock().apply_remote_update(update).unwrap();
+//                 async { true }
+//             }
+//         });
+//         snapshot
+//     });
+
+//     let entry = tree
+//         .update(cx, |tree, cx| {
+//             tree.as_local_mut()
+//                 .unwrap()
+//                 .create_entry("a/e".as_ref(), true, cx)
+//         })
+//         .await
+//         .unwrap();
+//     assert!(entry.is_dir());
+
+//     cx.foreground().run_until_parked();
+//     tree.read_with(cx, |tree, _| {
+//         assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
+//     });
+
+//     let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
+//     assert_eq!(
+//         snapshot1.lock().entries(true).collect::<Vec<_>>(),
+//         snapshot2.entries(true).collect::<Vec<_>>()
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
+//     let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+//     let fs_fake = FakeFs::new(cx.background());
+//     fs_fake
+//         .insert_tree(
+//             "/root",
+//             json!({
+//                 "a": {},
+//             }),
+//         )
+//         .await;
+
+//     let tree_fake = Worktree::local(
+//         client_fake,
+//         "/root".as_ref(),
+//         true,
+//         fs_fake,
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     let entry = tree_fake
+//         .update(cx, |tree, cx| {
+//             tree.as_local_mut()
+//                 .unwrap()
+//                 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+//         })
+//         .await
+//         .unwrap();
+//     assert!(entry.is_file());
+
+//     cx.foreground().run_until_parked();
+//     tree_fake.read_with(cx, |tree, _| {
+//         assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+//         assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+//         assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+//     });
+
+//     let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+//     let fs_real = Arc::new(RealFs);
+//     let temp_root = temp_tree(json!({
+//         "a": {}
+//     }));
+
+//     let tree_real = Worktree::local(
+//         client_real,
+//         temp_root.path(),
+//         true,
+//         fs_real,
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     let entry = tree_real
+//         .update(cx, |tree, cx| {
+//             tree.as_local_mut()
+//                 .unwrap()
+//                 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+//         })
+//         .await
+//         .unwrap();
+//     assert!(entry.is_file());
+
+//     cx.foreground().run_until_parked();
+//     tree_real.read_with(cx, |tree, _| {
+//         assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+//         assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+//         assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+//     });
+
+//     // Test smallest change
+//     let entry = tree_real
+//         .update(cx, |tree, cx| {
+//             tree.as_local_mut()
+//                 .unwrap()
+//                 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
+//         })
+//         .await
+//         .unwrap();
+//     assert!(entry.is_file());
+
+//     cx.foreground().run_until_parked();
+//     tree_real.read_with(cx, |tree, _| {
+//         assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
+//     });
+
+//     // Test largest change
+//     let entry = tree_real
+//         .update(cx, |tree, cx| {
+//             tree.as_local_mut()
+//                 .unwrap()
+//                 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
+//         })
+//         .await
+//         .unwrap();
+//     assert!(entry.is_file());
+
+//     cx.foreground().run_until_parked();
+//     tree_real.read_with(cx, |tree, _| {
+//         assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
+//         assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
+//         assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
+//         assert!(tree.entry_for_path("d/").unwrap().is_dir());
+//     });
+// }
+
+// #[gpui::test(iterations = 100)]
+// async fn test_random_worktree_operations_during_initial_scan(
+//     cx: &mut TestAppContext,
+//     mut rng: StdRng,
+// ) {
+//     let operations = env::var("OPERATIONS")
+//         .map(|o| o.parse().unwrap())
+//         .unwrap_or(5);
+//     let initial_entries = env::var("INITIAL_ENTRIES")
+//         .map(|o| o.parse().unwrap())
+//         .unwrap_or(20);
+
+//     let root_dir = Path::new("/test");
+//     let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
+//     fs.as_fake().insert_tree(root_dir, json!({})).await;
+//     for _ in 0..initial_entries {
+//         randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
+//     }
+//     log::info!("generated initial tree");
+
+//     let worktree = Worktree::local(
+//         build_client(cx),
+//         root_dir,
+//         true,
+//         fs.clone(),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
+//     let updates = Arc::new(Mutex::new(Vec::new()));
+//     worktree.update(cx, |tree, cx| {
+//         check_worktree_change_events(tree, cx);
+
+//         let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
+//             let updates = updates.clone();
+//             move |update| {
+//                 updates.lock().push(update);
+//                 async { true }
+//             }
+//         });
+//     });
+
+//     for _ in 0..operations {
+//         worktree
+//             .update(cx, |worktree, cx| {
+//                 randomly_mutate_worktree(worktree, &mut rng, cx)
+//             })
+//             .await
+//             .log_err();
+//         worktree.read_with(cx, |tree, _| {
+//             tree.as_local().unwrap().snapshot().check_invariants(true)
+//         });
+
+//         if rng.gen_bool(0.6) {
+//             snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
+//         }
+//     }
+
+//     worktree
+//         .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
+//         .await;
+
+//     cx.foreground().run_until_parked();
+
+//     let final_snapshot = worktree.read_with(cx, |tree, _| {
+//         let tree = tree.as_local().unwrap();
+//         let snapshot = tree.snapshot();
+//         snapshot.check_invariants(true);
+//         snapshot
+//     });
+
+//     for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
+//         let mut updated_snapshot = snapshot.clone();
+//         for update in updates.lock().iter() {
+//             if update.scan_id >= updated_snapshot.scan_id() as u64 {
+//                 updated_snapshot
+//                     .apply_remote_update(update.clone())
+//                     .unwrap();
+//             }
+//         }
+
+//         assert_eq!(
+//             updated_snapshot.entries(true).collect::<Vec<_>>(),
+//             final_snapshot.entries(true).collect::<Vec<_>>(),
+//             "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
+//         );
+//     }
+// }
+
+// #[gpui::test(iterations = 100)]
+// async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
+//     let operations = env::var("OPERATIONS")
+//         .map(|o| o.parse().unwrap())
+//         .unwrap_or(40);
+//     let initial_entries = env::var("INITIAL_ENTRIES")
+//         .map(|o| o.parse().unwrap())
+//         .unwrap_or(20);
+
+//     let root_dir = Path::new("/test");
+//     let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
+//     fs.as_fake().insert_tree(root_dir, json!({})).await;
+//     for _ in 0..initial_entries {
+//         randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
+//     }
+//     log::info!("generated initial tree");
+
+//     let worktree = Worktree::local(
+//         build_client(cx),
+//         root_dir,
+//         true,
+//         fs.clone(),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     let updates = Arc::new(Mutex::new(Vec::new()));
+//     worktree.update(cx, |tree, cx| {
+//         check_worktree_change_events(tree, cx);
+
+//         let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
+//             let updates = updates.clone();
+//             move |update| {
+//                 updates.lock().push(update);
+//                 async { true }
+//             }
+//         });
+//     });
+
+//     worktree
+//         .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
+//         .await;
+
+//     fs.as_fake().pause_events();
+//     let mut snapshots = Vec::new();
+//     let mut mutations_len = operations;
+//     while mutations_len > 1 {
+//         if rng.gen_bool(0.2) {
+//             worktree
+//                 .update(cx, |worktree, cx| {
+//                     randomly_mutate_worktree(worktree, &mut rng, cx)
+//                 })
+//                 .await
+//                 .log_err();
+//         } else {
+//             randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
+//         }
+
+//         let buffered_event_count = fs.as_fake().buffered_event_count();
+//         if buffered_event_count > 0 && rng.gen_bool(0.3) {
+//             let len = rng.gen_range(0..=buffered_event_count);
+//             log::info!("flushing {} events", len);
+//             fs.as_fake().flush_events(len);
+//         } else {
+//             randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
+//             mutations_len -= 1;
+//         }
+
+//         cx.foreground().run_until_parked();
+//         if rng.gen_bool(0.2) {
+//             log::info!("storing snapshot {}", snapshots.len());
+//             let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
+//             snapshots.push(snapshot);
+//         }
+//     }
+
+//     log::info!("quiescing");
+//     fs.as_fake().flush_events(usize::MAX);
+//     cx.foreground().run_until_parked();
+
+//     let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
+//     snapshot.check_invariants(true);
+//     let expanded_paths = snapshot
+//         .expanded_entries()
+//         .map(|e| e.path.clone())
+//         .collect::<Vec<_>>();
+
+//     {
+//         let new_worktree = Worktree::local(
+//             build_client(cx),
+//             root_dir,
+//             true,
+//             fs.clone(),
+//             Default::default(),
+//             &mut cx.to_async(),
+//         )
+//         .await
+//         .unwrap();
+//         new_worktree
+//             .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
+//             .await;
+//         new_worktree
+//             .update(cx, |tree, _| {
+//                 tree.as_local_mut()
+//                     .unwrap()
+//                     .refresh_entries_for_paths(expanded_paths)
+//             })
+//             .recv()
+//             .await;
+//         let new_snapshot =
+//             new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
+//         assert_eq!(
+//             snapshot.entries_without_ids(true),
+//             new_snapshot.entries_without_ids(true)
+//         );
+//     }
+
+//     for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
+//         for update in updates.lock().iter() {
+//             if update.scan_id >= prev_snapshot.scan_id() as u64 {
+//                 prev_snapshot.apply_remote_update(update.clone()).unwrap();
+//             }
+//         }
+
+//         assert_eq!(
+//             prev_snapshot
+//                 .entries(true)
+//                 .map(ignore_pending_dir)
+//                 .collect::<Vec<_>>(),
+//             snapshot
+//                 .entries(true)
+//                 .map(ignore_pending_dir)
+//                 .collect::<Vec<_>>(),
+//             "wrong updates after snapshot {i}: {updates:#?}",
+//         );
+//     }
+
+//     fn ignore_pending_dir(entry: &Entry) -> Entry {
+//         let mut entry = entry.clone();
+//         if entry.kind.is_dir() {
+//             entry.kind = EntryKind::Dir
+//         }
+//         entry
+//     }
+// }
+
+// // The worktree's `UpdatedEntries` event can be used to follow along with
+// // all changes to the worktree's snapshot.
+// fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
+//     let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
+//     cx.subscribe(&cx.handle(), move |tree, _, event, _| {
+//         if let Event::UpdatedEntries(changes) = event {
+//             for (path, _, change_type) in changes.iter() {
+//                 let entry = tree.entry_for_path(&path).cloned();
+//                 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
+//                     Ok(ix) | Err(ix) => ix,
+//                 };
+//                 match change_type {
+//                     PathChange::Added => entries.insert(ix, entry.unwrap()),
+//                     PathChange::Removed => drop(entries.remove(ix)),
+//                     PathChange::Updated => {
+//                         let entry = entry.unwrap();
+//                         let existing_entry = entries.get_mut(ix).unwrap();
+//                         assert_eq!(existing_entry.path, entry.path);
+//                         *existing_entry = entry;
+//                     }
+//                     PathChange::AddedOrUpdated | PathChange::Loaded => {
+//                         let entry = entry.unwrap();
+//                         if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
+//                             *entries.get_mut(ix).unwrap() = entry;
+//                         } else {
+//                             entries.insert(ix, entry);
+//                         }
+//                     }
+//                 }
+//             }
+
+//             let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
+//             assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
+//         }
+//     })
+//     .detach();
+// }
+
+// fn randomly_mutate_worktree(
+//     worktree: &mut Worktree,
+//     rng: &mut impl Rng,
+//     cx: &mut ModelContext<Worktree>,
+// ) -> Task<Result<()>> {
+//     log::info!("mutating worktree");
+//     let worktree = worktree.as_local_mut().unwrap();
+//     let snapshot = worktree.snapshot();
+//     let entry = snapshot.entries(false).choose(rng).unwrap();
+
+//     match rng.gen_range(0_u32..100) {
+//         0..=33 if entry.path.as_ref() != Path::new("") => {
+//             log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
+//             worktree.delete_entry(entry.id, cx).unwrap()
+//         }
+//         ..=66 if entry.path.as_ref() != Path::new("") => {
+//             let other_entry = snapshot.entries(false).choose(rng).unwrap();
+//             let new_parent_path = if other_entry.is_dir() {
+//                 other_entry.path.clone()
+//             } else {
+//                 other_entry.path.parent().unwrap().into()
+//             };
+//             let mut new_path = new_parent_path.join(random_filename(rng));
+//             if new_path.starts_with(&entry.path) {
+//                 new_path = random_filename(rng).into();
+//             }
+
+//             log::info!(
+//                 "renaming entry {:?} ({}) to {:?}",
+//                 entry.path,
+//                 entry.id.0,
+//                 new_path
+//             );
+//             let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
+//             cx.foreground().spawn(async move {
+//                 task.await?;
+//                 Ok(())
+//             })
+//         }
+//         _ => {
+//             let task = if entry.is_dir() {
+//                 let child_path = entry.path.join(random_filename(rng));
+//                 let is_dir = rng.gen_bool(0.3);
+//                 log::info!(
+//                     "creating {} at {:?}",
+//                     if is_dir { "dir" } else { "file" },
+//                     child_path,
+//                 );
+//                 worktree.create_entry(child_path, is_dir, cx)
+//             } else {
+//                 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
+//                 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
+//             };
+//             cx.foreground().spawn(async move {
+//                 task.await?;
+//                 Ok(())
+//             })
+//         }
+//     }
+// }
+
+// async fn randomly_mutate_fs(
+//     fs: &Arc<dyn Fs>,
+//     root_path: &Path,
+//     insertion_probability: f64,
+//     rng: &mut impl Rng,
+// ) {
+//     log::info!("mutating fs");
+//     let mut files = Vec::new();
+//     let mut dirs = Vec::new();
+//     for path in fs.as_fake().paths(false) {
+//         if path.starts_with(root_path) {
+//             if fs.is_file(&path).await {
+//                 files.push(path);
+//             } else {
+//                 dirs.push(path);
+//             }
+//         }
+//     }
+
+//     if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
+//         let path = dirs.choose(rng).unwrap();
+//         let new_path = path.join(random_filename(rng));
+
+//         if rng.gen() {
+//             log::info!(
+//                 "creating dir {:?}",
+//                 new_path.strip_prefix(root_path).unwrap()
+//             );
+//             fs.create_dir(&new_path).await.unwrap();
+//         } else {
+//             log::info!(
+//                 "creating file {:?}",
+//                 new_path.strip_prefix(root_path).unwrap()
+//             );
+//             fs.create_file(&new_path, Default::default()).await.unwrap();
+//         }
+//     } else if rng.gen_bool(0.05) {
+//         let ignore_dir_path = dirs.choose(rng).unwrap();
+//         let ignore_path = ignore_dir_path.join(&*GITIGNORE);
+
+//         let subdirs = dirs
+//             .iter()
+//             .filter(|d| d.starts_with(&ignore_dir_path))
+//             .cloned()
+//             .collect::<Vec<_>>();
+//         let subfiles = files
+//             .iter()
+//             .filter(|d| d.starts_with(&ignore_dir_path))
+//             .cloned()
+//             .collect::<Vec<_>>();
+//         let files_to_ignore = {
+//             let len = rng.gen_range(0..=subfiles.len());
+//             subfiles.choose_multiple(rng, len)
+//         };
+//         let dirs_to_ignore = {
+//             let len = rng.gen_range(0..subdirs.len());
+//             subdirs.choose_multiple(rng, len)
+//         };
+
+//         let mut ignore_contents = String::new();
+//         for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
+//             writeln!(
+//                 ignore_contents,
+//                 "{}",
+//                 path_to_ignore
+//                     .strip_prefix(&ignore_dir_path)
+//                     .unwrap()
+//                     .to_str()
+//                     .unwrap()
+//             )
+//             .unwrap();
+//         }
+//         log::info!(
+//             "creating gitignore {:?} with contents:\n{}",
+//             ignore_path.strip_prefix(&root_path).unwrap(),
+//             ignore_contents
+//         );
+//         fs.save(
+//             &ignore_path,
+//             &ignore_contents.as_str().into(),
+//             Default::default(),
+//         )
+//         .await
+//         .unwrap();
+//     } else {
+//         let old_path = {
+//             let file_path = files.choose(rng);
+//             let dir_path = dirs[1..].choose(rng);
+//             file_path.into_iter().chain(dir_path).choose(rng).unwrap()
+//         };
+
+//         let is_rename = rng.gen();
+//         if is_rename {
+//             let new_path_parent = dirs
+//                 .iter()
+//                 .filter(|d| !d.starts_with(old_path))
+//                 .choose(rng)
+//                 .unwrap();
+
+//             let overwrite_existing_dir =
+//                 !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3);
+//             let new_path = if overwrite_existing_dir {
+//                 fs.remove_dir(
+//                     &new_path_parent,
+//                     RemoveOptions {
+//                         recursive: true,
+//                         ignore_if_not_exists: true,
+//                     },
+//                 )
+//                 .await
+//                 .unwrap();
+//                 new_path_parent.to_path_buf()
+//             } else {
+//                 new_path_parent.join(random_filename(rng))
+//             };
+
+//             log::info!(
+//                 "renaming {:?} to {}{:?}",
+//                 old_path.strip_prefix(&root_path).unwrap(),
+//                 if overwrite_existing_dir {
+//                     "overwrite "
+//                 } else {
+//                     ""
+//                 },
+//                 new_path.strip_prefix(&root_path).unwrap()
+//             );
+//             fs.rename(
+//                 &old_path,
+//                 &new_path,
+//                 fs::RenameOptions {
+//                     overwrite: true,
+//                     ignore_if_exists: true,
+//                 },
+//             )
+//             .await
+//             .unwrap();
+//         } else if fs.is_file(&old_path).await {
+//             log::info!(
+//                 "deleting file {:?}",
+//                 old_path.strip_prefix(&root_path).unwrap()
+//             );
+//             fs.remove_file(old_path, Default::default()).await.unwrap();
+//         } else {
+//             log::info!(
+//                 "deleting dir {:?}",
+//                 old_path.strip_prefix(&root_path).unwrap()
+//             );
+//             fs.remove_dir(
+//                 &old_path,
+//                 RemoveOptions {
+//                     recursive: true,
+//                     ignore_if_not_exists: true,
+//                 },
+//             )
+//             .await
+//             .unwrap();
+//         }
+//     }
+// }
+
+// fn random_filename(rng: &mut impl Rng) -> String {
+//     (0..6)
+//         .map(|_| rng.sample(rand::distributions::Alphanumeric))
+//         .map(char::from)
+//         .collect()
+// }
+
+// #[gpui::test]
+// async fn test_rename_work_directory(cx: &mut TestAppContext) {
+//     let root = temp_tree(json!({
+//         "projects": {
+//             "project1": {
+//                 "a": "",
+//                 "b": "",
+//             }
+//         },
+
+//     }));
+//     let root_path = root.path();
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         root_path,
+//         true,
+//         Arc::new(RealFs),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     let repo = git_init(&root_path.join("projects/project1"));
+//     git_add("a", &repo);
+//     git_commit("init", &repo);
+//     std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
+
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+
+//     tree.flush_fs_events(cx).await;
+
+//     cx.read(|cx| {
+//         let tree = tree.read(cx);
+//         let (work_dir, _) = tree.repositories().next().unwrap();
+//         assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
+//         assert_eq!(
+//             tree.status_for_file(Path::new("projects/project1/a")),
+//             Some(GitFileStatus::Modified)
+//         );
+//         assert_eq!(
+//             tree.status_for_file(Path::new("projects/project1/b")),
+//             Some(GitFileStatus::Added)
+//         );
+//     });
+
+//     std::fs::rename(
+//         root_path.join("projects/project1"),
+//         root_path.join("projects/project2"),
+//     )
+//     .ok();
+//     tree.flush_fs_events(cx).await;
+
+//     cx.read(|cx| {
+//         let tree = tree.read(cx);
+//         let (work_dir, _) = tree.repositories().next().unwrap();
+//         assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
+//         assert_eq!(
+//             tree.status_for_file(Path::new("projects/project2/a")),
+//             Some(GitFileStatus::Modified)
+//         );
+//         assert_eq!(
+//             tree.status_for_file(Path::new("projects/project2/b")),
+//             Some(GitFileStatus::Added)
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_git_repository_for_path(cx: &mut TestAppContext) {
+//     let root = temp_tree(json!({
+//         "c.txt": "",
+//         "dir1": {
+//             ".git": {},
+//             "deps": {
+//                 "dep1": {
+//                     ".git": {},
+//                     "src": {
+//                         "a.txt": ""
+//                     }
+//                 }
+//             },
+//             "src": {
+//                 "b.txt": ""
+//             }
+//         },
+//     }));
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         root.path(),
+//         true,
+//         Arc::new(RealFs),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+//     tree.flush_fs_events(cx).await;
+
+//     tree.read_with(cx, |tree, _cx| {
+//         let tree = tree.as_local().unwrap();
+
+//         assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
+
+//         let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
+//         assert_eq!(
+//             entry
+//                 .work_directory(tree)
+//                 .map(|directory| directory.as_ref().to_owned()),
+//             Some(Path::new("dir1").to_owned())
+//         );
+
+//         let entry = tree
+//             .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
+//             .unwrap();
+//         assert_eq!(
+//             entry
+//                 .work_directory(tree)
+//                 .map(|directory| directory.as_ref().to_owned()),
+//             Some(Path::new("dir1/deps/dep1").to_owned())
+//         );
+
+//         let entries = tree.files(false, 0);
+
+//         let paths_with_repos = tree
+//             .entries_with_repositories(entries)
+//             .map(|(entry, repo)| {
+//                 (
+//                     entry.path.as_ref(),
+//                     repo.and_then(|repo| {
+//                         repo.work_directory(&tree)
+//                             .map(|work_directory| work_directory.0.to_path_buf())
+//                     }),
+//                 )
+//             })
+//             .collect::<Vec<_>>();
+
+//         assert_eq!(
+//             paths_with_repos,
+//             &[
+//                 (Path::new("c.txt"), None),
+//                 (
+//                     Path::new("dir1/deps/dep1/src/a.txt"),
+//                     Some(Path::new("dir1/deps/dep1").into())
+//                 ),
+//                 (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
+//             ]
+//         );
+//     });
+
+//     let repo_update_events = Arc::new(Mutex::new(vec![]));
+//     tree.update(cx, |_, cx| {
+//         let repo_update_events = repo_update_events.clone();
+//         cx.subscribe(&tree, move |_, _, event, _| {
+//             if let Event::UpdatedGitRepositories(update) = event {
+//                 repo_update_events.lock().push(update.clone());
+//             }
+//         })
+//         .detach();
+//     });
+
+//     std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
+//     tree.flush_fs_events(cx).await;
+
+//     assert_eq!(
+//         repo_update_events.lock()[0]
+//             .iter()
+//             .map(|e| e.0.clone())
+//             .collect::<Vec<Arc<Path>>>(),
+//         vec![Path::new("dir1").into()]
+//     );
+
+//     std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
+//     tree.flush_fs_events(cx).await;
+
+//     tree.read_with(cx, |tree, _cx| {
+//         let tree = tree.as_local().unwrap();
+
+//         assert!(tree
+//             .repository_for_path("dir1/src/b.txt".as_ref())
+//             .is_none());
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+//     const IGNORE_RULE: &'static str = "**/target";
+
+//     let root = temp_tree(json!({
+//         "project": {
+//             "a.txt": "a",
+//             "b.txt": "bb",
+//             "c": {
+//                 "d": {
+//                     "e.txt": "eee"
+//                 }
+//             },
+//             "f.txt": "ffff",
+//             "target": {
+//                 "build_file": "???"
+//             },
+//             ".gitignore": IGNORE_RULE
+//         },
+
+//     }));
+
+//     const A_TXT: &'static str = "a.txt";
+//     const B_TXT: &'static str = "b.txt";
+//     const E_TXT: &'static str = "c/d/e.txt";
+//     const F_TXT: &'static str = "f.txt";
+//     const DOTGITIGNORE: &'static str = ".gitignore";
+//     const BUILD_FILE: &'static str = "target/build_file";
+//     let project_path = Path::new("project");
+
+//     // Set up git repository before creating the worktree.
+//     let work_dir = root.path().join("project");
+//     let mut repo = git_init(work_dir.as_path());
+//     repo.add_ignore_rule(IGNORE_RULE).unwrap();
+//     git_add(A_TXT, &repo);
+//     git_add(E_TXT, &repo);
+//     git_add(DOTGITIGNORE, &repo);
+//     git_commit("Initial commit", &repo);
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         root.path(),
+//         true,
+//         Arc::new(RealFs),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     tree.flush_fs_events(cx).await;
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+//     deterministic.run_until_parked();
+
+//     // Check that the right git state is observed on startup
+//     tree.read_with(cx, |tree, _cx| {
+//         let snapshot = tree.snapshot();
+//         assert_eq!(snapshot.repositories().count(), 1);
+//         let (dir, _) = snapshot.repositories().next().unwrap();
+//         assert_eq!(dir.as_ref(), Path::new("project"));
+
+//         assert_eq!(
+//             snapshot.status_for_file(project_path.join(B_TXT)),
+//             Some(GitFileStatus::Added)
+//         );
+//         assert_eq!(
+//             snapshot.status_for_file(project_path.join(F_TXT)),
+//             Some(GitFileStatus::Added)
+//         );
+//     });
+
+//     // Modify a file in the working copy.
+//     std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
+//     tree.flush_fs_events(cx).await;
+//     deterministic.run_until_parked();
+
+//     // The worktree detects that the file's git status has changed.
+//     tree.read_with(cx, |tree, _cx| {
+//         let snapshot = tree.snapshot();
+//         assert_eq!(
+//             snapshot.status_for_file(project_path.join(A_TXT)),
+//             Some(GitFileStatus::Modified)
+//         );
+//     });
+
+//     // Create a commit in the git repository.
+//     git_add(A_TXT, &repo);
+//     git_add(B_TXT, &repo);
+//     git_commit("Committing modified and added", &repo);
+//     tree.flush_fs_events(cx).await;
+//     deterministic.run_until_parked();
+
+//     // The worktree detects that the files' git status have changed.
+//     tree.read_with(cx, |tree, _cx| {
+//         let snapshot = tree.snapshot();
+//         assert_eq!(
+//             snapshot.status_for_file(project_path.join(F_TXT)),
+//             Some(GitFileStatus::Added)
+//         );
+//         assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
+//         assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
+//     });
+
+//     // Modify files in the working copy and perform git operations on other files.
+//     git_reset(0, &repo);
+//     git_remove_index(Path::new(B_TXT), &repo);
+//     git_stash(&mut repo);
+//     std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
+//     std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
+//     tree.flush_fs_events(cx).await;
+//     deterministic.run_until_parked();
+
+//     // Check that more complex repo changes are tracked
+//     tree.read_with(cx, |tree, _cx| {
+//         let snapshot = tree.snapshot();
+
+//         assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
+//         assert_eq!(
+//             snapshot.status_for_file(project_path.join(B_TXT)),
+//             Some(GitFileStatus::Added)
+//         );
+//         assert_eq!(
+//             snapshot.status_for_file(project_path.join(E_TXT)),
+//             Some(GitFileStatus::Modified)
+//         );
+//     });
+
+//     std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
+//     std::fs::remove_dir_all(work_dir.join("c")).unwrap();
+//     std::fs::write(
+//         work_dir.join(DOTGITIGNORE),
+//         [IGNORE_RULE, "f.txt"].join("\n"),
+//     )
+//     .unwrap();
+
+//     git_add(Path::new(DOTGITIGNORE), &repo);
+//     git_commit("Committing modified git ignore", &repo);
+
+//     tree.flush_fs_events(cx).await;
+//     deterministic.run_until_parked();
+
+//     let mut renamed_dir_name = "first_directory/second_directory";
+//     const RENAMED_FILE: &'static str = "rf.txt";
+
+//     std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
+//     std::fs::write(
+//         work_dir.join(renamed_dir_name).join(RENAMED_FILE),
+//         "new-contents",
+//     )
+//     .unwrap();
+
+//     tree.flush_fs_events(cx).await;
+//     deterministic.run_until_parked();
+
+//     tree.read_with(cx, |tree, _cx| {
+//         let snapshot = tree.snapshot();
+//         assert_eq!(
+//             snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
+//             Some(GitFileStatus::Added)
+//         );
+//     });
+
+//     renamed_dir_name = "new_first_directory/second_directory";
+
+//     std::fs::rename(
+//         work_dir.join("first_directory"),
+//         work_dir.join("new_first_directory"),
+//     )
+//     .unwrap();
+
+//     tree.flush_fs_events(cx).await;
+//     deterministic.run_until_parked();
+
+//     tree.read_with(cx, |tree, _cx| {
+//         let snapshot = tree.snapshot();
+
+//         assert_eq!(
+//             snapshot.status_for_file(
+//                 project_path
+//                     .join(Path::new(renamed_dir_name))
+//                     .join(RENAMED_FILE)
+//             ),
+//             Some(GitFileStatus::Added)
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree(
+//         "/root",
+//         json!({
+//             ".git": {},
+//             "a": {
+//                 "b": {
+//                     "c1.txt": "",
+//                     "c2.txt": "",
+//                 },
+//                 "d": {
+//                     "e1.txt": "",
+//                     "e2.txt": "",
+//                     "e3.txt": "",
+//                 }
+//             },
+//             "f": {
+//                 "no-status.txt": ""
+//             },
+//             "g": {
+//                 "h1.txt": "",
+//                 "h2.txt": ""
+//             },
+
+//         }),
+//     )
+//     .await;
+
+//     fs.set_status_for_repo_via_git_operation(
+//         &Path::new("/root/.git"),
+//         &[
+//             (Path::new("a/b/c1.txt"), GitFileStatus::Added),
+//             (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
+//             (Path::new("g/h2.txt"), GitFileStatus::Conflict),
+//         ],
+//     );
+
+//     let tree = Worktree::local(
+//         build_client(cx),
+//         Path::new("/root"),
+//         true,
+//         fs.clone(),
+//         Default::default(),
+//         &mut cx.to_async(),
+//     )
+//     .await
+//     .unwrap();
+
+//     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+//         .await;
+
+//     cx.foreground().run_until_parked();
+//     let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
+
+//     check_propagated_statuses(
+//         &snapshot,
+//         &[
+//             (Path::new(""), Some(GitFileStatus::Conflict)),
+//             (Path::new("a"), Some(GitFileStatus::Modified)),
+//             (Path::new("a/b"), Some(GitFileStatus::Added)),
+//             (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
+//             (Path::new("a/b/c2.txt"), None),
+//             (Path::new("a/d"), Some(GitFileStatus::Modified)),
+//             (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
+//             (Path::new("f"), None),
+//             (Path::new("f/no-status.txt"), None),
+//             (Path::new("g"), Some(GitFileStatus::Conflict)),
+//             (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
+//         ],
+//     );
+
+//     check_propagated_statuses(
+//         &snapshot,
+//         &[
+//             (Path::new("a/b"), Some(GitFileStatus::Added)),
+//             (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
+//             (Path::new("a/b/c2.txt"), None),
+//             (Path::new("a/d"), Some(GitFileStatus::Modified)),
+//             (Path::new("a/d/e1.txt"), None),
+//             (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
+//             (Path::new("f"), None),
+//             (Path::new("f/no-status.txt"), None),
+//             (Path::new("g"), Some(GitFileStatus::Conflict)),
+//         ],
+//     );
+
+//     check_propagated_statuses(
+//         &snapshot,
+//         &[
+//             (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
+//             (Path::new("a/b/c2.txt"), None),
+//             (Path::new("a/d/e1.txt"), None),
+//             (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
+//             (Path::new("f/no-status.txt"), None),
+//         ],
+//     );
+
+//     #[track_caller]
+//     fn check_propagated_statuses(
+//         snapshot: &Snapshot,
+//         expected_statuses: &[(&Path, Option<GitFileStatus>)],
+//     ) {
+//         let mut entries = expected_statuses
+//             .iter()
+//             .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
+//             .collect::<Vec<_>>();
+//         snapshot.propagate_git_statuses(&mut entries);
+//         assert_eq!(
+//             entries
+//                 .iter()
+//                 .map(|e| (e.path.as_ref(), e.git_status))
+//                 .collect::<Vec<_>>(),
+//             expected_statuses
+//         );
+//     }
+// }
+
+// fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
+//     let http_client = FakeHttpClient::with_404_response();
+//     cx.read(|cx| Client::new(http_client, cx))
+// }
+
+// #[track_caller]
+// fn git_init(path: &Path) -> git2::Repository {
+//     git2::Repository::init(path).expect("Failed to initialize git repository")
+// }
+
+// #[track_caller]
+// fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
+//     let path = path.as_ref();
+//     let mut index = repo.index().expect("Failed to get index");
+//     index.add_path(path).expect("Failed to add a.txt");
+//     index.write().expect("Failed to write index");
+// }
+
+// #[track_caller]
+// fn git_remove_index(path: &Path, repo: &git2::Repository) {
+//     let mut index = repo.index().expect("Failed to get index");
+//     index.remove_path(path).expect("Failed to add a.txt");
+//     index.write().expect("Failed to write index");
+// }
+
+// #[track_caller]
+// fn git_commit(msg: &'static str, repo: &git2::Repository) {
+//     use git2::Signature;
+
+//     let signature = Signature::now("test", "test@zed.dev").unwrap();
+//     let oid = repo.index().unwrap().write_tree().unwrap();
+//     let tree = repo.find_tree(oid).unwrap();
+//     if let Some(head) = repo.head().ok() {
+//         let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
+
+//         let parent_commit = parent_obj.as_commit().unwrap();
+
+//         repo.commit(
+//             Some("HEAD"),
+//             &signature,
+//             &signature,
+//             msg,
+//             &tree,
+//             &[parent_commit],
+//         )
+//         .expect("Failed to commit with parent");
+//     } else {
+//         repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
+//             .expect("Failed to commit");
+//     }
+// }
+
+// #[track_caller]
+// fn git_stash(repo: &mut git2::Repository) {
+//     use git2::Signature;
+
+//     let signature = Signature::now("test", "test@zed.dev").unwrap();
+//     repo.stash_save(&signature, "N/A", None)
+//         .expect("Failed to stash");
+// }
+
+// #[track_caller]
+// fn git_reset(offset: usize, repo: &git2::Repository) {
+//     let head = repo.head().expect("Couldn't get repo head");
+//     let object = head.peel(git2::ObjectType::Commit).unwrap();
+//     let commit = object.as_commit().unwrap();
+//     let new_head = commit
+//         .parents()
+//         .inspect(|parnet| {
+//             parnet.message();
+//         })
+//         .skip(offset)
+//         .next()
+//         .expect("Not enough history");
+//     repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
+//         .expect("Could not reset");
+// }
+
+// #[allow(dead_code)]
+// #[track_caller]
+// fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
+//     repo.statuses(None)
+//         .unwrap()
+//         .iter()
+//         .map(|status| (status.path().unwrap().to_string(), status.status()))
+//         .collect()
+// }

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

@@ -35,7 +35,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
     let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect();
     let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect();
 
-    // Create trait bound that each wrapped type must implement Clone & Default
+    // Create trait bound that each wrapped type must implement Clone // & Default
     let type_param_bounds: Vec<_> = wrapped_types
         .iter()
         .map(|ty| {
@@ -51,13 +51,14 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
                         lifetimes: None,
                         path: parse_quote!(Clone),
                     }));
-                    punctuated.push_punct(syn::token::Add::default());
-                    punctuated.push_value(TypeParamBound::Trait(TraitBound {
-                        paren_token: None,
-                        modifier: syn::TraitBoundModifier::None,
-                        lifetimes: None,
-                        path: parse_quote!(Default),
-                    }));
+
+                    // punctuated.push_punct(syn::token::Add::default());
+                    // punctuated.push_value(TypeParamBound::Trait(TraitBound {
+                    //     paren_token: None,
+                    //     modifier: syn::TraitBoundModifier::None,
+                    //     lifetimes: None,
+                    //     path: parse_quote!(Default),
+                    // }));
                     punctuated
                 },
             })
@@ -78,7 +79,11 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         },
     };
 
-    let field_assignments: Vec<TokenStream2> = fields
+    // refinable_refine_assignments
+    // refinable_refined_assignments
+    // refinement_refine_assignments
+
+    let refineable_refine_assignments: Vec<TokenStream2> = fields
         .iter()
         .map(|field| {
             let name = &field.ident;
@@ -105,7 +110,34 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         })
         .collect();
 
-    let refinement_field_assignments: Vec<TokenStream2> = fields
+    let refineable_refined_assignments: Vec<TokenStream2> = fields
+        .iter()
+        .map(|field| {
+            let name = &field.ident;
+            let is_refineable = is_refineable_field(field);
+            let is_optional = is_optional_field(field);
+
+            if is_refineable {
+                quote! {
+                    self.#name = self.#name.refined(refinement.#name);
+                }
+            } else if is_optional {
+                quote! {
+                    if let Some(value) = refinement.#name {
+                        self.#name = Some(value);
+                    }
+                }
+            } else {
+                quote! {
+                    if let Some(value) = refinement.#name {
+                        self.#name = value;
+                    }
+                }
+            }
+        })
+        .collect();
+
+    let refinement_refine_assigments: Vec<TokenStream2> = fields
         .iter()
         .map(|field| {
             let name = &field.ident;
@@ -125,6 +157,49 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         })
         .collect();
 
+    let refinement_refined_assigments: Vec<TokenStream2> = fields
+        .iter()
+        .map(|field| {
+            let name = &field.ident;
+            let is_refineable = is_refineable_field(field);
+
+            if is_refineable {
+                quote! {
+                    self.#name = self.#name.refined(refinement.#name);
+                }
+            } else {
+                quote! {
+                    if let Some(value) = refinement.#name {
+                        self.#name = Some(value);
+                    }
+                }
+            }
+        })
+        .collect();
+
+    let from_refinement_assigments: Vec<TokenStream2> = fields
+        .iter()
+        .map(|field| {
+            let name = &field.ident;
+            let is_refineable = is_refineable_field(field);
+            let is_optional = is_optional_field(field);
+
+            if is_refineable {
+                quote! {
+                    #name: value.#name.into(),
+                }
+            } else if is_optional {
+                quote! {
+                    #name: value.#name.map(|v| v.into()),
+                }
+            } else {
+                quote! {
+                    #name: value.#name.map(|v| v.into()).unwrap_or_default(),
+                }
+            }
+        })
+        .collect();
+
     let debug_impl = if impl_debug_on_refinement {
         let refinement_field_debugs: Vec<TokenStream2> = fields
             .iter()
@@ -161,7 +236,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
     };
 
     let gen = quote! {
-        #[derive(Default, Clone)]
+        #[derive(Clone)]
         pub struct #refinement_ident #impl_generics {
             #( #field_visibilities #field_names: #wrapped_types ),*
         }
@@ -172,7 +247,12 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
             type Refinement = #refinement_ident #ty_generics;
 
             fn refine(&mut self, refinement: &Self::Refinement) {
-                #( #field_assignments )*
+                #( #refineable_refine_assignments )*
+            }
+
+            fn refined(mut self, refinement: Self::Refinement) -> Self {
+                #( #refineable_refined_assignments )*
+                self
             }
         }
 
@@ -182,7 +262,32 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
             type Refinement = #refinement_ident #ty_generics;
 
             fn refine(&mut self, refinement: &Self::Refinement) {
-                #( #refinement_field_assignments )*
+                #( #refinement_refine_assigments )*
+            }
+
+            fn refined(mut self, refinement: Self::Refinement) -> Self {
+                #( #refinement_refined_assigments )*
+                self
+            }
+        }
+
+        impl #impl_generics From<#refinement_ident #ty_generics> for #ident #ty_generics
+            #where_clause
+        {
+            fn from(value: #refinement_ident #ty_generics) -> Self {
+                Self {
+                    #( #from_refinement_assigments )*
+                }
+            }
+        }
+
+        impl #impl_generics ::core::default::Default for #refinement_ident #ty_generics
+            #where_clause
+        {
+            fn default() -> Self {
+                #refinement_ident {
+                    #( #field_names: Default::default() ),*
+                }
             }
         }
 

crates/refineable/src/refineable.rs 🔗

@@ -4,24 +4,18 @@ pub trait Refineable: Clone {
     type Refinement: Refineable<Refinement = Self::Refinement> + Default;
 
     fn refine(&mut self, refinement: &Self::Refinement);
-    fn refined(mut self, refinement: &Self::Refinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.refine(refinement);
-        self
-    }
-    fn from_refinement(refinement: &Self::Refinement) -> Self
+    fn refined(self, refinement: Self::Refinement) -> Self;
+    fn from_cascade(cascade: &Cascade<Self>) -> Self
     where
         Self: Default + Sized,
     {
-        Self::default().refined(refinement)
+        Self::default().refined(cascade.merged())
     }
 }
 
-pub struct RefinementCascade<S: Refineable>(Vec<Option<S::Refinement>>);
+pub struct Cascade<S: Refineable>(Vec<Option<S::Refinement>>);
 
-impl<S: Refineable + Default> Default for RefinementCascade<S> {
+impl<S: Refineable + Default> Default for Cascade<S> {
     fn default() -> Self {
         Self(vec![Some(Default::default())])
     }
@@ -30,7 +24,7 @@ impl<S: Refineable + Default> Default for RefinementCascade<S> {
 #[derive(Copy, Clone)]
 pub struct CascadeSlot(usize);
 
-impl<S: Refineable + Default> RefinementCascade<S> {
+impl<S: Refineable + Default> Cascade<S> {
     pub fn reserve(&mut self) -> CascadeSlot {
         self.0.push(None);
         return CascadeSlot(self.0.len() - 1);

crates/rpc2/Cargo.toml 🔗

@@ -0,0 +1,44 @@
+[package]
+description = "Shared logic for communication between the Zed app and the zed.dev server"
+edition = "2021"
+name = "rpc2"
+version = "0.1.0"
+publish = false
+
+[lib]
+path = "src/rpc.rs"
+doctest = false
+
+[features]
+test-support = ["collections/test-support", "gpui2/test-support"]
+
+[dependencies]
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+gpui2 = { path = "../gpui2", optional = true }
+util = { path = "../util" }
+anyhow.workspace = true
+async-lock = "2.4"
+async-tungstenite = "0.16"
+base64 = "0.13"
+futures.workspace = true
+parking_lot.workspace = true
+prost.workspace = true
+rand.workspace = true
+rsa = "0.4"
+serde.workspace = true
+serde_derive.workspace = true
+smol-timeout = "0.6"
+tracing = { version = "0.1.34", features = ["log"] }
+zstd = "0.11"
+
+[build-dependencies]
+prost-build = "0.9"
+
+[dev-dependencies]
+collections = { path = "../collections", features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+smol.workspace = true
+tempdir.workspace = true
+ctor.workspace = true
+env_logger.workspace = true

crates/rpc2/build.rs 🔗

@@ -0,0 +1,8 @@
+fn main() {
+    let mut build = prost_build::Config::new();
+    // build.protoc_arg("--experimental_allow_proto3_optional");
+    build
+        .type_attribute(".", "#[derive(serde::Serialize)]")
+        .compile_protos(&["proto/zed.proto"], &["proto"])
+        .unwrap();
+}

crates/rpc2/proto/zed.proto 🔗

@@ -0,0 +1,1559 @@
+syntax = "proto3";
+package zed.messages;
+
+// Looking for a number? Search "// Current max"
+
+message PeerId {
+    uint32 owner_id = 1;
+    uint32 id = 2;
+}
+
+message Envelope {
+    uint32 id = 1;
+    optional uint32 responding_to = 2;
+    optional PeerId original_sender_id = 3;
+    oneof payload {
+        Hello hello = 4;
+        Ack ack = 5;
+        Error error = 6;
+        Ping ping = 7;
+        Test test = 8;
+
+        CreateRoom create_room = 9;
+        CreateRoomResponse create_room_response = 10;
+        JoinRoom join_room = 11;
+        JoinRoomResponse join_room_response = 12;
+        RejoinRoom rejoin_room = 13;
+        RejoinRoomResponse rejoin_room_response = 14;
+        LeaveRoom leave_room = 15;
+        Call call = 16;
+        IncomingCall incoming_call = 17;
+        CallCanceled call_canceled = 18;
+        CancelCall cancel_call = 19;
+        DeclineCall decline_call = 20;
+        UpdateParticipantLocation update_participant_location = 21;
+        RoomUpdated room_updated = 22;
+
+        ShareProject share_project = 23;
+        ShareProjectResponse share_project_response = 24;
+        UnshareProject unshare_project = 25;
+        JoinProject join_project = 26;
+        JoinProjectResponse join_project_response = 27;
+        LeaveProject leave_project = 28;
+        AddProjectCollaborator add_project_collaborator = 29;
+        UpdateProjectCollaborator update_project_collaborator = 30;
+        RemoveProjectCollaborator remove_project_collaborator = 31;
+
+        GetDefinition get_definition = 32;
+        GetDefinitionResponse get_definition_response = 33;
+        GetTypeDefinition get_type_definition = 34;
+        GetTypeDefinitionResponse get_type_definition_response = 35;
+        GetReferences get_references = 36;
+        GetReferencesResponse get_references_response = 37;
+        GetDocumentHighlights get_document_highlights = 38;
+        GetDocumentHighlightsResponse get_document_highlights_response = 39;
+        GetProjectSymbols get_project_symbols = 40;
+        GetProjectSymbolsResponse get_project_symbols_response = 41;
+        OpenBufferForSymbol open_buffer_for_symbol = 42;
+        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43;
+
+        UpdateProject update_project = 44;
+        UpdateWorktree update_worktree = 45;
+
+        CreateProjectEntry create_project_entry = 46;
+        RenameProjectEntry rename_project_entry = 47;
+        CopyProjectEntry copy_project_entry = 48;
+        DeleteProjectEntry delete_project_entry = 49;
+        ProjectEntryResponse project_entry_response = 50;
+        ExpandProjectEntry expand_project_entry = 51;
+        ExpandProjectEntryResponse expand_project_entry_response = 52;
+
+        UpdateDiagnosticSummary update_diagnostic_summary = 53;
+        StartLanguageServer start_language_server = 54;
+        UpdateLanguageServer update_language_server = 55;
+
+        OpenBufferById open_buffer_by_id = 56;
+        OpenBufferByPath open_buffer_by_path = 57;
+        OpenBufferResponse open_buffer_response = 58;
+        CreateBufferForPeer create_buffer_for_peer = 59;
+        UpdateBuffer update_buffer = 60;
+        UpdateBufferFile update_buffer_file = 61;
+        SaveBuffer save_buffer = 62;
+        BufferSaved buffer_saved = 63;
+        BufferReloaded buffer_reloaded = 64;
+        ReloadBuffers reload_buffers = 65;
+        ReloadBuffersResponse reload_buffers_response = 66;
+        SynchronizeBuffers synchronize_buffers = 67;
+        SynchronizeBuffersResponse synchronize_buffers_response = 68;
+        FormatBuffers format_buffers = 69;
+        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
+    }
+}
+
+// Messages
+
+message Hello {
+    PeerId peer_id = 1;
+}
+
+message Ping {}
+
+message Ack {}
+
+message Error {
+    string message = 1;
+}
+
+message Test {
+    uint64 id = 1;
+}
+
+message CreateRoom {}
+
+message CreateRoomResponse {
+    Room room = 1;
+    optional LiveKitConnectionInfo live_kit_connection_info = 2;
+}
+
+message JoinRoom {
+    uint64 id = 1;
+}
+
+message JoinRoomResponse {
+    Room room = 1;
+    optional uint64 channel_id = 2;
+    optional LiveKitConnectionInfo live_kit_connection_info = 3;
+}
+
+message RejoinRoom {
+    uint64 id = 1;
+    repeated UpdateProject reshared_projects = 2;
+    repeated RejoinProject rejoined_projects = 3;
+}
+
+message RejoinProject {
+    uint64 id = 1;
+    repeated RejoinWorktree worktrees = 2;
+}
+
+message RejoinWorktree {
+    uint64 id = 1;
+    uint64 scan_id = 2;
+}
+
+message RejoinRoomResponse {
+    Room room = 1;
+    repeated ResharedProject reshared_projects = 2;
+    repeated RejoinedProject rejoined_projects = 3;
+}
+
+message ResharedProject {
+    uint64 id = 1;
+    repeated Collaborator collaborators = 2;
+}
+
+message RejoinedProject {
+    uint64 id = 1;
+    repeated WorktreeMetadata worktrees = 2;
+    repeated Collaborator collaborators = 3;
+    repeated LanguageServer language_servers = 4;
+}
+
+message LeaveRoom {}
+
+message Room {
+    uint64 id = 1;
+    repeated Participant participants = 2;
+    repeated PendingParticipant pending_participants = 3;
+    repeated Follower followers = 4;
+    string live_kit_room = 5;
+}
+
+message Participant {
+    uint64 user_id = 1;
+    PeerId peer_id = 2;
+    repeated ParticipantProject projects = 3;
+    ParticipantLocation location = 4;
+    uint32 participant_index = 5;
+}
+
+message PendingParticipant {
+    uint64 user_id = 1;
+    uint64 calling_user_id = 2;
+    optional uint64 initial_project_id = 3;
+}
+
+message ParticipantProject {
+    uint64 id = 1;
+    repeated string worktree_root_names = 2;
+}
+
+message Follower {
+    PeerId leader_id = 1;
+    PeerId follower_id = 2;
+    uint64 project_id = 3;
+}
+
+message ParticipantLocation {
+    oneof variant {
+        SharedProject shared_project = 1;
+        UnsharedProject unshared_project = 2;
+        External external = 3;
+    }
+
+    message SharedProject {
+        uint64 id = 1;
+    }
+
+    message UnsharedProject {}
+
+    message External {}
+}
+
+message Call {
+    uint64 room_id = 1;
+    uint64 called_user_id = 2;
+    optional uint64 initial_project_id = 3;
+}
+
+message IncomingCall {
+    uint64 room_id = 1;
+    uint64 calling_user_id = 2;
+    repeated uint64 participant_user_ids = 3;
+    optional ParticipantProject initial_project = 4;
+}
+
+message CallCanceled {
+    uint64 room_id = 1;
+}
+
+message CancelCall {
+    uint64 room_id = 1;
+    uint64 called_user_id = 2;
+}
+
+message DeclineCall {
+    uint64 room_id = 1;
+}
+
+message UpdateParticipantLocation {
+    uint64 room_id = 1;
+    ParticipantLocation location = 2;
+}
+
+message RoomUpdated {
+    Room room = 1;
+}
+
+message LiveKitConnectionInfo {
+    string server_url = 1;
+    string token = 2;
+}
+
+message ShareProject {
+    uint64 room_id = 1;
+    repeated WorktreeMetadata worktrees = 2;
+}
+
+message ShareProjectResponse {
+    uint64 project_id = 1;
+}
+
+message UnshareProject {
+    uint64 project_id = 1;
+}
+
+message UpdateProject {
+    uint64 project_id = 1;
+    repeated WorktreeMetadata worktrees = 2;
+}
+
+message JoinProject {
+    uint64 project_id = 1;
+}
+
+message JoinProjectResponse {
+    uint32 replica_id = 1;
+    repeated WorktreeMetadata worktrees = 2;
+    repeated Collaborator collaborators = 3;
+    repeated LanguageServer language_servers = 4;
+}
+
+message LeaveProject {
+    uint64 project_id = 1;
+}
+
+message UpdateWorktree {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    string root_name = 3;
+    repeated Entry updated_entries = 4;
+    repeated uint64 removed_entries = 5;
+    repeated RepositoryEntry updated_repositories = 6;
+    repeated uint64 removed_repositories = 7;
+    uint64 scan_id = 8;
+    bool is_last_update = 9;
+    string abs_path = 10;
+}
+
+message UpdateWorktreeSettings {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    string path = 3;
+    optional string content = 4;
+}
+
+message CreateProjectEntry {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    string path = 3;
+    bool is_directory = 4;
+}
+
+message RenameProjectEntry {
+    uint64 project_id = 1;
+    uint64 entry_id = 2;
+    string new_path = 3;
+}
+
+message CopyProjectEntry {
+    uint64 project_id = 1;
+    uint64 entry_id = 2;
+    string new_path = 3;
+}
+
+message DeleteProjectEntry {
+    uint64 project_id = 1;
+    uint64 entry_id = 2;
+}
+
+message ExpandProjectEntry {
+    uint64 project_id = 1;
+    uint64 entry_id = 2;
+}
+
+message ExpandProjectEntryResponse {
+    uint64 worktree_scan_id = 1;
+}
+
+message ProjectEntryResponse {
+    Entry entry = 1;
+    uint64 worktree_scan_id = 2;
+}
+
+message AddProjectCollaborator {
+    uint64 project_id = 1;
+    Collaborator collaborator = 2;
+}
+
+message UpdateProjectCollaborator {
+    uint64 project_id = 1;
+    PeerId old_peer_id = 2;
+    PeerId new_peer_id = 3;
+}
+
+message RemoveProjectCollaborator {
+    uint64 project_id = 1;
+    PeerId peer_id = 2;
+}
+
+message UpdateChannelBufferCollaborators {
+    uint64 channel_id = 1;
+    repeated Collaborator collaborators = 2;
+}
+
+message GetDefinition {
+     uint64 project_id = 1;
+     uint64 buffer_id = 2;
+     Anchor position = 3;
+     repeated VectorClockEntry version = 4;
+ }
+
+message GetDefinitionResponse {
+    repeated LocationLink links = 1;
+}
+
+message GetTypeDefinition {
+     uint64 project_id = 1;
+     uint64 buffer_id = 2;
+     Anchor position = 3;
+     repeated VectorClockEntry version = 4;
+ }
+
+message GetTypeDefinitionResponse {
+    repeated LocationLink links = 1;
+}
+
+message GetReferences {
+     uint64 project_id = 1;
+     uint64 buffer_id = 2;
+     Anchor position = 3;
+     repeated VectorClockEntry version = 4;
+ }
+
+message GetReferencesResponse {
+    repeated Location locations = 1;
+}
+
+message GetDocumentHighlights {
+     uint64 project_id = 1;
+     uint64 buffer_id = 2;
+     Anchor position = 3;
+     repeated VectorClockEntry version = 4;
+ }
+
+message GetDocumentHighlightsResponse {
+    repeated DocumentHighlight highlights = 1;
+}
+
+message Location {
+    uint64 buffer_id = 1;
+    Anchor start = 2;
+    Anchor end = 3;
+}
+
+message LocationLink {
+    optional Location origin = 1;
+    Location target = 2;
+}
+
+message DocumentHighlight {
+    Kind kind = 1;
+    Anchor start = 2;
+    Anchor end = 3;
+
+    enum Kind {
+        Text = 0;
+        Read = 1;
+        Write = 2;
+    }
+}
+
+message GetProjectSymbols {
+    uint64 project_id = 1;
+    string query = 2;
+}
+
+message GetProjectSymbolsResponse {
+    repeated Symbol symbols = 4;
+}
+
+message Symbol {
+    uint64 source_worktree_id = 1;
+    uint64 worktree_id = 2;
+    string language_server_name = 3;
+    string name = 4;
+    int32 kind = 5;
+    string path = 6;
+    // Cannot use generate anchors for unopened files,
+    // so we are forced to use point coords instead
+    PointUtf16 start = 7;
+    PointUtf16 end = 8;
+    bytes signature = 9;
+}
+
+message OpenBufferForSymbol {
+    uint64 project_id = 1;
+    Symbol symbol = 2;
+}
+
+message OpenBufferForSymbolResponse {
+    uint64 buffer_id = 1;
+}
+
+message OpenBufferByPath {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    string path = 3;
+}
+
+message OpenBufferById {
+    uint64 project_id = 1;
+    uint64 id = 2;
+}
+
+message OpenBufferResponse {
+    uint64 buffer_id = 1;
+}
+
+message CreateBufferForPeer {
+    uint64 project_id = 1;
+    PeerId peer_id = 2;
+    oneof variant {
+        BufferState state = 3;
+        BufferChunk chunk = 4;
+    }
+}
+
+message UpdateBuffer {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    repeated Operation operations = 3;
+}
+
+message UpdateChannelBuffer {
+    uint64 channel_id = 1;
+    repeated Operation operations = 2;
+}
+
+message UpdateBufferFile {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    File file = 3;
+}
+
+message SaveBuffer {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    repeated VectorClockEntry version = 3;
+}
+
+message BufferSaved {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    repeated VectorClockEntry version = 3;
+    Timestamp mtime = 4;
+    string fingerprint = 5;
+}
+
+message BufferReloaded {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    repeated VectorClockEntry version = 3;
+    Timestamp mtime = 4;
+    string fingerprint = 5;
+    LineEnding line_ending = 6;
+}
+
+message ReloadBuffers {
+    uint64 project_id = 1;
+    repeated uint64 buffer_ids = 2;
+}
+
+message ReloadBuffersResponse {
+    ProjectTransaction transaction = 1;
+}
+
+message SynchronizeBuffers {
+    uint64 project_id = 1;
+    repeated BufferVersion buffers = 2;
+}
+
+message SynchronizeBuffersResponse {
+    repeated BufferVersion buffers = 1;
+}
+
+message BufferVersion {
+    uint64 id = 1;
+    repeated VectorClockEntry version = 2;
+}
+
+message ChannelBufferVersion {
+    uint64 channel_id = 1;
+    repeated VectorClockEntry version = 2;
+    uint64 epoch = 3;
+}
+
+enum FormatTrigger {
+    Save = 0;
+    Manual = 1;
+}
+
+message FormatBuffers {
+    uint64 project_id = 1;
+    FormatTrigger trigger = 2;
+    repeated uint64 buffer_ids = 3;
+}
+
+message FormatBuffersResponse {
+    ProjectTransaction transaction = 1;
+}
+
+message GetCompletions {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor position = 3;
+    repeated VectorClockEntry version = 4;
+}
+
+message GetCompletionsResponse {
+    repeated Completion completions = 1;
+    repeated VectorClockEntry version = 2;
+}
+
+message ApplyCompletionAdditionalEdits {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Completion completion = 3;
+}
+
+message ApplyCompletionAdditionalEditsResponse {
+    Transaction transaction = 1;
+}
+
+message Completion {
+    Anchor old_start = 1;
+    Anchor old_end = 2;
+    string new_text = 3;
+    uint64 server_id = 4;
+    bytes lsp_completion = 5;
+}
+
+message GetCodeActions {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor start = 3;
+    Anchor end = 4;
+    repeated VectorClockEntry version = 5;
+}
+
+message GetCodeActionsResponse {
+    repeated CodeAction actions = 1;
+    repeated VectorClockEntry version = 2;
+}
+
+message GetHover {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor position = 3;
+    repeated VectorClockEntry version = 5;
+}
+
+message GetHoverResponse {
+    optional Anchor start = 1;
+    optional Anchor end = 2;
+    repeated HoverBlock contents = 3;
+}
+
+message HoverBlock {
+    string text = 1;
+    optional string language = 2;
+    bool is_markdown = 3;
+}
+
+message ApplyCodeAction {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    CodeAction action = 3;
+}
+
+message ApplyCodeActionResponse {
+    ProjectTransaction transaction = 1;
+}
+
+message PrepareRename {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor position = 3;
+    repeated VectorClockEntry version = 4;
+}
+
+message PrepareRenameResponse {
+    bool can_rename = 1;
+    Anchor start = 2;
+    Anchor end = 3;
+    repeated VectorClockEntry version = 4;
+}
+
+message PerformRename {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor position = 3;
+    string new_name = 4;
+    repeated VectorClockEntry version = 5;
+}
+
+message OnTypeFormatting {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor position = 3;
+    string trigger = 4;
+    repeated VectorClockEntry version = 5;
+}
+
+message OnTypeFormattingResponse {
+    Transaction transaction = 1;
+}
+
+message InlayHints {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor start = 3;
+    Anchor end = 4;
+    repeated VectorClockEntry version = 5;
+}
+
+message InlayHintsResponse {
+    repeated InlayHint hints = 1;
+    repeated VectorClockEntry version = 2;
+}
+
+message InlayHint {
+    Anchor position = 1;
+    InlayHintLabel label = 2;
+    optional string kind = 3;
+    bool padding_left = 4;
+    bool padding_right = 5;
+    InlayHintTooltip tooltip = 6;
+    ResolveState resolve_state = 7;
+}
+
+message InlayHintLabel {
+    oneof label {
+        string value = 1;
+        InlayHintLabelParts label_parts = 2;
+    }
+}
+
+message InlayHintLabelParts {
+    repeated InlayHintLabelPart parts = 1;
+}
+
+message InlayHintLabelPart {
+    string value = 1;
+    InlayHintLabelPartTooltip tooltip = 2;
+    optional string location_url = 3;
+    PointUtf16 location_range_start = 4;
+    PointUtf16 location_range_end = 5;
+    optional uint64 language_server_id = 6;
+}
+
+message InlayHintTooltip {
+    oneof content {
+        string value = 1;
+        MarkupContent markup_content = 2;
+    }
+}
+
+message InlayHintLabelPartTooltip {
+    oneof content {
+        string value = 1;
+        MarkupContent markup_content = 2;
+    }
+}
+
+message ResolveState {
+    State state = 1;
+    LspResolveState lsp_resolve_state = 2;
+
+    enum State {
+        Resolved = 0;
+        CanResolve = 1;
+        Resolving = 2;
+    }
+
+    message LspResolveState {
+        string value = 1;
+        uint64 server_id = 2;
+    }
+}
+
+message ResolveInlayHint {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    uint64 language_server_id = 3;
+    InlayHint hint = 4;
+}
+
+message ResolveInlayHintResponse {
+    InlayHint hint = 1;
+}
+
+message RefreshInlayHints {
+    uint64 project_id = 1;
+}
+
+message MarkupContent {
+    bool is_markdown = 1;
+    string value = 2;
+}
+
+message PerformRenameResponse {
+    ProjectTransaction transaction = 2;
+}
+
+message SearchProject {
+    uint64 project_id = 1;
+    string query = 2;
+    bool regex = 3;
+    bool whole_word = 4;
+    bool case_sensitive = 5;
+    string files_to_include = 6;
+    string files_to_exclude = 7;
+}
+
+message SearchProjectResponse {
+    repeated Location locations = 1;
+}
+
+message CodeAction {
+    uint64 server_id = 1;
+    Anchor start = 2;
+    Anchor end = 3;
+    bytes lsp_action = 4;
+}
+
+message ProjectTransaction {
+    repeated uint64 buffer_ids = 1;
+    repeated Transaction transactions = 2;
+}
+
+message Transaction {
+    LamportTimestamp id = 1;
+    repeated LamportTimestamp edit_ids = 2;
+    repeated VectorClockEntry start = 3;
+}
+
+message LamportTimestamp {
+    uint32 replica_id = 1;
+    uint32 value = 2;
+}
+
+message LanguageServer {
+    uint64 id = 1;
+    string name = 2;
+}
+
+message StartLanguageServer {
+    uint64 project_id = 1;
+    LanguageServer server = 2;
+}
+
+message UpdateDiagnosticSummary {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    DiagnosticSummary summary = 3;
+}
+
+message DiagnosticSummary {
+    string path = 1;
+    uint64 language_server_id = 2;
+    uint32 error_count = 3;
+    uint32 warning_count = 4;
+}
+
+message UpdateLanguageServer {
+    uint64 project_id = 1;
+    uint64 language_server_id = 2;
+    oneof variant {
+        LspWorkStart work_start = 3;
+        LspWorkProgress work_progress = 4;
+        LspWorkEnd work_end = 5;
+        LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6;
+        LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7;
+    }
+}
+
+message LspWorkStart {
+    string token = 1;
+    optional string message = 2;
+    optional uint32 percentage = 3;
+}
+
+message LspWorkProgress {
+    string token = 1;
+    optional string message = 2;
+    optional uint32 percentage = 3;
+}
+
+message LspWorkEnd {
+    string token = 1;
+}
+
+message LspDiskBasedDiagnosticsUpdating {}
+
+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;
+}
+
+message UnseenChannelMessage {
+    uint64 channel_id = 1;
+    uint64 message_id = 2;
+}
+
+message UnseenChannelBufferChange {
+    uint64 channel_id = 1;
+    uint64 epoch = 2;
+    repeated VectorClockEntry version = 3;
+}
+
+message ChannelEdge {
+    uint64 channel_id = 1;
+    uint64 parent_id = 2;
+}
+
+message ChannelPermission {
+    uint64 channel_id = 1;
+    bool is_admin = 2;
+}
+
+message ChannelParticipants {
+    uint64 channel_id = 1;
+    repeated uint64 participant_user_ids = 2;
+}
+
+message JoinChannel {
+    uint64 channel_id = 1;
+}
+
+message DeleteChannel {
+    uint64 channel_id = 1;
+}
+
+message GetChannelMembers {
+    uint64 channel_id = 1;
+}
+
+message GetChannelMembersResponse {
+    repeated ChannelMember members = 1;
+}
+
+message ChannelMember {
+    uint64 user_id = 1;
+    bool admin = 2;
+    Kind kind = 3;
+
+    enum Kind {
+        Member = 0;
+        Invitee = 1;
+        AncestorMember = 2;
+    }
+}
+
+message CreateChannel {
+    string name = 1;
+    optional uint64 parent_id = 2;
+}
+
+message CreateChannelResponse {
+    Channel channel = 1;
+    optional uint64 parent_id = 2;
+}
+
+message InviteChannelMember {
+    uint64 channel_id = 1;
+    uint64 user_id = 2;
+    bool admin = 3;
+}
+
+message RemoveChannelMember {
+    uint64 channel_id = 1;
+    uint64 user_id = 2;
+}
+
+message SetChannelMemberAdmin {
+    uint64 channel_id = 1;
+    uint64 user_id = 2;
+    bool admin = 3;
+}
+
+message RenameChannel {
+    uint64 channel_id = 1;
+    string name = 2;
+}
+
+message RenameChannelResponse {
+    Channel channel = 1;
+}
+
+message JoinChannelChat {
+    uint64 channel_id = 1;
+}
+
+message JoinChannelChatResponse {
+    repeated ChannelMessage messages = 1;
+    bool done = 2;
+}
+
+message LeaveChannelChat {
+    uint64 channel_id = 1;
+}
+
+message SendChannelMessage {
+    uint64 channel_id = 1;
+    string body = 2;
+    Nonce nonce = 3;
+}
+
+message RemoveChannelMessage {
+    uint64 channel_id = 1;
+    uint64 message_id = 2;
+}
+
+message AckChannelMessage {
+    uint64 channel_id = 1;
+    uint64 message_id = 2;
+}
+
+message SendChannelMessageResponse {
+    ChannelMessage message = 1;
+}
+
+message ChannelMessageSent {
+    uint64 channel_id = 1;
+    ChannelMessage message = 2;
+}
+
+message GetChannelMessages {
+    uint64 channel_id = 1;
+    uint64 before_message_id = 2;
+}
+
+message GetChannelMessagesResponse {
+    repeated ChannelMessage messages = 1;
+    bool done = 2;
+}
+
+message LinkChannel {
+    uint64 channel_id = 1;
+    uint64 to = 2;
+}
+
+message UnlinkChannel {
+    uint64 channel_id = 1;
+    uint64 from = 2;
+}
+
+message MoveChannel {
+    uint64 channel_id = 1;
+    uint64 from = 2;
+    uint64 to = 3;
+}
+
+message JoinChannelBuffer {
+    uint64 channel_id = 1;
+}
+
+message ChannelMessage {
+    uint64 id = 1;
+    string body = 2;
+    uint64 timestamp = 3;
+    uint64 sender_id = 4;
+    Nonce nonce = 5;
+}
+
+message RejoinChannelBuffers {
+    repeated ChannelBufferVersion buffers = 1;
+}
+
+message RejoinChannelBuffersResponse {
+    repeated RejoinedChannelBuffer buffers = 1;
+}
+
+message AckBufferOperation {
+    uint64 buffer_id = 1;
+    uint64 epoch = 2;
+    repeated VectorClockEntry version = 3;
+}
+
+message JoinChannelBufferResponse {
+    uint64 buffer_id = 1;
+    uint32 replica_id = 2;
+    string base_text = 3;
+    repeated Operation operations = 4;
+    repeated Collaborator collaborators = 5;
+    uint64 epoch = 6;
+}
+
+message RejoinedChannelBuffer {
+    uint64 channel_id = 1;
+    repeated VectorClockEntry version = 2;
+    repeated Operation operations = 3;
+    repeated Collaborator collaborators = 4;
+}
+
+message LeaveChannelBuffer {
+    uint64 channel_id = 1;
+}
+
+message RespondToChannelInvite {
+    uint64 channel_id = 1;
+    bool accept = 2;
+}
+
+message GetUsers {
+    repeated uint64 user_ids = 1;
+}
+
+message FuzzySearchUsers {
+    string query = 1;
+}
+
+message UsersResponse {
+    repeated User users = 1;
+}
+
+message RequestContact {
+    uint64 responder_id = 1;
+}
+
+message RemoveContact {
+    uint64 user_id = 1;
+}
+
+message RespondToContactRequest {
+    uint64 requester_id = 1;
+    ContactRequestResponse response = 2;
+}
+
+enum ContactRequestResponse {
+    Accept = 0;
+    Decline = 1;
+    Block = 2;
+    Dismiss = 3;
+}
+
+message UpdateContacts {
+    repeated Contact contacts = 1;
+    repeated uint64 remove_contacts = 2;
+    repeated IncomingContactRequest incoming_requests = 3;
+    repeated uint64 remove_incoming_requests = 4;
+    repeated uint64 outgoing_requests = 5;
+    repeated uint64 remove_outgoing_requests = 6;
+}
+
+message UpdateInviteInfo {
+    string url = 1;
+    uint32 count = 2;
+}
+
+message ShowContacts {}
+
+message IncomingContactRequest {
+    uint64 requester_id = 1;
+    bool should_notify = 2;
+}
+
+message UpdateDiagnostics {
+    uint32 replica_id = 1;
+    uint32 lamport_timestamp = 2;
+    uint64 server_id = 3;
+    repeated Diagnostic diagnostics = 4;
+}
+
+message Follow {
+    uint64 room_id = 1;
+    optional uint64 project_id = 2;
+    PeerId leader_id = 3;
+}
+
+message FollowResponse {
+    optional ViewId active_view_id = 1;
+    repeated View views = 2;
+}
+
+message UpdateFollowers {
+    uint64 room_id = 1;
+    optional uint64 project_id = 2;
+    repeated PeerId follower_ids = 3;
+    oneof variant {
+        UpdateActiveView update_active_view = 4;
+        View create_view = 5;
+        UpdateView update_view = 6;
+    }
+}
+
+message Unfollow {
+    uint64 room_id = 1;
+    optional uint64 project_id = 2;
+    PeerId leader_id = 3;
+}
+
+message GetPrivateUserInfo {}
+
+message GetPrivateUserInfoResponse {
+    string metrics_id = 1;
+    bool staff = 2;
+    repeated string flags = 3;
+}
+
+// Entities
+
+message ViewId {
+    PeerId creator = 1;
+    uint64 id = 2;
+}
+
+message UpdateActiveView {
+    optional ViewId id = 1;
+    optional PeerId leader_id = 2;
+}
+
+message UpdateView {
+    ViewId id = 1;
+    optional PeerId leader_id = 2;
+
+    oneof variant {
+        Editor editor = 3;
+    }
+
+    message Editor {
+        repeated ExcerptInsertion inserted_excerpts = 1;
+        repeated uint64 deleted_excerpts = 2;
+        repeated Selection selections = 3;
+        optional Selection pending_selection = 4;
+        EditorAnchor scroll_top_anchor = 5;
+        float scroll_x = 6;
+        float scroll_y = 7;
+    }
+}
+
+message View {
+    ViewId id = 1;
+    optional PeerId leader_id = 2;
+
+    oneof variant {
+        Editor editor = 3;
+        ChannelView channel_view = 4;
+    }
+
+    message Editor {
+        bool singleton = 1;
+        optional string title = 2;
+        repeated Excerpt excerpts = 3;
+        repeated Selection selections = 4;
+        optional Selection pending_selection = 5;
+        EditorAnchor scroll_top_anchor = 6;
+        float scroll_x = 7;
+        float scroll_y = 8;
+    }
+
+    message ChannelView {
+        uint64 channel_id = 1;
+        Editor editor = 2;
+    }
+}
+
+message Collaborator {
+    PeerId peer_id = 1;
+    uint32 replica_id = 2;
+    uint64 user_id = 3;
+}
+
+message User {
+    uint64 id = 1;
+    string github_login = 2;
+    string avatar_url = 3;
+}
+
+message File {
+    uint64 worktree_id = 1;
+    uint64 entry_id = 2;
+    string path = 3;
+    Timestamp mtime = 4;
+    bool is_deleted = 5;
+}
+
+message Entry {
+    uint64 id = 1;
+    bool is_dir = 2;
+    string path = 3;
+    uint64 inode = 4;
+    Timestamp mtime = 5;
+    bool is_symlink = 6;
+    bool is_ignored = 7;
+    bool is_external = 8;
+    optional GitStatus git_status = 9;
+}
+
+message RepositoryEntry {
+    uint64 work_directory_id = 1;
+    optional string branch = 2;
+}
+
+message StatusEntry {
+    string repo_path = 1;
+    GitStatus status = 2;
+}
+
+enum GitStatus {
+    Added = 0;
+    Modified = 1;
+    Conflict = 2;
+}
+
+message BufferState {
+    uint64 id = 1;
+    optional File file = 2;
+    string base_text = 3;
+    optional string diff_base = 4;
+    LineEnding line_ending = 5;
+    repeated VectorClockEntry saved_version = 6;
+    string saved_version_fingerprint = 7;
+    Timestamp saved_mtime = 8;
+}
+
+message BufferChunk {
+    uint64 buffer_id = 1;
+    repeated Operation operations = 2;
+    bool is_last = 3;
+}
+
+enum LineEnding {
+    Unix = 0;
+    Windows = 1;
+}
+
+message Selection {
+    uint64 id = 1;
+    EditorAnchor start = 2;
+    EditorAnchor end = 3;
+    bool reversed = 4;
+}
+
+message EditorAnchor {
+    uint64 excerpt_id = 1;
+    Anchor anchor = 2;
+}
+
+enum CursorShape {
+    CursorBar = 0;
+    CursorBlock = 1;
+    CursorUnderscore = 2;
+    CursorHollow = 3;
+}
+
+message ExcerptInsertion {
+    Excerpt excerpt = 1;
+    optional uint64 previous_excerpt_id = 2;
+}
+
+message Excerpt {
+    uint64 id = 1;
+    uint64 buffer_id = 2;
+    Anchor context_start = 3;
+    Anchor context_end = 4;
+    Anchor primary_start = 5;
+    Anchor primary_end = 6;
+}
+
+message Anchor {
+    uint32 replica_id = 1;
+    uint32 timestamp = 2;
+    uint64 offset = 3;
+    Bias bias = 4;
+    optional uint64 buffer_id = 5;
+}
+
+enum Bias {
+    Left = 0;
+    Right = 1;
+}
+
+message Diagnostic {
+    Anchor start = 1;
+    Anchor end = 2;
+    optional string source = 3;
+    Severity severity = 4;
+    string message = 5;
+    optional string code = 6;
+    uint64 group_id = 7;
+    bool is_primary = 8;
+    bool is_valid = 9;
+    bool is_disk_based = 10;
+    bool is_unnecessary = 11;
+
+    enum Severity {
+        None = 0;
+        Error = 1;
+        Warning = 2;
+        Information = 3;
+        Hint = 4;
+    }
+}
+
+message Operation {
+    oneof variant {
+        Edit edit = 1;
+        Undo undo = 2;
+        UpdateSelections update_selections = 3;
+        UpdateDiagnostics update_diagnostics = 4;
+        UpdateCompletionTriggers update_completion_triggers = 5;
+    }
+
+    message Edit {
+        uint32 replica_id = 1;
+        uint32 lamport_timestamp = 2;
+        repeated VectorClockEntry version = 3;
+        repeated Range ranges = 4;
+        repeated string new_text = 5;
+    }
+
+    message Undo {
+        uint32 replica_id = 1;
+        uint32 lamport_timestamp = 2;
+        repeated VectorClockEntry version = 3;
+        repeated UndoCount counts = 4;
+    }
+
+    message UpdateSelections {
+        uint32 replica_id = 1;
+        uint32 lamport_timestamp = 2;
+        repeated Selection selections = 3;
+        bool line_mode = 4;
+        CursorShape cursor_shape = 5;
+    }
+
+    message UpdateCompletionTriggers {
+        uint32 replica_id = 1;
+        uint32 lamport_timestamp = 2;
+        repeated string triggers = 3;
+    }
+}
+
+message UndoMapEntry {
+    uint32 replica_id = 1;
+    uint32 local_timestamp = 2;
+    repeated UndoCount counts = 3;
+}
+
+message UndoCount {
+    uint32 replica_id = 1;
+    uint32 lamport_timestamp = 2;
+    uint32 count = 3;
+}
+
+message VectorClockEntry {
+    uint32 replica_id = 1;
+    uint32 timestamp = 2;
+}
+
+message Timestamp {
+    uint64 seconds = 1;
+    uint32 nanos = 2;
+}
+
+message Range {
+    uint64 start = 1;
+    uint64 end = 2;
+}
+
+message PointUtf16 {
+    uint32 row = 1;
+    uint32 column = 2;
+}
+
+message Nonce {
+    uint64 upper_half = 1;
+    uint64 lower_half = 2;
+}
+
+message Channel {
+    uint64 id = 1;
+    string name = 2;
+}
+
+message Contact {
+    uint64 user_id = 1;
+    bool online = 2;
+    bool busy = 3;
+    bool should_notify = 4;
+}
+
+message WorktreeMetadata {
+    uint64 id = 1;
+    string root_name = 2;
+    bool visible = 3;
+    string abs_path = 4;
+}
+
+message UpdateDiffBase {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    optional string diff_base = 3;
+}

crates/rpc2/src/auth.rs 🔗

@@ -0,0 +1,136 @@
+use anyhow::{Context, Result};
+use rand::{thread_rng, Rng as _};
+use rsa::{PublicKey as _, PublicKeyEncoding, RSAPrivateKey, RSAPublicKey};
+use std::convert::TryFrom;
+
+pub struct PublicKey(RSAPublicKey);
+
+pub struct PrivateKey(RSAPrivateKey);
+
+/// Generate a public and private key for asymmetric encryption.
+pub fn keypair() -> Result<(PublicKey, PrivateKey)> {
+    let mut rng = thread_rng();
+    let bits = 1024;
+    let private_key = RSAPrivateKey::new(&mut rng, bits)?;
+    let public_key = RSAPublicKey::from(&private_key);
+    Ok((PublicKey(public_key), PrivateKey(private_key)))
+}
+
+/// Generate a random 64-character base64 string.
+pub fn random_token() -> String {
+    let mut rng = thread_rng();
+    let mut token_bytes = [0; 48];
+    for byte in token_bytes.iter_mut() {
+        *byte = rng.gen();
+    }
+    base64::encode_config(token_bytes, base64::URL_SAFE)
+}
+
+impl PublicKey {
+    /// Convert a string to a base64-encoded string that can only be decoded with the corresponding
+    /// private key.
+    pub fn encrypt_string(&self, string: &str) -> Result<String> {
+        let mut rng = thread_rng();
+        let bytes = string.as_bytes();
+        let encrypted_bytes = self
+            .0
+            .encrypt(&mut rng, PADDING_SCHEME, bytes)
+            .context("failed to encrypt string with public key")?;
+        let encrypted_string = base64::encode_config(&encrypted_bytes, base64::URL_SAFE);
+        Ok(encrypted_string)
+    }
+}
+
+impl PrivateKey {
+    /// Decrypt a base64-encoded string that was encrypted by the corresponding public key.
+    pub fn decrypt_string(&self, encrypted_string: &str) -> Result<String> {
+        let encrypted_bytes = base64::decode_config(encrypted_string, base64::URL_SAFE)
+            .context("failed to base64-decode encrypted string")?;
+        let bytes = self
+            .0
+            .decrypt(PADDING_SCHEME, &encrypted_bytes)
+            .context("failed to decrypt string with private key")?;
+        let string = String::from_utf8(bytes).context("decrypted content was not valid utf8")?;
+        Ok(string)
+    }
+}
+
+impl TryFrom<PublicKey> for String {
+    type Error = anyhow::Error;
+    fn try_from(key: PublicKey) -> Result<Self> {
+        let bytes = key.0.to_pkcs1().context("failed to serialize public key")?;
+        let string = base64::encode_config(&bytes, base64::URL_SAFE);
+        Ok(string)
+    }
+}
+
+impl TryFrom<String> for PublicKey {
+    type Error = anyhow::Error;
+    fn try_from(value: String) -> Result<Self> {
+        let bytes = base64::decode_config(&value, base64::URL_SAFE)
+            .context("failed to base64-decode public key string")?;
+        let key = Self(RSAPublicKey::from_pkcs1(&bytes).context("failed to parse public key")?);
+        Ok(key)
+    }
+}
+
+const PADDING_SCHEME: rsa::PaddingScheme = rsa::PaddingScheme::PKCS1v15Encrypt;
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_generate_encrypt_and_decrypt_token() {
+        // CLIENT:
+        // * generate a keypair for asymmetric encryption
+        // * serialize the public key to send it to the server.
+        let (public, private) = keypair().unwrap();
+        let public_string = String::try_from(public).unwrap();
+        assert_printable(&public_string);
+
+        // SERVER:
+        // * parse the public key
+        // * generate a random token.
+        // * encrypt the token using the public key.
+        let public = PublicKey::try_from(public_string).unwrap();
+        let token = random_token();
+        let encrypted_token = public.encrypt_string(&token).unwrap();
+        assert_eq!(token.len(), 64);
+        assert_ne!(encrypted_token, token);
+        assert_printable(&token);
+        assert_printable(&encrypted_token);
+
+        // CLIENT:
+        // * decrypt the token using the private key.
+        let decrypted_token = private.decrypt_string(&encrypted_token).unwrap();
+        assert_eq!(decrypted_token, token);
+    }
+
+    #[test]
+    fn test_tokens_are_always_url_safe() {
+        for _ in 0..5 {
+            let token = random_token();
+            let (public_key, _) = keypair().unwrap();
+            let encrypted_token = public_key.encrypt_string(&token).unwrap();
+            let public_key_str = String::try_from(public_key).unwrap();
+
+            assert_printable(&token);
+            assert_printable(&public_key_str);
+            assert_printable(&encrypted_token);
+        }
+    }
+
+    fn assert_printable(token: &str) {
+        for c in token.chars() {
+            assert!(
+                c.is_ascii_graphic(),
+                "token {:?} has non-printable char {}",
+                token,
+                c
+            );
+            assert_ne!(c, '/', "token {:?} is not URL-safe", token);
+            assert_ne!(c, '&', "token {:?} is not URL-safe", token);
+        }
+    }
+}

crates/rpc2/src/conn.rs 🔗

@@ -0,0 +1,108 @@
+use async_tungstenite::tungstenite::Message as WebSocketMessage;
+use futures::{SinkExt as _, StreamExt as _};
+
+pub struct Connection {
+    pub(crate) tx:
+        Box<dyn 'static + Send + Unpin + futures::Sink<WebSocketMessage, Error = anyhow::Error>>,
+    pub(crate) rx: Box<
+        dyn 'static
+            + Send
+            + Unpin
+            + futures::Stream<Item = Result<WebSocketMessage, anyhow::Error>>,
+    >,
+}
+
+impl Connection {
+    pub fn new<S>(stream: S) -> Self
+    where
+        S: 'static
+            + Send
+            + Unpin
+            + futures::Sink<WebSocketMessage, Error = anyhow::Error>
+            + futures::Stream<Item = Result<WebSocketMessage, anyhow::Error>>,
+    {
+        let (tx, rx) = stream.split();
+        Self {
+            tx: Box::new(tx),
+            rx: Box::new(rx),
+        }
+    }
+
+    pub async fn send(&mut self, message: WebSocketMessage) -> Result<(), anyhow::Error> {
+        self.tx.send(message).await
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn in_memory(
+        executor: gpui2::Executor,
+    ) -> (Self, Self, std::sync::Arc<std::sync::atomic::AtomicBool>) {
+        use std::sync::{
+            atomic::{AtomicBool, Ordering::SeqCst},
+            Arc,
+        };
+
+        let killed = Arc::new(AtomicBool::new(false));
+        let (a_tx, a_rx) = channel(killed.clone(), executor.clone());
+        let (b_tx, b_rx) = channel(killed.clone(), executor);
+        return (
+            Self { tx: a_tx, rx: b_rx },
+            Self { tx: b_tx, rx: a_rx },
+            killed,
+        );
+
+        #[allow(clippy::type_complexity)]
+        fn channel(
+            killed: Arc<AtomicBool>,
+            executor: gpui2::Executor,
+        ) -> (
+            Box<dyn Send + Unpin + futures::Sink<WebSocketMessage, Error = anyhow::Error>>,
+            Box<dyn Send + Unpin + futures::Stream<Item = Result<WebSocketMessage, anyhow::Error>>>,
+        ) {
+            use anyhow::anyhow;
+            use futures::channel::mpsc;
+            use std::io::{Error, ErrorKind};
+
+            let (tx, rx) = mpsc::unbounded::<WebSocketMessage>();
+
+            let tx = tx.sink_map_err(|error| anyhow!(error)).with({
+                let killed = killed.clone();
+                let executor = executor.clone();
+                move |msg| {
+                    let killed = killed.clone();
+                    let executor = executor.clone();
+                    Box::pin(async move {
+                        executor.simulate_random_delay().await;
+
+                        // Writes to a half-open TCP connection will error.
+                        if killed.load(SeqCst) {
+                            std::io::Result::Err(Error::new(ErrorKind::Other, "connection lost"))?;
+                        }
+
+                        Ok(msg)
+                    })
+                }
+            });
+
+            let rx = rx.then({
+                let killed = killed;
+                let executor = executor.clone();
+                move |msg| {
+                    let killed = killed.clone();
+                    let executor = executor.clone();
+                    Box::pin(async move {
+                        executor.simulate_random_delay().await;
+
+                        // Reads from a half-open TCP connection will hang.
+                        if killed.load(SeqCst) {
+                            futures::future::pending::<()>().await;
+                        }
+
+                        Ok(msg)
+                    })
+                }
+            });
+
+            (Box::new(tx), Box::new(rx))
+        }
+    }
+}

crates/rpc2/src/macros.rs 🔗

@@ -0,0 +1,70 @@
+#[macro_export]
+macro_rules! messages {
+    ($(($name:ident, $priority:ident)),* $(,)?) => {
+        pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option<Box<dyn AnyTypedEnvelope>> {
+            match envelope.payload {
+                $(Some(envelope::Payload::$name(payload)) => {
+                    Some(Box::new(TypedEnvelope {
+                        sender_id,
+                        original_sender_id: envelope.original_sender_id.map(|original_sender| PeerId {
+                            owner_id: original_sender.owner_id,
+                            id: original_sender.id
+                        }),
+                        message_id: envelope.id,
+                        payload,
+                    }))
+                }, )*
+                _ => None
+            }
+        }
+
+        $(
+            impl EnvelopedMessage for $name {
+                const NAME: &'static str = std::stringify!($name);
+                const PRIORITY: MessagePriority = MessagePriority::$priority;
+
+                fn into_envelope(
+                    self,
+                    id: u32,
+                    responding_to: Option<u32>,
+                    original_sender_id: Option<PeerId>,
+                ) -> Envelope {
+                    Envelope {
+                        id,
+                        responding_to,
+                        original_sender_id,
+                        payload: Some(envelope::Payload::$name(self)),
+                    }
+                }
+
+                fn from_envelope(envelope: Envelope) -> Option<Self> {
+                    if let Some(envelope::Payload::$name(msg)) = envelope.payload {
+                        Some(msg)
+                    } else {
+                        None
+                    }
+                }
+            }
+        )*
+    };
+}
+
+#[macro_export]
+macro_rules! request_messages {
+    ($(($request_name:ident, $response_name:ident)),* $(,)?) => {
+        $(impl RequestMessage for $request_name {
+            type Response = $response_name;
+        })*
+    };
+}
+
+#[macro_export]
+macro_rules! entity_messages {
+    ($id_field:ident, $($name:ident),* $(,)?) => {
+        $(impl EntityMessage for $name {
+            fn remote_entity_id(&self) -> u64 {
+                self.$id_field
+            }
+        })*
+    };
+}

crates/rpc2/src/peer.rs 🔗

@@ -0,0 +1,933 @@
+use super::{
+    proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, PeerId, RequestMessage},
+    Connection,
+};
+use anyhow::{anyhow, Context, Result};
+use collections::HashMap;
+use futures::{
+    channel::{mpsc, oneshot},
+    stream::BoxStream,
+    FutureExt, SinkExt, StreamExt, TryFutureExt,
+};
+use parking_lot::{Mutex, RwLock};
+use serde::{ser::SerializeStruct, Serialize};
+use std::{fmt, sync::atomic::Ordering::SeqCst};
+use std::{
+    future::Future,
+    marker::PhantomData,
+    sync::{
+        atomic::{self, AtomicU32},
+        Arc,
+    },
+    time::Duration,
+};
+use tracing::instrument;
+
+#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)]
+pub struct ConnectionId {
+    pub owner_id: u32,
+    pub id: u32,
+}
+
+impl Into<PeerId> for ConnectionId {
+    fn into(self) -> PeerId {
+        PeerId {
+            owner_id: self.owner_id,
+            id: self.id,
+        }
+    }
+}
+
+impl From<PeerId> for ConnectionId {
+    fn from(peer_id: PeerId) -> Self {
+        Self {
+            owner_id: peer_id.owner_id,
+            id: peer_id.id,
+        }
+    }
+}
+
+impl fmt::Display for ConnectionId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}/{}", self.owner_id, self.id)
+    }
+}
+
+pub struct Receipt<T> {
+    pub sender_id: ConnectionId,
+    pub message_id: u32,
+    payload_type: PhantomData<T>,
+}
+
+impl<T> Clone for Receipt<T> {
+    fn clone(&self) -> Self {
+        Self {
+            sender_id: self.sender_id,
+            message_id: self.message_id,
+            payload_type: PhantomData,
+        }
+    }
+}
+
+impl<T> Copy for Receipt<T> {}
+
+#[derive(Clone, Debug)]
+pub struct TypedEnvelope<T> {
+    pub sender_id: ConnectionId,
+    pub original_sender_id: Option<PeerId>,
+    pub message_id: u32,
+    pub payload: T,
+}
+
+impl<T> TypedEnvelope<T> {
+    pub fn original_sender_id(&self) -> Result<PeerId> {
+        self.original_sender_id
+            .ok_or_else(|| anyhow!("missing original_sender_id"))
+    }
+}
+
+impl<T: RequestMessage> TypedEnvelope<T> {
+    pub fn receipt(&self) -> Receipt<T> {
+        Receipt {
+            sender_id: self.sender_id,
+            message_id: self.message_id,
+            payload_type: PhantomData,
+        }
+    }
+}
+
+pub struct Peer {
+    epoch: AtomicU32,
+    pub connections: RwLock<HashMap<ConnectionId, ConnectionState>>,
+    next_connection_id: AtomicU32,
+}
+
+#[derive(Clone, Serialize)]
+pub struct ConnectionState {
+    #[serde(skip)]
+    outgoing_tx: mpsc::UnboundedSender<proto::Message>,
+    next_message_id: Arc<AtomicU32>,
+    #[allow(clippy::type_complexity)]
+    #[serde(skip)]
+    response_channels:
+        Arc<Mutex<Option<HashMap<u32, oneshot::Sender<(proto::Envelope, oneshot::Sender<()>)>>>>>,
+}
+
+const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
+const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
+pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10);
+
+impl Peer {
+    pub fn new(epoch: u32) -> Arc<Self> {
+        Arc::new(Self {
+            epoch: AtomicU32::new(epoch),
+            connections: Default::default(),
+            next_connection_id: Default::default(),
+        })
+    }
+
+    pub fn epoch(&self) -> u32 {
+        self.epoch.load(SeqCst)
+    }
+
+    #[instrument(skip_all)]
+    pub fn add_connection<F, Fut, Out>(
+        self: &Arc<Self>,
+        connection: Connection,
+        create_timer: F,
+    ) -> (
+        ConnectionId,
+        impl Future<Output = anyhow::Result<()>> + Send,
+        BoxStream<'static, Box<dyn AnyTypedEnvelope>>,
+    )
+    where
+        F: Send + Fn(Duration) -> Fut,
+        Fut: Send + Future<Output = Out>,
+        Out: Send,
+    {
+        // For outgoing messages, use an unbounded channel so that application code
+        // can always send messages without yielding. For incoming messages, use a
+        // bounded channel so that other peers will receive backpressure if they send
+        // messages faster than this peer can process them.
+        #[cfg(any(test, feature = "test-support"))]
+        const INCOMING_BUFFER_SIZE: usize = 1;
+        #[cfg(not(any(test, feature = "test-support")))]
+        const INCOMING_BUFFER_SIZE: usize = 64;
+        let (mut incoming_tx, incoming_rx) = mpsc::channel(INCOMING_BUFFER_SIZE);
+        let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded();
+
+        let connection_id = ConnectionId {
+            owner_id: self.epoch.load(SeqCst),
+            id: self.next_connection_id.fetch_add(1, SeqCst),
+        };
+        let connection_state = ConnectionState {
+            outgoing_tx,
+            next_message_id: Default::default(),
+            response_channels: Arc::new(Mutex::new(Some(Default::default()))),
+        };
+        let mut writer = MessageStream::new(connection.tx);
+        let mut reader = MessageStream::new(connection.rx);
+
+        let this = self.clone();
+        let response_channels = connection_state.response_channels.clone();
+        let handle_io = async move {
+            tracing::trace!(%connection_id, "handle io future: start");
+
+            let _end_connection = util::defer(|| {
+                response_channels.lock().take();
+                this.connections.write().remove(&connection_id);
+                tracing::trace!(%connection_id, "handle io future: end");
+            });
+
+            // Send messages on this frequency so the connection isn't closed.
+            let keepalive_timer = create_timer(KEEPALIVE_INTERVAL).fuse();
+            futures::pin_mut!(keepalive_timer);
+
+            // Disconnect if we don't receive messages at least this frequently.
+            let receive_timeout = create_timer(RECEIVE_TIMEOUT).fuse();
+            futures::pin_mut!(receive_timeout);
+
+            loop {
+                tracing::trace!(%connection_id, "outer loop iteration start");
+                let read_message = reader.read().fuse();
+                futures::pin_mut!(read_message);
+
+                loop {
+                    tracing::trace!(%connection_id, "inner loop iteration start");
+                    futures::select_biased! {
+                        outgoing = outgoing_rx.next().fuse() => match outgoing {
+                            Some(outgoing) => {
+                                tracing::trace!(%connection_id, "outgoing rpc message: writing");
+                                futures::select_biased! {
+                                    result = writer.write(outgoing).fuse() => {
+                                        tracing::trace!(%connection_id, "outgoing rpc message: done writing");
+                                        result.context("failed to write RPC message")?;
+                                        tracing::trace!(%connection_id, "keepalive interval: resetting after sending message");
+                                        keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse());
+                                    }
+                                    _ = create_timer(WRITE_TIMEOUT).fuse() => {
+                                        tracing::trace!(%connection_id, "outgoing rpc message: writing timed out");
+                                        Err(anyhow!("timed out writing message"))?;
+                                    }
+                                }
+                            }
+                            None => {
+                                tracing::trace!(%connection_id, "outgoing rpc message: channel closed");
+                                return Ok(())
+                            },
+                        },
+                        _ = keepalive_timer => {
+                            tracing::trace!(%connection_id, "keepalive interval: pinging");
+                            futures::select_biased! {
+                                result = writer.write(proto::Message::Ping).fuse() => {
+                                    tracing::trace!(%connection_id, "keepalive interval: done pinging");
+                                    result.context("failed to send keepalive")?;
+                                    tracing::trace!(%connection_id, "keepalive interval: resetting after pinging");
+                                    keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse());
+                                }
+                                _ = create_timer(WRITE_TIMEOUT).fuse() => {
+                                    tracing::trace!(%connection_id, "keepalive interval: pinging timed out");
+                                    Err(anyhow!("timed out sending keepalive"))?;
+                                }
+                            }
+                        }
+                        incoming = read_message => {
+                            let incoming = incoming.context("error reading rpc message from socket")?;
+                            tracing::trace!(%connection_id, "incoming rpc message: received");
+                            tracing::trace!(%connection_id, "receive timeout: resetting");
+                            receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse());
+                            if let proto::Message::Envelope(incoming) = incoming {
+                                tracing::trace!(%connection_id, "incoming rpc message: processing");
+                                futures::select_biased! {
+                                    result = incoming_tx.send(incoming).fuse() => match result {
+                                        Ok(_) => {
+                                            tracing::trace!(%connection_id, "incoming rpc message: processed");
+                                        }
+                                        Err(_) => {
+                                            tracing::trace!(%connection_id, "incoming rpc message: channel closed");
+                                            return Ok(())
+                                        }
+                                    },
+                                    _ = create_timer(WRITE_TIMEOUT).fuse() => {
+                                        tracing::trace!(%connection_id, "incoming rpc message: processing timed out");
+                                        Err(anyhow!("timed out processing incoming message"))?
+                                    }
+                                }
+                            }
+                            break;
+                        },
+                        _ = receive_timeout => {
+                            tracing::trace!(%connection_id, "receive timeout: delay between messages too long");
+                            Err(anyhow!("delay between messages too long"))?
+                        }
+                    }
+                }
+            }
+        };
+
+        let response_channels = connection_state.response_channels.clone();
+        self.connections
+            .write()
+            .insert(connection_id, connection_state);
+
+        let incoming_rx = incoming_rx.filter_map(move |incoming| {
+            let response_channels = response_channels.clone();
+            async move {
+                let message_id = incoming.id;
+                tracing::trace!(?incoming, "incoming message future: start");
+                let _end = util::defer(move || {
+                    tracing::trace!(%connection_id, message_id, "incoming message future: end");
+                });
+
+                if let Some(responding_to) = incoming.responding_to {
+                    tracing::trace!(
+                        %connection_id,
+                        message_id,
+                        responding_to,
+                        "incoming response: received"
+                    );
+                    let channel = response_channels.lock().as_mut()?.remove(&responding_to);
+                    if let Some(tx) = channel {
+                        let requester_resumed = oneshot::channel();
+                        if let Err(error) = tx.send((incoming, requester_resumed.0)) {
+                            tracing::trace!(
+                                %connection_id,
+                                message_id,
+                                responding_to = responding_to,
+                                ?error,
+                                "incoming response: request future dropped",
+                            );
+                        }
+
+                        tracing::trace!(
+                            %connection_id,
+                            message_id,
+                            responding_to,
+                            "incoming response: waiting to resume requester"
+                        );
+                        let _ = requester_resumed.1.await;
+                        tracing::trace!(
+                            %connection_id,
+                            message_id,
+                            responding_to,
+                            "incoming response: requester resumed"
+                        );
+                    } else {
+                        tracing::warn!(
+                            %connection_id,
+                            message_id,
+                            responding_to,
+                            "incoming response: unknown request"
+                        );
+                    }
+
+                    None
+                } else {
+                    tracing::trace!(%connection_id, message_id, "incoming message: received");
+                    proto::build_typed_envelope(connection_id, incoming).or_else(|| {
+                        tracing::error!(
+                            %connection_id,
+                            message_id,
+                            "unable to construct a typed envelope"
+                        );
+                        None
+                    })
+                }
+            }
+        });
+        (connection_id, handle_io, incoming_rx.boxed())
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn add_test_connection(
+        self: &Arc<Self>,
+        connection: Connection,
+        executor: gpui2::Executor,
+    ) -> (
+        ConnectionId,
+        impl Future<Output = anyhow::Result<()>> + Send,
+        BoxStream<'static, Box<dyn AnyTypedEnvelope>>,
+    ) {
+        let executor = executor.clone();
+        self.add_connection(connection, move |duration| executor.timer(duration))
+    }
+
+    pub fn disconnect(&self, connection_id: ConnectionId) {
+        self.connections.write().remove(&connection_id);
+    }
+
+    pub fn reset(&self, epoch: u32) {
+        self.teardown();
+        self.next_connection_id.store(0, SeqCst);
+        self.epoch.store(epoch, SeqCst);
+    }
+
+    pub fn teardown(&self) {
+        self.connections.write().clear();
+    }
+
+    pub fn request<T: RequestMessage>(
+        &self,
+        receiver_id: ConnectionId,
+        request: T,
+    ) -> impl Future<Output = Result<T::Response>> {
+        self.request_internal(None, receiver_id, request)
+            .map_ok(|envelope| envelope.payload)
+    }
+
+    pub fn request_envelope<T: RequestMessage>(
+        &self,
+        receiver_id: ConnectionId,
+        request: T,
+    ) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
+        self.request_internal(None, receiver_id, request)
+    }
+
+    pub fn forward_request<T: RequestMessage>(
+        &self,
+        sender_id: ConnectionId,
+        receiver_id: ConnectionId,
+        request: T,
+    ) -> impl Future<Output = Result<T::Response>> {
+        self.request_internal(Some(sender_id), receiver_id, request)
+            .map_ok(|envelope| envelope.payload)
+    }
+
+    pub fn request_internal<T: RequestMessage>(
+        &self,
+        original_sender_id: Option<ConnectionId>,
+        receiver_id: ConnectionId,
+        request: T,
+    ) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
+        let (tx, rx) = oneshot::channel();
+        let send = self.connection_state(receiver_id).and_then(|connection| {
+            let message_id = connection.next_message_id.fetch_add(1, SeqCst);
+            connection
+                .response_channels
+                .lock()
+                .as_mut()
+                .ok_or_else(|| anyhow!("connection was closed"))?
+                .insert(message_id, tx);
+            connection
+                .outgoing_tx
+                .unbounded_send(proto::Message::Envelope(request.into_envelope(
+                    message_id,
+                    None,
+                    original_sender_id.map(Into::into),
+                )))
+                .map_err(|_| anyhow!("connection was closed"))?;
+            Ok(())
+        });
+        async move {
+            send?;
+            let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
+
+            if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
+                Err(anyhow!(
+                    "RPC request {} failed - {}",
+                    T::NAME,
+                    error.message
+                ))
+            } else {
+                Ok(TypedEnvelope {
+                    message_id: response.id,
+                    sender_id: receiver_id,
+                    original_sender_id: response.original_sender_id,
+                    payload: T::Response::from_envelope(response)
+                        .ok_or_else(|| anyhow!("received response of the wrong type"))?,
+                })
+            }
+        }
+    }
+
+    pub fn send<T: EnvelopedMessage>(&self, receiver_id: ConnectionId, message: T) -> Result<()> {
+        let connection = self.connection_state(receiver_id)?;
+        let message_id = connection
+            .next_message_id
+            .fetch_add(1, atomic::Ordering::SeqCst);
+        connection
+            .outgoing_tx
+            .unbounded_send(proto::Message::Envelope(
+                message.into_envelope(message_id, None, None),
+            ))?;
+        Ok(())
+    }
+
+    pub fn forward_send<T: EnvelopedMessage>(
+        &self,
+        sender_id: ConnectionId,
+        receiver_id: ConnectionId,
+        message: T,
+    ) -> Result<()> {
+        let connection = self.connection_state(receiver_id)?;
+        let message_id = connection
+            .next_message_id
+            .fetch_add(1, atomic::Ordering::SeqCst);
+        connection
+            .outgoing_tx
+            .unbounded_send(proto::Message::Envelope(message.into_envelope(
+                message_id,
+                None,
+                Some(sender_id.into()),
+            )))?;
+        Ok(())
+    }
+
+    pub fn respond<T: RequestMessage>(
+        &self,
+        receipt: Receipt<T>,
+        response: T::Response,
+    ) -> Result<()> {
+        let connection = self.connection_state(receipt.sender_id)?;
+        let message_id = connection
+            .next_message_id
+            .fetch_add(1, atomic::Ordering::SeqCst);
+        connection
+            .outgoing_tx
+            .unbounded_send(proto::Message::Envelope(response.into_envelope(
+                message_id,
+                Some(receipt.message_id),
+                None,
+            )))?;
+        Ok(())
+    }
+
+    pub fn respond_with_error<T: RequestMessage>(
+        &self,
+        receipt: Receipt<T>,
+        response: proto::Error,
+    ) -> Result<()> {
+        let connection = self.connection_state(receipt.sender_id)?;
+        let message_id = connection
+            .next_message_id
+            .fetch_add(1, atomic::Ordering::SeqCst);
+        connection
+            .outgoing_tx
+            .unbounded_send(proto::Message::Envelope(response.into_envelope(
+                message_id,
+                Some(receipt.message_id),
+                None,
+            )))?;
+        Ok(())
+    }
+
+    pub fn respond_with_unhandled_message(
+        &self,
+        envelope: Box<dyn AnyTypedEnvelope>,
+    ) -> Result<()> {
+        let connection = self.connection_state(envelope.sender_id())?;
+        let response = proto::Error {
+            message: format!("message {} was not handled", envelope.payload_type_name()),
+        };
+        let message_id = connection
+            .next_message_id
+            .fetch_add(1, atomic::Ordering::SeqCst);
+        connection
+            .outgoing_tx
+            .unbounded_send(proto::Message::Envelope(response.into_envelope(
+                message_id,
+                Some(envelope.message_id()),
+                None,
+            )))?;
+        Ok(())
+    }
+
+    fn connection_state(&self, connection_id: ConnectionId) -> Result<ConnectionState> {
+        let connections = self.connections.read();
+        let connection = connections
+            .get(&connection_id)
+            .ok_or_else(|| anyhow!("no such connection: {}", connection_id))?;
+        Ok(connection.clone())
+    }
+}
+
+impl Serialize for Peer {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let mut state = serializer.serialize_struct("Peer", 2)?;
+        state.serialize_field("connections", &*self.connections.read())?;
+        state.end()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::TypedEnvelope;
+    use async_tungstenite::tungstenite::Message as WebSocketMessage;
+    use gpui2::TestAppContext;
+
+    #[ctor::ctor]
+    fn init_logger() {
+        if std::env::var("RUST_LOG").is_ok() {
+            env_logger::init();
+        }
+    }
+
+    #[gpui2::test(iterations = 50)]
+    async fn test_request_response(cx: &mut TestAppContext) {
+        let executor = cx.executor();
+
+        // create 2 clients connected to 1 server
+        let server = Peer::new(0);
+        let client1 = Peer::new(0);
+        let client2 = Peer::new(0);
+
+        let (client1_to_server_conn, server_to_client_1_conn, _kill) =
+            Connection::in_memory(cx.executor().clone());
+        let (client1_conn_id, io_task1, client1_incoming) =
+            client1.add_test_connection(client1_to_server_conn, cx.executor().clone());
+        let (_, io_task2, server_incoming1) =
+            server.add_test_connection(server_to_client_1_conn, cx.executor().clone());
+
+        let (client2_to_server_conn, server_to_client_2_conn, _kill) =
+            Connection::in_memory(cx.executor().clone());
+        let (client2_conn_id, io_task3, client2_incoming) =
+            client2.add_test_connection(client2_to_server_conn, cx.executor().clone());
+        let (_, io_task4, server_incoming2) =
+            server.add_test_connection(server_to_client_2_conn, cx.executor().clone());
+
+        executor.spawn(io_task1).detach();
+        executor.spawn(io_task2).detach();
+        executor.spawn(io_task3).detach();
+        executor.spawn(io_task4).detach();
+        executor
+            .spawn(handle_messages(server_incoming1, server.clone()))
+            .detach();
+        executor
+            .spawn(handle_messages(client1_incoming, client1.clone()))
+            .detach();
+        executor
+            .spawn(handle_messages(server_incoming2, server.clone()))
+            .detach();
+        executor
+            .spawn(handle_messages(client2_incoming, client2.clone()))
+            .detach();
+
+        assert_eq!(
+            client1
+                .request(client1_conn_id, proto::Ping {},)
+                .await
+                .unwrap(),
+            proto::Ack {}
+        );
+
+        assert_eq!(
+            client2
+                .request(client2_conn_id, proto::Ping {},)
+                .await
+                .unwrap(),
+            proto::Ack {}
+        );
+
+        assert_eq!(
+            client1
+                .request(client1_conn_id, proto::Test { id: 1 },)
+                .await
+                .unwrap(),
+            proto::Test { id: 1 }
+        );
+
+        assert_eq!(
+            client2
+                .request(client2_conn_id, proto::Test { id: 2 })
+                .await
+                .unwrap(),
+            proto::Test { id: 2 }
+        );
+
+        client1.disconnect(client1_conn_id);
+        client2.disconnect(client1_conn_id);
+
+        async fn handle_messages(
+            mut messages: BoxStream<'static, Box<dyn AnyTypedEnvelope>>,
+            peer: Arc<Peer>,
+        ) -> Result<()> {
+            while let Some(envelope) = messages.next().await {
+                let envelope = envelope.into_any();
+                if let Some(envelope) = envelope.downcast_ref::<TypedEnvelope<proto::Ping>>() {
+                    let receipt = envelope.receipt();
+                    peer.respond(receipt, proto::Ack {})?
+                } else if let Some(envelope) = envelope.downcast_ref::<TypedEnvelope<proto::Test>>()
+                {
+                    peer.respond(envelope.receipt(), envelope.payload.clone())?
+                } else {
+                    panic!("unknown message type");
+                }
+            }
+
+            Ok(())
+        }
+    }
+
+    #[gpui2::test(iterations = 50)]
+    async fn test_order_of_response_and_incoming(cx: &mut TestAppContext) {
+        let executor = cx.executor();
+        let server = Peer::new(0);
+        let client = Peer::new(0);
+
+        let (client_to_server_conn, server_to_client_conn, _kill) =
+            Connection::in_memory(executor.clone());
+        let (client_to_server_conn_id, io_task1, mut client_incoming) =
+            client.add_test_connection(client_to_server_conn, executor.clone());
+
+        let (server_to_client_conn_id, io_task2, mut server_incoming) =
+            server.add_test_connection(server_to_client_conn, executor.clone());
+
+        executor.spawn(io_task1).detach();
+        executor.spawn(io_task2).detach();
+
+        executor
+            .spawn(async move {
+                let future = server_incoming.next().await;
+                let request = future
+                    .unwrap()
+                    .into_any()
+                    .downcast::<TypedEnvelope<proto::Ping>>()
+                    .unwrap();
+
+                server
+                    .send(
+                        server_to_client_conn_id,
+                        proto::Error {
+                            message: "message 1".to_string(),
+                        },
+                    )
+                    .unwrap();
+                server
+                    .send(
+                        server_to_client_conn_id,
+                        proto::Error {
+                            message: "message 2".to_string(),
+                        },
+                    )
+                    .unwrap();
+                server.respond(request.receipt(), proto::Ack {}).unwrap();
+
+                // Prevent the connection from being dropped
+                server_incoming.next().await;
+            })
+            .detach();
+
+        let events = Arc::new(Mutex::new(Vec::new()));
+
+        let response = client.request(client_to_server_conn_id, proto::Ping {});
+        let response_task = executor.spawn({
+            let events = events.clone();
+            async move {
+                response.await.unwrap();
+                events.lock().push("response".to_string());
+            }
+        });
+
+        executor
+            .spawn({
+                let events = events.clone();
+                async move {
+                    let incoming1 = client_incoming
+                        .next()
+                        .await
+                        .unwrap()
+                        .into_any()
+                        .downcast::<TypedEnvelope<proto::Error>>()
+                        .unwrap();
+                    events.lock().push(incoming1.payload.message);
+                    let incoming2 = client_incoming
+                        .next()
+                        .await
+                        .unwrap()
+                        .into_any()
+                        .downcast::<TypedEnvelope<proto::Error>>()
+                        .unwrap();
+                    events.lock().push(incoming2.payload.message);
+
+                    // Prevent the connection from being dropped
+                    client_incoming.next().await;
+                }
+            })
+            .detach();
+
+        response_task.await;
+        assert_eq!(
+            &*events.lock(),
+            &[
+                "message 1".to_string(),
+                "message 2".to_string(),
+                "response".to_string()
+            ]
+        );
+    }
+
+    #[gpui2::test(iterations = 50)]
+    async fn test_dropping_request_before_completion(cx: &mut TestAppContext) {
+        let executor = cx.executor().clone();
+        let server = Peer::new(0);
+        let client = Peer::new(0);
+
+        let (client_to_server_conn, server_to_client_conn, _kill) =
+            Connection::in_memory(cx.executor().clone());
+        let (client_to_server_conn_id, io_task1, mut client_incoming) =
+            client.add_test_connection(client_to_server_conn, cx.executor().clone());
+        let (server_to_client_conn_id, io_task2, mut server_incoming) =
+            server.add_test_connection(server_to_client_conn, cx.executor().clone());
+
+        executor.spawn(io_task1).detach();
+        executor.spawn(io_task2).detach();
+
+        executor
+            .spawn(async move {
+                let request1 = server_incoming
+                    .next()
+                    .await
+                    .unwrap()
+                    .into_any()
+                    .downcast::<TypedEnvelope<proto::Ping>>()
+                    .unwrap();
+                let request2 = server_incoming
+                    .next()
+                    .await
+                    .unwrap()
+                    .into_any()
+                    .downcast::<TypedEnvelope<proto::Ping>>()
+                    .unwrap();
+
+                server
+                    .send(
+                        server_to_client_conn_id,
+                        proto::Error {
+                            message: "message 1".to_string(),
+                        },
+                    )
+                    .unwrap();
+                server
+                    .send(
+                        server_to_client_conn_id,
+                        proto::Error {
+                            message: "message 2".to_string(),
+                        },
+                    )
+                    .unwrap();
+                server.respond(request1.receipt(), proto::Ack {}).unwrap();
+                server.respond(request2.receipt(), proto::Ack {}).unwrap();
+
+                // Prevent the connection from being dropped
+                server_incoming.next().await;
+            })
+            .detach();
+
+        let events = Arc::new(Mutex::new(Vec::new()));
+
+        let request1 = client.request(client_to_server_conn_id, proto::Ping {});
+        let request1_task = executor.spawn(request1);
+        let request2 = client.request(client_to_server_conn_id, proto::Ping {});
+        let request2_task = executor.spawn({
+            let events = events.clone();
+            async move {
+                request2.await.unwrap();
+                events.lock().push("response 2".to_string());
+            }
+        });
+
+        executor
+            .spawn({
+                let events = events.clone();
+                async move {
+                    let incoming1 = client_incoming
+                        .next()
+                        .await
+                        .unwrap()
+                        .into_any()
+                        .downcast::<TypedEnvelope<proto::Error>>()
+                        .unwrap();
+                    events.lock().push(incoming1.payload.message);
+                    let incoming2 = client_incoming
+                        .next()
+                        .await
+                        .unwrap()
+                        .into_any()
+                        .downcast::<TypedEnvelope<proto::Error>>()
+                        .unwrap();
+                    events.lock().push(incoming2.payload.message);
+
+                    // Prevent the connection from being dropped
+                    client_incoming.next().await;
+                }
+            })
+            .detach();
+
+        // Allow the request to make some progress before dropping it.
+        cx.executor().simulate_random_delay().await;
+        drop(request1_task);
+
+        request2_task.await;
+        assert_eq!(
+            &*events.lock(),
+            &[
+                "message 1".to_string(),
+                "message 2".to_string(),
+                "response 2".to_string()
+            ]
+        );
+    }
+
+    #[gpui2::test(iterations = 50)]
+    async fn test_disconnect(cx: &mut TestAppContext) {
+        let executor = cx.executor();
+
+        let (client_conn, mut server_conn, _kill) = Connection::in_memory(executor.clone());
+
+        let client = Peer::new(0);
+        let (connection_id, io_handler, mut incoming) =
+            client.add_test_connection(client_conn, executor.clone());
+
+        let (io_ended_tx, io_ended_rx) = oneshot::channel();
+        executor
+            .spawn(async move {
+                io_handler.await.ok();
+                io_ended_tx.send(()).unwrap();
+            })
+            .detach();
+
+        let (messages_ended_tx, messages_ended_rx) = oneshot::channel();
+        executor
+            .spawn(async move {
+                incoming.next().await;
+                messages_ended_tx.send(()).unwrap();
+            })
+            .detach();
+
+        client.disconnect(connection_id);
+
+        let _ = io_ended_rx.await;
+        let _ = messages_ended_rx.await;
+        assert!(server_conn
+            .send(WebSocketMessage::Binary(vec![]))
+            .await
+            .is_err());
+    }
+
+    #[gpui2::test(iterations = 50)]
+    async fn test_io_error(cx: &mut TestAppContext) {
+        let executor = cx.executor();
+        let (client_conn, mut server_conn, _kill) = Connection::in_memory(executor.clone());
+
+        let client = Peer::new(0);
+        let (connection_id, io_handler, mut incoming) =
+            client.add_test_connection(client_conn, executor.clone());
+        executor.spawn(io_handler).detach();
+        executor
+            .spawn(async move { incoming.next().await })
+            .detach();
+
+        let response = executor.spawn(client.request(connection_id, proto::Ping {}));
+        let _request = server_conn.rx.next().await.unwrap().unwrap();
+
+        drop(server_conn);
+        assert_eq!(
+            response.await.unwrap_err().to_string(),
+            "connection was closed"
+        );
+    }
+}

crates/rpc2/src/proto.rs 🔗

@@ -0,0 +1,674 @@
+#![allow(non_snake_case)]
+
+use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope};
+use anyhow::{anyhow, Result};
+use async_tungstenite::tungstenite::Message as WebSocketMessage;
+use collections::HashMap;
+use futures::{SinkExt as _, StreamExt as _};
+use prost::Message as _;
+use serde::Serialize;
+use std::any::{Any, TypeId};
+use std::{
+    cmp,
+    fmt::Debug,
+    io, iter,
+    time::{Duration, SystemTime, UNIX_EPOCH},
+};
+use std::{fmt, mem};
+
+include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
+
+pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 'static {
+    const NAME: &'static str;
+    const PRIORITY: MessagePriority;
+    fn into_envelope(
+        self,
+        id: u32,
+        responding_to: Option<u32>,
+        original_sender_id: Option<PeerId>,
+    ) -> Envelope;
+    fn from_envelope(envelope: Envelope) -> Option<Self>;
+}
+
+pub trait EntityMessage: EnvelopedMessage {
+    fn remote_entity_id(&self) -> u64;
+}
+
+pub trait RequestMessage: EnvelopedMessage {
+    type Response: EnvelopedMessage;
+}
+
+pub trait AnyTypedEnvelope: 'static + Send + Sync {
+    fn payload_type_id(&self) -> TypeId;
+    fn payload_type_name(&self) -> &'static str;
+    fn as_any(&self) -> &dyn Any;
+    fn into_any(self: Box<Self>) -> Box<dyn Any + Send + Sync>;
+    fn is_background(&self) -> bool;
+    fn original_sender_id(&self) -> Option<PeerId>;
+    fn sender_id(&self) -> ConnectionId;
+    fn message_id(&self) -> u32;
+}
+
+pub enum MessagePriority {
+    Foreground,
+    Background,
+}
+
+impl<T: EnvelopedMessage> AnyTypedEnvelope for TypedEnvelope<T> {
+    fn payload_type_id(&self) -> TypeId {
+        TypeId::of::<T>()
+    }
+
+    fn payload_type_name(&self) -> &'static str {
+        T::NAME
+    }
+
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn into_any(self: Box<Self>) -> Box<dyn Any + Send + Sync> {
+        self
+    }
+
+    fn is_background(&self) -> bool {
+        matches!(T::PRIORITY, MessagePriority::Background)
+    }
+
+    fn original_sender_id(&self) -> Option<PeerId> {
+        self.original_sender_id
+    }
+
+    fn sender_id(&self) -> ConnectionId {
+        self.sender_id
+    }
+
+    fn message_id(&self) -> u32 {
+        self.message_id
+    }
+}
+
+impl PeerId {
+    pub fn from_u64(peer_id: u64) -> Self {
+        let owner_id = (peer_id >> 32) as u32;
+        let id = peer_id as u32;
+        Self { owner_id, id }
+    }
+
+    pub fn as_u64(self) -> u64 {
+        ((self.owner_id as u64) << 32) | (self.id as u64)
+    }
+}
+
+impl Copy for PeerId {}
+
+impl Eq for PeerId {}
+
+impl Ord for PeerId {
+    fn cmp(&self, other: &Self) -> cmp::Ordering {
+        self.owner_id
+            .cmp(&other.owner_id)
+            .then_with(|| self.id.cmp(&other.id))
+    }
+}
+
+impl PartialOrd for PeerId {
+    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl std::hash::Hash for PeerId {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.owner_id.hash(state);
+        self.id.hash(state);
+    }
+}
+
+impl fmt::Display for PeerId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}/{}", self.owner_id, self.id)
+    }
+}
+
+messages!(
+    (Ack, Foreground),
+    (AddProjectCollaborator, Foreground),
+    (ApplyCodeAction, Background),
+    (ApplyCodeActionResponse, Background),
+    (ApplyCompletionAdditionalEdits, Background),
+    (ApplyCompletionAdditionalEditsResponse, Background),
+    (BufferReloaded, Foreground),
+    (BufferSaved, Foreground),
+    (Call, Foreground),
+    (CallCanceled, Foreground),
+    (CancelCall, Foreground),
+    (CopyProjectEntry, Foreground),
+    (CreateBufferForPeer, Foreground),
+    (CreateChannel, Foreground),
+    (CreateChannelResponse, Foreground),
+    (ChannelMessageSent, Foreground),
+    (CreateProjectEntry, Foreground),
+    (CreateRoom, Foreground),
+    (CreateRoomResponse, Foreground),
+    (DeclineCall, Foreground),
+    (DeleteProjectEntry, Foreground),
+    (Error, Foreground),
+    (ExpandProjectEntry, Foreground),
+    (Follow, Foreground),
+    (FollowResponse, Foreground),
+    (FormatBuffers, Foreground),
+    (FormatBuffersResponse, Foreground),
+    (FuzzySearchUsers, Foreground),
+    (GetCodeActions, Background),
+    (GetCodeActionsResponse, Background),
+    (GetHover, Background),
+    (GetHoverResponse, Background),
+    (GetChannelMessages, Background),
+    (GetChannelMessagesResponse, Background),
+    (SendChannelMessage, Background),
+    (SendChannelMessageResponse, Background),
+    (GetCompletions, Background),
+    (GetCompletionsResponse, Background),
+    (GetDefinition, Background),
+    (GetDefinitionResponse, Background),
+    (GetTypeDefinition, Background),
+    (GetTypeDefinitionResponse, Background),
+    (GetDocumentHighlights, Background),
+    (GetDocumentHighlightsResponse, Background),
+    (GetReferences, Background),
+    (GetReferencesResponse, Background),
+    (GetProjectSymbols, Background),
+    (GetProjectSymbolsResponse, Background),
+    (GetUsers, Foreground),
+    (Hello, Foreground),
+    (IncomingCall, Foreground),
+    (InviteChannelMember, Foreground),
+    (UsersResponse, Foreground),
+    (JoinProject, Foreground),
+    (JoinProjectResponse, Foreground),
+    (JoinRoom, Foreground),
+    (JoinRoomResponse, Foreground),
+    (JoinChannelChat, Foreground),
+    (JoinChannelChatResponse, Foreground),
+    (LeaveChannelChat, Foreground),
+    (LeaveProject, Foreground),
+    (LeaveRoom, Foreground),
+    (OpenBufferById, Background),
+    (OpenBufferByPath, Background),
+    (OpenBufferForSymbol, Background),
+    (OpenBufferForSymbolResponse, Background),
+    (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),
+    (RejoinRoom, Foreground),
+    (RejoinRoomResponse, Foreground),
+    (RemoveContact, Foreground),
+    (RemoveChannelMember, Foreground),
+    (RemoveChannelMessage, Foreground),
+    (ReloadBuffers, Foreground),
+    (ReloadBuffersResponse, Foreground),
+    (RemoveProjectCollaborator, Foreground),
+    (RenameProjectEntry, Foreground),
+    (RequestContact, Foreground),
+    (RespondToContactRequest, Foreground),
+    (RespondToChannelInvite, Foreground),
+    (JoinChannel, Foreground),
+    (RoomUpdated, Foreground),
+    (SaveBuffer, Foreground),
+    (RenameChannel, Foreground),
+    (RenameChannelResponse, Foreground),
+    (SetChannelMemberAdmin, Foreground),
+    (SearchProject, Background),
+    (SearchProjectResponse, 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),
+    (UpdateChannels, Foreground),
+    (UpdateDiagnosticSummary, Foreground),
+    (UpdateFollowers, Foreground),
+    (UpdateInviteInfo, Foreground),
+    (UpdateLanguageServer, Foreground),
+    (UpdateParticipantLocation, Foreground),
+    (UpdateProject, Foreground),
+    (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),
+);
+
+request_messages!(
+    (ApplyCodeAction, ApplyCodeActionResponse),
+    (
+        ApplyCompletionAdditionalEdits,
+        ApplyCompletionAdditionalEditsResponse
+    ),
+    (Call, Ack),
+    (CancelCall, Ack),
+    (CopyProjectEntry, ProjectEntryResponse),
+    (CreateProjectEntry, ProjectEntryResponse),
+    (CreateRoom, CreateRoomResponse),
+    (CreateChannel, CreateChannelResponse),
+    (DeclineCall, Ack),
+    (DeleteProjectEntry, ProjectEntryResponse),
+    (ExpandProjectEntry, ExpandProjectEntryResponse),
+    (Follow, FollowResponse),
+    (FormatBuffers, FormatBuffersResponse),
+    (GetCodeActions, GetCodeActionsResponse),
+    (GetHover, GetHoverResponse),
+    (GetCompletions, GetCompletionsResponse),
+    (GetDefinition, GetDefinitionResponse),
+    (GetTypeDefinition, GetTypeDefinitionResponse),
+    (GetDocumentHighlights, GetDocumentHighlightsResponse),
+    (GetReferences, GetReferencesResponse),
+    (GetPrivateUserInfo, GetPrivateUserInfoResponse),
+    (GetProjectSymbols, GetProjectSymbolsResponse),
+    (FuzzySearchUsers, UsersResponse),
+    (GetUsers, UsersResponse),
+    (InviteChannelMember, Ack),
+    (JoinProject, JoinProjectResponse),
+    (JoinRoom, JoinRoomResponse),
+    (JoinChannelChat, JoinChannelChatResponse),
+    (LeaveRoom, Ack),
+    (RejoinRoom, RejoinRoomResponse),
+    (IncomingCall, Ack),
+    (OpenBufferById, OpenBufferResponse),
+    (OpenBufferByPath, OpenBufferResponse),
+    (OpenBufferForSymbol, OpenBufferForSymbolResponse),
+    (Ping, Ack),
+    (PerformRename, PerformRenameResponse),
+    (PrepareRename, PrepareRenameResponse),
+    (OnTypeFormatting, OnTypeFormattingResponse),
+    (InlayHints, InlayHintsResponse),
+    (ResolveInlayHint, ResolveInlayHintResponse),
+    (RefreshInlayHints, Ack),
+    (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),
+    (RenameChannel, RenameChannelResponse),
+    (LinkChannel, Ack),
+    (UnlinkChannel, Ack),
+    (MoveChannel, Ack),
+    (SaveBuffer, BufferSaved),
+    (SearchProject, SearchProjectResponse),
+    (ShareProject, ShareProjectResponse),
+    (SynchronizeBuffers, SynchronizeBuffersResponse),
+    (RejoinChannelBuffers, RejoinChannelBuffersResponse),
+    (Test, Test),
+    (UpdateBuffer, Ack),
+    (UpdateParticipantLocation, Ack),
+    (UpdateProject, Ack),
+    (UpdateWorktree, Ack),
+    (JoinChannelBuffer, JoinChannelBufferResponse),
+    (LeaveChannelBuffer, Ack)
+);
+
+entity_messages!(
+    project_id,
+    AddProjectCollaborator,
+    ApplyCodeAction,
+    ApplyCompletionAdditionalEdits,
+    BufferReloaded,
+    BufferSaved,
+    CopyProjectEntry,
+    CreateBufferForPeer,
+    CreateProjectEntry,
+    DeleteProjectEntry,
+    ExpandProjectEntry,
+    FormatBuffers,
+    GetCodeActions,
+    GetCompletions,
+    GetDefinition,
+    GetTypeDefinition,
+    GetDocumentHighlights,
+    GetHover,
+    GetReferences,
+    GetProjectSymbols,
+    JoinProject,
+    LeaveProject,
+    OpenBufferById,
+    OpenBufferByPath,
+    OpenBufferForSymbol,
+    PerformRename,
+    OnTypeFormatting,
+    InlayHints,
+    ResolveInlayHint,
+    RefreshInlayHints,
+    PrepareRename,
+    ReloadBuffers,
+    RemoveProjectCollaborator,
+    RenameProjectEntry,
+    SaveBuffer,
+    SearchProject,
+    StartLanguageServer,
+    SynchronizeBuffers,
+    UnshareProject,
+    UpdateBuffer,
+    UpdateBufferFile,
+    UpdateDiagnosticSummary,
+    UpdateLanguageServer,
+    UpdateProject,
+    UpdateProjectCollaborator,
+    UpdateWorktree,
+    UpdateWorktreeSettings,
+    UpdateDiffBase
+);
+
+entity_messages!(
+    channel_id,
+    ChannelMessageSent,
+    UpdateChannelBuffer,
+    RemoveChannelMessage,
+    UpdateChannelBufferCollaborators,
+);
+
+const KIB: usize = 1024;
+const MIB: usize = KIB * 1024;
+const MAX_BUFFER_LEN: usize = MIB;
+
+/// A stream of protobuf messages.
+pub struct MessageStream<S> {
+    stream: S,
+    encoding_buffer: Vec<u8>,
+}
+
+#[allow(clippy::large_enum_variant)]
+#[derive(Debug)]
+pub enum Message {
+    Envelope(Envelope),
+    Ping,
+    Pong,
+}
+
+impl<S> MessageStream<S> {
+    pub fn new(stream: S) -> Self {
+        Self {
+            stream,
+            encoding_buffer: Vec::new(),
+        }
+    }
+
+    pub fn inner_mut(&mut self) -> &mut S {
+        &mut self.stream
+    }
+}
+
+impl<S> MessageStream<S>
+where
+    S: futures::Sink<WebSocketMessage, Error = anyhow::Error> + Unpin,
+{
+    pub async fn write(&mut self, message: Message) -> Result<(), anyhow::Error> {
+        #[cfg(any(test, feature = "test-support"))]
+        const COMPRESSION_LEVEL: i32 = -7;
+
+        #[cfg(not(any(test, feature = "test-support")))]
+        const COMPRESSION_LEVEL: i32 = 4;
+
+        match message {
+            Message::Envelope(message) => {
+                self.encoding_buffer.reserve(message.encoded_len());
+                message
+                    .encode(&mut self.encoding_buffer)
+                    .map_err(io::Error::from)?;
+                let buffer =
+                    zstd::stream::encode_all(self.encoding_buffer.as_slice(), COMPRESSION_LEVEL)
+                        .unwrap();
+
+                self.encoding_buffer.clear();
+                self.encoding_buffer.shrink_to(MAX_BUFFER_LEN);
+                self.stream.send(WebSocketMessage::Binary(buffer)).await?;
+            }
+            Message::Ping => {
+                self.stream
+                    .send(WebSocketMessage::Ping(Default::default()))
+                    .await?;
+            }
+            Message::Pong => {
+                self.stream
+                    .send(WebSocketMessage::Pong(Default::default()))
+                    .await?;
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl<S> MessageStream<S>
+where
+    S: futures::Stream<Item = Result<WebSocketMessage, anyhow::Error>> + Unpin,
+{
+    pub async fn read(&mut self) -> Result<Message, anyhow::Error> {
+        while let Some(bytes) = self.stream.next().await {
+            match bytes? {
+                WebSocketMessage::Binary(bytes) => {
+                    zstd::stream::copy_decode(bytes.as_slice(), &mut self.encoding_buffer).unwrap();
+                    let envelope = Envelope::decode(self.encoding_buffer.as_slice())
+                        .map_err(io::Error::from)?;
+
+                    self.encoding_buffer.clear();
+                    self.encoding_buffer.shrink_to(MAX_BUFFER_LEN);
+                    return Ok(Message::Envelope(envelope));
+                }
+                WebSocketMessage::Ping(_) => return Ok(Message::Ping),
+                WebSocketMessage::Pong(_) => return Ok(Message::Pong),
+                WebSocketMessage::Close(_) => break,
+                _ => {}
+            }
+        }
+        Err(anyhow!("connection closed"))
+    }
+}
+
+impl From<Timestamp> for SystemTime {
+    fn from(val: Timestamp) -> Self {
+        UNIX_EPOCH
+            .checked_add(Duration::new(val.seconds, val.nanos))
+            .unwrap()
+    }
+}
+
+impl From<SystemTime> for Timestamp {
+    fn from(time: SystemTime) -> Self {
+        let duration = time.duration_since(UNIX_EPOCH).unwrap();
+        Self {
+            seconds: duration.as_secs(),
+            nanos: duration.subsec_nanos(),
+        }
+    }
+}
+
+impl From<u128> for Nonce {
+    fn from(nonce: u128) -> Self {
+        let upper_half = (nonce >> 64) as u64;
+        let lower_half = nonce as u64;
+        Self {
+            upper_half,
+            lower_half,
+        }
+    }
+}
+
+impl From<Nonce> for u128 {
+    fn from(nonce: Nonce) -> Self {
+        let upper_half = (nonce.upper_half as u128) << 64;
+        let lower_half = nonce.lower_half as u128;
+        upper_half | lower_half
+    }
+}
+
+pub fn split_worktree_update(
+    mut message: UpdateWorktree,
+    max_chunk_size: usize,
+) -> impl Iterator<Item = UpdateWorktree> {
+    let mut done_files = false;
+
+    let mut repository_map = message
+        .updated_repositories
+        .into_iter()
+        .map(|repo| (repo.work_directory_id, repo))
+        .collect::<HashMap<_, _>>();
+
+    iter::from_fn(move || {
+        if done_files {
+            return None;
+        }
+
+        let updated_entries_chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size);
+        let updated_entries: Vec<_> = message
+            .updated_entries
+            .drain(..updated_entries_chunk_size)
+            .collect();
+
+        let removed_entries_chunk_size = cmp::min(message.removed_entries.len(), max_chunk_size);
+        let removed_entries = message
+            .removed_entries
+            .drain(..removed_entries_chunk_size)
+            .collect();
+
+        done_files = message.updated_entries.is_empty() && message.removed_entries.is_empty();
+
+        let mut updated_repositories = Vec::new();
+
+        if !repository_map.is_empty() {
+            for entry in &updated_entries {
+                if let Some(repo) = repository_map.remove(&entry.id) {
+                    updated_repositories.push(repo)
+                }
+            }
+        }
+
+        let removed_repositories = if done_files {
+            mem::take(&mut message.removed_repositories)
+        } else {
+            Default::default()
+        };
+
+        if done_files {
+            updated_repositories.extend(mem::take(&mut repository_map).into_values());
+        }
+
+        Some(UpdateWorktree {
+            project_id: message.project_id,
+            worktree_id: message.worktree_id,
+            root_name: message.root_name.clone(),
+            abs_path: message.abs_path.clone(),
+            updated_entries,
+            removed_entries,
+            scan_id: message.scan_id,
+            is_last_update: done_files && message.is_last_update,
+            updated_repositories,
+            removed_repositories,
+        })
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[gpui2::test]
+    async fn test_buffer_size() {
+        let (tx, rx) = futures::channel::mpsc::unbounded();
+        let mut sink = MessageStream::new(tx.sink_map_err(|_| anyhow!("")));
+        sink.write(Message::Envelope(Envelope {
+            payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree {
+                root_name: "abcdefg".repeat(10),
+                ..Default::default()
+            })),
+            ..Default::default()
+        }))
+        .await
+        .unwrap();
+        assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN);
+        sink.write(Message::Envelope(Envelope {
+            payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree {
+                root_name: "abcdefg".repeat(1000000),
+                ..Default::default()
+            })),
+            ..Default::default()
+        }))
+        .await
+        .unwrap();
+        assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN);
+
+        let mut stream = MessageStream::new(rx.map(anyhow::Ok));
+        stream.read().await.unwrap();
+        assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN);
+        stream.read().await.unwrap();
+        assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN);
+    }
+
+    #[gpui2::test]
+    fn test_converting_peer_id_from_and_to_u64() {
+        let peer_id = PeerId {
+            owner_id: 10,
+            id: 3,
+        };
+        assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
+        let peer_id = PeerId {
+            owner_id: u32::MAX,
+            id: 3,
+        };
+        assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
+        let peer_id = PeerId {
+            owner_id: 10,
+            id: u32::MAX,
+        };
+        assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
+        let peer_id = PeerId {
+            owner_id: u32::MAX,
+            id: u32::MAX,
+        };
+        assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
+    }
+}

crates/rpc2/src/rpc.rs 🔗

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

crates/settings2/Cargo.toml 🔗

@@ -0,0 +1,42 @@
+[package]
+name = "settings2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/settings2.rs"
+doctest = false
+
+[features]
+test-support = ["gpui2/test-support", "fs/test-support"]
+
+[dependencies]
+collections = { path = "../collections" }
+gpui2 = { path = "../gpui2" }
+sqlez = { path = "../sqlez" }
+fs2 = { path = "../fs2" }
+feature_flags2 = { path = "../feature_flags2" }
+util = { path = "../util" }
+
+anyhow.workspace = true
+futures.workspace = true
+serde_json_lenient = {version = "0.1", features = ["preserve_order", "raw_value"]}
+lazy_static.workspace = true
+postage.workspace = true
+rust-embed.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+smallvec.workspace = true
+toml.workspace = true
+tree-sitter.workspace = true
+tree-sitter-json = "*"
+
+[dev-dependencies]
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }
+indoc.workspace = true
+pretty_assertions.workspace = true
+unindent.workspace = true

crates/settings2/src/keymap_file.rs 🔗

@@ -0,0 +1,163 @@
+use crate::{settings_store::parse_json_with_comments, SettingsAssets};
+use anyhow::{anyhow, Context, Result};
+use collections::BTreeMap;
+use gpui2::{AppContext, KeyBinding, SharedString};
+use schemars::{
+    gen::{SchemaGenerator, SchemaSettings},
+    schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
+    JsonSchema,
+};
+use serde::Deserialize;
+use serde_json::Value;
+use util::{asset_str, ResultExt};
+
+#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
+#[serde(transparent)]
+pub struct KeymapFile(Vec<KeymapBlock>);
+
+#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
+pub struct KeymapBlock {
+    #[serde(default)]
+    context: Option<String>,
+    bindings: BTreeMap<String, KeymapAction>,
+}
+
+#[derive(Debug, Deserialize, Default, Clone)]
+#[serde(transparent)]
+pub struct KeymapAction(Value);
+
+impl JsonSchema for KeymapAction {
+    fn schema_name() -> String {
+        "KeymapAction".into()
+    }
+
+    fn json_schema(_: &mut SchemaGenerator) -> Schema {
+        Schema::Bool(true)
+    }
+}
+
+#[derive(Deserialize)]
+struct ActionWithData(Box<str>, Value);
+
+impl KeymapFile {
+    pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
+        let content = asset_str::<SettingsAssets>(asset_path);
+
+        Self::parse(content.as_ref())?.add_to_cx(cx)
+    }
+
+    pub fn parse(content: &str) -> Result<Self> {
+        parse_json_with_comments::<Self>(content)
+    }
+
+    pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
+        for KeymapBlock { context, bindings } in self.0 {
+            let bindings = bindings
+                .into_iter()
+                .filter_map(|(keystroke, action)| {
+                    let action = action.0;
+
+                    // This is a workaround for a limitation in serde: serde-rs/json#497
+                    // We want to deserialize the action data as a `RawValue` so that we can
+                    // deserialize the action itself dynamically directly from the JSON
+                    // string. But `RawValue` currently does not work inside of an untagged enum.
+                    match action {
+                        Value::Array(items) => {
+                            let Ok([name, data]): Result<[serde_json::Value; 2], _> =
+                                items.try_into()
+                            else {
+                                return Some(Err(anyhow!("Expected array of length 2")));
+                            };
+                            let serde_json::Value::String(name) = name else {
+                                return Some(Err(anyhow!(
+                                    "Expected first item in array to be a string."
+                                )));
+                            };
+                            cx.build_action(&name, Some(data))
+                        }
+                        Value::String(name) => cx.build_action(&name, None),
+                        Value::Null => Ok(no_action()),
+                        _ => {
+                            return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
+                        }
+                    }
+                    .with_context(|| {
+                        format!(
+                            "invalid binding value for keystroke {keystroke}, context {context:?}"
+                        )
+                    })
+                    .log_err()
+                    .map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
+                })
+                .collect::<Result<Vec<_>>>()?;
+
+            cx.bind_keys(bindings);
+        }
+        Ok(())
+    }
+
+    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()
+            .into_root_schema_for::<KeymapFile>();
+
+        let action_schema = Schema::Object(SchemaObject {
+            subschemas: Some(Box::new(SubschemaValidation {
+                one_of: Some(vec![
+                    Schema::Object(SchemaObject {
+                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
+                        enum_values: Some(
+                            action_names
+                                .iter()
+                                .map(|name| Value::String(name.to_string()))
+                                .collect(),
+                        ),
+                        ..Default::default()
+                    }),
+                    Schema::Object(SchemaObject {
+                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
+                        ..Default::default()
+                    }),
+                    Schema::Object(SchemaObject {
+                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
+                        ..Default::default()
+                    }),
+                ]),
+                ..Default::default()
+            })),
+            ..Default::default()
+        });
+
+        root_schema
+            .definitions
+            .insert("KeymapAction".to_owned(), action_schema);
+
+        serde_json::to_value(root_schema).unwrap()
+    }
+}
+
+fn no_action() -> Box<dyn gpui2::Action> {
+    todo!()
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::KeymapFile;
+
+    #[test]
+    fn can_deserialize_keymap_with_trailing_comma() {
+        let json = indoc::indoc! {"[
+              // Standard macOS bindings
+              {
+                \"bindings\": {
+                  \"up\": \"menu::SelectPrev\",
+                },
+              },
+            ]
+                  "
+
+        };
+        KeymapFile::parse(json).unwrap();
+    }
+}

crates/settings2/src/settings2.rs 🔗

@@ -0,0 +1,38 @@
+mod keymap_file;
+mod settings_file;
+mod settings_store;
+
+use rust_embed::RustEmbed;
+use std::{borrow::Cow, str};
+use util::asset_str;
+
+pub use keymap_file::KeymapFile;
+pub use settings_file::*;
+pub use settings_store::{Settings, SettingsJsonSchemaParams, SettingsStore};
+
+#[derive(RustEmbed)]
+#[folder = "../../assets"]
+#[include = "settings/*"]
+#[include = "keymaps/*"]
+#[exclude = "*.DS_Store"]
+pub struct SettingsAssets;
+
+pub fn default_settings() -> Cow<'static, str> {
+    asset_str::<SettingsAssets>("settings/default.json")
+}
+
+pub fn default_keymap() -> Cow<'static, str> {
+    asset_str::<SettingsAssets>("keymaps/default.json")
+}
+
+pub fn vim_keymap() -> Cow<'static, str> {
+    asset_str::<SettingsAssets>("keymaps/vim.json")
+}
+
+pub fn initial_user_settings_content() -> Cow<'static, str> {
+    asset_str::<SettingsAssets>("settings/initial_user_settings.json")
+}
+
+pub fn initial_local_settings_content() -> Cow<'static, str> {
+    asset_str::<SettingsAssets>("settings/initial_local_settings.json")
+}

crates/settings2/src/settings_file.rs 🔗

@@ -0,0 +1,116 @@
+use crate::{settings_store::SettingsStore, Settings};
+use anyhow::Result;
+use fs2::Fs;
+use futures::{channel::mpsc, StreamExt};
+use gpui2::{AppContext, Executor};
+use std::{io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
+use util::{paths, ResultExt};
+
+pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
+
+#[cfg(any(test, feature = "test-support"))]
+pub fn test_settings() -> String {
+    let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
+        crate::default_settings().as_ref(),
+    )
+    .unwrap();
+    util::merge_non_null_json_value_into(
+        serde_json::json!({
+            "buffer_font_family": "Courier",
+            "buffer_font_features": {},
+            "buffer_font_size": 14,
+            "theme": EMPTY_THEME_NAME,
+        }),
+        &mut value,
+    );
+    value.as_object_mut().unwrap().remove("languages");
+    serde_json::to_string(&value).unwrap()
+}
+
+pub fn watch_config_file(
+    executor: &Executor,
+    fs: Arc<dyn Fs>,
+    path: PathBuf,
+) -> mpsc::UnboundedReceiver<String> {
+    let (tx, rx) = mpsc::unbounded();
+    executor
+        .spawn(async move {
+            let events = fs.watch(&path, Duration::from_millis(100)).await;
+            futures::pin_mut!(events);
+
+            let contents = fs.load(&path).await.unwrap_or_default();
+            if tx.unbounded_send(contents).is_err() {
+                return;
+            }
+
+            loop {
+                if events.next().await.is_none() {
+                    break;
+                }
+
+                if let Ok(contents) = fs.load(&path).await {
+                    if !tx.unbounded_send(contents).is_ok() {
+                        break;
+                    }
+                }
+            }
+        })
+        .detach();
+    rx
+}
+
+pub fn handle_settings_file_changes(
+    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
+    cx: &mut AppContext,
+) {
+    let user_settings_content = cx.executor().block(user_settings_file_rx.next()).unwrap();
+    cx.update_global(|store: &mut SettingsStore, cx| {
+        store
+            .set_user_settings(&user_settings_content, cx)
+            .log_err();
+    });
+    cx.spawn(move |mut cx| async move {
+        while let Some(user_settings_content) = user_settings_file_rx.next().await {
+            let result = cx.update_global(|store: &mut SettingsStore, cx| {
+                store
+                    .set_user_settings(&user_settings_content, cx)
+                    .log_err();
+                cx.refresh();
+            });
+            if result.is_err() {
+                break; // App dropped
+            }
+        }
+    })
+    .detach();
+}
+
+async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
+    match fs.load(&paths::SETTINGS).await {
+        result @ Ok(_) => result,
+        Err(err) => {
+            if let Some(e) = err.downcast_ref::<std::io::Error>() {
+                if e.kind() == ErrorKind::NotFound {
+                    return Ok(crate::initial_user_settings_content().to_string());
+                }
+            }
+            return Err(err);
+        }
+    }
+}
+
+pub fn update_settings_file<T: Settings>(
+    fs: Arc<dyn Fs>,
+    cx: &mut AppContext,
+    update: impl 'static + Send + FnOnce(&mut T::FileContent),
+) {
+    cx.spawn(|cx| async move {
+        let old_text = load_settings(&fs).await?;
+        let new_text = cx.read_global(|store: &SettingsStore, _cx| {
+            store.new_text_for_update::<T>(old_text, update)
+        })?;
+        fs.atomic_write(paths::SETTINGS.clone(), new_text).await?;
+        anyhow::Ok(())
+    })
+    .detach_and_log_err(cx);
+}

crates/settings2/src/settings_store.rs 🔗

@@ -0,0 +1,1301 @@
+use anyhow::{anyhow, Context, Result};
+use collections::{btree_map, hash_map, BTreeMap, HashMap};
+use gpui2::AppContext;
+use lazy_static::lazy_static;
+use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
+use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
+use smallvec::SmallVec;
+use std::{
+    any::{type_name, Any, TypeId},
+    fmt::Debug,
+    ops::Range,
+    path::Path,
+    str,
+    sync::Arc,
+};
+use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
+
+/// A value that can be defined as a user setting.
+///
+/// Settings can be loaded from a combination of multiple JSON files.
+pub trait Settings: 'static + Send + Sync {
+    /// The name of a key within the JSON file from which this setting should
+    /// be deserialized. If this is `None`, then the setting will be deserialized
+    /// from the root object.
+    const KEY: Option<&'static str>;
+
+    /// The type that is stored in an individual JSON file.
+    type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema;
+
+    /// The logic for combining together values from one or more JSON files into the
+    /// final value for this setting.
+    ///
+    /// The user values are ordered from least specific (the global settings file)
+    /// to most specific (the innermost local settings file).
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        cx: &mut AppContext,
+    ) -> Result<Self>
+    where
+        Self: Sized;
+
+    fn json_schema(
+        generator: &mut SchemaGenerator,
+        _: &SettingsJsonSchemaParams,
+        _: &AppContext,
+    ) -> RootSchema {
+        generator.root_schema_for::<Self::FileContent>()
+    }
+
+    fn json_merge(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+    ) -> Result<Self::FileContent> {
+        let mut merged = serde_json::Value::Null;
+        for value in [default_value].iter().chain(user_values) {
+            merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
+        }
+        Ok(serde_json::from_value(merged)?)
+    }
+
+    fn load_via_json_merge(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+    ) -> Result<Self>
+    where
+        Self: DeserializeOwned,
+    {
+        let mut merged = serde_json::Value::Null;
+        for value in [default_value].iter().chain(user_values) {
+            merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
+        }
+        Ok(serde_json::from_value(merged)?)
+    }
+
+    fn missing_default() -> anyhow::Error {
+        anyhow::anyhow!("missing default")
+    }
+
+    fn register(cx: &mut AppContext)
+    where
+        Self: Sized,
+    {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.register_setting::<Self>(cx);
+        });
+    }
+
+    fn get<'a>(path: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a Self
+    where
+        Self: Sized,
+    {
+        cx.global::<SettingsStore>().get(path)
+    }
+
+    fn get_global<'a>(cx: &'a AppContext) -> &'a Self
+    where
+        Self: Sized,
+    {
+        cx.global::<SettingsStore>().get(None)
+    }
+
+    fn override_global<'a>(settings: Self, cx: &'a mut AppContext)
+    where
+        Self: Sized,
+    {
+        cx.global_mut::<SettingsStore>().override_global(settings)
+    }
+}
+
+pub struct SettingsJsonSchemaParams<'a> {
+    pub staff_mode: bool,
+    pub language_names: &'a [String],
+}
+
+/// A set of strongly-typed setting values defined via multiple JSON files.
+pub struct SettingsStore {
+    setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
+    raw_default_settings: serde_json::Value,
+    raw_user_settings: serde_json::Value,
+    raw_local_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
+    tab_size_callback: Option<(
+        TypeId,
+        Box<dyn Fn(&dyn Any) -> Option<usize> + Send + Sync + 'static>,
+    )>,
+}
+
+impl Default for SettingsStore {
+    fn default() -> Self {
+        SettingsStore {
+            setting_values: Default::default(),
+            raw_default_settings: serde_json::json!({}),
+            raw_user_settings: serde_json::json!({}),
+            raw_local_settings: Default::default(),
+            tab_size_callback: Default::default(),
+        }
+    }
+}
+
+#[derive(Debug)]
+struct SettingValue<T> {
+    global_value: Option<T>,
+    local_values: Vec<(usize, Arc<Path>, T)>,
+}
+
+trait AnySettingValue: 'static + Send + Sync {
+    fn key(&self) -> Option<&'static str>;
+    fn setting_type_name(&self) -> &'static str;
+    fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting>;
+    fn load_setting(
+        &self,
+        default_value: &DeserializedSetting,
+        custom: &[DeserializedSetting],
+        cx: &mut AppContext,
+    ) -> Result<Box<dyn Any>>;
+    fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any;
+    fn set_global_value(&mut self, value: Box<dyn Any>);
+    fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
+    fn json_schema(
+        &self,
+        generator: &mut SchemaGenerator,
+        _: &SettingsJsonSchemaParams,
+        cx: &AppContext,
+    ) -> RootSchema;
+}
+
+struct DeserializedSetting(Box<dyn Any>);
+
+impl SettingsStore {
+    /// Add a new type of setting to the store.
+    pub fn register_setting<T: Settings>(&mut self, cx: &mut AppContext) {
+        let setting_type_id = TypeId::of::<T>();
+        let entry = self.setting_values.entry(setting_type_id);
+        if matches!(entry, hash_map::Entry::Occupied(_)) {
+            return;
+        }
+
+        let setting_value = entry.or_insert(Box::new(SettingValue::<T> {
+            global_value: None,
+            local_values: Vec::new(),
+        }));
+
+        if let Some(default_settings) = setting_value
+            .deserialize_setting(&self.raw_default_settings)
+            .log_err()
+        {
+            let mut user_values_stack = Vec::new();
+
+            if let Some(user_settings) = setting_value
+                .deserialize_setting(&self.raw_user_settings)
+                .log_err()
+            {
+                user_values_stack = vec![user_settings];
+            }
+
+            if let Some(setting) = setting_value
+                .load_setting(&default_settings, &user_values_stack, cx)
+                .context("A default setting must be added to the `default.json` file")
+                .log_err()
+            {
+                setting_value.set_global_value(setting);
+            }
+        }
+    }
+
+    /// Get the value of a setting.
+    ///
+    /// Panics if the given setting type has not been registered, or if there is no
+    /// value for this setting.
+    pub fn get<T: Settings>(&self, path: Option<(usize, &Path)>) -> &T {
+        self.setting_values
+            .get(&TypeId::of::<T>())
+            .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+            .value_for_path(path)
+            .downcast_ref::<T>()
+            .expect("no default value for setting type")
+    }
+
+    /// Override the global value for a setting.
+    ///
+    /// The given value will be overwritten if the user settings file changes.
+    pub fn override_global<T: Settings>(&mut self, value: T) {
+        self.setting_values
+            .get_mut(&TypeId::of::<T>())
+            .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+            .set_global_value(Box::new(value))
+    }
+
+    /// Get the user's settings as a raw JSON value.
+    ///
+    /// This is only for debugging and reporting. For user-facing functionality,
+    /// use the typed setting interface.
+    pub fn raw_user_settings(&self) -> &serde_json::Value {
+        &self.raw_user_settings
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test(cx: &mut AppContext) -> Self {
+        let mut this = Self::default();
+        this.set_default_settings(&crate::test_settings(), cx)
+            .unwrap();
+        this.set_user_settings("{}", cx).unwrap();
+        this
+    }
+
+    /// Update the value of a setting in the user's global configuration.
+    ///
+    /// This is only for tests. Normally, settings are only loaded from
+    /// JSON files.
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn update_user_settings<T: Settings>(
+        &mut self,
+        cx: &mut AppContext,
+        update: impl FnOnce(&mut T::FileContent),
+    ) {
+        let old_text = serde_json::to_string(&self.raw_user_settings).unwrap();
+        let new_text = self.new_text_for_update::<T>(old_text, update);
+        self.set_user_settings(&new_text, cx).unwrap();
+    }
+
+    /// Update the value of a setting in a JSON file, returning the new text
+    /// for that JSON file.
+    pub fn new_text_for_update<T: Settings>(
+        &self,
+        old_text: String,
+        update: impl FnOnce(&mut T::FileContent),
+    ) -> String {
+        let edits = self.edits_for_update::<T>(&old_text, update);
+        let mut new_text = old_text;
+        for (range, replacement) in edits.into_iter() {
+            new_text.replace_range(range, &replacement);
+        }
+        new_text
+    }
+
+    /// Update the value of a setting in a JSON file, returning a list
+    /// of edits to apply to the JSON file.
+    pub fn edits_for_update<T: Settings>(
+        &self,
+        text: &str,
+        update: impl FnOnce(&mut T::FileContent),
+    ) -> Vec<(Range<usize>, String)> {
+        let setting_type_id = TypeId::of::<T>();
+
+        let setting = self
+            .setting_values
+            .get(&setting_type_id)
+            .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()));
+        let raw_settings = parse_json_with_comments::<serde_json::Value>(text).unwrap_or_default();
+        let old_content = match setting.deserialize_setting(&raw_settings) {
+            Ok(content) => content.0.downcast::<T::FileContent>().unwrap(),
+            Err(_) => Box::new(T::FileContent::default()),
+        };
+        let mut new_content = old_content.clone();
+        update(&mut new_content);
+
+        let old_value = serde_json::to_value(&old_content).unwrap();
+        let new_value = serde_json::to_value(new_content).unwrap();
+
+        let mut key_path = Vec::new();
+        if let Some(key) = T::KEY {
+            key_path.push(key);
+        }
+
+        let mut edits = Vec::new();
+        let tab_size = self.json_tab_size();
+        let mut text = text.to_string();
+        update_value_in_json_text(
+            &mut text,
+            &mut key_path,
+            tab_size,
+            &old_value,
+            &new_value,
+            &mut edits,
+        );
+        return edits;
+    }
+
+    /// Configure the tab sized when updating JSON files.
+    pub fn set_json_tab_size_callback<T: Settings>(
+        &mut self,
+        get_tab_size: fn(&T) -> Option<usize>,
+    ) {
+        self.tab_size_callback = Some((
+            TypeId::of::<T>(),
+            Box::new(move |value| get_tab_size(value.downcast_ref::<T>().unwrap())),
+        ));
+    }
+
+    fn json_tab_size(&self) -> usize {
+        const DEFAULT_JSON_TAB_SIZE: usize = 2;
+
+        if let Some((setting_type_id, callback)) = &self.tab_size_callback {
+            let setting_value = self.setting_values.get(setting_type_id).unwrap();
+            let value = setting_value.value_for_path(None);
+            if let Some(value) = callback(value) {
+                return value;
+            }
+        }
+
+        DEFAULT_JSON_TAB_SIZE
+    }
+
+    /// Set the default settings via a JSON string.
+    ///
+    /// The string should contain a JSON object with a default value for every setting.
+    pub fn set_default_settings(
+        &mut self,
+        default_settings_content: &str,
+        cx: &mut AppContext,
+    ) -> Result<()> {
+        let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?;
+        if settings.is_object() {
+            self.raw_default_settings = settings;
+            self.recompute_values(None, cx)?;
+            Ok(())
+        } else {
+            Err(anyhow!("settings must be an object"))
+        }
+    }
+
+    /// Set the user settings via a JSON string.
+    pub fn set_user_settings(
+        &mut self,
+        user_settings_content: &str,
+        cx: &mut AppContext,
+    ) -> Result<()> {
+        let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?;
+        if settings.is_object() {
+            self.raw_user_settings = settings;
+            self.recompute_values(None, cx)?;
+            Ok(())
+        } else {
+            Err(anyhow!("settings must be an object"))
+        }
+    }
+
+    /// Add or remove a set of local settings via a JSON string.
+    pub fn set_local_settings(
+        &mut self,
+        root_id: usize,
+        path: Arc<Path>,
+        settings_content: Option<&str>,
+        cx: &mut AppContext,
+    ) -> Result<()> {
+        if let Some(content) = settings_content {
+            self.raw_local_settings
+                .insert((root_id, path.clone()), parse_json_with_comments(content)?);
+        } else {
+            self.raw_local_settings.remove(&(root_id, path.clone()));
+        }
+        self.recompute_values(Some((root_id, &path)), cx)?;
+        Ok(())
+    }
+
+    /// Add or remove a set of local settings via a JSON string.
+    pub fn clear_local_settings(&mut self, root_id: usize, cx: &mut AppContext) -> Result<()> {
+        self.raw_local_settings.retain(|k, _| k.0 != root_id);
+        self.recompute_values(Some((root_id, "".as_ref())), cx)?;
+        Ok(())
+    }
+
+    pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
+        self.raw_local_settings
+            .range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into()))
+            .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
+    }
+
+    pub fn json_schema(
+        &self,
+        schema_params: &SettingsJsonSchemaParams,
+        cx: &AppContext,
+    ) -> serde_json::Value {
+        use schemars::{
+            gen::SchemaSettings,
+            schema::{Schema, SchemaObject},
+        };
+
+        let settings = SchemaSettings::draft07().with(|settings| {
+            settings.option_add_null_type = false;
+        });
+        let mut generator = SchemaGenerator::new(settings);
+        let mut combined_schema = RootSchema::default();
+
+        for setting_value in self.setting_values.values() {
+            let setting_schema = setting_value.json_schema(&mut generator, schema_params, cx);
+            combined_schema
+                .definitions
+                .extend(setting_schema.definitions);
+
+            let target_schema = if let Some(key) = setting_value.key() {
+                let key_schema = combined_schema
+                    .schema
+                    .object()
+                    .properties
+                    .entry(key.to_string())
+                    .or_insert_with(|| Schema::Object(SchemaObject::default()));
+                if let Schema::Object(key_schema) = key_schema {
+                    key_schema
+                } else {
+                    continue;
+                }
+            } else {
+                &mut combined_schema.schema
+            };
+
+            merge_schema(target_schema, setting_schema.schema);
+        }
+
+        fn merge_schema(target: &mut SchemaObject, source: SchemaObject) {
+            if let Some(source) = source.object {
+                let target_properties = &mut target.object().properties;
+                for (key, value) in source.properties {
+                    match target_properties.entry(key) {
+                        btree_map::Entry::Vacant(e) => {
+                            e.insert(value);
+                        }
+                        btree_map::Entry::Occupied(e) => {
+                            if let (Schema::Object(target), Schema::Object(src)) =
+                                (e.into_mut(), value)
+                            {
+                                merge_schema(target, src);
+                            }
+                        }
+                    }
+                }
+            }
+
+            overwrite(&mut target.instance_type, source.instance_type);
+            overwrite(&mut target.string, source.string);
+            overwrite(&mut target.number, source.number);
+            overwrite(&mut target.reference, source.reference);
+            overwrite(&mut target.array, source.array);
+            overwrite(&mut target.enum_values, source.enum_values);
+
+            fn overwrite<T>(target: &mut Option<T>, source: Option<T>) {
+                if let Some(source) = source {
+                    *target = Some(source);
+                }
+            }
+        }
+
+        serde_json::to_value(&combined_schema).unwrap()
+    }
+
+    fn recompute_values(
+        &mut self,
+        changed_local_path: Option<(usize, &Path)>,
+        cx: &mut AppContext,
+    ) -> Result<()> {
+        // Reload the global and local values for every setting.
+        let mut user_settings_stack = Vec::<DeserializedSetting>::new();
+        let mut paths_stack = Vec::<Option<(usize, &Path)>>::new();
+        for setting_value in self.setting_values.values_mut() {
+            let default_settings = setting_value.deserialize_setting(&self.raw_default_settings)?;
+
+            user_settings_stack.clear();
+            paths_stack.clear();
+
+            if let Some(user_settings) = setting_value
+                .deserialize_setting(&self.raw_user_settings)
+                .log_err()
+            {
+                user_settings_stack.push(user_settings);
+                paths_stack.push(None);
+            }
+
+            // If the global settings file changed, reload the global value for the field.
+            if changed_local_path.is_none() {
+                if let Some(value) = setting_value
+                    .load_setting(&default_settings, &user_settings_stack, cx)
+                    .log_err()
+                {
+                    setting_value.set_global_value(value);
+                }
+            }
+
+            // Reload the local values for the setting.
+            for ((root_id, path), local_settings) in &self.raw_local_settings {
+                // Build a stack of all of the local values for that setting.
+                while let Some(prev_entry) = paths_stack.last() {
+                    if let Some((prev_root_id, prev_path)) = prev_entry {
+                        if root_id != prev_root_id || !path.starts_with(prev_path) {
+                            paths_stack.pop();
+                            user_settings_stack.pop();
+                            continue;
+                        }
+                    }
+                    break;
+                }
+
+                if let Some(local_settings) =
+                    setting_value.deserialize_setting(&local_settings).log_err()
+                {
+                    paths_stack.push(Some((*root_id, path.as_ref())));
+                    user_settings_stack.push(local_settings);
+
+                    // If a local settings file changed, then avoid recomputing local
+                    // settings for any path outside of that directory.
+                    if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| {
+                        *root_id != changed_root_id || !path.starts_with(changed_local_path)
+                    }) {
+                        continue;
+                    }
+
+                    if let Some(value) = setting_value
+                        .load_setting(&default_settings, &user_settings_stack, cx)
+                        .log_err()
+                    {
+                        setting_value.set_local_value(*root_id, path.clone(), value);
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+}
+
+impl Debug for SettingsStore {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("SettingsStore")
+            .field(
+                "types",
+                &self
+                    .setting_values
+                    .values()
+                    .map(|value| value.setting_type_name())
+                    .collect::<Vec<_>>(),
+            )
+            .field("default_settings", &self.raw_default_settings)
+            .field("user_settings", &self.raw_user_settings)
+            .field("local_settings", &self.raw_local_settings)
+            .finish_non_exhaustive()
+    }
+}
+
+impl<T: Settings> AnySettingValue for SettingValue<T> {
+    fn key(&self) -> Option<&'static str> {
+        T::KEY
+    }
+
+    fn setting_type_name(&self) -> &'static str {
+        type_name::<T>()
+    }
+
+    fn load_setting(
+        &self,
+        default_value: &DeserializedSetting,
+        user_values: &[DeserializedSetting],
+        cx: &mut AppContext,
+    ) -> Result<Box<dyn Any>> {
+        let default_value = default_value.0.downcast_ref::<T::FileContent>().unwrap();
+        let values: SmallVec<[&T::FileContent; 6]> = user_values
+            .iter()
+            .map(|value| value.0.downcast_ref().unwrap())
+            .collect();
+        Ok(Box::new(T::load(default_value, &values, cx)?))
+    }
+
+    fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result<DeserializedSetting> {
+        if let Some(key) = T::KEY {
+            if let Some(value) = json.get(key) {
+                json = value;
+            } else {
+                let value = T::FileContent::default();
+                return Ok(DeserializedSetting(Box::new(value)));
+            }
+        }
+        let value = T::FileContent::deserialize(json)?;
+        Ok(DeserializedSetting(Box::new(value)))
+    }
+
+    fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
+        if let Some((root_id, path)) = path {
+            for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
+                if root_id == *settings_root_id && path.starts_with(&settings_path) {
+                    return value;
+                }
+            }
+        }
+        self.global_value
+            .as_ref()
+            .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name()))
+    }
+
+    fn set_global_value(&mut self, value: Box<dyn Any>) {
+        self.global_value = Some(*value.downcast().unwrap());
+    }
+
+    fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>) {
+        let value = *value.downcast().unwrap();
+        match self
+            .local_values
+            .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1))
+        {
+            Ok(ix) => self.local_values[ix].2 = value,
+            Err(ix) => self.local_values.insert(ix, (root_id, path, value)),
+        }
+    }
+
+    fn json_schema(
+        &self,
+        generator: &mut SchemaGenerator,
+        params: &SettingsJsonSchemaParams,
+        cx: &AppContext,
+    ) -> RootSchema {
+        T::json_schema(generator, params, cx)
+    }
+}
+
+fn update_value_in_json_text<'a>(
+    text: &mut String,
+    key_path: &mut Vec<&'a str>,
+    tab_size: usize,
+    old_value: &'a serde_json::Value,
+    new_value: &'a serde_json::Value,
+    edits: &mut Vec<(Range<usize>, String)>,
+) {
+    // If the old and new values are both objects, then compare them key by key,
+    // preserving the comments and formatting of the unchanged parts. Otherwise,
+    // replace the old value with the new value.
+    if let (serde_json::Value::Object(old_object), serde_json::Value::Object(new_object)) =
+        (old_value, new_value)
+    {
+        for (key, old_sub_value) in old_object.iter() {
+            key_path.push(key);
+            let new_sub_value = new_object.get(key).unwrap_or(&serde_json::Value::Null);
+            update_value_in_json_text(
+                text,
+                key_path,
+                tab_size,
+                old_sub_value,
+                new_sub_value,
+                edits,
+            );
+            key_path.pop();
+        }
+        for (key, new_sub_value) in new_object.iter() {
+            key_path.push(key);
+            if !old_object.contains_key(key) {
+                update_value_in_json_text(
+                    text,
+                    key_path,
+                    tab_size,
+                    &serde_json::Value::Null,
+                    new_sub_value,
+                    edits,
+                );
+            }
+            key_path.pop();
+        }
+    } else if old_value != new_value {
+        let mut new_value = new_value.clone();
+        if let Some(new_object) = new_value.as_object_mut() {
+            new_object.retain(|_, v| !v.is_null());
+        }
+        let (range, replacement) =
+            replace_value_in_json_text(text, &key_path, tab_size, &new_value);
+        text.replace_range(range.clone(), &replacement);
+        edits.push((range, replacement));
+    }
+}
+
+fn replace_value_in_json_text(
+    text: &str,
+    key_path: &[&str],
+    tab_size: usize,
+    new_value: &serde_json::Value,
+) -> (Range<usize>, String) {
+    const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
+    const LANGUAGES: &'static str = "languages";
+
+    lazy_static! {
+        static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new(
+            tree_sitter_json::language(),
+            "(pair key: (string) @key value: (_) @value)",
+        )
+        .unwrap();
+    }
+
+    let mut parser = tree_sitter::Parser::new();
+    parser.set_language(tree_sitter_json::language()).unwrap();
+    let syntax_tree = parser.parse(text, None).unwrap();
+
+    let mut cursor = tree_sitter::QueryCursor::new();
+
+    let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
+
+    let mut depth = 0;
+    let mut last_value_range = 0..0;
+    let mut first_key_start = None;
+    let mut existing_value_range = 0..text.len();
+    let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
+    for mat in matches {
+        if mat.captures.len() != 2 {
+            continue;
+        }
+
+        let key_range = mat.captures[0].node.byte_range();
+        let value_range = mat.captures[1].node.byte_range();
+
+        // Don't enter sub objects until we find an exact
+        // match for the current keypath
+        if last_value_range.contains_inclusive(&value_range) {
+            continue;
+        }
+
+        last_value_range = value_range.clone();
+
+        if key_range.start > existing_value_range.end {
+            break;
+        }
+
+        first_key_start.get_or_insert_with(|| key_range.start);
+
+        let found_key = text
+            .get(key_range.clone())
+            .map(|key_text| {
+                if key_path[depth] == LANGUAGES && has_language_overrides {
+                    return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
+                } else {
+                    return key_text == format!("\"{}\"", key_path[depth]);
+                }
+            })
+            .unwrap_or(false);
+
+        if found_key {
+            existing_value_range = value_range;
+            // Reset last value range when increasing in depth
+            last_value_range = existing_value_range.start..existing_value_range.start;
+            depth += 1;
+
+            if depth == key_path.len() {
+                break;
+            } else {
+                first_key_start = None;
+            }
+        }
+    }
+
+    // We found the exact key we want, insert the new value
+    if depth == key_path.len() {
+        let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth);
+        (existing_value_range, new_val)
+    } else {
+        // We have key paths, construct the sub objects
+        let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
+            LANGUAGE_OVERRIDES
+        } else {
+            key_path[depth]
+        };
+
+        // We don't have the key, construct the nested objects
+        let mut new_value = serde_json::to_value(new_value).unwrap();
+        for key in key_path[(depth + 1)..].iter().rev() {
+            if has_language_overrides && key == &LANGUAGES {
+                new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
+            } else {
+                new_value = serde_json::json!({ key.to_string(): new_value });
+            }
+        }
+
+        if let Some(first_key_start) = first_key_start {
+            let mut row = 0;
+            let mut column = 0;
+            for (ix, char) in text.char_indices() {
+                if ix == first_key_start {
+                    break;
+                }
+                if char == '\n' {
+                    row += 1;
+                    column = 0;
+                } else {
+                    column += char.len_utf8();
+                }
+            }
+
+            if row > 0 {
+                // depth is 0 based, but division needs to be 1 based.
+                let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
+                let space = ' ';
+                let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
+                (first_key_start..first_key_start, content)
+            } else {
+                let new_val = serde_json::to_string(&new_value).unwrap();
+                let mut content = format!(r#""{new_key}": {new_val},"#);
+                content.push(' ');
+                (first_key_start..first_key_start, content)
+            }
+        } else {
+            new_value = serde_json::json!({ new_key.to_string(): new_value });
+            let indent_prefix_len = 4 * depth;
+            let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
+            if depth == 0 {
+                new_val.push('\n');
+            }
+
+            (existing_value_range, new_val)
+        }
+    }
+}
+
+fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
+    const SPACES: [u8; 32] = [b' '; 32];
+
+    debug_assert!(indent_size <= SPACES.len());
+    debug_assert!(indent_prefix_len <= SPACES.len());
+
+    let mut output = Vec::new();
+    let mut ser = serde_json::Serializer::with_formatter(
+        &mut output,
+        serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
+    );
+
+    value.serialize(&mut ser).unwrap();
+    let text = String::from_utf8(output).unwrap();
+
+    let mut adjusted_text = String::new();
+    for (i, line) in text.split('\n').enumerate() {
+        if i > 0 {
+            adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
+        }
+        adjusted_text.push_str(line);
+        adjusted_text.push('\n');
+    }
+    adjusted_text.pop();
+    adjusted_text
+}
+
+pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
+    Ok(serde_json_lenient::from_str(content)?)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde_derive::Deserialize;
+    use unindent::Unindent;
+
+    #[gpui2::test]
+    fn test_settings_store_basic(cx: &mut AppContext) {
+        let mut store = SettingsStore::default();
+        store.register_setting::<UserSettings>(cx);
+        store.register_setting::<TurboSetting>(cx);
+        store.register_setting::<MultiKeySettings>(cx);
+        store
+            .set_default_settings(
+                r#"{
+                    "turbo": false,
+                    "user": {
+                        "name": "John Doe",
+                        "age": 30,
+                        "staff": false
+                    }
+                }"#,
+                cx,
+            )
+            .unwrap();
+
+        assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
+        assert_eq!(
+            store.get::<UserSettings>(None),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 30,
+                staff: false,
+            }
+        );
+        assert_eq!(
+            store.get::<MultiKeySettings>(None),
+            &MultiKeySettings {
+                key1: String::new(),
+                key2: String::new(),
+            }
+        );
+
+        store
+            .set_user_settings(
+                r#"{
+                    "turbo": true,
+                    "user": { "age": 31 },
+                    "key1": "a"
+                }"#,
+                cx,
+            )
+            .unwrap();
+
+        assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(true));
+        assert_eq!(
+            store.get::<UserSettings>(None),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 31,
+                staff: false
+            }
+        );
+
+        store
+            .set_local_settings(
+                1,
+                Path::new("/root1").into(),
+                Some(r#"{ "user": { "staff": true } }"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                1,
+                Path::new("/root1/subdir").into(),
+                Some(r#"{ "user": { "name": "Jane Doe" } }"#),
+                cx,
+            )
+            .unwrap();
+
+        store
+            .set_local_settings(
+                1,
+                Path::new("/root2").into(),
+                Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
+                cx,
+            )
+            .unwrap();
+
+        assert_eq!(
+            store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 31,
+                staff: true
+            }
+        );
+        assert_eq!(
+            store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
+            &UserSettings {
+                name: "Jane Doe".to_string(),
+                age: 31,
+                staff: true
+            }
+        );
+        assert_eq!(
+            store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 42,
+                staff: false
+            }
+        );
+        assert_eq!(
+            store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
+            &MultiKeySettings {
+                key1: "a".to_string(),
+                key2: "b".to_string(),
+            }
+        );
+    }
+
+    #[gpui2::test]
+    fn test_setting_store_assign_json_before_register(cx: &mut AppContext) {
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(
+                r#"{
+                    "turbo": true,
+                    "user": {
+                        "name": "John Doe",
+                        "age": 30,
+                        "staff": false
+                    },
+                    "key1": "x"
+                }"#,
+                cx,
+            )
+            .unwrap();
+        store
+            .set_user_settings(r#"{ "turbo": false }"#, cx)
+            .unwrap();
+        store.register_setting::<UserSettings>(cx);
+        store.register_setting::<TurboSetting>(cx);
+
+        assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
+        assert_eq!(
+            store.get::<UserSettings>(None),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 30,
+                staff: false,
+            }
+        );
+
+        store.register_setting::<MultiKeySettings>(cx);
+        assert_eq!(
+            store.get::<MultiKeySettings>(None),
+            &MultiKeySettings {
+                key1: "x".into(),
+                key2: String::new(),
+            }
+        );
+    }
+
+    #[gpui2::test]
+    fn test_setting_store_update(cx: &mut AppContext) {
+        let mut store = SettingsStore::default();
+        store.register_setting::<MultiKeySettings>(cx);
+        store.register_setting::<UserSettings>(cx);
+        store.register_setting::<LanguageSettings>(cx);
+
+        // entries added and updated
+        check_settings_update::<LanguageSettings>(
+            &mut store,
+            r#"{
+                "languages": {
+                    "JSON": {
+                        "language_setting_1": true
+                    }
+                }
+            }"#
+            .unindent(),
+            |settings| {
+                settings
+                    .languages
+                    .get_mut("JSON")
+                    .unwrap()
+                    .language_setting_1 = Some(false);
+                settings.languages.insert(
+                    "Rust".into(),
+                    LanguageSettingEntry {
+                        language_setting_2: Some(true),
+                        ..Default::default()
+                    },
+                );
+            },
+            r#"{
+                "languages": {
+                    "Rust": {
+                        "language_setting_2": true
+                    },
+                    "JSON": {
+                        "language_setting_1": false
+                    }
+                }
+            }"#
+            .unindent(),
+            cx,
+        );
+
+        // weird formatting
+        check_settings_update::<UserSettings>(
+            &mut store,
+            r#"{
+                "user":   { "age": 36, "name": "Max", "staff": true }
+            }"#
+            .unindent(),
+            |settings| settings.age = Some(37),
+            r#"{
+                "user":   { "age": 37, "name": "Max", "staff": true }
+            }"#
+            .unindent(),
+            cx,
+        );
+
+        // single-line formatting, other keys
+        check_settings_update::<MultiKeySettings>(
+            &mut store,
+            r#"{ "one": 1, "two": 2 }"#.unindent(),
+            |settings| settings.key1 = Some("x".into()),
+            r#"{ "key1": "x", "one": 1, "two": 2 }"#.unindent(),
+            cx,
+        );
+
+        // empty object
+        check_settings_update::<UserSettings>(
+            &mut store,
+            r#"{
+                "user": {}
+            }"#
+            .unindent(),
+            |settings| settings.age = Some(37),
+            r#"{
+                "user": {
+                    "age": 37
+                }
+            }"#
+            .unindent(),
+            cx,
+        );
+
+        // no content
+        check_settings_update::<UserSettings>(
+            &mut store,
+            r#""#.unindent(),
+            |settings| settings.age = Some(37),
+            r#"{
+                "user": {
+                    "age": 37
+                }
+            }
+            "#
+            .unindent(),
+            cx,
+        );
+
+        check_settings_update::<UserSettings>(
+            &mut store,
+            r#"{
+            }
+            "#
+            .unindent(),
+            |settings| settings.age = Some(37),
+            r#"{
+                "user": {
+                    "age": 37
+                }
+            }
+            "#
+            .unindent(),
+            cx,
+        );
+    }
+
+    fn check_settings_update<T: Settings>(
+        store: &mut SettingsStore,
+        old_json: String,
+        update: fn(&mut T::FileContent),
+        expected_new_json: String,
+        cx: &mut AppContext,
+    ) {
+        store.set_user_settings(&old_json, cx).ok();
+        let edits = store.edits_for_update::<T>(&old_json, update);
+        let mut new_json = old_json;
+        for (range, replacement) in edits.into_iter() {
+            new_json.replace_range(range, &replacement);
+        }
+        pretty_assertions::assert_eq!(new_json, expected_new_json);
+    }
+
+    #[derive(Debug, PartialEq, Deserialize)]
+    struct UserSettings {
+        name: String,
+        age: u32,
+        staff: bool,
+    }
+
+    #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
+    struct UserSettingsJson {
+        name: Option<String>,
+        age: Option<u32>,
+        staff: Option<bool>,
+    }
+
+    impl Settings for UserSettings {
+        const KEY: Option<&'static str> = Some("user");
+        type FileContent = UserSettingsJson;
+
+        fn load(
+            default_value: &UserSettingsJson,
+            user_values: &[&UserSettingsJson],
+            _: &mut AppContext,
+        ) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct TurboSetting(bool);
+
+    impl Settings for TurboSetting {
+        const KEY: Option<&'static str> = Some("turbo");
+        type FileContent = Option<bool>;
+
+        fn load(
+            default_value: &Option<bool>,
+            user_values: &[&Option<bool>],
+            _: &mut AppContext,
+        ) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+
+    #[derive(Clone, Debug, PartialEq, Deserialize)]
+    struct MultiKeySettings {
+        #[serde(default)]
+        key1: String,
+        #[serde(default)]
+        key2: String,
+    }
+
+    #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+    struct MultiKeySettingsJson {
+        key1: Option<String>,
+        key2: Option<String>,
+    }
+
+    impl Settings for MultiKeySettings {
+        const KEY: Option<&'static str> = None;
+
+        type FileContent = MultiKeySettingsJson;
+
+        fn load(
+            default_value: &MultiKeySettingsJson,
+            user_values: &[&MultiKeySettingsJson],
+            _: &mut AppContext,
+        ) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+
+    #[derive(Debug, Deserialize)]
+    struct JournalSettings {
+        pub path: String,
+        pub hour_format: HourFormat,
+    }
+
+    #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+    #[serde(rename_all = "snake_case")]
+    enum HourFormat {
+        Hour12,
+        Hour24,
+    }
+
+    #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
+    struct JournalSettingsJson {
+        pub path: Option<String>,
+        pub hour_format: Option<HourFormat>,
+    }
+
+    impl Settings for JournalSettings {
+        const KEY: Option<&'static str> = Some("journal");
+
+        type FileContent = JournalSettingsJson;
+
+        fn load(
+            default_value: &JournalSettingsJson,
+            user_values: &[&JournalSettingsJson],
+            _: &mut AppContext,
+        ) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+
+    #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+    struct LanguageSettings {
+        #[serde(default)]
+        languages: HashMap<String, LanguageSettingEntry>,
+    }
+
+    #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+    struct LanguageSettingEntry {
+        language_setting_1: Option<bool>,
+        language_setting_2: Option<bool>,
+    }
+
+    impl Settings for LanguageSettings {
+        const KEY: Option<&'static str> = None;
+
+        type FileContent = Self;
+
+        fn load(default_value: &Self, user_values: &[&Self], _: &mut AppContext) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+}

crates/storybook2/Cargo.lock 🔗

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

crates/storybook2/Cargo.toml 🔗

@@ -0,0 +1,32 @@
+[package]
+name = "storybook2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "storybook"
+path = "src/storybook2.rs"
+
+[dependencies]
+anyhow.workspace = true
+# TODO: Remove after diagnosing stack overflow.
+backtrace-on-stack-overflow = "0.3.0"
+clap = { version = "4.4", features = ["derive", "string"] }
+chrono = "0.4"
+gpui2 = { path = "../gpui2" }
+itertools = "0.11.0"
+log.workspace = true
+rust-embed.workspace = true
+serde.workspace = true
+settings2 = { path = "../settings2" }
+simplelog = "0.9"
+smallvec.workspace = true
+strum = { version = "0.25.0", features = ["derive"] }
+theme = { path = "../theme" }
+theme2 = { path = "../theme2" }
+ui = { package = "ui2", path = "../ui2", features = ["stories"] }
+util = { path = "../util" }
+
+[dev-dependencies]
+gpui2 = { path = "../gpui2", features = ["test-support"] }

crates/storybook2/build.rs 🔗

@@ -0,0 +1,5 @@
+fn main() {
+    // Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
+    // TODO: We shouldn't depend on WebRTC in editor
+    println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
+}

crates/storybook2/docs/thoughts.md 🔗

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

crates/storybook2/src/assets.rs 🔗

@@ -0,0 +1,30 @@
+use std::borrow::Cow;
+
+use anyhow::{anyhow, Result};
+use gpui2::{AssetSource, SharedString};
+use rust_embed::RustEmbed;
+
+#[derive(RustEmbed)]
+#[folder = "../../assets"]
+#[include = "fonts/**/*"]
+#[include = "icons/**/*"]
+#[include = "themes/**/*"]
+#[include = "sounds/**/*"]
+#[include = "*.md"]
+#[exclude = "*.DS_Store"]
+pub struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, path: &str) -> Result<Cow<[u8]>> {
+        Self::get(path)
+            .map(|f| f.data)
+            .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
+    }
+
+    fn list(&self, path: &str) -> Result<Vec<SharedString>> {
+        Ok(Self::iter()
+            .filter(|p| p.starts_with(path))
+            .map(SharedString::from)
+            .collect())
+    }
+}

crates/storybook2/src/components.rs 🔗

@@ -0,0 +1,97 @@
+use gpui2::{
+    div, ArcCow, Element, EventContext, Interactive, IntoElement, MouseButton, ParentElement,
+    StyleHelpers, ViewContext,
+};
+use std::{marker::PhantomData, rc::Rc};
+
+struct ButtonHandlers<V, D> {
+    click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
+}
+
+impl<V, D> Default for ButtonHandlers<V, D> {
+    fn default() -> Self {
+        Self { click: None }
+    }
+}
+
+#[derive(Component)]
+pub struct Button<V: 'static, D: 'static> {
+    handlers: ButtonHandlers<V, D>,
+    label: Option<ArcCow<'static, str>>,
+    icon: Option<ArcCow<'static, str>>,
+    data: Rc<D>,
+    view_type: PhantomData<V>,
+}
+
+// Impl block for buttons without data.
+// See below for an impl block for any button.
+impl<V: 'static> Button<V, ()> {
+    fn new() -> Self {
+        Self {
+            handlers: ButtonHandlers::default(),
+            label: None,
+            icon: None,
+            data: Rc::new(()),
+            view_type: PhantomData,
+        }
+    }
+
+    pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
+        Button {
+            handlers: ButtonHandlers::default(),
+            label: self.label,
+            icon: self.icon,
+            data: Rc::new(data),
+            view_type: PhantomData,
+        }
+    }
+}
+
+// Impl block for button regardless of its data type.
+impl<V: 'static, D: 'static> Button<V, D> {
+    pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
+        self.label = Some(label.into());
+        self
+    }
+
+    pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
+        self.icon = Some(icon.into());
+        self
+    }
+
+    pub fn on_click(
+        mut self,
+        handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
+    ) -> Self {
+        self.handlers.click = Some(Rc::new(handler));
+        self
+    }
+}
+
+pub fn button<V>() -> Button<V, ()> {
+    Button::new()
+}
+
+impl<V: 'static, D: 'static> Button<V, D> {
+    fn render(
+        &mut self,
+        view: &mut V,
+        cx: &mut ViewContext<V>,
+    ) -> impl IntoElement<V> + Interactive<V> {
+        // let colors = &cx.theme::<Theme>().colors;
+
+        let button = div()
+            // .fill(colors.error(0.5))
+            .h_4()
+            .children(self.label.clone());
+
+        if let Some(handler) = self.handlers.click.clone() {
+            let data = self.data.clone();
+            button.on_mouse_down(MouseButton::Left, move |view, event, cx| {
+                handler(view, data.as_ref(), cx)
+            })
+        } else {
+            button
+        }
+    }
+}

crates/storybook2/src/stories.rs 🔗

@@ -0,0 +1,13 @@
+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::*;
+pub use text::*;
+pub use z_index::*;

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 🔗

@@ -0,0 +1,116 @@
+use gpui2::{
+    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;
+
+#[derive(Clone, Default, PartialEq, Deserialize)]
+struct ActionB;
+
+#[derive(Clone, Default, PartialEq, Deserialize)]
+struct ActionC;
+
+pub struct FocusStory {}
+
+impl FocusStory {
+    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")),
+            KeyBinding::new("cmd-c", ActionC, None),
+        ]);
+        cx.register_action_type::<ActionA>();
+        cx.register_action_type::<ActionB>();
+
+        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();
+
+        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 🔗

@@ -0,0 +1,40 @@
+use crate::{
+    story::Story,
+    story_selector::{ComponentStory, ElementStory},
+};
+use gpui2::{Div, Render, StatefulInteraction, View, VisualContext};
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+
+pub struct KitchenSinkStory;
+
+impl KitchenSinkStory {
+    pub fn view(cx: &mut WindowContext) -> View<Self> {
+        cx.build_view(|cx| Self)
+    }
+}
+
+impl Render for KitchenSinkStory {
+    type Element = Div<Self, StatefulInteraction<Self>>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let element_stories = ElementStory::iter()
+            .map(|selector| selector.story(cx))
+            .collect::<Vec<_>>();
+        let component_stories = ComponentStory::iter()
+            .map(|selector| selector.story(cx))
+            .collect::<Vec<_>>();
+
+        Story::container(cx)
+            .id("kitchen-sink")
+            .overflow_y_scroll()
+            .child(Story::title(cx, "Kitchen Sink"))
+            .child(Story::label(cx, "Elements"))
+            .child(div().flex().flex_col().children(element_stories))
+            .child(Story::label(cx, "Components"))
+            .child(div().flex().flex_col().children(component_stories))
+            // Add a bit of space at the bottom of the kitchen sink so elements
+            // don't end up squished right up against the bottom of the screen.
+            .child(div().p_4())
+    }
+}

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

@@ -0,0 +1,54 @@
+use gpui2::{
+    div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteraction, Styled,
+    View, VisualContext, WindowContext,
+};
+use theme2::theme;
+
+pub struct ScrollStory;
+
+impl ScrollStory {
+    pub fn view(cx: &mut WindowContext) -> View<ScrollStory> {
+        cx.build_view(|cx| ScrollStory)
+    }
+}
+
+impl Render for ScrollStory {
+    type Element = Div<Self, StatefulInteraction<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;
+
+        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 🔗

@@ -0,0 +1,21 @@
+use gpui2::{div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext};
+
+pub struct TextStory;
+
+impl TextStory {
+    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 🔗

@@ -0,0 +1,175 @@
+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).
+pub struct ZIndexStory;
+
+impl Render for ZIndexStory {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        Story::container(cx)
+            .child(Story::title(cx, "z-index"))
+            .child(
+                div()
+                    .flex()
+                    .child(
+                        div()
+                            .w(px(250.))
+                            .child(Story::label(cx, "z-index: auto"))
+                            .child(ZIndexExample::new(0)),
+                    )
+                    .child(
+                        div()
+                            .w(px(250.))
+                            .child(Story::label(cx, "z-index: 1"))
+                            .child(ZIndexExample::new(1)),
+                    )
+                    .child(
+                        div()
+                            .w(px(250.))
+                            .child(Story::label(cx, "z-index: 3"))
+                            .child(ZIndexExample::new(3)),
+                    )
+                    .child(
+                        div()
+                            .w(px(250.))
+                            .child(Story::label(cx, "z-index: 5"))
+                            .child(ZIndexExample::new(5)),
+                    )
+                    .child(
+                        div()
+                            .w(px(250.))
+                            .child(Story::label(cx, "z-index: 7"))
+                            .child(ZIndexExample::new(7)),
+                    ),
+            )
+    }
+}
+
+trait Styles: Styled + Sized {
+    // Trailing `_` is so we don't collide with `block` style `StyleHelpers`.
+    fn block_(self) -> Self {
+        self.absolute()
+            .w(px(150.))
+            .h(px(50.))
+            .text_color(rgb::<Hsla>(0x000000))
+    }
+
+    fn blue(self) -> Self {
+        self.bg(rgb::<Hsla>(0xe5e8fc))
+            .border_5()
+            .border_color(rgb::<Hsla>(0x112382))
+            .line_height(px(55.))
+            // HACK: Simulate `text-align: center`.
+            .pl(px(24.))
+    }
+
+    fn red(self) -> Self {
+        self.bg(rgb::<Hsla>(0xfce5e7))
+            .border_5()
+            .border_color(rgb::<Hsla>(0xe3a1a7))
+            // HACK: Simulate `text-align: center`.
+            .pl(px(8.))
+    }
+}
+
+impl<V: 'static> Styles for Div<V> {}
+
+#[derive(Component)]
+struct ZIndexExample {
+    z_index: u32,
+}
+
+impl ZIndexExample {
+    pub fn new(z_index: u32) -> Self {
+        Self { z_index }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div()
+            .relative()
+            .size_full()
+            // Example element.
+            .child(
+                div()
+                    .absolute()
+                    .top(px(15.))
+                    .left(px(15.))
+                    .w(px(180.))
+                    .h(px(230.))
+                    .bg(rgb::<Hsla>(0xfcfbe5))
+                    .text_color(rgb::<Hsla>(0x000000))
+                    .border_5()
+                    .border_color(rgb::<Hsla>(0xe3e0a1))
+                    .line_height(px(215.))
+                    // HACK: Simulate `text-align: center`.
+                    .pl(px(24.))
+                    .z_index(self.z_index)
+                    .child(format!(
+                        "z-index: {}",
+                        if self.z_index == 0 {
+                            "auto".to_string()
+                        } else {
+                            self.z_index.to_string()
+                        }
+                    )),
+            )
+            // Blue blocks.
+            .child(
+                div()
+                    .blue()
+                    .block_()
+                    .top(px(0.))
+                    .left(px(0.))
+                    .z_index(6)
+                    .child("z-index: 6"),
+            )
+            .child(
+                div()
+                    .blue()
+                    .block_()
+                    .top(px(30.))
+                    .left(px(30.))
+                    .z_index(4)
+                    .child("z-index: 4"),
+            )
+            .child(
+                div()
+                    .blue()
+                    .block_()
+                    .top(px(60.))
+                    .left(px(60.))
+                    .z_index(2)
+                    .child("z-index: 2"),
+            )
+            // Red blocks.
+            .child(
+                div()
+                    .red()
+                    .block_()
+                    .top(px(150.))
+                    .left(px(0.))
+                    .child("z-index: auto"),
+            )
+            .child(
+                div()
+                    .red()
+                    .block_()
+                    .top(px(180.))
+                    .left(px(30.))
+                    .child("z-index: auto"),
+            )
+            .child(
+                div()
+                    .red()
+                    .block_()
+                    .top(px(210.))
+                    .left(px(60.))
+                    .child("z-index: auto"),
+            )
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -0,0 +1,184 @@
+use std::str::FromStr;
+use std::sync::OnceLock;
+
+use crate::stories::*;
+use anyhow::anyhow;
+use clap::builder::PossibleValue;
+use clap::ValueEnum;
+use gpui2::{AnyView, VisualContext};
+use strum::{EnumIter, EnumString, IntoEnumIterator};
+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,
+    Input,
+    Label,
+    Scroll,
+    Text,
+    ZIndex,
+}
+
+impl ElementStory {
+    pub fn story(&self, cx: &mut WindowContext) -> AnyView {
+        match self {
+            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(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
+#[strum(serialize_all = "snake_case")]
+pub enum ComponentStory {
+    AssistantPanel,
+    Breadcrumb,
+    Buffer,
+    ChatPanel,
+    CollabPanel,
+    CommandPalette,
+    Copilot,
+    ContextMenu,
+    Facepile,
+    Keybinding,
+    LanguageSelector,
+    MultiBuffer,
+    NotificationsPanel,
+    Palette,
+    Panel,
+    ProjectPanel,
+    RecentProjects,
+    Tab,
+    TabBar,
+    Terminal,
+    ThemeSelector,
+    TitleBar,
+    Toast,
+    Toolbar,
+    TrafficLights,
+    Workspace,
+}
+
+impl ComponentStory {
+    pub fn story(&self, cx: &mut WindowContext) -> AnyView {
+        match self {
+            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(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum StorySelector {
+    Element(ElementStory),
+    Component(ComponentStory),
+    KitchenSink,
+}
+
+impl FromStr for StorySelector {
+    type Err = anyhow::Error;
+
+    fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
+        use anyhow::Context;
+
+        let story = raw_story_name.to_ascii_lowercase();
+
+        if story == "kitchen_sink" {
+            return Ok(Self::KitchenSink);
+        }
+
+        if let Some((_, story)) = story.split_once("elements/") {
+            let element_story = ElementStory::from_str(story)
+                .with_context(|| format!("story not found for element '{story}'"))?;
+
+            return Ok(Self::Element(element_story));
+        }
+
+        if let Some((_, story)) = story.split_once("components/") {
+            let component_story = ComponentStory::from_str(story)
+                .with_context(|| format!("story not found for component '{story}'"))?;
+
+            return Ok(Self::Component(component_story));
+        }
+
+        Err(anyhow!("story not found for '{raw_story_name}'"))
+    }
+}
+
+impl StorySelector {
+    pub fn story(&self, cx: &mut WindowContext) -> AnyView {
+        match self {
+            Self::Element(element_story) => element_story.story(cx),
+            Self::Component(component_story) => component_story.story(cx),
+            Self::KitchenSink => KitchenSinkStory::view(cx).into(),
+        }
+    }
+}
+
+/// The list of all stories available in the storybook.
+static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
+
+impl ValueEnum for StorySelector {
+    fn value_variants<'a>() -> &'a [Self] {
+        let stories = ALL_STORY_SELECTORS.get_or_init(|| {
+            let element_stories = ElementStory::iter().map(StorySelector::Element);
+            let component_stories = ComponentStory::iter().map(StorySelector::Component);
+
+            element_stories
+                .chain(component_stories)
+                .chain(std::iter::once(StorySelector::KitchenSink))
+                .collect::<Vec<_>>()
+        });
+
+        stories
+    }
+
+    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
+        let value = match self {
+            Self::Element(story) => format!("elements/{story}"),
+            Self::Component(story) => format!("components/{story}"),
+            Self::KitchenSink => "kitchen_sink".to_string(),
+        };
+
+        Some(PossibleValue::new(value))
+    }
+}

crates/storybook2/src/storybook2.rs 🔗

@@ -0,0 +1,126 @@
+#![allow(dead_code, unused_variables)]
+
+mod assets;
+mod stories;
+mod story;
+mod story_selector;
+
+use std::sync::Arc;
+
+use clap::Parser;
+use gpui2::{
+    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::*;
+
+use crate::assets::Assets;
+use crate::story_selector::StorySelector;
+
+// gpui2::actions! {
+//     storybook,
+//     [ToggleInspector]
+// }
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    #[arg(value_enum)]
+    story: Option<StorySelector>,
+
+    /// The name of the theme to use in the storybook.
+    ///
+    /// If not provided, the default theme will be used.
+    #[arg(long)]
+    theme: Option<String>,
+}
+
+fn main() {
+    // unsafe { backtrace_on_stack_overflow::enable() };
+
+    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+
+    let args = Args::parse();
+
+    let story_selector = args.story.clone();
+    let theme_name = args.theme.unwrap_or("One Dark".to_string());
+
+    let asset_source = Arc::new(Assets);
+    gpui2::App::production(asset_source).run(move |cx| {
+        load_embedded_fonts(cx).unwrap();
+
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+
+        theme2::init(cx);
+
+        let selector =
+            story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace));
+
+        let theme_registry = cx.global::<ThemeRegistry>();
+
+        let mut theme_settings = ThemeSettings::get_global(cx).clone();
+        theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
+        ThemeSettings::override_global(theme_settings, cx);
+
+        cx.set_global(theme.clone());
+        ui::settings::init(cx);
+
+        let window = cx.open_window(
+            WindowOptions {
+                bounds: WindowBounds::Fixed(Bounds {
+                    origin: Default::default(),
+                    size: size(px(1700.), px(980.)).into(),
+                }),
+                ..Default::default()
+            },
+            move |cx| cx.build_view(|cx| StoryWrapper::new(selector.story(cx))),
+        );
+
+        cx.activate(true);
+    });
+}
+
+#[derive(Clone)]
+pub struct StoryWrapper {
+    story: AnyView,
+}
+
+impl StoryWrapper {
+    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>) -> Self::Element {
+        div()
+            .flex()
+            .flex_col()
+            .size_full()
+            .child(self.story.clone())
+    }
+}
+
+fn load_embedded_fonts(cx: &AppContext) -> gpui2::Result<()> {
+    let font_paths = cx.asset_source().list("fonts")?;
+    let mut embedded_fonts = Vec::new();
+    for font_path in font_paths {
+        if font_path.ends_with(".ttf") {
+            let font_bytes = cx.asset_source().load(&font_path)?.to_vec();
+            embedded_fonts.push(Arc::from(font_bytes));
+        }
+    }
+
+    cx.text_system().add_fonts(&embedded_fonts)
+}

crates/terminal2/Cargo.toml 🔗

@@ -0,0 +1,38 @@
+[package]
+name = "terminal2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/terminal2.rs"
+doctest = false
+
+
+[dependencies]
+gpui2 = { path = "../gpui2" }
+settings2 = { path = "../settings2" }
+db2 = { path = "../db2" }
+theme2 = { path = "../theme2" }
+util = { path = "../util" }
+
+alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "33306142195b354ef3485ca2b1d8a85dfc6605ca" }
+procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
+smallvec.workspace = true
+smol.workspace = true
+mio-extras = "2.0.6"
+futures.workspace = true
+ordered-float.workspace = true
+itertools = "0.10"
+dirs = "4.0.0"
+shellexpand = "2.1.0"
+libc = "0.2"
+anyhow.workspace = true
+schemars.workspace = true
+thiserror.workspace = true
+lazy_static.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+
+[dev-dependencies]
+rand.workspace = true

crates/terminal2/src/mappings/colors.rs 🔗

@@ -0,0 +1,137 @@
+// todo!()
+use alacritty_terminal::term::color::Rgb as AlacRgb;
+// use gpui2::color::Color;
+// use theme2::TerminalStyle;
+
+///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
+// pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
+//     match alac_color {
+//         //Named and theme defined colors
+//         alacritty_terminal::ansi::Color::Named(n) => match n {
+//             alacritty_terminal::ansi::NamedColor::Black => style.black,
+//             alacritty_terminal::ansi::NamedColor::Red => style.red,
+//             alacritty_terminal::ansi::NamedColor::Green => style.green,
+//             alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
+//             alacritty_terminal::ansi::NamedColor::Blue => style.blue,
+//             alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
+//             alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
+//             alacritty_terminal::ansi::NamedColor::White => style.white,
+//             alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
+//             alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
+//             alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
+//             alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
+//             alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
+//             alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
+//             alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
+//             alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
+//             alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
+//             alacritty_terminal::ansi::NamedColor::Background => style.background,
+//             alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
+//             alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
+//             alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
+//             alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
+//             alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
+//             alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
+//             alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
+//             alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
+//             alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
+//             alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
+//             alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
+//         },
+//         //'True' colors
+//         alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
+//         //8 bit, indexed colors
+//         alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style),
+//     }
+// }
+
+/// TODO: Move this
+///Converts an 8 bit ANSI color to it's GPUI equivalent.
+///Accepts usize for compatibility with the alacritty::Colors interface,
+///Other than that use case, should only be called with values in the [0,255] range
+// pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
+//     match index {
+//         //0-15 are the same as the named colors above
+//         0 => style.black,
+//         1 => style.red,
+//         2 => style.green,
+//         3 => style.yellow,
+//         4 => style.blue,
+//         5 => style.magenta,
+//         6 => style.cyan,
+//         7 => style.white,
+//         8 => style.bright_black,
+//         9 => style.bright_red,
+//         10 => style.bright_green,
+//         11 => style.bright_yellow,
+//         12 => style.bright_blue,
+//         13 => style.bright_magenta,
+//         14 => style.bright_cyan,
+//         15 => style.bright_white,
+//         //16-231 are mapped to their RGB colors on a 0-5 range per channel
+//         16..=231 => {
+//             let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components
+//             let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
+//             Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
+//         }
+//         //232-255 are a 24 step grayscale from black to white
+//         232..=255 => {
+//             let i = *index as u8 - 232; //Align index to 0..24
+//             let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
+//             Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
+//         }
+//         //For compatibility with the alacritty::Colors interface
+//         256 => style.foreground,
+//         257 => style.background,
+//         258 => style.cursor,
+//         259 => style.dim_black,
+//         260 => style.dim_red,
+//         261 => style.dim_green,
+//         262 => style.dim_yellow,
+//         263 => style.dim_blue,
+//         264 => style.dim_magenta,
+//         265 => style.dim_cyan,
+//         266 => style.dim_white,
+//         267 => style.bright_foreground,
+//         268 => style.black, //'Dim Background', non-standard color
+//         _ => Color::new(0, 0, 0, 255),
+//     }
+// }
+///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
+///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
+///
+///Wikipedia gives a formula for calculating the index for a given color:
+///
+///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
+///
+///This function does the reverse, calculating the r, g, and b components from a given index.
+// fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
+//     debug_assert!((&16..=&231).contains(&i));
+//     let i = i - 16;
+//     let r = (i - (i % 36)) / 36;
+//     let g = ((i % 36) - (i % 6)) / 6;
+//     let b = (i % 36) % 6;
+//     (r, g, b)
+// }
+use gpui2::Rgba;
+
+//Convenience method to convert from a GPUI color to an alacritty Rgb
+pub fn to_alac_rgb(color: impl Into<Rgba>) -> AlacRgb {
+    let color = color.into();
+    let r = ((color.r * color.a) * 255.) as u8;
+    let g = ((color.g * color.a) * 255.) as u8;
+    let b = ((color.b * color.a) * 255.) as u8;
+    AlacRgb::new(r, g, b)
+}
+
+// #[cfg(test)]
+// mod tests {
+//     #[test]
+//     fn test_rgb_for_index() {
+//         //Test every possible value in the color cube
+//         for i in 16..=231 {
+//             let (r, g, b) = crate::mappings::colors::rgb_for_index(&(i as u8));
+//             assert_eq!(i, 16 + 36 * r + 6 * g + b);
+//         }
+//     }
+// }

crates/terminal2/src/mappings/keys.rs 🔗

@@ -0,0 +1,464 @@
+/// The mappings defined in this file where created from reading the alacritty source
+use alacritty_terminal::term::TermMode;
+use gpui2::Keystroke;
+
+#[derive(Debug, PartialEq, Eq)]
+enum AlacModifiers {
+    None,
+    Alt,
+    Ctrl,
+    Shift,
+    CtrlShift,
+    Other,
+}
+
+impl AlacModifiers {
+    fn new(ks: &Keystroke) -> Self {
+        match (
+            ks.modifiers.alt,
+            ks.modifiers.control,
+            ks.modifiers.shift,
+            ks.modifiers.command,
+        ) {
+            (false, false, false, false) => AlacModifiers::None,
+            (true, false, false, false) => AlacModifiers::Alt,
+            (false, true, false, false) => AlacModifiers::Ctrl,
+            (false, false, true, false) => AlacModifiers::Shift,
+            (false, true, true, false) => AlacModifiers::CtrlShift,
+            _ => AlacModifiers::Other,
+        }
+    }
+
+    fn any(&self) -> bool {
+        match &self {
+            AlacModifiers::None => false,
+            AlacModifiers::Alt => true,
+            AlacModifiers::Ctrl => true,
+            AlacModifiers::Shift => true,
+            AlacModifiers::CtrlShift => true,
+            AlacModifiers::Other => true,
+        }
+    }
+}
+
+pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode, alt_is_meta: bool) -> Option<String> {
+    let modifiers = AlacModifiers::new(keystroke);
+
+    // Manual Bindings including modifiers
+    let manual_esc_str = match (keystroke.key.as_ref(), &modifiers) {
+        //Basic special keys
+        ("tab", AlacModifiers::None) => Some("\x09".to_string()),
+        ("escape", AlacModifiers::None) => Some("\x1b".to_string()),
+        ("enter", AlacModifiers::None) => Some("\x0d".to_string()),
+        ("enter", AlacModifiers::Shift) => Some("\x0d".to_string()),
+        ("backspace", AlacModifiers::None) => Some("\x7f".to_string()),
+        //Interesting escape codes
+        ("tab", AlacModifiers::Shift) => Some("\x1b[Z".to_string()),
+        ("backspace", AlacModifiers::Alt) => Some("\x1b\x7f".to_string()),
+        ("backspace", AlacModifiers::Shift) => Some("\x7f".to_string()),
+        ("home", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[1;2H".to_string())
+        }
+        ("end", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[1;2F".to_string())
+        }
+        ("pageup", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[5;2~".to_string())
+        }
+        ("pagedown", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => {
+            Some("\x1b[6;2~".to_string())
+        }
+        ("home", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOH".to_string())
+        }
+        ("home", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[H".to_string())
+        }
+        ("end", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOF".to_string())
+        }
+        ("end", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[F".to_string())
+        }
+        ("up", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOA".to_string())
+        }
+        ("up", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[A".to_string())
+        }
+        ("down", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOB".to_string())
+        }
+        ("down", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[B".to_string())
+        }
+        ("right", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOC".to_string())
+        }
+        ("right", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[C".to_string())
+        }
+        ("left", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1bOD".to_string())
+        }
+        ("left", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => {
+            Some("\x1b[D".to_string())
+        }
+        ("back", AlacModifiers::None) => Some("\x7f".to_string()),
+        ("insert", AlacModifiers::None) => Some("\x1b[2~".to_string()),
+        ("delete", AlacModifiers::None) => Some("\x1b[3~".to_string()),
+        ("pageup", AlacModifiers::None) => Some("\x1b[5~".to_string()),
+        ("pagedown", AlacModifiers::None) => Some("\x1b[6~".to_string()),
+        ("f1", AlacModifiers::None) => Some("\x1bOP".to_string()),
+        ("f2", AlacModifiers::None) => Some("\x1bOQ".to_string()),
+        ("f3", AlacModifiers::None) => Some("\x1bOR".to_string()),
+        ("f4", AlacModifiers::None) => Some("\x1bOS".to_string()),
+        ("f5", AlacModifiers::None) => Some("\x1b[15~".to_string()),
+        ("f6", AlacModifiers::None) => Some("\x1b[17~".to_string()),
+        ("f7", AlacModifiers::None) => Some("\x1b[18~".to_string()),
+        ("f8", AlacModifiers::None) => Some("\x1b[19~".to_string()),
+        ("f9", AlacModifiers::None) => Some("\x1b[20~".to_string()),
+        ("f10", AlacModifiers::None) => Some("\x1b[21~".to_string()),
+        ("f11", AlacModifiers::None) => Some("\x1b[23~".to_string()),
+        ("f12", AlacModifiers::None) => Some("\x1b[24~".to_string()),
+        ("f13", AlacModifiers::None) => Some("\x1b[25~".to_string()),
+        ("f14", AlacModifiers::None) => Some("\x1b[26~".to_string()),
+        ("f15", AlacModifiers::None) => Some("\x1b[28~".to_string()),
+        ("f16", AlacModifiers::None) => Some("\x1b[29~".to_string()),
+        ("f17", AlacModifiers::None) => Some("\x1b[31~".to_string()),
+        ("f18", AlacModifiers::None) => Some("\x1b[32~".to_string()),
+        ("f19", AlacModifiers::None) => Some("\x1b[33~".to_string()),
+        ("f20", AlacModifiers::None) => Some("\x1b[34~".to_string()),
+        // NumpadEnter, Action::Esc("\n".into());
+        //Mappings for caret notation keys
+        ("a", AlacModifiers::Ctrl) => Some("\x01".to_string()), //1
+        ("A", AlacModifiers::CtrlShift) => Some("\x01".to_string()), //1
+        ("b", AlacModifiers::Ctrl) => Some("\x02".to_string()), //2
+        ("B", AlacModifiers::CtrlShift) => Some("\x02".to_string()), //2
+        ("c", AlacModifiers::Ctrl) => Some("\x03".to_string()), //3
+        ("C", AlacModifiers::CtrlShift) => Some("\x03".to_string()), //3
+        ("d", AlacModifiers::Ctrl) => Some("\x04".to_string()), //4
+        ("D", AlacModifiers::CtrlShift) => Some("\x04".to_string()), //4
+        ("e", AlacModifiers::Ctrl) => Some("\x05".to_string()), //5
+        ("E", AlacModifiers::CtrlShift) => Some("\x05".to_string()), //5
+        ("f", AlacModifiers::Ctrl) => Some("\x06".to_string()), //6
+        ("F", AlacModifiers::CtrlShift) => Some("\x06".to_string()), //6
+        ("g", AlacModifiers::Ctrl) => Some("\x07".to_string()), //7
+        ("G", AlacModifiers::CtrlShift) => Some("\x07".to_string()), //7
+        ("h", AlacModifiers::Ctrl) => Some("\x08".to_string()), //8
+        ("H", AlacModifiers::CtrlShift) => Some("\x08".to_string()), //8
+        ("i", AlacModifiers::Ctrl) => Some("\x09".to_string()), //9
+        ("I", AlacModifiers::CtrlShift) => Some("\x09".to_string()), //9
+        ("j", AlacModifiers::Ctrl) => Some("\x0a".to_string()), //10
+        ("J", AlacModifiers::CtrlShift) => Some("\x0a".to_string()), //10
+        ("k", AlacModifiers::Ctrl) => Some("\x0b".to_string()), //11
+        ("K", AlacModifiers::CtrlShift) => Some("\x0b".to_string()), //11
+        ("l", AlacModifiers::Ctrl) => Some("\x0c".to_string()), //12
+        ("L", AlacModifiers::CtrlShift) => Some("\x0c".to_string()), //12
+        ("m", AlacModifiers::Ctrl) => Some("\x0d".to_string()), //13
+        ("M", AlacModifiers::CtrlShift) => Some("\x0d".to_string()), //13
+        ("n", AlacModifiers::Ctrl) => Some("\x0e".to_string()), //14
+        ("N", AlacModifiers::CtrlShift) => Some("\x0e".to_string()), //14
+        ("o", AlacModifiers::Ctrl) => Some("\x0f".to_string()), //15
+        ("O", AlacModifiers::CtrlShift) => Some("\x0f".to_string()), //15
+        ("p", AlacModifiers::Ctrl) => Some("\x10".to_string()), //16
+        ("P", AlacModifiers::CtrlShift) => Some("\x10".to_string()), //16
+        ("q", AlacModifiers::Ctrl) => Some("\x11".to_string()), //17
+        ("Q", AlacModifiers::CtrlShift) => Some("\x11".to_string()), //17
+        ("r", AlacModifiers::Ctrl) => Some("\x12".to_string()), //18
+        ("R", AlacModifiers::CtrlShift) => Some("\x12".to_string()), //18
+        ("s", AlacModifiers::Ctrl) => Some("\x13".to_string()), //19
+        ("S", AlacModifiers::CtrlShift) => Some("\x13".to_string()), //19
+        ("t", AlacModifiers::Ctrl) => Some("\x14".to_string()), //20
+        ("T", AlacModifiers::CtrlShift) => Some("\x14".to_string()), //20
+        ("u", AlacModifiers::Ctrl) => Some("\x15".to_string()), //21
+        ("U", AlacModifiers::CtrlShift) => Some("\x15".to_string()), //21
+        ("v", AlacModifiers::Ctrl) => Some("\x16".to_string()), //22
+        ("V", AlacModifiers::CtrlShift) => Some("\x16".to_string()), //22
+        ("w", AlacModifiers::Ctrl) => Some("\x17".to_string()), //23
+        ("W", AlacModifiers::CtrlShift) => Some("\x17".to_string()), //23
+        ("x", AlacModifiers::Ctrl) => Some("\x18".to_string()), //24
+        ("X", AlacModifiers::CtrlShift) => Some("\x18".to_string()), //24
+        ("y", AlacModifiers::Ctrl) => Some("\x19".to_string()), //25
+        ("Y", AlacModifiers::CtrlShift) => Some("\x19".to_string()), //25
+        ("z", AlacModifiers::Ctrl) => Some("\x1a".to_string()), //26
+        ("Z", AlacModifiers::CtrlShift) => Some("\x1a".to_string()), //26
+        ("@", AlacModifiers::Ctrl) => Some("\x00".to_string()), //0
+        ("[", AlacModifiers::Ctrl) => Some("\x1b".to_string()), //27
+        ("\\", AlacModifiers::Ctrl) => Some("\x1c".to_string()), //28
+        ("]", AlacModifiers::Ctrl) => Some("\x1d".to_string()), //29
+        ("^", AlacModifiers::Ctrl) => Some("\x1e".to_string()), //30
+        ("_", AlacModifiers::Ctrl) => Some("\x1f".to_string()), //31
+        ("?", AlacModifiers::Ctrl) => Some("\x7f".to_string()), //127
+        _ => None,
+    };
+    if manual_esc_str.is_some() {
+        return manual_esc_str;
+    }
+
+    // Automated bindings applying modifiers
+    if modifiers.any() {
+        let modifier_code = modifier_code(keystroke);
+        let modified_esc_str = match keystroke.key.as_ref() {
+            "up" => Some(format!("\x1b[1;{}A", modifier_code)),
+            "down" => Some(format!("\x1b[1;{}B", modifier_code)),
+            "right" => Some(format!("\x1b[1;{}C", modifier_code)),
+            "left" => Some(format!("\x1b[1;{}D", modifier_code)),
+            "f1" => Some(format!("\x1b[1;{}P", modifier_code)),
+            "f2" => Some(format!("\x1b[1;{}Q", modifier_code)),
+            "f3" => Some(format!("\x1b[1;{}R", modifier_code)),
+            "f4" => Some(format!("\x1b[1;{}S", modifier_code)),
+            "F5" => Some(format!("\x1b[15;{}~", modifier_code)),
+            "f6" => Some(format!("\x1b[17;{}~", modifier_code)),
+            "f7" => Some(format!("\x1b[18;{}~", modifier_code)),
+            "f8" => Some(format!("\x1b[19;{}~", modifier_code)),
+            "f9" => Some(format!("\x1b[20;{}~", modifier_code)),
+            "f10" => Some(format!("\x1b[21;{}~", modifier_code)),
+            "f11" => Some(format!("\x1b[23;{}~", modifier_code)),
+            "f12" => Some(format!("\x1b[24;{}~", modifier_code)),
+            "f13" => Some(format!("\x1b[25;{}~", modifier_code)),
+            "f14" => Some(format!("\x1b[26;{}~", modifier_code)),
+            "f15" => Some(format!("\x1b[28;{}~", modifier_code)),
+            "f16" => Some(format!("\x1b[29;{}~", modifier_code)),
+            "f17" => Some(format!("\x1b[31;{}~", modifier_code)),
+            "f18" => Some(format!("\x1b[32;{}~", modifier_code)),
+            "f19" => Some(format!("\x1b[33;{}~", modifier_code)),
+            "f20" => Some(format!("\x1b[34;{}~", modifier_code)),
+            _ if modifier_code == 2 => None,
+            "insert" => Some(format!("\x1b[2;{}~", modifier_code)),
+            "pageup" => Some(format!("\x1b[5;{}~", modifier_code)),
+            "pagedown" => Some(format!("\x1b[6;{}~", modifier_code)),
+            "end" => Some(format!("\x1b[1;{}F", modifier_code)),
+            "home" => Some(format!("\x1b[1;{}H", modifier_code)),
+            _ => None,
+        };
+        if modified_esc_str.is_some() {
+            return modified_esc_str;
+        }
+    }
+
+    let alt_meta_binding =
+        if alt_is_meta && modifiers == AlacModifiers::Alt && keystroke.key.is_ascii() {
+            Some(format!("\x1b{}", keystroke.key))
+        } else {
+            None
+        };
+
+    if alt_meta_binding.is_some() {
+        return alt_meta_binding;
+    }
+
+    None
+}
+
+///   Code     Modifiers
+/// ---------+---------------------------
+///    2     | Shift
+///    3     | Alt
+///    4     | Shift + Alt
+///    5     | Control
+///    6     | Shift + Control
+///    7     | Alt + Control
+///    8     | Shift + Alt + Control
+/// ---------+---------------------------
+/// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
+fn modifier_code(keystroke: &Keystroke) -> u32 {
+    let mut modifier_code = 0;
+    if keystroke.modifiers.shift {
+        modifier_code |= 1;
+    }
+    if keystroke.modifiers.alt {
+        modifier_code |= 1 << 1;
+    }
+    if keystroke.modifiers.control {
+        modifier_code |= 1 << 2;
+    }
+    modifier_code + 1
+}
+
+#[cfg(test)]
+mod test {
+    use gpui2::Modifiers;
+
+    use super::*;
+
+    #[test]
+    fn test_scroll_keys() {
+        //These keys should be handled by the scrolling element directly
+        //Need to signify this by returning 'None'
+        let shift_pageup = Keystroke::parse("shift-pageup").unwrap();
+        let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap();
+        let shift_home = Keystroke::parse("shift-home").unwrap();
+        let shift_end = Keystroke::parse("shift-end").unwrap();
+
+        let none = TermMode::NONE;
+        assert_eq!(to_esc_str(&shift_pageup, &none, false), None);
+        assert_eq!(to_esc_str(&shift_pagedown, &none, false), None);
+        assert_eq!(to_esc_str(&shift_home, &none, false), None);
+        assert_eq!(to_esc_str(&shift_end, &none, false), None);
+
+        let alt_screen = TermMode::ALT_SCREEN;
+        assert_eq!(
+            to_esc_str(&shift_pageup, &alt_screen, false),
+            Some("\x1b[5;2~".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&shift_pagedown, &alt_screen, false),
+            Some("\x1b[6;2~".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&shift_home, &alt_screen, false),
+            Some("\x1b[1;2H".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&shift_end, &alt_screen, false),
+            Some("\x1b[1;2F".to_string())
+        );
+
+        let pageup = Keystroke::parse("pageup").unwrap();
+        let pagedown = Keystroke::parse("pagedown").unwrap();
+        let any = TermMode::ANY;
+
+        assert_eq!(
+            to_esc_str(&pageup, &any, false),
+            Some("\x1b[5~".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&pagedown, &any, false),
+            Some("\x1b[6~".to_string())
+        );
+    }
+
+    #[test]
+    fn test_plain_inputs() {
+        let ks = Keystroke {
+            modifiers: Modifiers {
+                control: false,
+                alt: false,
+                shift: false,
+                command: false,
+                function: false,
+            },
+            key: "🖖🏻".to_string(), //2 char string
+            ime_key: None,
+        };
+        assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None);
+    }
+
+    #[test]
+    fn test_application_mode() {
+        let app_cursor = TermMode::APP_CURSOR;
+        let none = TermMode::NONE;
+
+        let up = Keystroke::parse("up").unwrap();
+        let down = Keystroke::parse("down").unwrap();
+        let left = Keystroke::parse("left").unwrap();
+        let right = Keystroke::parse("right").unwrap();
+
+        assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".to_string()));
+        assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".to_string()));
+        assert_eq!(to_esc_str(&right, &none, false), Some("\x1b[C".to_string()));
+        assert_eq!(to_esc_str(&left, &none, false), Some("\x1b[D".to_string()));
+
+        assert_eq!(
+            to_esc_str(&up, &app_cursor, false),
+            Some("\x1bOA".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&down, &app_cursor, false),
+            Some("\x1bOB".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&right, &app_cursor, false),
+            Some("\x1bOC".to_string())
+        );
+        assert_eq!(
+            to_esc_str(&left, &app_cursor, false),
+            Some("\x1bOD".to_string())
+        );
+    }
+
+    #[test]
+    fn test_ctrl_codes() {
+        let letters_lower = 'a'..='z';
+        let letters_upper = 'A'..='Z';
+        let mode = TermMode::ANY;
+
+        for (lower, upper) in letters_lower.zip(letters_upper) {
+            assert_eq!(
+                to_esc_str(
+                    &Keystroke::parse(&format!("ctrl-{}", lower)).unwrap(),
+                    &mode,
+                    false
+                ),
+                to_esc_str(
+                    &Keystroke::parse(&format!("ctrl-shift-{}", upper)).unwrap(),
+                    &mode,
+                    false
+                ),
+                "On letter: {}/{}",
+                lower,
+                upper
+            )
+        }
+    }
+
+    #[test]
+    fn alt_is_meta() {
+        let ascii_printable = ' '..='~';
+        for character in ascii_printable {
+            assert_eq!(
+                to_esc_str(
+                    &Keystroke::parse(&format!("alt-{}", character)).unwrap(),
+                    &TermMode::NONE,
+                    true
+                )
+                .unwrap(),
+                format!("\x1b{}", character)
+            );
+        }
+
+        let gpui_keys = [
+            "up", "down", "right", "left", "f1", "f2", "f3", "f4", "F5", "f6", "f7", "f8", "f9",
+            "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "insert",
+            "pageup", "pagedown", "end", "home",
+        ];
+
+        for key in gpui_keys {
+            assert_ne!(
+                to_esc_str(
+                    &Keystroke::parse(&format!("alt-{}", key)).unwrap(),
+                    &TermMode::NONE,
+                    true
+                )
+                .unwrap(),
+                format!("\x1b{}", key)
+            );
+        }
+    }
+
+    #[test]
+    fn test_modifier_code_calc() {
+        //   Code     Modifiers
+        // ---------+---------------------------
+        //    2     | Shift
+        //    3     | Alt
+        //    4     | Shift + Alt
+        //    5     | Control
+        //    6     | Shift + Control
+        //    7     | Alt + Control
+        //    8     | Shift + Alt + Control
+        // ---------+---------------------------
+        // from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
+        assert_eq!(2, modifier_code(&Keystroke::parse("shift-A").unwrap()));
+        assert_eq!(3, modifier_code(&Keystroke::parse("alt-A").unwrap()));
+        assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-A").unwrap()));
+        assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-A").unwrap()));
+        assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-A").unwrap()));
+        assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-A").unwrap()));
+        assert_eq!(
+            8,
+            modifier_code(&Keystroke::parse("shift-ctrl-alt-A").unwrap())
+        );
+    }
+}

crates/terminal2/src/mappings/mouse.rs 🔗

@@ -0,0 +1,277 @@
+use std::cmp::{self, max, min};
+use std::iter::repeat;
+
+use alacritty_terminal::grid::Dimensions;
+/// Most of the code, and specifically the constants, in this are copied from Alacritty,
+/// with modifications for our circumstances
+use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point as AlacPoint, Side};
+use alacritty_terminal::term::TermMode;
+use gpui2::{px, Modifiers, MouseButton, MouseMoveEvent, Pixels, Point, ScrollWheelEvent};
+
+use crate::TerminalSize;
+
+enum MouseFormat {
+    SGR,
+    Normal(bool),
+}
+
+impl MouseFormat {
+    fn from_mode(mode: TermMode) -> Self {
+        if mode.contains(TermMode::SGR_MOUSE) {
+            MouseFormat::SGR
+        } else if mode.contains(TermMode::UTF8_MOUSE) {
+            MouseFormat::Normal(true)
+        } else {
+            MouseFormat::Normal(false)
+        }
+    }
+}
+
+#[derive(Debug)]
+enum AlacMouseButton {
+    LeftButton = 0,
+    MiddleButton = 1,
+    RightButton = 2,
+    LeftMove = 32,
+    MiddleMove = 33,
+    RightMove = 34,
+    NoneMove = 35,
+    ScrollUp = 64,
+    ScrollDown = 65,
+    Other = 99,
+}
+
+impl AlacMouseButton {
+    fn from_move(e: &MouseMoveEvent) -> Self {
+        match e.pressed_button {
+            Some(b) => match b {
+                gpui2::MouseButton::Left => AlacMouseButton::LeftMove,
+                gpui2::MouseButton::Middle => AlacMouseButton::MiddleMove,
+                gpui2::MouseButton::Right => AlacMouseButton::RightMove,
+                gpui2::MouseButton::Navigate(_) => AlacMouseButton::Other,
+            },
+            None => AlacMouseButton::NoneMove,
+        }
+    }
+
+    fn from_button(e: MouseButton) -> Self {
+        match e {
+            gpui2::MouseButton::Left => AlacMouseButton::LeftButton,
+            gpui2::MouseButton::Right => AlacMouseButton::MiddleButton,
+            gpui2::MouseButton::Middle => AlacMouseButton::RightButton,
+            gpui2::MouseButton::Navigate(_) => AlacMouseButton::Other,
+        }
+    }
+
+    fn from_scroll(e: &ScrollWheelEvent) -> Self {
+        let is_positive = match e.delta {
+            gpui2::ScrollDelta::Pixels(pixels) => pixels.y > px(0.),
+            gpui2::ScrollDelta::Lines(lines) => lines.y > 0.,
+        };
+
+        if is_positive {
+            AlacMouseButton::ScrollUp
+        } else {
+            AlacMouseButton::ScrollDown
+        }
+    }
+
+    fn is_other(&self) -> bool {
+        match self {
+            AlacMouseButton::Other => true,
+            _ => false,
+        }
+    }
+}
+
+pub fn scroll_report(
+    point: AlacPoint,
+    scroll_lines: i32,
+    e: &ScrollWheelEvent,
+    mode: TermMode,
+) -> Option<impl Iterator<Item = Vec<u8>>> {
+    if mode.intersects(TermMode::MOUSE_MODE) {
+        mouse_report(
+            point,
+            AlacMouseButton::from_scroll(e),
+            true,
+            e.modifiers,
+            MouseFormat::from_mode(mode),
+        )
+        .map(|report| repeat(report).take(max(scroll_lines, 1) as usize))
+    } else {
+        None
+    }
+}
+
+pub fn alt_scroll(scroll_lines: i32) -> Vec<u8> {
+    let cmd = if scroll_lines > 0 { b'A' } else { b'B' };
+
+    let mut content = Vec::with_capacity(scroll_lines.abs() as usize * 3);
+    for _ in 0..scroll_lines.abs() {
+        content.push(0x1b);
+        content.push(b'O');
+        content.push(cmd);
+    }
+    content
+}
+
+pub fn mouse_button_report(
+    point: AlacPoint,
+    button: gpui2::MouseButton,
+    modifiers: Modifiers,
+    pressed: bool,
+    mode: TermMode,
+) -> Option<Vec<u8>> {
+    let button = AlacMouseButton::from_button(button);
+    if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) {
+        mouse_report(
+            point,
+            button,
+            pressed,
+            modifiers,
+            MouseFormat::from_mode(mode),
+        )
+    } else {
+        None
+    }
+}
+
+pub fn mouse_moved_report(point: AlacPoint, e: &MouseMoveEvent, mode: TermMode) -> Option<Vec<u8>> {
+    let button = AlacMouseButton::from_move(e);
+
+    if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) {
+        //Only drags are reported in drag mode, so block NoneMove.
+        if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, AlacMouseButton::NoneMove) {
+            None
+        } else {
+            mouse_report(
+                point,
+                button,
+                true,
+                e.modifiers,
+                MouseFormat::from_mode(mode),
+            )
+        }
+    } else {
+        None
+    }
+}
+
+pub fn mouse_side(
+    pos: Point<Pixels>,
+    cur_size: TerminalSize,
+) -> alacritty_terminal::index::Direction {
+    let cell_width = cur_size.cell_width.floor();
+    if cell_width == px(0.) {
+        return Side::Right;
+    }
+
+    let x = pos.x.floor();
+
+    let cell_x = cmp::max(px(0.), x - cell_width) % cell_width;
+    let half_cell_width = (cur_size.cell_width / 2.0).floor();
+    let additional_padding = (cur_size.width() - cur_size.cell_width * 2.) % cur_size.cell_width;
+    let end_of_grid = cur_size.width() - cur_size.cell_width - additional_padding;
+
+    //Width: Pixels or columns?
+    if cell_x > half_cell_width
+    // Edge case when mouse leaves the window.
+    || x >= end_of_grid
+    {
+        Side::Right
+    } else {
+        Side::Left
+    }
+}
+
+pub fn grid_point(pos: Point<Pixels>, cur_size: TerminalSize, display_offset: usize) -> AlacPoint {
+    let col = GridCol((pos.x / cur_size.cell_width).as_usize());
+    let col = min(col, cur_size.last_column());
+    let line = (pos.y / cur_size.line_height).as_isize() as i32;
+    let line = min(line, cur_size.bottommost_line().0);
+    AlacPoint::new(GridLine(line - display_offset as i32), col)
+}
+
+///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode
+fn mouse_report(
+    point: AlacPoint,
+    button: AlacMouseButton,
+    pressed: bool,
+    modifiers: Modifiers,
+    format: MouseFormat,
+) -> Option<Vec<u8>> {
+    if point.line < 0 {
+        return None;
+    }
+
+    let mut mods = 0;
+    if modifiers.shift {
+        mods += 4;
+    }
+    if modifiers.alt {
+        mods += 8;
+    }
+    if modifiers.control {
+        mods += 16;
+    }
+
+    match format {
+        MouseFormat::SGR => {
+            Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes())
+        }
+        MouseFormat::Normal(utf8) => {
+            if pressed {
+                normal_mouse_report(point, button as u8 + mods, utf8)
+            } else {
+                normal_mouse_report(point, 3 + mods, utf8)
+            }
+        }
+    }
+}
+
+fn normal_mouse_report(point: AlacPoint, button: u8, utf8: bool) -> Option<Vec<u8>> {
+    let AlacPoint { line, column } = point;
+    let max_point = if utf8 { 2015 } else { 223 };
+
+    if line >= max_point || column >= max_point {
+        return None;
+    }
+
+    let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
+
+    let mouse_pos_encode = |pos: usize| -> Vec<u8> {
+        let pos = 32 + 1 + pos;
+        let first = 0xC0 + pos / 64;
+        let second = 0x80 + (pos & 63);
+        vec![first as u8, second as u8]
+    };
+
+    if utf8 && column >= 95 {
+        msg.append(&mut mouse_pos_encode(column.0));
+    } else {
+        msg.push(32 + 1 + column.0 as u8);
+    }
+
+    if utf8 && line >= 95 {
+        msg.append(&mut mouse_pos_encode(line.0 as usize));
+    } else {
+        msg.push(32 + 1 + line.0 as u8);
+    }
+
+    Some(msg)
+}
+
+fn sgr_mouse_report(point: AlacPoint, button: u8, pressed: bool) -> String {
+    let c = if pressed { 'M' } else { 'm' };
+
+    let msg = format!(
+        "\x1b[<{};{};{}{}",
+        button,
+        point.column + 1,
+        point.line + 1,
+        c
+    );
+
+    msg
+}

crates/terminal2/src/terminal2.rs 🔗

@@ -0,0 +1,1528 @@
+pub mod mappings;
+pub use alacritty_terminal;
+pub mod terminal_settings;
+
+use alacritty_terminal::{
+    ansi::{ClearMode, Handler},
+    config::{Config, Program, PtyConfig, Scrolling},
+    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
+    event_loop::{EventLoop, Msg, Notifier},
+    grid::{Dimensions, Scroll as AlacScroll},
+    index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint},
+    selection::{Selection, SelectionRange, SelectionType},
+    sync::FairMutex,
+    term::{
+        cell::Cell,
+        color::Rgb,
+        search::{Match, RegexIter, RegexSearch},
+        RenderableCursor, TermMode,
+    },
+    tty::{self, setup_env},
+    Term,
+};
+use anyhow::{bail, Result};
+
+use futures::{
+    channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
+    FutureExt,
+};
+
+use mappings::mouse::{
+    alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
+};
+
+use procinfo::LocalProcessInfo;
+use serde::{Deserialize, Serialize};
+use settings2::Settings;
+use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings};
+use util::truncate_and_trailoff;
+
+use std::{
+    cmp::{self, min},
+    collections::{HashMap, VecDeque},
+    fmt::Display,
+    ops::{Deref, Index, RangeInclusive},
+    os::unix::prelude::AsRawFd,
+    path::PathBuf,
+    sync::Arc,
+    time::{Duration, Instant},
+};
+use thiserror::Error;
+
+use gpui2::{
+    px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke,
+    MainThread, ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
+    Pixels, Point, ScrollWheelEvent, Size, Task, TouchPhase,
+};
+
+use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
+use lazy_static::lazy_static;
+
+///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
+///Scroll multiplier that is set to 3 by default. This will be removed when I
+///Implement scroll bars.
+const SCROLL_MULTIPLIER: f32 = 4.;
+const MAX_SEARCH_LINES: usize = 100;
+const DEBUG_TERMINAL_WIDTH: Pixels = px(500.);
+const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.);
+const DEBUG_CELL_WIDTH: Pixels = px(5.);
+const DEBUG_LINE_HEIGHT: Pixels = px(5.);
+
+lazy_static! {
+    // Regex Copied from alacritty's ui_config.rs and modified its declaration slightly:
+    // * avoid Rust-specific escaping.
+    // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings.
+    static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
+
+    static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap();
+}
+
+///Upward flowing events, for changing the title and such
+#[derive(Clone, Debug)]
+pub enum Event {
+    TitleChanged,
+    BreadcrumbsChanged,
+    CloseTerminal,
+    Bell,
+    Wakeup,
+    BlinkChanged,
+    SelectionsChanged,
+    NewNavigationTarget(Option<MaybeNavigationTarget>),
+    Open(MaybeNavigationTarget),
+}
+
+/// A string inside terminal, potentially useful as a URI that can be opened.
+#[derive(Clone, Debug)]
+pub enum MaybeNavigationTarget {
+    /// HTTP, git, etc. string determined by the [`URL_REGEX`] regex.
+    Url(String),
+    /// File system path, absolute or relative, existing or not.
+    /// Might have line and column number(s) attached as `file.rs:1:23`
+    PathLike(String),
+}
+
+#[derive(Clone)]
+enum InternalEvent {
+    ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
+    Resize(TerminalSize),
+    Clear,
+    // FocusNextMatch,
+    Scroll(AlacScroll),
+    ScrollToAlacPoint(AlacPoint),
+    SetSelection(Option<(Selection, AlacPoint)>),
+    UpdateSelection(Point<Pixels>),
+    // Adjusted mouse position, should open
+    FindHyperlink(Point<Pixels>, bool),
+    Copy,
+}
+
+///A translation struct for Alacritty to communicate with us from their event loop
+#[derive(Clone)]
+pub struct ZedListener(UnboundedSender<AlacTermEvent>);
+
+impl EventListener for ZedListener {
+    fn send_event(&self, event: AlacTermEvent) {
+        self.0.unbounded_send(event).ok();
+    }
+}
+
+pub fn init(cx: &mut AppContext) {
+    TerminalSettings::register(cx);
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
+pub struct TerminalSize {
+    pub cell_width: Pixels,
+    pub line_height: Pixels,
+    pub size: Size<Pixels>,
+}
+
+impl TerminalSize {
+    pub fn new(line_height: Pixels, cell_width: Pixels, size: Size<Pixels>) -> Self {
+        TerminalSize {
+            cell_width,
+            line_height,
+            size,
+        }
+    }
+
+    pub fn num_lines(&self) -> usize {
+        f32::from((self.size.height / self.line_height).floor()) as usize
+    }
+
+    pub fn num_columns(&self) -> usize {
+        f32::from((self.size.width / self.cell_width).floor()) as usize
+    }
+
+    pub fn height(&self) -> Pixels {
+        self.size.height
+    }
+
+    pub fn width(&self) -> Pixels {
+        self.size.width
+    }
+
+    pub fn cell_width(&self) -> Pixels {
+        self.cell_width
+    }
+
+    pub fn line_height(&self) -> Pixels {
+        self.line_height
+    }
+}
+impl Default for TerminalSize {
+    fn default() -> Self {
+        TerminalSize::new(
+            DEBUG_LINE_HEIGHT,
+            DEBUG_CELL_WIDTH,
+            Size {
+                width: DEBUG_TERMINAL_WIDTH,
+                height: DEBUG_TERMINAL_HEIGHT,
+            },
+        )
+    }
+}
+
+impl From<TerminalSize> for WindowSize {
+    fn from(val: TerminalSize) -> Self {
+        WindowSize {
+            num_lines: val.num_lines() as u16,
+            num_cols: val.num_columns() as u16,
+            cell_width: f32::from(val.cell_width()) as u16,
+            cell_height: f32::from(val.line_height()) as u16,
+        }
+    }
+}
+
+impl Dimensions for TerminalSize {
+    /// Note: this is supposed to be for the back buffer's length,
+    /// but we exclusively use it to resize the terminal, which does not
+    /// use this method. We still have to implement it for the trait though,
+    /// hence, this comment.
+    fn total_lines(&self) -> usize {
+        self.screen_lines()
+    }
+
+    fn screen_lines(&self) -> usize {
+        self.num_lines()
+    }
+
+    fn columns(&self) -> usize {
+        self.num_columns()
+    }
+}
+
+#[derive(Error, Debug)]
+pub struct TerminalError {
+    pub directory: Option<PathBuf>,
+    pub shell: Shell,
+    pub source: std::io::Error,
+}
+
+impl TerminalError {
+    pub fn fmt_directory(&self) -> String {
+        self.directory
+            .clone()
+            .map(|path| {
+                match path
+                    .into_os_string()
+                    .into_string()
+                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
+                {
+                    Ok(s) => s,
+                    Err(s) => s,
+                }
+            })
+            .unwrap_or_else(|| {
+                let default_dir =
+                    dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
+                match default_dir {
+                    Some(dir) => format!("<none specified, using home directory> {}", dir),
+                    None => "<none specified, could not find home directory>".to_string(),
+                }
+            })
+    }
+
+    pub fn shell_to_string(&self) -> String {
+        match &self.shell {
+            Shell::System => "<system shell>".to_string(),
+            Shell::Program(p) => p.to_string(),
+            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
+        }
+    }
+
+    pub fn fmt_shell(&self) -> String {
+        match &self.shell {
+            Shell::System => "<system defined shell>".to_string(),
+            Shell::Program(s) => s.to_string(),
+            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
+        }
+    }
+}
+
+impl Display for TerminalError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let dir_string: String = self.fmt_directory();
+        let shell = self.fmt_shell();
+
+        write!(
+            f,
+            "Working directory: {} Shell command: `{}`, IOError: {}",
+            dir_string, shell, self.source
+        )
+    }
+}
+
+pub struct TerminalBuilder {
+    terminal: Terminal,
+    events_rx: UnboundedReceiver<AlacTermEvent>,
+}
+
+impl TerminalBuilder {
+    pub fn new(
+        working_directory: Option<PathBuf>,
+        shell: Shell,
+        mut env: HashMap<String, String>,
+        blink_settings: Option<TerminalBlink>,
+        alternate_scroll: AlternateScroll,
+        window: AnyWindowHandle,
+        color_for_index: impl Fn(usize, &mut AppContext) -> Hsla + Send + Sync + 'static,
+    ) -> Result<TerminalBuilder> {
+        let pty_config = {
+            let alac_shell = match shell.clone() {
+                Shell::System => None,
+                Shell::Program(program) => Some(Program::Just(program)),
+                Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
+            };
+
+            PtyConfig {
+                shell: alac_shell,
+                working_directory: working_directory.clone(),
+                hold: false,
+            }
+        };
+
+        //TODO: Properly set the current locale,
+        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
+        env.insert("ZED_TERM".to_string(), true.to_string());
+
+        let alac_scrolling = Scrolling::default();
+        // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
+
+        let config = Config {
+            pty_config: pty_config.clone(),
+            env,
+            scrolling: alac_scrolling,
+            ..Default::default()
+        };
+
+        setup_env(&config);
+
+        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
+        //TODO: Remove with a bounded sender which can be dispatched on &self
+        let (events_tx, events_rx) = unbounded();
+        //Set up the terminal...
+        let mut term = Term::new(
+            &config,
+            &TerminalSize::default(),
+            ZedListener(events_tx.clone()),
+        );
+
+        //Start off blinking if we need to
+        if let Some(TerminalBlink::On) = blink_settings {
+            term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
+        }
+
+        //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
+        if let AlternateScroll::Off = alternate_scroll {
+            term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
+        }
+
+        let term = Arc::new(FairMutex::new(term));
+
+        //Setup the pty...
+        let pty = match tty::new(
+            &pty_config,
+            TerminalSize::default().into(),
+            window.window_id().as_u64(),
+        ) {
+            Ok(pty) => pty,
+            Err(error) => {
+                bail!(TerminalError {
+                    directory: working_directory,
+                    shell,
+                    source: error,
+                });
+            }
+        };
+
+        let fd = pty.file().as_raw_fd();
+        let shell_pid = pty.child().id();
+
+        //And connect them together
+        let event_loop = EventLoop::new(
+            term.clone(),
+            ZedListener(events_tx.clone()),
+            pty,
+            pty_config.hold,
+            false,
+        );
+
+        //Kick things off
+        let pty_tx = event_loop.channel();
+        let _io_thread = event_loop.spawn();
+
+        let terminal = Terminal {
+            pty_tx: Notifier(pty_tx),
+            term,
+            events: VecDeque::with_capacity(10), //Should never get this high.
+            last_content: Default::default(),
+            last_mouse: None,
+            matches: Vec::new(),
+            last_synced: Instant::now(),
+            sync_task: None,
+            selection_head: None,
+            shell_fd: fd as u32,
+            shell_pid,
+            foreground_process_info: None,
+            breadcrumb_text: String::new(),
+            scroll_px: px(0.),
+            last_mouse_position: None,
+            next_link_id: 0,
+            selection_phase: SelectionPhase::Ended,
+            cmd_pressed: false,
+            hovered_word: false,
+            color_for_index: Box::new(color_for_index),
+        };
+
+        Ok(TerminalBuilder {
+            terminal,
+            events_rx,
+        })
+    }
+
+    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
+        //Event loop
+        cx.spawn_on_main(|this, mut cx| async move {
+            use futures::StreamExt;
+
+            while let Some(event) = self.events_rx.next().await {
+                this.update(&mut cx, |this, cx| {
+                    //Process the first event immediately for lowered latency
+                    this.process_event(&event, cx);
+                })?;
+
+                'outer: loop {
+                    let mut events = vec![];
+                    let mut timer = cx.executor().timer(Duration::from_millis(4)).fuse();
+                    let mut wakeup = false;
+                    loop {
+                        futures::select_biased! {
+                            _ = timer => break,
+                            event = self.events_rx.next() => {
+                                if let Some(event) = event {
+                                    if matches!(event, AlacTermEvent::Wakeup) {
+                                        wakeup = true;
+                                    } else {
+                                        events.push(event);
+                                    }
+
+                                    if events.len() > 100 {
+                                        break;
+                                    }
+                                } else {
+                                    break;
+                                }
+                            },
+                        }
+                    }
+
+                    if events.is_empty() && wakeup == false {
+                        smol::future::yield_now().await;
+                        break 'outer;
+                    } else {
+                        this.update(&mut cx, |this, cx| {
+                            if wakeup {
+                                this.process_event(&AlacTermEvent::Wakeup, cx);
+                            }
+
+                            for event in events {
+                                this.process_event(&event, cx);
+                            }
+                        })?;
+                        smol::future::yield_now().await;
+                    }
+                }
+            }
+
+            anyhow::Ok(())
+        })
+        .detach();
+
+        self.terminal
+    }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct IndexedCell {
+    pub point: AlacPoint,
+    pub cell: Cell,
+}
+
+impl Deref for IndexedCell {
+    type Target = Cell;
+
+    #[inline]
+    fn deref(&self) -> &Cell {
+        &self.cell
+    }
+}
+
+// TODO: Un-pub
+#[derive(Clone)]
+pub struct TerminalContent {
+    pub cells: Vec<IndexedCell>,
+    pub mode: TermMode,
+    pub display_offset: usize,
+    pub selection_text: Option<String>,
+    pub selection: Option<SelectionRange>,
+    pub cursor: RenderableCursor,
+    pub cursor_char: char,
+    pub size: TerminalSize,
+    pub last_hovered_word: Option<HoveredWord>,
+}
+
+#[derive(Clone)]
+pub struct HoveredWord {
+    pub word: String,
+    pub word_match: RangeInclusive<AlacPoint>,
+    pub id: usize,
+}
+
+impl Default for TerminalContent {
+    fn default() -> Self {
+        TerminalContent {
+            cells: Default::default(),
+            mode: Default::default(),
+            display_offset: Default::default(),
+            selection_text: Default::default(),
+            selection: Default::default(),
+            cursor: RenderableCursor {
+                shape: alacritty_terminal::ansi::CursorShape::Block,
+                point: AlacPoint::new(Line(0), Column(0)),
+            },
+            cursor_char: Default::default(),
+            size: Default::default(),
+            last_hovered_word: None,
+        }
+    }
+}
+
+#[derive(PartialEq, Eq)]
+pub enum SelectionPhase {
+    Selecting,
+    Ended,
+}
+
+pub struct Terminal {
+    pty_tx: Notifier,
+    term: Arc<FairMutex<Term<ZedListener>>>,
+    events: VecDeque<InternalEvent>,
+    /// This is only used for mouse mode cell change detection
+    last_mouse: Option<(AlacPoint, AlacDirection)>,
+    /// This is only used for terminal hovered word checking
+    last_mouse_position: Option<Point<Pixels>>,
+    pub matches: Vec<RangeInclusive<AlacPoint>>,
+    pub last_content: TerminalContent,
+    last_synced: Instant,
+    sync_task: Option<Task<()>>,
+    pub selection_head: Option<AlacPoint>,
+    pub breadcrumb_text: String,
+    shell_pid: u32,
+    shell_fd: u32,
+    pub foreground_process_info: Option<LocalProcessInfo>,
+    scroll_px: Pixels,
+    next_link_id: usize,
+    selection_phase: SelectionPhase,
+    cmd_pressed: bool,
+    hovered_word: bool,
+    // An implementation of the 8 bit ANSI color palette
+    color_for_index: Box<dyn Fn(usize, &mut AppContext) -> Hsla + Send + Sync + 'static>,
+}
+
+impl Terminal {
+    fn process_event(&mut self, event: &AlacTermEvent, cx: &mut MainThread<ModelContext<Self>>) {
+        match event {
+            AlacTermEvent::Title(title) => {
+                self.breadcrumb_text = title.to_string();
+                cx.emit(Event::BreadcrumbsChanged);
+            }
+            AlacTermEvent::ResetTitle => {
+                self.breadcrumb_text = String::new();
+                cx.emit(Event::BreadcrumbsChanged);
+            }
+            AlacTermEvent::ClipboardStore(_, data) => {
+                cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
+            }
+            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
+                &cx.read_from_clipboard()
+                    .map(|ci| ci.text().to_string())
+                    .unwrap_or_else(|| "".to_string()),
+            )),
+            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
+            AlacTermEvent::TextAreaSizeRequest(format) => {
+                self.write_to_pty(format(self.last_content.size.into()))
+            }
+            AlacTermEvent::CursorBlinkingChange => {
+                cx.emit(Event::BlinkChanged);
+            }
+            AlacTermEvent::Bell => {
+                cx.emit(Event::Bell);
+            }
+            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
+            AlacTermEvent::MouseCursorDirty => {
+                //NOOP, Handled in render
+            }
+            AlacTermEvent::Wakeup => {
+                cx.emit(Event::Wakeup);
+
+                if self.update_process_info() {
+                    cx.emit(Event::TitleChanged);
+                }
+            }
+            AlacTermEvent::ColorRequest(idx, fun_ptr) => {
+                self.events
+                    .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
+            }
+        }
+    }
+
+    /// Update the cached process info, returns whether the Zed-relevant info has changed
+    fn update_process_info(&mut self) -> bool {
+        let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
+        if pid < 0 {
+            pid = self.shell_pid as i32;
+        }
+
+        if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) {
+            let res = self
+                .foreground_process_info
+                .as_ref()
+                .map(|old_info| {
+                    process_info.cwd != old_info.cwd || process_info.name != old_info.name
+                })
+                .unwrap_or(true);
+
+            self.foreground_process_info = Some(process_info.clone());
+
+            res
+        } else {
+            false
+        }
+    }
+
+    ///Takes events from Alacritty and translates them to behavior on this view
+    fn process_terminal_event(
+        &mut self,
+        event: &InternalEvent,
+        term: &mut Term<ZedListener>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        match event {
+            InternalEvent::ColorRequest(index, format) => {
+                let color = term.colors()[*index]
+                    .unwrap_or_else(|| to_alac_rgb((self.color_for_index)(*index, cx)));
+                self.write_to_pty(format(color))
+            }
+            InternalEvent::Resize(mut new_size) => {
+                new_size.size.height = cmp::max(new_size.line_height, new_size.height());
+                new_size.size.width = cmp::max(new_size.cell_width, new_size.width());
+
+                self.last_content.size = new_size.clone();
+
+                self.pty_tx.0.send(Msg::Resize(new_size.into())).ok();
+
+                term.resize(new_size);
+            }
+            InternalEvent::Clear => {
+                // Clear back buffer
+                term.clear_screen(ClearMode::Saved);
+
+                let cursor = term.grid().cursor.point;
+
+                // Clear the lines above
+                term.grid_mut().reset_region(..cursor.line);
+
+                // Copy the current line up
+                let line = term.grid()[cursor.line][..Column(term.grid().columns())]
+                    .iter()
+                    .cloned()
+                    .enumerate()
+                    .collect::<Vec<(usize, Cell)>>();
+
+                for (i, cell) in line {
+                    term.grid_mut()[Line(0)][Column(i)] = cell;
+                }
+
+                // Reset the cursor
+                term.grid_mut().cursor.point =
+                    AlacPoint::new(Line(0), term.grid_mut().cursor.point.column);
+                let new_cursor = term.grid().cursor.point;
+
+                // Clear the lines below the new cursor
+                if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
+                    term.grid_mut().reset_region((new_cursor.line + 1)..);
+                }
+
+                cx.emit(Event::Wakeup);
+            }
+            InternalEvent::Scroll(scroll) => {
+                term.scroll_display(*scroll);
+                self.refresh_hovered_word();
+            }
+            InternalEvent::SetSelection(selection) => {
+                term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
+
+                if let Some((_, head)) = selection {
+                    self.selection_head = Some(*head);
+                }
+                cx.emit(Event::SelectionsChanged)
+            }
+            InternalEvent::UpdateSelection(position) => {
+                if let Some(mut selection) = term.selection.take() {
+                    let point = grid_point(
+                        *position,
+                        self.last_content.size,
+                        term.grid().display_offset(),
+                    );
+
+                    let side = mouse_side(*position, self.last_content.size);
+
+                    selection.update(point, side);
+                    term.selection = Some(selection);
+
+                    self.selection_head = Some(point);
+                    cx.emit(Event::SelectionsChanged)
+                }
+            }
+
+            InternalEvent::Copy => {
+                if let Some(txt) = term.selection_to_string() {
+                    cx.run_on_main(|cx| cx.write_to_clipboard(ClipboardItem::new(txt)))
+                        .detach();
+                }
+            }
+            InternalEvent::ScrollToAlacPoint(point) => {
+                term.scroll_to_point(*point);
+                self.refresh_hovered_word();
+            }
+            InternalEvent::FindHyperlink(position, open) => {
+                let prev_hovered_word = self.last_content.last_hovered_word.take();
+
+                let point = grid_point(
+                    *position,
+                    self.last_content.size,
+                    term.grid().display_offset(),
+                )
+                .grid_clamp(term, Boundary::Grid);
+
+                let link = term.grid().index(point).hyperlink();
+                let found_word = if link.is_some() {
+                    let mut min_index = point;
+                    loop {
+                        let new_min_index = min_index.sub(term, Boundary::Cursor, 1);
+                        if new_min_index == min_index {
+                            break;
+                        } else if term.grid().index(new_min_index).hyperlink() != link {
+                            break;
+                        } else {
+                            min_index = new_min_index
+                        }
+                    }
+
+                    let mut max_index = point;
+                    loop {
+                        let new_max_index = max_index.add(term, Boundary::Cursor, 1);
+                        if new_max_index == max_index {
+                            break;
+                        } else if term.grid().index(new_max_index).hyperlink() != link {
+                            break;
+                        } else {
+                            max_index = new_max_index
+                        }
+                    }
+
+                    let url = link.unwrap().uri().to_owned();
+                    let url_match = min_index..=max_index;
+
+                    Some((url, true, url_match))
+                } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) {
+                    let maybe_url_or_path =
+                        term.bounds_to_string(*word_match.start(), *word_match.end());
+                    let original_match = word_match.clone();
+                    let (sanitized_match, sanitized_word) =
+                        if maybe_url_or_path.starts_with('[') && maybe_url_or_path.ends_with(']') {
+                            (
+                                Match::new(
+                                    word_match.start().add(term, Boundary::Cursor, 1),
+                                    word_match.end().sub(term, Boundary::Cursor, 1),
+                                ),
+                                maybe_url_or_path[1..maybe_url_or_path.len() - 1].to_owned(),
+                            )
+                        } else {
+                            (word_match, maybe_url_or_path)
+                        };
+
+                    let is_url = match regex_match_at(term, point, &URL_REGEX) {
+                        Some(url_match) => {
+                            // `]` is a valid symbol in the `file://` URL, so the regex match will include it
+                            // consider that when ensuring that the URL match is the same as the original word
+                            if sanitized_match != original_match {
+                                url_match.start() == sanitized_match.start()
+                                    && url_match.end() == original_match.end()
+                            } else {
+                                url_match == sanitized_match
+                            }
+                        }
+                        None => false,
+                    };
+                    Some((sanitized_word, is_url, sanitized_match))
+                } else {
+                    None
+                };
+
+                match found_word {
+                    Some((maybe_url_or_path, is_url, url_match)) => {
+                        if *open {
+                            let target = if is_url {
+                                MaybeNavigationTarget::Url(maybe_url_or_path)
+                            } else {
+                                MaybeNavigationTarget::PathLike(maybe_url_or_path)
+                            };
+                            cx.emit(Event::Open(target));
+                        } else {
+                            self.update_selected_word(
+                                prev_hovered_word,
+                                url_match,
+                                maybe_url_or_path,
+                                is_url,
+                                cx,
+                            );
+                        }
+                        self.hovered_word = true;
+                    }
+                    None => {
+                        if self.hovered_word {
+                            cx.emit(Event::NewNavigationTarget(None));
+                        }
+                        self.hovered_word = false;
+                    }
+                }
+            }
+        }
+    }
+
+    fn update_selected_word(
+        &mut self,
+        prev_word: Option<HoveredWord>,
+        word_match: RangeInclusive<AlacPoint>,
+        word: String,
+        is_url: bool,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(prev_word) = prev_word {
+            if prev_word.word == word && prev_word.word_match == word_match {
+                self.last_content.last_hovered_word = Some(HoveredWord {
+                    word,
+                    word_match,
+                    id: prev_word.id,
+                });
+                return;
+            }
+        }
+
+        self.last_content.last_hovered_word = Some(HoveredWord {
+            word: word.clone(),
+            word_match,
+            id: self.next_link_id(),
+        });
+        let navigation_target = if is_url {
+            MaybeNavigationTarget::Url(word)
+        } else {
+            MaybeNavigationTarget::PathLike(word)
+        };
+        cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
+    }
+
+    fn next_link_id(&mut self) -> usize {
+        let res = self.next_link_id;
+        self.next_link_id = self.next_link_id.wrapping_add(1);
+        res
+    }
+
+    pub fn last_content(&self) -> &TerminalContent {
+        &self.last_content
+    }
+
+    //To test:
+    //- Activate match on terminal (scrolling and selection)
+    //- Editor search snapping behavior
+
+    pub fn activate_match(&mut self, index: usize) {
+        if let Some(search_match) = self.matches.get(index).cloned() {
+            self.set_selection(Some((make_selection(&search_match), *search_match.end())));
+
+            self.events
+                .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start()));
+        }
+    }
+
+    pub fn select_matches(&mut self, matches: Vec<RangeInclusive<AlacPoint>>) {
+        let matches_to_select = self
+            .matches
+            .iter()
+            .filter(|self_match| matches.contains(self_match))
+            .cloned()
+            .collect::<Vec<_>>();
+        for match_to_select in matches_to_select {
+            self.set_selection(Some((
+                make_selection(&match_to_select),
+                *match_to_select.end(),
+            )));
+        }
+    }
+
+    pub fn select_all(&mut self) {
+        let term = self.term.lock();
+        let start = AlacPoint::new(term.topmost_line(), Column(0));
+        let end = AlacPoint::new(term.bottommost_line(), term.last_column());
+        drop(term);
+        self.set_selection(Some((make_selection(&(start..=end)), end)));
+    }
+
+    fn set_selection(&mut self, selection: Option<(Selection, AlacPoint)>) {
+        self.events
+            .push_back(InternalEvent::SetSelection(selection));
+    }
+
+    pub fn copy(&mut self) {
+        self.events.push_back(InternalEvent::Copy);
+    }
+
+    pub fn clear(&mut self) {
+        self.events.push_back(InternalEvent::Clear)
+    }
+
+    ///Resize the terminal and the PTY.
+    pub fn set_size(&mut self, new_size: TerminalSize) {
+        self.events.push_back(InternalEvent::Resize(new_size))
+    }
+
+    ///Write the Input payload to the tty.
+    fn write_to_pty(&self, input: String) {
+        self.pty_tx.notify(input.into_bytes());
+    }
+
+    fn write_bytes_to_pty(&self, input: Vec<u8>) {
+        self.pty_tx.notify(input);
+    }
+
+    pub fn input(&mut self, input: String) {
+        self.events
+            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
+        self.events.push_back(InternalEvent::SetSelection(None));
+
+        self.write_to_pty(input);
+    }
+
+    pub fn input_bytes(&mut self, input: Vec<u8>) {
+        self.events
+            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
+        self.events.push_back(InternalEvent::SetSelection(None));
+
+        self.write_bytes_to_pty(input);
+    }
+
+    pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
+        let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
+        if let Some(esc) = esc {
+            self.input(esc);
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool {
+        let changed = self.cmd_pressed != modifiers.command;
+        if !self.cmd_pressed && modifiers.command {
+            self.refresh_hovered_word();
+        }
+        self.cmd_pressed = modifiers.command;
+        changed
+    }
+
+    ///Paste text into the terminal
+    pub fn paste(&mut self, text: &str) {
+        let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
+            format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
+        } else {
+            text.replace("\r\n", "\r").replace('\n', "\r")
+        };
+
+        self.input(paste_text);
+    }
+
+    pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
+        let term = self.term.clone();
+
+        let mut terminal = if let Some(term) = term.try_lock_unfair() {
+            term
+        } else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
+            term.lock_unfair() //It's been too long, force block
+        } else if let None = self.sync_task {
+            //Skip this frame
+            let delay = cx.executor().timer(Duration::from_millis(16));
+            self.sync_task = Some(cx.spawn(|weak_handle, mut cx| async move {
+                delay.await;
+                if let Some(handle) = weak_handle.upgrade() {
+                    handle
+                        .update(&mut cx, |terminal, cx| {
+                            terminal.sync_task.take();
+                            cx.notify();
+                        })
+                        .ok();
+                }
+            }));
+            return;
+        } else {
+            //No lock and delayed rendering already scheduled, nothing to do
+            return;
+        };
+
+        //Note that the ordering of events matters for event processing
+        while let Some(e) = self.events.pop_front() {
+            self.process_terminal_event(&e, &mut terminal, cx)
+        }
+
+        self.last_content = Self::make_content(&terminal, &self.last_content);
+        self.last_synced = Instant::now();
+    }
+
+    fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
+        let content = term.renderable_content();
+        TerminalContent {
+            cells: content
+                .display_iter
+                //TODO: Add this once there's a way to retain empty lines
+                // .filter(|ic| {
+                //     !ic.flags.contains(Flags::HIDDEN)
+                //         && !(ic.bg == Named(NamedColor::Background)
+                //             && ic.c == ' '
+                //             && !ic.flags.contains(Flags::INVERSE))
+                // })
+                .map(|ic| IndexedCell {
+                    point: ic.point,
+                    cell: ic.cell.clone(),
+                })
+                .collect::<Vec<IndexedCell>>(),
+            mode: content.mode,
+            display_offset: content.display_offset,
+            selection_text: term.selection_to_string(),
+            selection: content.selection,
+            cursor: content.cursor,
+            cursor_char: term.grid()[content.cursor.point].c,
+            size: last_content.size,
+            last_hovered_word: last_content.last_hovered_word.clone(),
+        }
+    }
+
+    pub fn focus_in(&self) {
+        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
+            self.write_to_pty("\x1b[I".to_string());
+        }
+    }
+
+    pub fn focus_out(&mut self) {
+        self.last_mouse_position = None;
+        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
+            self.write_to_pty("\x1b[O".to_string());
+        }
+    }
+
+    pub fn mouse_changed(&mut self, point: AlacPoint, side: AlacDirection) -> bool {
+        match self.last_mouse {
+            Some((old_point, old_side)) => {
+                if old_point == point && old_side == side {
+                    false
+                } else {
+                    self.last_mouse = Some((point, side));
+                    true
+                }
+            }
+            None => {
+                self.last_mouse = Some((point, side));
+                true
+            }
+        }
+    }
+
+    pub fn mouse_mode(&self, shift: bool) -> bool {
+        self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
+    }
+
+    pub fn mouse_move(&mut self, e: &MouseMoveEvent, origin: Point<Pixels>) {
+        let position = e.position - origin;
+        self.last_mouse_position = Some(position);
+        if self.mouse_mode(e.modifiers.shift) {
+            let point = grid_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
+            let side = mouse_side(position, self.last_content.size);
+
+            if self.mouse_changed(point, side) {
+                if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
+                    self.pty_tx.notify(bytes);
+                }
+            }
+        } else if self.cmd_pressed {
+            self.word_from_position(Some(position));
+        }
+    }
+
+    fn word_from_position(&mut self, position: Option<Point<Pixels>>) {
+        if self.selection_phase == SelectionPhase::Selecting {
+            self.last_content.last_hovered_word = None;
+        } else if let Some(position) = position {
+            self.events
+                .push_back(InternalEvent::FindHyperlink(position, false));
+        }
+    }
+
+    pub fn mouse_drag(&mut self, e: MouseMoveEvent, origin: Point<Pixels>, region: Bounds<Pixels>) {
+        let position = e.position - origin;
+        self.last_mouse_position = Some(position);
+
+        if !self.mouse_mode(e.modifiers.shift) {
+            self.selection_phase = SelectionPhase::Selecting;
+            // Alacritty has the same ordering, of first updating the selection
+            // then scrolling 15ms later
+            self.events
+                .push_back(InternalEvent::UpdateSelection(position));
+
+            // Doesn't make sense to scroll the alt screen
+            if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
+                let scroll_delta = match self.drag_line_delta(e, region) {
+                    Some(value) => value,
+                    None => return,
+                };
+
+                let scroll_lines =
+                    (scroll_delta / self.last_content.size.line_height).as_isize() as i32;
+
+                self.events
+                    .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
+            }
+        }
+    }
+
+    fn drag_line_delta(&mut self, e: MouseMoveEvent, region: Bounds<Pixels>) -> Option<Pixels> {
+        //TODO: Why do these need to be doubled? Probably the same problem that the IME has
+        let top = region.origin.y + (self.last_content.size.line_height * 2.);
+        let bottom = region.lower_left().y - (self.last_content.size.line_height * 2.);
+        let scroll_delta = if e.position.y < top {
+            (top - e.position.y).pow(1.1)
+        } else if e.position.y > bottom {
+            -((e.position.y - bottom).pow(1.1))
+        } else {
+            return None; //Nothing to do
+        };
+        Some(scroll_delta)
+    }
+
+    pub fn mouse_down(&mut self, e: &MouseDownEvent, origin: Point<Pixels>) {
+        let position = e.position - origin;
+        let point = grid_point(
+            position,
+            self.last_content.size,
+            self.last_content.display_offset,
+        );
+
+        if self.mouse_mode(e.modifiers.shift) {
+            if let Some(bytes) =
+                mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode)
+            {
+                self.pty_tx.notify(bytes);
+            }
+        } else if e.button == MouseButton::Left {
+            let position = e.position - origin;
+            let point = grid_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
+
+            // Use .opposite so that selection is inclusive of the cell clicked.
+            let side = mouse_side(position, self.last_content.size);
+
+            let selection_type = match e.click_count {
+                0 => return, //This is a release
+                1 => Some(SelectionType::Simple),
+                2 => Some(SelectionType::Semantic),
+                3 => Some(SelectionType::Lines),
+                _ => None,
+            };
+
+            let selection =
+                selection_type.map(|selection_type| Selection::new(selection_type, point, side));
+
+            if let Some(sel) = selection {
+                self.events
+                    .push_back(InternalEvent::SetSelection(Some((sel, point))));
+            }
+        }
+    }
+
+    pub fn mouse_up(
+        &mut self,
+        e: &MouseUpEvent,
+        origin: Point<Pixels>,
+        cx: &mut MainThread<ModelContext<Self>>,
+    ) {
+        let setting = TerminalSettings::get_global(cx);
+
+        let position = e.position - origin;
+        if self.mouse_mode(e.modifiers.shift) {
+            let point = grid_point(
+                position,
+                self.last_content.size,
+                self.last_content.display_offset,
+            );
+
+            if let Some(bytes) =
+                mouse_button_report(point, e.button, e.modifiers, false, self.last_content.mode)
+            {
+                self.pty_tx.notify(bytes);
+            }
+        } else {
+            if e.button == MouseButton::Left && setting.copy_on_select {
+                self.copy();
+            }
+
+            //Hyperlinks
+            if self.selection_phase == SelectionPhase::Ended {
+                let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
+                if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
+                    cx.open_url(link.uri());
+                } else if self.cmd_pressed {
+                    self.events
+                        .push_back(InternalEvent::FindHyperlink(position, true));
+                }
+            }
+        }
+
+        self.selection_phase = SelectionPhase::Ended;
+        self.last_mouse = None;
+    }
+
+    ///Scroll the terminal
+    pub fn scroll_wheel(&mut self, e: ScrollWheelEvent, origin: Point<Pixels>) {
+        let mouse_mode = self.mouse_mode(e.shift);
+
+        if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
+            if mouse_mode {
+                let point = grid_point(
+                    e.position - origin,
+                    self.last_content.size,
+                    self.last_content.display_offset,
+                );
+
+                if let Some(scrolls) =
+                    scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
+                {
+                    for scroll in scrolls {
+                        self.pty_tx.notify(scroll);
+                    }
+                };
+            } else if self
+                .last_content
+                .mode
+                .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
+                && !e.shift
+            {
+                self.pty_tx.notify(alt_scroll(scroll_lines))
+            } else {
+                if scroll_lines != 0 {
+                    let scroll = AlacScroll::Delta(scroll_lines);
+
+                    self.events.push_back(InternalEvent::Scroll(scroll));
+                }
+            }
+        }
+    }
+
+    fn refresh_hovered_word(&mut self) {
+        self.word_from_position(self.last_mouse_position);
+    }
+
+    fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
+        let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
+        let line_height = self.last_content.size.line_height;
+        match e.touch_phase {
+            /* Reset scroll state on started */
+            TouchPhase::Started => {
+                self.scroll_px = px(0.);
+                None
+            }
+            /* Calculate the appropriate scroll lines */
+            TouchPhase::Moved => {
+                let old_offset = (self.scroll_px / line_height).as_isize() as i32;
+
+                self.scroll_px += e.delta.pixel_delta(line_height).y * scroll_multiplier;
+
+                let new_offset = (self.scroll_px / line_height).as_isize() as i32;
+
+                // Whenever we hit the edges, reset our stored scroll to 0
+                // so we can respond to changes in direction quickly
+                self.scroll_px %= self.last_content.size.height();
+
+                Some(new_offset - old_offset)
+            }
+            TouchPhase::Ended => None,
+        }
+    }
+
+    pub fn find_matches(
+        &mut self,
+        searcher: RegexSearch,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Vec<RangeInclusive<AlacPoint>>> {
+        let term = self.term.clone();
+        cx.executor().spawn(async move {
+            let term = term.lock();
+
+            all_search_matches(&term, &searcher).collect()
+        })
+    }
+
+    pub fn title(&self) -> String {
+        self.foreground_process_info
+            .as_ref()
+            .map(|fpi| {
+                format!(
+                    "{} — {}",
+                    truncate_and_trailoff(
+                        &fpi.cwd
+                            .file_name()
+                            .map(|name| name.to_string_lossy().to_string())
+                            .unwrap_or_default(),
+                        25
+                    ),
+                    truncate_and_trailoff(
+                        &{
+                            format!(
+                                "{}{}",
+                                fpi.name,
+                                if fpi.argv.len() >= 1 {
+                                    format!(" {}", (&fpi.argv[1..]).join(" "))
+                                } else {
+                                    "".to_string()
+                                }
+                            )
+                        },
+                        25
+                    )
+                )
+            })
+            .unwrap_or_else(|| "Terminal".to_string())
+    }
+
+    pub fn can_navigate_to_selected_word(&self) -> bool {
+        self.cmd_pressed && self.hovered_word
+    }
+}
+
+impl Drop for Terminal {
+    fn drop(&mut self) {
+        self.pty_tx.0.send(Msg::Shutdown).ok();
+    }
+}
+
+impl EventEmitter for Terminal {
+    type Event = Event;
+}
+
+/// Based on alacritty/src/display/hint.rs > regex_match_at
+/// Retrieve the match, if the specified point is inside the content matching the regex.
+fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &RegexSearch) -> Option<Match> {
+    visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
+}
+
+/// Copied from alacritty/src/display/hint.rs:
+/// Iterate over all visible regex matches.
+pub fn visible_regex_match_iter<'a, T>(
+    term: &'a Term<T>,
+    regex: &'a RegexSearch,
+) -> impl Iterator<Item = Match> + 'a {
+    let viewport_start = Line(-(term.grid().display_offset() as i32));
+    let viewport_end = viewport_start + term.bottommost_line();
+    let mut start = term.line_search_left(AlacPoint::new(viewport_start, Column(0)));
+    let mut end = term.line_search_right(AlacPoint::new(viewport_end, Column(0)));
+    start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
+    end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
+
+    RegexIter::new(start, end, AlacDirection::Right, term, regex)
+        .skip_while(move |rm| rm.end().line < viewport_start)
+        .take_while(move |rm| rm.start().line <= viewport_end)
+}
+
+fn make_selection(range: &RangeInclusive<AlacPoint>) -> Selection {
+    let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
+    selection.update(*range.end(), AlacDirection::Right);
+    selection
+}
+
+fn all_search_matches<'a, T>(
+    term: &'a Term<T>,
+    regex: &'a RegexSearch,
+) -> impl Iterator<Item = Match> + 'a {
+    let start = AlacPoint::new(term.grid().topmost_line(), Column(0));
+    let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column());
+    RegexIter::new(start, end, AlacDirection::Right, term, regex)
+}
+
+fn content_index_for_mouse(pos: Point<Pixels>, size: &TerminalSize) -> usize {
+    let col = (pos.x / size.cell_width()).round().as_usize();
+    let clamped_col = min(col, size.columns() - 1);
+    let row = (pos.y / size.line_height()).round().as_usize();
+    let clamped_row = min(row, size.screen_lines() - 1);
+    clamped_row * size.columns() + clamped_col
+}
+
+#[cfg(test)]
+mod tests {
+    use alacritty_terminal::{
+        index::{Column, Line, Point as AlacPoint},
+        term::cell::Cell,
+    };
+    use gpui2::{point, size, Pixels};
+    use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
+
+    use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize};
+
+    #[test]
+    fn test_mouse_to_cell_test() {
+        let mut rng = thread_rng();
+        const ITERATIONS: usize = 10;
+        const PRECISION: usize = 1000;
+
+        for _ in 0..ITERATIONS {
+            let viewport_cells = rng.gen_range(15..20);
+            let cell_size = rng.gen_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32;
+
+            let size = crate::TerminalSize {
+                cell_width: Pixels::from(cell_size),
+                line_height: Pixels::from(cell_size),
+                size: size(
+                    Pixels::from(cell_size * (viewport_cells as f32)),
+                    Pixels::from(cell_size * (viewport_cells as f32)),
+                ),
+            };
+
+            let cells = get_cells(size, &mut rng);
+            let content = convert_cells_to_content(size, &cells);
+
+            for row in 0..(viewport_cells - 1) {
+                let row = row as usize;
+                for col in 0..(viewport_cells - 1) {
+                    let col = col as usize;
+
+                    let row_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
+                    let col_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
+
+                    let mouse_pos = point(
+                        Pixels::from(col as f32 * cell_size + col_offset),
+                        Pixels::from(row as f32 * cell_size + row_offset),
+                    );
+
+                    let content_index = content_index_for_mouse(mouse_pos, &content.size);
+                    let mouse_cell = content.cells[content_index].c;
+                    let real_cell = cells[row][col];
+
+                    assert_eq!(mouse_cell, real_cell);
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn test_mouse_to_cell_clamp() {
+        let mut rng = thread_rng();
+
+        let size = crate::TerminalSize {
+            cell_width: Pixels::from(10.),
+            line_height: Pixels::from(10.),
+            size: size(Pixels::from(100.), Pixels::from(100.)),
+        };
+
+        let cells = get_cells(size, &mut rng);
+        let content = convert_cells_to_content(size, &cells);
+
+        assert_eq!(
+            content.cells[content_index_for_mouse(
+                point(Pixels::from(-10.), Pixels::from(-10.)),
+                &content.size
+            )]
+            .c,
+            cells[0][0]
+        );
+        assert_eq!(
+            content.cells[content_index_for_mouse(
+                point(Pixels::from(1000.), Pixels::from(1000.)),
+                &content.size
+            )]
+            .c,
+            cells[9][9]
+        );
+    }
+
+    fn get_cells(size: TerminalSize, rng: &mut ThreadRng) -> Vec<Vec<char>> {
+        let mut cells = Vec::new();
+
+        for _ in 0..(f32::from(size.height() / size.line_height()) as usize) {
+            let mut row_vec = Vec::new();
+            for _ in 0..(f32::from(size.width() / size.cell_width()) as usize) {
+                let cell_char = rng.sample(Alphanumeric) as char;
+                row_vec.push(cell_char)
+            }
+            cells.push(row_vec)
+        }
+
+        cells
+    }
+
+    fn convert_cells_to_content(size: TerminalSize, cells: &Vec<Vec<char>>) -> TerminalContent {
+        let mut ic = Vec::new();
+
+        for row in 0..cells.len() {
+            for col in 0..cells[row].len() {
+                let cell_char = cells[row][col];
+                ic.push(IndexedCell {
+                    point: AlacPoint::new(Line(row as i32), Column(col)),
+                    cell: Cell {
+                        c: cell_char,
+                        ..Default::default()
+                    },
+                });
+            }
+        }
+
+        TerminalContent {
+            cells: ic,
+            size,
+            ..Default::default()
+        }
+    }
+}

crates/terminal2/src/terminal_settings.rs 🔗

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

crates/theme2/Cargo.toml 🔗

@@ -0,0 +1,36 @@
+[package]
+name = "theme2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[features]
+test-support = [
+    "gpui2/test-support",
+    "fs/test-support",
+    "settings2/test-support"
+]
+
+[lib]
+path = "src/theme2.rs"
+doctest = false
+
+[dependencies]
+gpui2 = { path = "../gpui2" }
+fs = { path = "../fs" }
+schemars.workspace = true
+settings2 = { path = "../settings2" }
+util = { path = "../util" }
+
+anyhow.workspace = true
+indexmap = "1.6.2"
+parking_lot.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+toml.workspace = true
+
+[dev-dependencies]
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }
+settings2 = { path = "../settings2", features = ["test-support"] }

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 🔗

@@ -0,0 +1,84 @@
+use crate::{themes, Theme, ThemeMetadata};
+use anyhow::{anyhow, Result};
+use gpui2::SharedString;
+use std::{collections::HashMap, sync::Arc};
+
+pub struct ThemeRegistry {
+    themes: HashMap<SharedString, Arc<Theme>>,
+}
+
+impl ThemeRegistry {
+    fn insert_themes(&mut self, themes: impl IntoIterator<Item = Theme>) {
+        for theme in themes.into_iter() {
+            self.themes
+                .insert(theme.metadata.name.clone(), Arc::new(theme));
+        }
+    }
+
+    pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
+        self.themes.keys().cloned()
+    }
+
+    pub fn list(&self, _staff: bool) -> impl Iterator<Item = ThemeMetadata> + '_ {
+        self.themes.values().map(|theme| theme.metadata.clone())
+    }
+
+    pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
+        self.themes
+            .get(name)
+            .ok_or_else(|| anyhow!("theme not found: {}", name))
+            .cloned()
+    }
+}
+
+impl Default for ThemeRegistry {
+    fn default() -> Self {
+        let mut this = Self {
+            themes: HashMap::default(),
+        };
+
+        this.insert_themes([
+            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/settings.rs 🔗

@@ -0,0 +1,194 @@
+use crate::{Theme, ThemeRegistry};
+use anyhow::Result;
+use gpui2::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels};
+use schemars::{
+    gen::SchemaGenerator,
+    schema::{InstanceType, Schema, SchemaObject},
+    JsonSchema,
+};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use settings2::{Settings, SettingsJsonSchemaParams};
+use std::sync::Arc;
+use util::ResultExt as _;
+
+const MIN_FONT_SIZE: Pixels = px(6.0);
+const MIN_LINE_HEIGHT: f32 = 1.0;
+
+#[derive(Clone)]
+pub struct ThemeSettings {
+    pub buffer_font: Font,
+    pub buffer_font_size: Pixels,
+    pub buffer_line_height: BufferLineHeight,
+    pub active_theme: Arc<Theme>,
+}
+
+#[derive(Default)]
+pub struct AdjustedBufferFontSize(Option<Pixels>);
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ThemeSettingsContent {
+    #[serde(default)]
+    pub buffer_font_family: Option<String>,
+    #[serde(default)]
+    pub buffer_font_size: Option<f32>,
+    #[serde(default)]
+    pub buffer_line_height: Option<BufferLineHeight>,
+    #[serde(default)]
+    pub buffer_font_features: Option<FontFeatures>,
+    #[serde(default)]
+    pub theme: Option<String>,
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum BufferLineHeight {
+    #[default]
+    Comfortable,
+    Standard,
+    Custom(f32),
+}
+
+impl BufferLineHeight {
+    pub fn value(&self) -> f32 {
+        match self {
+            BufferLineHeight::Comfortable => 1.618,
+            BufferLineHeight::Standard => 1.3,
+            BufferLineHeight::Custom(line_height) => *line_height,
+        }
+    }
+}
+
+impl ThemeSettings {
+    pub fn buffer_font_size(&self, cx: &mut AppContext) -> Pixels {
+        let font_size = *cx
+            .default_global::<AdjustedBufferFontSize>()
+            .0
+            .get_or_insert(self.buffer_font_size.into());
+        font_size.max(MIN_FONT_SIZE)
+    }
+
+    pub fn line_height(&self) -> f32 {
+        f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
+    }
+}
+
+pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
+    if let Some(adjusted_size) = cx.default_global::<AdjustedBufferFontSize>().0 {
+        let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
+        let delta = adjusted_size - buffer_font_size;
+        size + delta
+    } else {
+        size
+    }
+    .max(MIN_FONT_SIZE)
+}
+
+pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
+    let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
+    let adjusted_size = cx
+        .default_global::<AdjustedBufferFontSize>()
+        .0
+        .get_or_insert(buffer_font_size);
+    f(adjusted_size);
+    *adjusted_size = (*adjusted_size).max(MIN_FONT_SIZE - buffer_font_size);
+    cx.refresh();
+}
+
+pub fn reset_font_size(cx: &mut AppContext) {
+    if cx.has_global::<AdjustedBufferFontSize>() {
+        cx.global_mut::<AdjustedBufferFontSize>().0 = None;
+        cx.refresh();
+    }
+}
+
+impl settings2::Settings for ThemeSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = ThemeSettingsContent;
+
+    fn load(
+        defaults: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        cx: &mut AppContext,
+    ) -> Result<Self> {
+        let themes = cx.default_global::<Arc<ThemeRegistry>>();
+
+        let mut this = Self {
+            buffer_font: Font {
+                family: defaults.buffer_font_family.clone().unwrap().into(),
+                features: defaults.buffer_font_features.clone().unwrap(),
+                weight: FontWeight::default(),
+                style: FontStyle::default(),
+            },
+            buffer_font_size: defaults.buffer_font_size.unwrap().into(),
+            buffer_line_height: defaults.buffer_line_height.unwrap(),
+            active_theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
+        };
+
+        for value in user_values.into_iter().copied().cloned() {
+            if let Some(value) = value.buffer_font_family {
+                this.buffer_font.family = value.into();
+            }
+            if let Some(value) = value.buffer_font_features {
+                this.buffer_font.features = value;
+            }
+
+            if let Some(value) = &value.theme {
+                if let Some(theme) = themes.get(value).log_err() {
+                    this.active_theme = theme;
+                }
+            }
+
+            merge(
+                &mut this.buffer_font_size,
+                value.buffer_font_size.map(Into::into),
+            );
+            merge(&mut this.buffer_line_height, value.buffer_line_height);
+        }
+
+        Ok(this)
+    }
+
+    fn json_schema(
+        generator: &mut SchemaGenerator,
+        params: &SettingsJsonSchemaParams,
+        cx: &AppContext,
+    ) -> schemars::schema::RootSchema {
+        let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
+        let theme_names = cx
+            .global::<Arc<ThemeRegistry>>()
+            .list_names(params.staff_mode)
+            .map(|theme_name| Value::String(theme_name.to_string()))
+            .collect();
+
+        let theme_name_schema = SchemaObject {
+            instance_type: Some(InstanceType::String.into()),
+            enum_values: Some(theme_names),
+            ..Default::default()
+        };
+
+        root_schema
+            .definitions
+            .extend([("ThemeName".into(), theme_name_schema.into())]);
+
+        root_schema
+            .schema
+            .object
+            .as_mut()
+            .unwrap()
+            .properties
+            .extend([(
+                "theme".to_owned(),
+                Schema::new_ref("#/definitions/ThemeName".into()),
+            )]);
+
+        root_schema
+    }
+}
+
+fn merge<T: Copy>(target: &mut T, value: Option<T>) {
+    if let Some(value) = value {
+        *target = value;
+    }
+}

crates/theme2/src/theme2.rs 🔗

@@ -0,0 +1,155 @@
+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);
+}
+
+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,
+
+    pub transparent: Hsla,
+    pub mac_os_traffic_light_red: Hsla,
+    pub mac_os_traffic_light_yellow: Hsla,
+    pub mac_os_traffic_light_green: Hsla,
+    pub border: Hsla,
+    pub border_variant: Hsla,
+    pub border_focused: Hsla,
+    pub border_transparent: Hsla,
+    /// The background color of an elevated surface, like a modal, tooltip or toast.
+    pub elevated_surface: Hsla,
+    pub surface: Hsla,
+    /// Window background color of the base app
+    pub background: Hsla,
+    /// Default background for elements like filled buttons,
+    /// text fields, checkboxes, radio buttons, etc.
+    /// - TODO: Map to step 3.
+    pub filled_element: Hsla,
+    /// The background color of a hovered element, like a button being hovered
+    /// with a mouse, or hovered on a touch screen.
+    /// - TODO: Map to step 4.
+    pub filled_element_hover: Hsla,
+    /// The background color of an active element, like a button being pressed,
+    /// or tapped on a touch screen.
+    /// - TODO: Map to step 5.
+    pub filled_element_active: Hsla,
+    /// The background color of a selected element, like a selected tab,
+    /// a button toggled on, or a checkbox that is checked.
+    pub filled_element_selected: Hsla,
+    pub filled_element_disabled: Hsla,
+    pub ghost_element: Hsla,
+    /// The background color of a hovered element with no default background,
+    /// like a ghost-style button or an interactable list item.
+    /// - TODO: Map to step 3.
+    pub ghost_element_hover: Hsla,
+    /// - TODO: Map to step 4.
+    pub ghost_element_active: Hsla,
+    pub ghost_element_selected: Hsla,
+    pub ghost_element_disabled: Hsla,
+    pub text: Hsla,
+    pub text_muted: Hsla,
+    pub text_placeholder: Hsla,
+    pub text_disabled: Hsla,
+    pub text_accent: Hsla,
+    pub icon_muted: Hsla,
+    pub syntax: SyntaxTheme,
+
+    pub status_bar: Hsla,
+    pub title_bar: Hsla,
+    pub toolbar: Hsla,
+    pub tab_bar: Hsla,
+    /// The background of the editor
+    pub editor: Hsla,
+    pub editor_subheader: Hsla,
+    pub editor_active_line: Hsla,
+    pub terminal: Hsla,
+    pub image_fallback_background: Hsla,
+
+    pub git_created: Hsla,
+    pub git_modified: Hsla,
+    pub git_deleted: Hsla,
+    pub git_conflict: Hsla,
+    pub git_ignored: Hsla,
+    pub git_renamed: Hsla,
+
+    pub players: [PlayerTheme; 8],
+}
+
+#[derive(Clone)]
+pub struct SyntaxTheme {
+    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,
+    pub selection: Hsla,
+}
+
+#[derive(Clone)]
+pub struct ThemeMetadata {
+    pub name: SharedString,
+    pub is_light: bool,
+}
+
+pub struct Editor {
+    pub syntax: Arc<SyntaxTheme>,
+}

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 🔗

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

@@ -0,0 +1,131 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn one_dark() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "One 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(0x464b57ff).into(),
+        border_variant: rgba(0x464b57ff).into(),
+        border_focused: rgba(0x293b5bff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x3b414dff).into(),
+        surface: rgba(0x2f343eff).into(),
+        background: rgba(0x3b414dff).into(),
+        filled_element: rgba(0x3b414dff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x18243dff).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(0x18243dff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xc8ccd4ff).into(),
+        text_muted: rgba(0x838994ff).into(),
+        text_placeholder: rgba(0xd07277ff).into(),
+        text_disabled: rgba(0x555a63ff).into(),
+        text_accent: rgba(0x74ade8ff).into(),
+        icon_muted: rgba(0x838994ff).into(),
+        syntax: SyntaxTheme {
+            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(),
+        toolbar: rgba(0x282c33ff).into(),
+        tab_bar: rgba(0x2f343eff).into(),
+        editor: rgba(0x282c33ff).into(),
+        editor_subheader: rgba(0x2f343eff).into(),
+        editor_active_line: rgba(0x2f343eff).into(),
+        terminal: rgba(0x282c33ff).into(),
+        image_fallback_background: rgba(0x3b414dff).into(),
+        git_created: rgba(0xa1c181ff).into(),
+        git_modified: rgba(0x74ade8ff).into(),
+        git_deleted: rgba(0xd07277ff).into(),
+        git_conflict: rgba(0xdec184ff).into(),
+        git_ignored: rgba(0x555a63ff).into(),
+        git_renamed: rgba(0xdec184ff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x74ade8ff).into(),
+                selection: rgba(0x74ade83d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa1c181ff).into(),
+                selection: rgba(0xa1c1813d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbe5046ff).into(),
+                selection: rgba(0xbe50463d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xbf956aff).into(),
+                selection: rgba(0xbf956a3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb477cfff).into(),
+                selection: rgba(0xb477cf3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x6eb4bfff).into(),
+                selection: rgba(0x6eb4bf3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd07277ff).into(),
+                selection: rgba(0xd072773d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xdec184ff).into(),
+                selection: rgba(0xdec1843d).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 🔗

@@ -0,0 +1,132 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn rose_pine() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Rosé Pine".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(0x423f55ff).into(),
+        border_variant: rgba(0x423f55ff).into(),
+        border_focused: rgba(0x435255ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x292738ff).into(),
+        surface: rgba(0x1c1b2aff).into(),
+        background: rgba(0x292738ff).into(),
+        filled_element: rgba(0x292738ff).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(0x74708dff).into(),
+        text_placeholder: rgba(0xea6e92ff).into(),
+        text_disabled: rgba(0x2f2b43ff).into(),
+        text_accent: rgba(0x9bced6ff).into(),
+        icon_muted: rgba(0x74708dff).into(),
+        syntax: SyntaxTheme {
+            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(),
+        toolbar: rgba(0x191724ff).into(),
+        tab_bar: rgba(0x1c1b2aff).into(),
+        editor: rgba(0x191724ff).into(),
+        editor_subheader: rgba(0x1c1b2aff).into(),
+        editor_active_line: rgba(0x1c1b2aff).into(),
+        terminal: rgba(0x191724ff).into(),
+        image_fallback_background: rgba(0x292738ff).into(),
+        git_created: rgba(0x5cc1a3ff).into(),
+        git_modified: rgba(0x9bced6ff).into(),
+        git_deleted: rgba(0xea6e92ff).into(),
+        git_conflict: rgba(0xf5c177ff).into(),
+        git_ignored: rgba(0x2f2b43ff).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(0x9d7591ff).into(),
+                selection: rgba(0x9d75913d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc4a7e6ff).into(),
+                selection: rgba(0xc4a7e63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xc4a7e6ff).into(),
+                selection: rgba(0xc4a7e63d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x31738fff).into(),
+                selection: rgba(0x31738f3d).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 🔗

@@ -0,0 +1,130 @@
+use gpui2::rgba;
+
+use crate::{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata};
+
+pub fn sandcastle() -> Theme {
+    Theme {
+        metadata: ThemeMetadata {
+            name: "Sandcastle".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(0x3d4350ff).into(),
+        border_variant: rgba(0x3d4350ff).into(),
+        border_focused: rgba(0x223131ff).into(),
+        border_transparent: rgba(0x00000000).into(),
+        elevated_surface: rgba(0x333944ff).into(),
+        surface: rgba(0x2b3038ff).into(),
+        background: rgba(0x333944ff).into(),
+        filled_element: rgba(0x333944ff).into(),
+        filled_element_hover: rgba(0xffffff1e).into(),
+        filled_element_active: rgba(0xffffff28).into(),
+        filled_element_selected: rgba(0x171e1eff).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(0x171e1eff).into(),
+        ghost_element_disabled: rgba(0x00000000).into(),
+        text: rgba(0xfdf4c1ff).into(),
+        text_muted: rgba(0xa69782ff).into(),
+        text_placeholder: rgba(0xb3627aff).into(),
+        text_disabled: rgba(0x827568ff).into(),
+        text_accent: rgba(0x518b8bff).into(),
+        icon_muted: rgba(0xa69782ff).into(),
+        syntax: SyntaxTheme {
+            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(),
+        toolbar: rgba(0x282c33ff).into(),
+        tab_bar: rgba(0x2b3038ff).into(),
+        editor: rgba(0x282c33ff).into(),
+        editor_subheader: rgba(0x2b3038ff).into(),
+        editor_active_line: rgba(0x2b3038ff).into(),
+        terminal: rgba(0x282c33ff).into(),
+        image_fallback_background: rgba(0x333944ff).into(),
+        git_created: rgba(0x83a598ff).into(),
+        git_modified: rgba(0x518b8bff).into(),
+        git_deleted: rgba(0xb3627aff).into(),
+        git_conflict: rgba(0xa07d3aff).into(),
+        git_ignored: rgba(0x827568ff).into(),
+        git_renamed: rgba(0xa07d3aff).into(),
+        players: [
+            PlayerTheme {
+                cursor: rgba(0x518b8bff).into(),
+                selection: rgba(0x518b8b3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x83a598ff).into(),
+                selection: rgba(0x83a5983d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa87222ff).into(),
+                selection: rgba(0xa872223d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa07d3aff).into(),
+                selection: rgba(0xa07d3a3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xd75f5fff).into(),
+                selection: rgba(0xd75f5f3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0x83a598ff).into(),
+                selection: rgba(0x83a5983d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xb3627aff).into(),
+                selection: rgba(0xb3627a3d).into(),
+            },
+            PlayerTheme {
+                cursor: rgba(0xa07d3aff).into(),
+                selection: rgba(0xa07d3a3d).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 🔗

@@ -0,0 +1,18 @@
+[package]
+name = "theme_converter"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[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
+serde.workspace = true
+simplelog = "0.9"
+theme2 = { path = "../theme2" }

crates/theme_converter/src/main.rs 🔗

@@ -0,0 +1,390 @@
+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 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};
+
+use crate::theme_printer::ThemePrinter;
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    /// The name of the theme to convert.
+    theme: String,
+}
+
+fn main() -> Result<()> {
+    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+
+    // 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 mut output_file = File::create(themes_path.join(format!("{theme_slug}.rs")))?;
+
+        let theme_module = format!(
+            r#"
+                use gpui2::rgba;
+
+                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(())
+}
+
+#[derive(RustEmbed)]
+#[folder = "../../assets"]
+#[include = "fonts/**/*"]
+#[include = "icons/**/*"]
+#[include = "themes/**/*"]
+#[include = "sounds/**/*"]
+#[include = "*.md"]
+#[exclude = "*.DS_Store"]
+pub struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, path: &str) -> Result<Cow<[u8]>> {
+        Self::get(path)
+            .map(|f| f.data)
+            .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
+    }
+
+    fn list(&self, path: &str) -> Result<Vec<SharedString>> {
+        Ok(Self::iter()
+            .filter(|p| p.starts_with(path))
+            .map(SharedString::from)
+            .collect())
+    }
+}
+
+#[derive(Clone, Copy)]
+pub struct PlayerThemeColors {
+    pub cursor: Hsla,
+    pub selection: Hsla,
+}
+
+impl PlayerThemeColors {
+    pub fn new(theme: &LegacyTheme, ix: usize) -> Self {
+        if ix < theme.players.len() {
+            Self {
+                cursor: theme.players[ix].cursor,
+                selection: theme.players[ix].selection,
+            }
+        } else {
+            Self {
+                cursor: rgb::<Hsla>(0xff00ff),
+                selection: rgb::<Hsla>(0xff00ff),
+            }
+        }
+    }
+}
+
+impl From<PlayerThemeColors> for PlayerTheme {
+    fn from(value: PlayerThemeColors) -> Self {
+        Self {
+            cursor: value.cursor,
+            selection: value.selection,
+        }
+    }
+}
+
+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(&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: 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: 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: 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: 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: legacy_theme.lowest.accent.default.background,
+        ghost_element_disabled: transparent,
+        text: legacy_theme.lowest.base.default.foreground,
+        text_muted: legacy_theme.lowest.variant.default.foreground,
+        /// TODO: map this to a real value
+        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,
+    };
+
+    Ok(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.
+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")?;
+
+    let legacy_theme: LegacyTheme = serde_json::from_value(json_theme.base_theme.clone())
+        .context("failed to parse `base_theme`")?;
+
+    Ok((json_theme, legacy_theme))
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct LegacyTheme {
+    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)
+}

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 🔗

@@ -0,0 +1,20 @@
+[package]
+name = "ui2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[dependencies]
+anyhow.workspace = true
+chrono = "0.4"
+gpui2 = { path = "../gpui2" }
+itertools = { version = "0.11.0", optional = true }
+serde.workspace = true
+smallvec.workspace = true
+strum = { version = "0.25.0", features = ["derive"] }
+theme2 = { path = "../theme2" }
+rand = "0.8"
+
+[features]
+default = ["stories"]
+stories = ["dep:itertools"]

crates/ui2/docs/elevation.md 🔗

@@ -0,0 +1,57 @@
+# Elevation
+
+Elevation in Zed applies to all surfaces and components. Elevation is categorized into levels.
+
+Elevation accomplishes the following:
+- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars.
+- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards.
+- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces.
+
+Elevations are the initial elevation values assigned to components by default.
+
+Components may transition to a higher elevation in some cases, like user interations.
+
+On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest.
+
+## Understanding Elevation
+
+Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations.
+
+Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview)
+
+## Elevation Levels
+
+Zed integrates six unique elevation levels in its design system. The elevation of a surface is expressed as a whole number ranging from 0 to 5, both numbers inclusive. A component’s elevation is ascertained by combining the component’s resting elevation with any dynamic elevation offsets.
+
+The levels are detailed as follows:
+
+0. App Background
+1. UI Surface
+2. Elevated Elements
+3. Wash
+4. Focused Element
+5. Dragged Element
+
+### 0. App Background
+
+The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app.
+
+### 1. UI Surface
+
+The UI Surface is the standard elevation for components and is placed above the app background. It is generally used for the background color of the app bar, card, and sheet.
+
+### 2. Elevated Elements
+
+Elevated elements appear above the UI surface layer surfaces and components. Elevated elements are predominantly used for creating popovers, context menus, and tooltips.
+
+### 3. Wash
+
+Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design.
+
+### 4. Focused Element
+
+Focused elements obtain a higher elevation above surfaces and components at wash elevation. They are often used for modals, notifications, and overlaid panels and indicate that they are the sole element the user is interacting with at the moment.
+
+### 5. Dragged Element
+
+Dragged elements gain the highest elevation, thus appearing above surfaces and components at the elevation of focused elements. These are typically used for elements that are being dragged, following the cursor

crates/ui2/src/components.rs 🔗

@@ -0,0 +1,71 @@
+mod assistant_panel;
+mod breadcrumb;
+mod buffer;
+mod buffer_search;
+mod chat_panel;
+mod collab_panel;
+mod command_palette;
+mod context_menu;
+mod copilot;
+mod editor_pane;
+mod facepile;
+mod icon_button;
+mod keybinding;
+mod language_selector;
+mod list;
+mod modal;
+mod multi_buffer;
+mod notification_toast;
+mod notifications_panel;
+mod palette;
+mod panel;
+mod panes;
+mod player_stack;
+mod project_panel;
+mod recent_projects;
+mod status_bar;
+mod tab;
+mod tab_bar;
+mod terminal;
+mod theme_selector;
+mod title_bar;
+mod toast;
+mod toolbar;
+mod traffic_lights;
+mod workspace;
+
+pub use assistant_panel::*;
+pub use breadcrumb::*;
+pub use buffer::*;
+pub use buffer_search::*;
+pub use chat_panel::*;
+pub use collab_panel::*;
+pub use command_palette::*;
+pub use context_menu::*;
+pub use copilot::*;
+pub use editor_pane::*;
+pub use facepile::*;
+pub use icon_button::*;
+pub use keybinding::*;
+pub use language_selector::*;
+pub use list::*;
+pub use modal::*;
+pub use multi_buffer::*;
+pub use notification_toast::*;
+pub use notifications_panel::*;
+pub use palette::*;
+pub use panel::*;
+pub use panes::*;
+pub use player_stack::*;
+pub use project_panel::*;
+pub use recent_projects::*;
+pub use status_bar::*;
+pub use tab::*;
+pub use tab_bar::*;
+pub use terminal::*;
+pub use theme_selector::*;
+pub use title_bar::*;
+pub use toast::*;
+pub use toolbar::*;
+pub use traffic_lights::*;
+pub use workspace::*;

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

@@ -0,0 +1,93 @@
+use crate::prelude::*;
+use crate::{Icon, IconButton, Label, Panel, PanelSide};
+use gpui2::{rems, AbsoluteLength};
+
+#[derive(Component)]
+pub struct AssistantPanel {
+    id: ElementId,
+    current_side: PanelSide,
+}
+
+impl AssistantPanel {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            current_side: PanelSide::default(),
+        }
+    }
+
+    pub fn side(mut self, side: PanelSide) -> Self {
+        self.current_side = side;
+        self
+    }
+
+    fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        Panel::new(self.id.clone(), cx)
+            .children(vec![div()
+                .flex()
+                .flex_col()
+                .h_full()
+                .px_2()
+                .gap_2()
+                // Header
+                .child(
+                    div()
+                        .flex()
+                        .justify_between()
+                        .gap_2()
+                        .child(
+                            div()
+                                .flex()
+                                .child(IconButton::new("menu", Icon::Menu))
+                                .child(Label::new("New Conversation")),
+                        )
+                        .child(
+                            div()
+                                .flex()
+                                .items_center()
+                                .gap_px()
+                                .child(IconButton::new("split_message", Icon::SplitMessage))
+                                .child(IconButton::new("quote", Icon::Quote))
+                                .child(IconButton::new("magic_wand", Icon::MagicWand))
+                                .child(IconButton::new("plus", Icon::Plus))
+                                .child(IconButton::new("maximize", Icon::Maximize)),
+                        ),
+                )
+                // Chat Body
+                .child(
+                    div()
+                        .id("chat-body")
+                        .w_full()
+                        .flex()
+                        .flex_col()
+                        .gap_3()
+                        .overflow_y_scroll()
+                        .child(Label::new("Is this thing on?")),
+                )
+                .render()])
+            .side(self.current_side)
+            .width(AbsoluteLength::Rems(rems(32.)))
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+    pub struct AssistantPanelStory;
+
+    impl Render for AssistantPanelStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, AssistantPanel>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(AssistantPanel::new("assistant-panel"))
+        }
+    }
+}

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

@@ -0,0 +1,119 @@
+use std::path::PathBuf;
+
+use crate::prelude::*;
+use crate::{h_stack, HighlightedText};
+use gpui2::Div;
+
+#[derive(Clone)]
+pub struct Symbol(pub Vec<HighlightedText>);
+
+#[derive(Component)]
+pub struct Breadcrumb {
+    path: PathBuf,
+    symbols: Vec<Symbol>,
+}
+
+impl Breadcrumb {
+    pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
+        Self { path, symbols }
+    }
+
+    fn render_separator<V: 'static>(&self, cx: &WindowContext) -> Div<V> {
+        let theme = theme(cx);
+
+        div().child(" › ").text_color(theme.text_muted)
+    }
+
+    fn render<V: 'static>(self, view_state: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let symbols_len = self.symbols.len();
+
+        h_stack()
+            .id("breadcrumb")
+            .px_1()
+            .text_sm()
+            .text_color(theme.text_muted)
+            .rounded_md()
+            .hover(|style| style.bg(theme.ghost_element_hover))
+            .active(|style| style.bg(theme.ghost_element_active))
+            .child(self.path.clone().to_str().unwrap().to_string())
+            .child(if !self.symbols.is_empty() {
+                self.render_separator(cx)
+            } else {
+                div()
+            })
+            .child(
+                div().flex().children(
+                    self.symbols
+                        .iter()
+                        .enumerate()
+                        // TODO: Could use something like `intersperse` here instead.
+                        .flat_map(|(ix, symbol)| {
+                            let mut items =
+                                vec![div().flex().children(symbol.0.iter().map(|segment| {
+                                    div().child(segment.text.clone()).text_color(segment.color)
+                                }))];
+
+                            let is_last_segment = ix == symbols_len - 1;
+                            if !is_last_segment {
+                                items.push(self.render_separator(cx));
+                            }
+
+                            items
+                        })
+                        .collect::<Vec<_>>(),
+                ),
+            )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::Render;
+    use std::str::FromStr;
+
+    pub struct BreadcrumbStory;
+
+    impl Render for BreadcrumbStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            let theme = theme(cx);
+
+            Story::container(cx)
+                .child(Story::title_for::<_, Breadcrumb>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(Breadcrumb::new(
+                    PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
+                    vec![
+                        Symbol(vec![
+                            HighlightedText {
+                                text: "impl ".to_string(),
+                                color: theme.syntax.color("keyword"),
+                            },
+                            HighlightedText {
+                                text: "BreadcrumbStory".to_string(),
+                                color: theme.syntax.color("function"),
+                            },
+                        ]),
+                        Symbol(vec![
+                            HighlightedText {
+                                text: "fn ".to_string(),
+                                color: theme.syntax.color("keyword"),
+                            },
+                            HighlightedText {
+                                text: "render".to_string(),
+                                color: theme.syntax.color("function"),
+                            },
+                        ]),
+                    ],
+                ))
+        }
+    }
+}

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

@@ -0,0 +1,271 @@
+use gpui2::{Hsla, WindowContext};
+
+use crate::prelude::*;
+use crate::{h_stack, v_stack, Icon, IconElement};
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub struct PlayerCursor {
+    color: Hsla,
+    index: usize,
+}
+
+#[derive(Default, PartialEq, Clone)]
+pub struct HighlightedText {
+    pub text: String,
+    pub color: Hsla,
+}
+
+#[derive(Default, PartialEq, Clone)]
+pub struct HighlightedLine {
+    pub highlighted_texts: Vec<HighlightedText>,
+}
+
+#[derive(Default, PartialEq, Clone)]
+pub struct BufferRow {
+    pub line_number: usize,
+    pub code_action: bool,
+    pub current: bool,
+    pub line: Option<HighlightedLine>,
+    pub cursors: Option<Vec<PlayerCursor>>,
+    pub status: GitStatus,
+    pub show_line_number: bool,
+}
+
+#[derive(Clone)]
+pub struct BufferRows {
+    pub show_line_numbers: bool,
+    pub rows: Vec<BufferRow>,
+}
+
+impl Default for BufferRows {
+    fn default() -> Self {
+        Self {
+            show_line_numbers: true,
+            rows: vec![BufferRow {
+                line_number: 1,
+                code_action: false,
+                current: true,
+                line: None,
+                cursors: None,
+                status: GitStatus::None,
+                show_line_number: true,
+            }],
+        }
+    }
+}
+
+impl BufferRow {
+    pub fn new(line_number: usize) -> Self {
+        Self {
+            line_number,
+            code_action: false,
+            current: false,
+            line: None,
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number: true,
+        }
+    }
+
+    pub fn set_line(mut self, line: Option<HighlightedLine>) -> Self {
+        self.line = line;
+        self
+    }
+
+    pub fn set_cursors(mut self, cursors: Option<Vec<PlayerCursor>>) -> Self {
+        self.cursors = cursors;
+        self
+    }
+
+    pub fn add_cursor(mut self, cursor: PlayerCursor) -> Self {
+        if let Some(cursors) = &mut self.cursors {
+            cursors.push(cursor);
+        } else {
+            self.cursors = Some(vec![cursor]);
+        }
+        self
+    }
+
+    pub fn set_status(mut self, status: GitStatus) -> Self {
+        self.status = status;
+        self
+    }
+
+    pub fn set_show_line_number(mut self, show_line_number: bool) -> Self {
+        self.show_line_number = show_line_number;
+        self
+    }
+
+    pub fn set_code_action(mut self, code_action: bool) -> Self {
+        self.code_action = code_action;
+        self
+    }
+
+    pub fn set_current(mut self, current: bool) -> Self {
+        self.current = current;
+        self
+    }
+}
+
+#[derive(Component, Clone)]
+pub struct Buffer {
+    id: ElementId,
+    rows: Option<BufferRows>,
+    readonly: bool,
+    language: Option<String>,
+    title: Option<String>,
+    path: Option<String>,
+}
+
+impl Buffer {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            rows: Some(BufferRows::default()),
+            readonly: false,
+            language: None,
+            title: Some("untitled".to_string()),
+            path: None,
+        }
+    }
+
+    pub fn set_title<T: Into<Option<String>>>(mut self, title: T) -> Self {
+        self.title = title.into();
+        self
+    }
+
+    pub fn set_path<P: Into<Option<String>>>(mut self, path: P) -> Self {
+        self.path = path.into();
+        self
+    }
+
+    pub fn set_readonly(mut self, readonly: bool) -> Self {
+        self.readonly = readonly;
+        self
+    }
+
+    pub fn set_rows<R: Into<Option<BufferRows>>>(mut self, rows: R) -> Self {
+        self.rows = rows.into();
+        self
+    }
+
+    pub fn set_language<L: Into<Option<String>>>(mut self, language: L) -> Self {
+        self.language = language.into();
+        self
+    }
+
+    fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let line_background = if row.current {
+            theme.editor_active_line
+        } else {
+            theme.transparent
+        };
+
+        let line_number_color = if row.current {
+            theme.text
+        } else {
+            theme.syntax.get("comment").color.unwrap_or_default()
+        };
+
+        h_stack()
+            .bg(line_background)
+            .w_full()
+            .gap_2()
+            .px_1()
+            .child(
+                h_stack()
+                    .w_4()
+                    .h_full()
+                    .px_0p5()
+                    .when(row.code_action, |c| {
+                        div().child(IconElement::new(Icon::Bolt))
+                    }),
+            )
+            .when(row.show_line_number, |this| {
+                this.child(
+                    h_stack().justify_end().px_0p5().w_3().child(
+                        div()
+                            .text_color(line_number_color)
+                            .child(row.line_number.to_string()),
+                    ),
+                )
+            })
+            .child(div().mx_0p5().w_1().h_full().bg(row.status.hsla(cx)))
+            .children(row.line.map(|line| {
+                div()
+                    .flex()
+                    .children(line.highlighted_texts.iter().map(|highlighted_text| {
+                        div()
+                            .text_color(highlighted_text.color)
+                            .child(highlighted_text.text.clone())
+                    }))
+            }))
+    }
+
+    fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl Component<V>> {
+        match &self.rows {
+            Some(rows) => rows
+                .rows
+                .iter()
+                .map(|row| Self::render_row(row.clone(), cx))
+                .collect(),
+            None => vec![],
+        }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+        let rows = self.render_rows(cx);
+
+        v_stack()
+            .flex_1()
+            .w_full()
+            .h_full()
+            .bg(theme.editor)
+            .children(rows)
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    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};
+
+    pub struct BufferStory;
+
+    impl Render for BufferStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            let theme = theme(cx);
+
+            Story::container(cx)
+                .child(Story::title_for::<_, Buffer>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
+                .child(Story::label(cx, "Hello World (Rust)"))
+                .child(
+                    div()
+                        .w(rems(64.))
+                        .h_96()
+                        .child(hello_world_rust_buffer_example(&theme)),
+                )
+                .child(Story::label(cx, "Hello World (Rust) with Status"))
+                .child(
+                    div()
+                        .w(rems(64.))
+                        .h_96()
+                        .child(hello_world_rust_buffer_with_status_example(&theme)),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,45 @@
+use gpui2::{Div, Render, View, VisualContext};
+
+use crate::prelude::*;
+use crate::{h_stack, Icon, IconButton, IconColor, Input};
+
+#[derive(Clone)]
+pub struct BufferSearch {
+    is_replace_open: bool,
+}
+
+impl BufferSearch {
+    pub fn new() -> Self {
+        Self {
+            is_replace_open: false,
+        }
+    }
+
+    fn toggle_replace(&mut self, cx: &mut ViewContext<Self>) {
+        self.is_replace_open = !self.is_replace_open;
+
+        cx.notify();
+    }
+
+    pub fn view(cx: &mut WindowContext) -> View<Self> {
+        cx.build_view(|cx| Self::new())
+    }
+}
+
+impl Render for BufferSearch {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
+        let theme = theme(cx);
+
+        h_stack().bg(theme.toolbar).p_2().child(
+            h_stack().child(Input::new("Search")).child(
+                IconButton::<Self>::new("replace", Icon::Replace)
+                    .when(self.is_replace_open, |this| this.color(IconColor::Accent))
+                    .on_click(|buffer_search, cx| {
+                        buffer_search.toggle_replace(cx);
+                    }),
+            ),
+        )
+    }
+}

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

@@ -0,0 +1,151 @@
+use chrono::NaiveDateTime;
+
+use crate::prelude::*;
+use crate::{Icon, IconButton, Input, Label, LabelColor};
+
+#[derive(Component)]
+pub struct ChatPanel {
+    element_id: ElementId,
+    messages: Vec<ChatMessage>,
+}
+
+impl ChatPanel {
+    pub fn new(element_id: impl Into<ElementId>) -> Self {
+        Self {
+            element_id: element_id.into(),
+            messages: Vec::new(),
+        }
+    }
+
+    pub fn messages(mut self, messages: Vec<ChatMessage>) -> Self {
+        self.messages = messages;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div()
+            .id(self.element_id.clone())
+            .flex()
+            .flex_col()
+            .justify_between()
+            .h_full()
+            .px_2()
+            .gap_2()
+            // Header
+            .child(
+                div()
+                    .flex()
+                    .justify_between()
+                    .py_2()
+                    .child(div().flex().child(Label::new("#design")))
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(IconButton::new("file", Icon::File))
+                            .child(IconButton::new("audio_on", Icon::AudioOn)),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    // Chat Body
+                    .child(
+                        div()
+                            .id("chat-body")
+                            .w_full()
+                            .flex()
+                            .flex_col()
+                            .gap_3()
+                            .overflow_y_scroll()
+                            .children(self.messages),
+                    )
+                    // Composer
+                    .child(div().flex().my_2().child(Input::new("Message #design"))),
+            )
+    }
+}
+
+#[derive(Component)]
+pub struct ChatMessage {
+    author: String,
+    text: String,
+    sent_at: NaiveDateTime,
+}
+
+impl ChatMessage {
+    pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self {
+        Self {
+            author,
+            text,
+            sent_at,
+        }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div()
+            .flex()
+            .flex_col()
+            .child(
+                div()
+                    .flex()
+                    .gap_2()
+                    .child(Label::new(self.author.clone()))
+                    .child(
+                        Label::new(self.sent_at.format("%m/%d/%Y").to_string())
+                            .color(LabelColor::Muted),
+                    ),
+            )
+            .child(div().child(Label::new(self.text.clone())))
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use chrono::DateTime;
+    use gpui2::{Div, Render};
+
+    use crate::{Panel, Story};
+
+    use super::*;
+
+    pub struct ChatPanelStory;
+
+    impl Render for ChatPanelStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, ChatPanel>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(
+                    Panel::new("chat-panel-1-outer", cx)
+                        .child(ChatPanel::new("chat-panel-1-inner")),
+                )
+                .child(Story::label(cx, "With Mesages"))
+                .child(Panel::new("chat-panel-2-outer", cx).child(
+                    ChatPanel::new("chat-panel-2-inner").messages(vec![
+                        ChatMessage::new(
+                            "osiewicz".to_string(),
+                            "is this thing on?".to_string(),
+                            DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
+                                .unwrap()
+                                .naive_local(),
+                        ),
+                        ChatMessage::new(
+                            "maxdeviant".to_string(),
+                            "Reading you loud and clear!".to_string(),
+                            DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
+                                .unwrap()
+                                .naive_local(),
+                        ),
+                    ]),
+                ))
+        }
+    }
+}

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

@@ -0,0 +1,108 @@
+use crate::prelude::*;
+use crate::{
+    static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List,
+    ListHeader, ToggleState,
+};
+
+#[derive(Component)]
+pub struct CollabPanel {
+    id: ElementId,
+}
+
+impl CollabPanel {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self { id: id.into() }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .id(self.id.clone())
+            .h_full()
+            .bg(theme.surface)
+            .child(
+                v_stack()
+                    .id("crdb")
+                    .w_full()
+                    .overflow_y_scroll()
+                    .child(
+                        div().pb_1().border_color(theme.border).border_b().child(
+                            List::new(static_collab_panel_current_call())
+                                .header(
+                                    ListHeader::new("CRDB")
+                                        .left_icon(Icon::Hash.into())
+                                        .toggle(ToggleState::Toggled),
+                                )
+                                .toggle(ToggleState::Toggled),
+                        ),
+                    )
+                    .child(
+                        v_stack().id("channels").py_1().child(
+                            List::new(static_collab_panel_channels())
+                                .header(ListHeader::new("CHANNELS").toggle(ToggleState::Toggled))
+                                .empty_message("No channels yet. Add a channel to get started.")
+                                .toggle(ToggleState::Toggled),
+                        ),
+                    )
+                    .child(
+                        v_stack().id("contacts-online").py_1().child(
+                            List::new(static_collab_panel_current_call())
+                                .header(
+                                    ListHeader::new("CONTACTS – ONLINE")
+                                        .toggle(ToggleState::Toggled),
+                                )
+                                .toggle(ToggleState::Toggled),
+                        ),
+                    )
+                    .child(
+                        v_stack().id("contacts-offline").py_1().child(
+                            List::new(static_collab_panel_current_call())
+                                .header(
+                                    ListHeader::new("CONTACTS – OFFLINE")
+                                        .toggle(ToggleState::NotToggled),
+                                )
+                                .toggle(ToggleState::NotToggled),
+                        ),
+                    ),
+            )
+            .child(
+                div()
+                    .h_7()
+                    .px_2()
+                    .border_t()
+                    .border_color(theme.border)
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .text_sm()
+                            .text_color(theme.text_placeholder)
+                            .child("Find..."),
+                    ),
+            )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+
+    pub struct CollabPanelStory;
+
+    impl Render for CollabPanelStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, CollabPanel>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(CollabPanel::new("collab-panel"))
+        }
+    }
+}

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

@@ -0,0 +1,48 @@
+use crate::prelude::*;
+use crate::{example_editor_actions, OrderMethod, Palette};
+
+#[derive(Component)]
+pub struct CommandPalette {
+    id: ElementId,
+}
+
+impl CommandPalette {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self { id: id.into() }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div().id(self.id.clone()).child(
+            Palette::new("palette")
+                .items(example_editor_actions())
+                .placeholder("Execute a command...")
+                .empty_string("No items found.")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use gpui2::{Div, Render};
+
+    use crate::Story;
+
+    use super::*;
+
+    pub struct CommandPaletteStory;
+
+    impl Render for CommandPaletteStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, CommandPalette>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(CommandPalette::new("command-palette"))
+        }
+    }
+}

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

@@ -0,0 +1,91 @@
+use crate::{prelude::*, ListItemVariant};
+use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
+
+pub enum ContextMenuItem {
+    Header(SharedString),
+    Entry(Label),
+    Separator,
+}
+
+impl ContextMenuItem {
+    fn to_list_item<V: 'static>(self) -> ListItem<V> {
+        match self {
+            ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
+            ContextMenuItem::Entry(label) => {
+                ListEntry::new(label).variant(ListItemVariant::Inset).into()
+            }
+            ContextMenuItem::Separator => ListSeparator::new().into(),
+        }
+    }
+
+    pub fn header(label: impl Into<SharedString>) -> Self {
+        Self::Header(label.into())
+    }
+
+    pub fn separator() -> Self {
+        Self::Separator
+    }
+
+    pub fn entry(label: Label) -> Self {
+        Self::Entry(label)
+    }
+}
+
+#[derive(Component)]
+pub struct ContextMenu {
+    items: Vec<ContextMenuItem>,
+}
+
+impl ContextMenu {
+    pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
+        Self {
+            items: items.into_iter().collect(),
+        }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .flex()
+            .bg(theme.elevated_surface)
+            .border()
+            .border_color(theme.border)
+            .child(
+                List::new(
+                    self.items
+                        .into_iter()
+                        .map(ContextMenuItem::to_list_item)
+                        .collect(),
+                )
+                .toggle(ToggleState::Toggled),
+            )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::story::Story;
+    use gpui2::{Div, Render};
+
+    pub struct ContextMenuStory;
+
+    impl Render for ContextMenuStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, ContextMenu>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(ContextMenu::new([
+                    ContextMenuItem::header("Section header"),
+                    ContextMenuItem::Separator,
+                    ContextMenuItem::entry(Label::new("Some entry")),
+                ]))
+        }
+    }
+}

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

@@ -0,0 +1,46 @@
+use crate::{prelude::*, Button, Label, LabelColor, Modal};
+
+#[derive(Component)]
+pub struct CopilotModal {
+    id: ElementId,
+}
+
+impl CopilotModal {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self { id: id.into() }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div().id(self.id.clone()).child(
+            Modal::new("some-id")
+                .title("Connect Copilot to Zed")
+                .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(LabelColor::Muted))
+                .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)),
+        )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use gpui2::{Div, Render};
+
+    use crate::Story;
+
+    use super::*;
+
+    pub struct CopilotModalStory;
+
+    impl Render for CopilotModalStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, CopilotModal>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(CopilotModal::new("copilot-modal"))
+        }
+    }
+}

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

@@ -0,0 +1,77 @@
+use std::path::PathBuf;
+
+use gpui2::{Div, Render, View, VisualContext};
+
+use crate::prelude::*;
+use crate::{
+    hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon,
+    IconButton, IconColor, Symbol, Tab, TabBar, Toolbar,
+};
+
+#[derive(Clone)]
+pub struct EditorPane {
+    tabs: Vec<Tab>,
+    path: PathBuf,
+    symbols: Vec<Symbol>,
+    buffer: Buffer,
+    buffer_search: View<BufferSearch>,
+    is_buffer_search_open: bool,
+}
+
+impl EditorPane {
+    pub fn new(
+        cx: &mut ViewContext<Self>,
+        tabs: Vec<Tab>,
+        path: PathBuf,
+        symbols: Vec<Symbol>,
+        buffer: Buffer,
+    ) -> Self {
+        Self {
+            tabs,
+            path,
+            symbols,
+            buffer,
+            buffer_search: BufferSearch::view(cx),
+            is_buffer_search_open: false,
+        }
+    }
+
+    pub fn toggle_buffer_search(&mut self, cx: &mut ViewContext<Self>) {
+        self.is_buffer_search_open = !self.is_buffer_search_open;
+
+        cx.notify();
+    }
+
+    pub fn view(cx: &mut WindowContext) -> View<Self> {
+        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>) -> Div<Self> {
+        v_stack()
+            .w_full()
+            .h_full()
+            .flex_1()
+            .child(TabBar::new("editor-pane-tabs", self.tabs.clone()).can_navigate((false, true)))
+            .child(
+                Toolbar::new()
+                    .left_item(Breadcrumb::new(self.path.clone(), self.symbols.clone()))
+                    .right_items(vec![
+                        IconButton::new("toggle_inlay_hints", Icon::InlayHint),
+                        IconButton::<Self>::new("buffer_search", Icon::MagnifyingGlass)
+                            .when(self.is_buffer_search_open, |this| {
+                                this.color(IconColor::Accent)
+                            })
+                            .on_click(|editor, cx| {
+                                editor.toggle_buffer_search(cx);
+                            }),
+                        IconButton::new("inline_assist", Icon::MagicWand),
+                    ]),
+            )
+            .children(Some(self.buffer_search.clone()).filter(|_| self.is_buffer_search_open))
+            .child(self.buffer.clone())
+    }
+}

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

@@ -0,0 +1,59 @@
+use crate::prelude::*;
+use crate::{Avatar, Player};
+
+#[derive(Component)]
+pub struct Facepile {
+    players: Vec<Player>,
+}
+
+impl Facepile {
+    pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
+        Self {
+            players: players.collect(),
+        }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let player_count = self.players.len();
+        let player_list = self.players.iter().enumerate().map(|(ix, player)| {
+            let isnt_last = ix < player_count - 1;
+
+            div()
+                .when(isnt_last, |div| div.neg_mr_1())
+                .child(Avatar::new(player.avatar_src().to_string()))
+        });
+        div().p_1().flex().items_center().children(player_list)
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::{static_players, Story};
+    use gpui2::{Div, Render};
+
+    pub struct FacepileStory;
+
+    impl Render for FacepileStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            let players = static_players();
+
+            Story::container(cx)
+                .child(Story::title_for::<_, Facepile>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(
+                    div()
+                        .flex()
+                        .gap_3()
+                        .child(Facepile::new(players.clone().into_iter().take(1)))
+                        .child(Facepile::new(players.clone().into_iter().take(2)))
+                        .child(Facepile::new(players.clone().into_iter().take(3))),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,108 @@
+use std::sync::Arc;
+
+use gpui2::MouseButton;
+
+use crate::{h_stack, prelude::*};
+use crate::{ClickHandler, Icon, IconColor, IconElement};
+
+struct IconButtonHandlers<V: 'static> {
+    click: Option<ClickHandler<V>>,
+}
+
+impl<V: 'static> Default for IconButtonHandlers<V> {
+    fn default() -> Self {
+        Self { click: None }
+    }
+}
+
+#[derive(Component)]
+pub struct IconButton<V: 'static> {
+    id: ElementId,
+    icon: Icon,
+    color: IconColor,
+    variant: ButtonVariant,
+    state: InteractionState,
+    handlers: IconButtonHandlers<V>,
+}
+
+impl<V: 'static> IconButton<V> {
+    pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
+        Self {
+            id: id.into(),
+            icon,
+            color: IconColor::default(),
+            variant: ButtonVariant::default(),
+            state: InteractionState::default(),
+            handlers: IconButtonHandlers::default(),
+        }
+    }
+
+    pub fn icon(mut self, icon: Icon) -> Self {
+        self.icon = icon;
+        self
+    }
+
+    pub fn color(mut self, color: IconColor) -> Self {
+        self.color = color;
+        self
+    }
+
+    pub fn variant(mut self, variant: ButtonVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    pub fn on_click(
+        mut self,
+        handler: impl 'static + Fn(&mut V, &mut ViewContext<V>) + Send + Sync,
+    ) -> Self {
+        self.handlers.click = Some(Arc::new(handler));
+        self
+    }
+
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let icon_color = match (self.state, self.color) {
+            (InteractionState::Disabled, _) => IconColor::Disabled,
+            _ => self.color,
+        };
+
+        let (bg_color, bg_hover_color, bg_active_color) = match self.variant {
+            ButtonVariant::Filled => (
+                theme.filled_element,
+                theme.filled_element_hover,
+                theme.filled_element_active,
+            ),
+            ButtonVariant::Ghost => (
+                theme.ghost_element,
+                theme.ghost_element_hover,
+                theme.ghost_element_active,
+            ),
+        };
+
+        let mut button = h_stack()
+            .id(self.id.clone())
+            .justify_center()
+            .rounded_md()
+            .py(ui_size(cx, 0.25))
+            .px(ui_size(cx, 6. / 14.))
+            .bg(bg_color)
+            .hover(|style| style.bg(bg_hover_color))
+            .active(|style| style.bg(bg_active_color))
+            .child(IconElement::new(self.icon).color(icon_color));
+
+        if let Some(click_handler) = self.handlers.click.clone() {
+            button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
+                click_handler(state, cx);
+            });
+        }
+
+        button
+    }
+}

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

@@ -0,0 +1,224 @@
+use std::collections::HashSet;
+
+use strum::{EnumIter, IntoEnumIterator};
+
+use crate::prelude::*;
+
+#[derive(Component)]
+pub struct Keybinding {
+    /// A keybinding consists of a key and a set of modifier keys.
+    /// More then one keybinding produces a chord.
+    ///
+    /// This should always contain at least one element.
+    keybinding: Vec<(String, ModifierKeys)>,
+}
+
+impl Keybinding {
+    pub fn new(key: String, modifiers: ModifierKeys) -> Self {
+        Self {
+            keybinding: vec![(key, modifiers)],
+        }
+    }
+
+    pub fn new_chord(
+        first_note: (String, ModifierKeys),
+        second_note: (String, ModifierKeys),
+    ) -> Self {
+        Self {
+            keybinding: vec![first_note, second_note],
+        }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div()
+            .flex()
+            .gap_2()
+            .children(self.keybinding.iter().map(|(key, modifiers)| {
+                div()
+                    .flex()
+                    .gap_1()
+                    .children(ModifierKey::iter().filter_map(|modifier| {
+                        if modifiers.0.contains(&modifier) {
+                            Some(Key::new(modifier.glyph().to_string()))
+                        } else {
+                            None
+                        }
+                    }))
+                    .child(Key::new(key.clone()))
+            }))
+    }
+}
+
+#[derive(Component)]
+pub struct Key {
+    key: SharedString,
+}
+
+impl Key {
+    pub fn new(key: impl Into<SharedString>) -> Self {
+        Self { key: key.into() }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        div()
+            .px_2()
+            .py_0()
+            .rounded_md()
+            .text_sm()
+            .text_color(theme.text)
+            .bg(theme.filled_element)
+            .child(self.key.clone())
+    }
+}
+
+// NOTE: The order the modifier keys appear in this enum impacts the order in
+// which they are rendered in the UI.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum ModifierKey {
+    Control,
+    Alt,
+    Command,
+    Shift,
+}
+
+impl ModifierKey {
+    /// Returns the glyph for the [`ModifierKey`].
+    pub fn glyph(&self) -> char {
+        match self {
+            Self::Control => '^',
+            Self::Alt => '⌥',
+            Self::Command => '⌘',
+            Self::Shift => '⇧',
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct ModifierKeys(HashSet<ModifierKey>);
+
+impl ModifierKeys {
+    pub fn new() -> Self {
+        Self(HashSet::new())
+    }
+
+    pub fn all() -> Self {
+        Self(HashSet::from_iter(ModifierKey::iter()))
+    }
+
+    pub fn add(mut self, modifier: ModifierKey) -> Self {
+        self.0.insert(modifier);
+        self
+    }
+
+    pub fn control(mut self, control: bool) -> Self {
+        if control {
+            self.0.insert(ModifierKey::Control);
+        } else {
+            self.0.remove(&ModifierKey::Control);
+        }
+
+        self
+    }
+
+    pub fn alt(mut self, alt: bool) -> Self {
+        if alt {
+            self.0.insert(ModifierKey::Alt);
+        } else {
+            self.0.remove(&ModifierKey::Alt);
+        }
+
+        self
+    }
+
+    pub fn command(mut self, command: bool) -> Self {
+        if command {
+            self.0.insert(ModifierKey::Command);
+        } else {
+            self.0.remove(&ModifierKey::Command);
+        }
+
+        self
+    }
+
+    pub fn shift(mut self, shift: bool) -> Self {
+        if shift {
+            self.0.insert(ModifierKey::Shift);
+        } else {
+            self.0.remove(&ModifierKey::Shift);
+        }
+
+        self
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+    use itertools::Itertools;
+
+    pub struct KeybindingStory;
+
+    impl Render for KeybindingStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            let all_modifier_permutations = ModifierKey::iter().permutations(2);
+
+            Story::container(cx)
+                .child(Story::title_for::<_, Keybinding>(cx))
+                .child(Story::label(cx, "Single Key"))
+                .child(Keybinding::new("Z".to_string(), ModifierKeys::new()))
+                .child(Story::label(cx, "Single Key with Modifier"))
+                .child(
+                    div()
+                        .flex()
+                        .gap_3()
+                        .children(ModifierKey::iter().map(|modifier| {
+                            Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier))
+                        })),
+                )
+                .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
+                .child(
+                    div().flex().flex_col().children(
+                        all_modifier_permutations
+                            .chunks(4)
+                            .into_iter()
+                            .map(|chunk| {
+                                div()
+                                    .flex()
+                                    .gap_4()
+                                    .py_3()
+                                    .children(chunk.map(|permutation| {
+                                        let mut modifiers = ModifierKeys::new();
+
+                                        for modifier in permutation {
+                                            modifiers = modifiers.add(modifier);
+                                        }
+
+                                        Keybinding::new("X".to_string(), modifiers)
+                                    }))
+                            }),
+                    ),
+                )
+                .child(Story::label(cx, "Single Key with All Modifiers"))
+                .child(Keybinding::new("Z".to_string(), ModifierKeys::all()))
+                .child(Story::label(cx, "Chord"))
+                .child(Keybinding::new_chord(
+                    ("A".to_string(), ModifierKeys::new()),
+                    ("Z".to_string(), ModifierKeys::new()),
+                ))
+                .child(Story::label(cx, "Chord with Modifier"))
+                .child(Keybinding::new_chord(
+                    ("A".to_string(), ModifierKeys::new().control(true)),
+                    ("Z".to_string(), ModifierKeys::new().shift(true)),
+                ))
+        }
+    }
+}

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

@@ -0,0 +1,57 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Component)]
+pub struct LanguageSelector {
+    id: ElementId,
+}
+
+impl LanguageSelector {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self { id: id.into() }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div().id(self.id.clone()).child(
+            Palette::new("palette")
+                .items(vec![
+                    PaletteItem::new("C"),
+                    PaletteItem::new("C++"),
+                    PaletteItem::new("CSS"),
+                    PaletteItem::new("Elixir"),
+                    PaletteItem::new("Elm"),
+                    PaletteItem::new("ERB"),
+                    PaletteItem::new("Rust (current)"),
+                    PaletteItem::new("Scheme"),
+                    PaletteItem::new("TOML"),
+                    PaletteItem::new("TypeScript"),
+                ])
+                .placeholder("Select a language...")
+                .empty_string("No matches")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+
+    pub struct LanguageSelectorStory;
+
+    impl Render for LanguageSelectorStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, LanguageSelector>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(LanguageSelector::new("language-selector"))
+        }
+    }
+}

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

@@ -0,0 +1,583 @@
+use gpui2::{div, relative, Div};
+
+use crate::settings::user_settings;
+use crate::{
+    h_stack, v_stack, Avatar, ClickHandler, Icon, IconColor, IconElement, IconSize, Label,
+    LabelColor,
+};
+use crate::{prelude::*, Button};
+
+#[derive(Clone, Copy, Default, Debug, PartialEq)]
+pub enum ListItemVariant {
+    /// The list item extends to the far left and right of the list.
+    FullWidth,
+    #[default]
+    Inset,
+}
+
+#[derive(Component)]
+pub struct ListHeader {
+    label: SharedString,
+    left_icon: Option<Icon>,
+    variant: ListItemVariant,
+    state: InteractionState,
+    toggleable: Toggleable,
+}
+
+impl ListHeader {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            left_icon: None,
+            variant: ListItemVariant::default(),
+            state: InteractionState::default(),
+            toggleable: Toggleable::Toggleable(ToggleState::Toggled),
+        }
+    }
+
+    pub fn toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggleable = toggle.into();
+        self
+    }
+
+    pub fn toggleable(mut self, toggleable: Toggleable) -> Self {
+        self.toggleable = toggleable;
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+        self.left_icon = left_icon;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    fn disclosure_control<V: 'static>(&self) -> Div<V> {
+        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+        let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+        match (is_toggleable, is_toggled) {
+            (false, _) => div(),
+            (_, true) => div().child(
+                IconElement::new(Icon::ChevronDown)
+                    .color(IconColor::Muted)
+                    .size(IconSize::Small),
+            ),
+            (_, false) => div().child(
+                IconElement::new(Icon::ChevronRight)
+                    .color(IconColor::Muted)
+                    .size(IconSize::Small),
+            ),
+        }
+    }
+
+    fn label_color(&self) -> LabelColor {
+        match self.state {
+            InteractionState::Disabled => LabelColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn icon_color(&self) -> IconColor {
+        match self.state {
+            InteractionState::Disabled => IconColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+        let is_toggled = self.toggleable.is_toggled();
+
+        let disclosure_control = self.disclosure_control();
+
+        h_stack()
+            .flex_1()
+            .w_full()
+            .bg(theme.surface)
+            .when(self.state == InteractionState::Focused, |this| {
+                this.border().border_color(theme.border_focused)
+            })
+            .relative()
+            .child(
+                div()
+                    .h_5()
+                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                    .flex()
+                    .flex_1()
+                    .w_full()
+                    .gap_1()
+                    .items_center()
+                    .child(
+                        div()
+                            .flex()
+                            .gap_1()
+                            .items_center()
+                            .children(self.left_icon.map(|i| {
+                                IconElement::new(i)
+                                    .color(IconColor::Muted)
+                                    .size(IconSize::Small)
+                            }))
+                            .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
+                    )
+                    .child(disclosure_control),
+            )
+    }
+}
+
+#[derive(Component)]
+pub struct ListSubHeader {
+    label: SharedString,
+    left_icon: Option<Icon>,
+    variant: ListItemVariant,
+}
+
+impl ListSubHeader {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            left_icon: None,
+            variant: ListItemVariant::default(),
+        }
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+        self.left_icon = left_icon;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        h_stack().flex_1().w_full().relative().py_1().child(
+            div()
+                .h_6()
+                .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                .flex()
+                .flex_1()
+                .w_full()
+                .gap_1()
+                .items_center()
+                .justify_between()
+                .child(
+                    div()
+                        .flex()
+                        .gap_1()
+                        .items_center()
+                        .children(self.left_icon.map(|i| {
+                            IconElement::new(i)
+                                .color(IconColor::Muted)
+                                .size(IconSize::Small)
+                        }))
+                        .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
+                ),
+        )
+    }
+}
+
+#[derive(Clone)]
+pub enum LeftContent {
+    Icon(Icon),
+    Avatar(SharedString),
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum ListEntrySize {
+    #[default]
+    Small,
+    Medium,
+}
+
+#[derive(Component)]
+pub enum ListItem<V: 'static> {
+    Entry(ListEntry),
+    Details(ListDetailsEntry<V>),
+    Separator(ListSeparator),
+    Header(ListSubHeader),
+}
+
+impl<V: 'static> From<ListEntry> for ListItem<V> {
+    fn from(entry: ListEntry) -> Self {
+        Self::Entry(entry)
+    }
+}
+
+impl<V: 'static> From<ListDetailsEntry<V>> for ListItem<V> {
+    fn from(entry: ListDetailsEntry<V>) -> Self {
+        Self::Details(entry)
+    }
+}
+
+impl<V: 'static> From<ListSeparator> for ListItem<V> {
+    fn from(entry: ListSeparator) -> Self {
+        Self::Separator(entry)
+    }
+}
+
+impl<V: 'static> From<ListSubHeader> for ListItem<V> {
+    fn from(entry: ListSubHeader) -> Self {
+        Self::Header(entry)
+    }
+}
+
+impl<V: 'static> ListItem<V> {
+    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        match self {
+            ListItem::Entry(entry) => div().child(entry.render(view, cx)),
+            ListItem::Separator(separator) => div().child(separator.render(view, cx)),
+            ListItem::Header(header) => div().child(header.render(view, cx)),
+            ListItem::Details(details) => div().child(details.render(view, cx)),
+        }
+    }
+
+    pub fn new(label: Label) -> Self {
+        Self::Entry(ListEntry::new(label))
+    }
+
+    pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
+        if let Self::Entry(entry) = self {
+            Some(entry)
+        } else {
+            None
+        }
+    }
+}
+
+#[derive(Component)]
+pub struct ListEntry {
+    disclosure_control_style: DisclosureControlVisibility,
+    indent_level: u32,
+    label: Label,
+    left_content: Option<LeftContent>,
+    variant: ListItemVariant,
+    size: ListEntrySize,
+    state: InteractionState,
+    toggle: Option<ToggleState>,
+    overflow: OverflowStyle,
+}
+
+impl ListEntry {
+    pub fn new(label: Label) -> Self {
+        Self {
+            disclosure_control_style: DisclosureControlVisibility::default(),
+            indent_level: 0,
+            label,
+            variant: ListItemVariant::default(),
+            left_content: None,
+            size: ListEntrySize::default(),
+            state: InteractionState::default(),
+            // TODO: Should use Toggleable::NotToggleable
+            // or remove Toggleable::NotToggleable from the system
+            toggle: None,
+            overflow: OverflowStyle::Hidden,
+        }
+    }
+
+    pub fn variant(mut self, variant: ListItemVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    pub fn indent_level(mut self, indent_level: u32) -> Self {
+        self.indent_level = indent_level;
+        self
+    }
+
+    pub fn toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggle = Some(toggle);
+        self
+    }
+
+    pub fn left_content(mut self, left_content: LeftContent) -> Self {
+        self.left_content = Some(left_content);
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Icon) -> Self {
+        self.left_content = Some(LeftContent::Icon(left_icon));
+        self
+    }
+
+    pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
+        self.left_content = Some(LeftContent::Avatar(left_avatar.into()));
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    pub fn size(mut self, size: ListEntrySize) -> Self {
+        self.size = size;
+        self
+    }
+
+    pub fn disclosure_control_style(
+        mut self,
+        disclosure_control_style: DisclosureControlVisibility,
+    ) -> Self {
+        self.disclosure_control_style = disclosure_control_style;
+        self
+    }
+
+    fn label_color(&self) -> LabelColor {
+        match self.state {
+            InteractionState::Disabled => LabelColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn icon_color(&self) -> IconColor {
+        match self.state {
+            InteractionState::Disabled => IconColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn disclosure_control<V: 'static>(
+        &mut self,
+        cx: &mut ViewContext<V>,
+    ) -> Option<impl Component<V>> {
+        let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
+            IconElement::new(Icon::ChevronDown)
+        } else {
+            IconElement::new(Icon::ChevronRight)
+        }
+        .color(IconColor::Muted)
+        .size(IconSize::Small);
+
+        match (self.toggle, self.disclosure_control_style) {
+            (Some(_), DisclosureControlVisibility::OnHover) => {
+                Some(div().absolute().neg_left_5().child(disclosure_control_icon))
+            }
+            (Some(_), DisclosureControlVisibility::Always) => {
+                Some(div().child(disclosure_control_icon))
+            }
+            (None, _) => None,
+        }
+    }
+
+    fn render<V: 'static>(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let settings = user_settings(cx);
+        let theme = theme(cx);
+
+        let left_content = match self.left_content.clone() {
+            Some(LeftContent::Icon(i)) => Some(
+                h_stack().child(
+                    IconElement::new(i)
+                        .size(IconSize::Small)
+                        .color(IconColor::Muted),
+                ),
+            ),
+            Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
+            None => None,
+        };
+
+        let sized_item = match self.size {
+            ListEntrySize::Small => div().h_6(),
+            ListEntrySize::Medium => div().h_7(),
+        };
+
+        div()
+            .relative()
+            .group("")
+            .bg(theme.surface)
+            .when(self.state == InteractionState::Focused, |this| {
+                this.border().border_color(theme.border_focused)
+            })
+            .child(
+                sized_item
+                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                    // .ml(rems(0.75 * self.indent_level as f32))
+                    .children((0..self.indent_level).map(|_| {
+                        div()
+                            .w(*settings.list_indent_depth)
+                            .h_full()
+                            .flex()
+                            .justify_center()
+                            .group_hover("", |style| style.bg(theme.border_focused))
+                            .child(
+                                h_stack()
+                                    .child(div().w_px().h_full())
+                                    .child(div().w_px().h_full().bg(theme.border)),
+                            )
+                    }))
+                    .flex()
+                    .gap_1()
+                    .items_center()
+                    .relative()
+                    .children(self.disclosure_control(cx))
+                    .children(left_content)
+                    .child(self.label),
+            )
+    }
+}
+
+struct ListDetailsEntryHandlers<V: 'static> {
+    click: Option<ClickHandler<V>>,
+}
+
+impl<V: 'static> Default for ListDetailsEntryHandlers<V> {
+    fn default() -> Self {
+        Self { click: None }
+    }
+}
+
+#[derive(Component)]
+pub struct ListDetailsEntry<V: 'static> {
+    label: SharedString,
+    meta: Option<SharedString>,
+    left_content: Option<LeftContent>,
+    handlers: ListDetailsEntryHandlers<V>,
+    actions: Option<Vec<Button<V>>>,
+    // TODO: make this more generic instead of
+    // specifically for notifications
+    seen: bool,
+}
+
+impl<V: 'static> ListDetailsEntry<V> {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            meta: None,
+            left_content: None,
+            handlers: ListDetailsEntryHandlers::default(),
+            actions: None,
+            seen: false,
+        }
+    }
+
+    pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
+        self.meta = Some(meta.into());
+        self
+    }
+
+    pub fn seen(mut self, seen: bool) -> Self {
+        self.seen = seen;
+        self
+    }
+
+    pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
+        self.handlers.click = Some(handler);
+        self
+    }
+
+    pub fn actions(mut self, actions: Vec<Button<V>>) -> Self {
+        self.actions = Some(actions);
+        self
+    }
+
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+        let settings = user_settings(cx);
+
+        let (item_bg, item_bg_hover, item_bg_active) = match self.seen {
+            true => (
+                theme.ghost_element,
+                theme.ghost_element_hover,
+                theme.ghost_element_active,
+            ),
+            false => (
+                theme.filled_element,
+                theme.filled_element_hover,
+                theme.filled_element_active,
+            ),
+        };
+
+        let label_color = match self.seen {
+            true => LabelColor::Muted,
+            false => LabelColor::Default,
+        };
+
+        v_stack()
+            .relative()
+            .group("")
+            .bg(item_bg)
+            .px_1()
+            .py_1_5()
+            .w_full()
+            .line_height(relative(1.2))
+            .child(Label::new(self.label.clone()).color(label_color))
+            .children(
+                self.meta
+                    .map(|meta| Label::new(meta).color(LabelColor::Muted)),
+            )
+            .child(
+                h_stack()
+                    .gap_1()
+                    .justify_end()
+                    .children(self.actions.unwrap_or_default()),
+            )
+    }
+}
+
+#[derive(Clone, Component)]
+pub struct ListSeparator;
+
+impl ListSeparator {
+    pub fn new() -> Self {
+        Self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        div().h_px().w_full().bg(theme.border)
+    }
+}
+
+#[derive(Component)]
+pub struct List<V: 'static> {
+    items: Vec<ListItem<V>>,
+    empty_message: SharedString,
+    header: Option<ListHeader>,
+    toggleable: Toggleable,
+}
+
+impl<V: 'static> List<V> {
+    pub fn new(items: Vec<ListItem<V>>) -> Self {
+        Self {
+            items,
+            empty_message: "No items".into(),
+            header: None,
+            toggleable: Toggleable::default(),
+        }
+    }
+
+    pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
+        self.empty_message = empty_message.into();
+        self
+    }
+
+    pub fn header(mut self, header: ListHeader) -> Self {
+        self.header = Some(header);
+        self
+    }
+
+    pub fn toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggleable = toggle.into();
+        self
+    }
+
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+        let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+        let list_content = match (self.items.is_empty(), is_toggled) {
+            (_, false) => div(),
+            (false, _) => div().children(self.items),
+            (true, _) => {
+                div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
+            }
+        };
+
+        v_stack()
+            .py_1()
+            .children(self.header.map(|header| header.toggleable(self.toggleable)))
+            .child(list_content)
+    }
+}

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

@@ -0,0 +1,83 @@
+use gpui2::AnyElement;
+use smallvec::SmallVec;
+
+use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label};
+
+#[derive(Component)]
+pub struct Modal<V: 'static> {
+    id: ElementId,
+    title: Option<SharedString>,
+    primary_action: Option<Button<V>>,
+    secondary_action: Option<Button<V>>,
+    children: SmallVec<[AnyElement<V>; 2]>,
+}
+
+impl<V: 'static> Modal<V> {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            title: None,
+            primary_action: None,
+            secondary_action: None,
+            children: SmallVec::new(),
+        }
+    }
+
+    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
+        self.title = Some(title.into());
+        self
+    }
+
+    pub fn primary_action(mut self, action: Button<V>) -> Self {
+        self.primary_action = Some(action);
+        self
+    }
+
+    pub fn secondary_action(mut self, action: Button<V>) -> Self {
+        self.secondary_action = Some(action);
+        self
+    }
+
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .id(self.id.clone())
+            .w_96()
+            // .rounded_xl()
+            .bg(theme.background)
+            .border()
+            .border_color(theme.border)
+            .shadow_2xl()
+            .child(
+                h_stack()
+                    .justify_between()
+                    .p_1()
+                    .border_b()
+                    .border_color(theme.border)
+                    .child(div().children(self.title.clone().map(|t| Label::new(t))))
+                    .child(IconButton::new("close", Icon::Close)),
+            )
+            .child(v_stack().p_1().children(self.children))
+            .when(
+                self.primary_action.is_some() || self.secondary_action.is_some(),
+                |this| {
+                    this.child(
+                        h_stack()
+                            .border_t()
+                            .border_color(theme.border)
+                            .p_1()
+                            .justify_end()
+                            .children(self.secondary_action)
+                            .children(self.primary_action),
+                    )
+                },
+            )
+    }
+}
+
+impl<V: 'static> ParentElement<V> for Modal<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
+}

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

@@ -0,0 +1,67 @@
+use crate::prelude::*;
+use crate::{v_stack, Buffer, Icon, IconButton, Label};
+
+#[derive(Component)]
+pub struct MultiBuffer {
+    buffers: Vec<Buffer>,
+}
+
+impl MultiBuffer {
+    pub fn new(buffers: Vec<Buffer>) -> Self {
+        Self { buffers }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .w_full()
+            .h_full()
+            .flex_1()
+            .children(self.buffers.clone().into_iter().map(|buffer| {
+                v_stack()
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .justify_between()
+                            .p_4()
+                            .bg(theme.editor_subheader)
+                            .child(Label::new("main.rs"))
+                            .child(IconButton::new("arrow_up_right", Icon::ArrowUpRight)),
+                    )
+                    .child(buffer)
+            }))
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::{hello_world_rust_buffer_example, Story};
+    use gpui2::{Div, Render};
+
+    pub struct MultiBufferStory;
+
+    impl Render for MultiBufferStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            let theme = theme(cx);
+
+            Story::container(cx)
+                .child(Story::title_for::<_, MultiBuffer>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(MultiBuffer::new(vec![
+                    hello_world_rust_buffer_example(&theme),
+                    hello_world_rust_buffer_example(&theme),
+                    hello_world_rust_buffer_example(&theme),
+                    hello_world_rust_buffer_example(&theme),
+                    hello_world_rust_buffer_example(&theme),
+                ]))
+        }
+    }
+}

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

@@ -0,0 +1,41 @@
+use gpui2::rems;
+
+use crate::{h_stack, prelude::*, Icon};
+
+#[derive(Component)]
+pub struct NotificationToast {
+    label: SharedString,
+    icon: Option<Icon>,
+}
+
+impl NotificationToast {
+    pub fn new(label: SharedString) -> Self {
+        Self { label, icon: None }
+    }
+
+    pub fn icon<I>(mut self, icon: I) -> Self
+    where
+        I: Into<Option<Icon>>,
+    {
+        self.icon = icon.into();
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        h_stack()
+            .z_index(5)
+            .absolute()
+            .top_1()
+            .right_1()
+            .w(rems(9999.))
+            .max_w_56()
+            .py_1()
+            .px_1p5()
+            .rounded_lg()
+            .shadow_md()
+            .bg(theme.elevated_surface)
+            .child(div().size_full().child(self.label.clone()))
+    }
+}

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

@@ -0,0 +1,69 @@
+use crate::{prelude::*, static_new_notification_items, static_read_notification_items};
+use crate::{List, ListHeader};
+
+#[derive(Component)]
+pub struct NotificationsPanel {
+    id: ElementId,
+}
+
+impl NotificationsPanel {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self { id: id.into() }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        div()
+            .id(self.id.clone())
+            .flex()
+            .flex_col()
+            .w_full()
+            .h_full()
+            .bg(theme.surface)
+            .child(
+                div()
+                    .id("header")
+                    .w_full()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll()
+                    .child(
+                        List::new(static_new_notification_items())
+                            .header(ListHeader::new("NEW").toggle(ToggleState::Toggled))
+                            .toggle(ToggleState::Toggled),
+                    )
+                    .child(
+                        List::new(static_read_notification_items())
+                            .header(ListHeader::new("EARLIER").toggle(ToggleState::Toggled))
+                            .empty_message("No new notifications")
+                            .toggle(ToggleState::Toggled),
+                    ),
+            )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::{Panel, Story};
+    use gpui2::{Div, Render};
+
+    pub struct NotificationsPanelStory;
+
+    impl Render for NotificationsPanelStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, NotificationsPanel>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(
+                    Panel::new("panel", cx).child(NotificationsPanel::new("notifications_panel")),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,222 @@
+use crate::prelude::*;
+use crate::{h_stack, v_stack, Keybinding, Label, LabelColor};
+
+#[derive(Component)]
+pub struct Palette {
+    id: ElementId,
+    input_placeholder: SharedString,
+    empty_string: SharedString,
+    items: Vec<PaletteItem>,
+    default_order: OrderMethod,
+}
+
+impl Palette {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            input_placeholder: "Find something...".into(),
+            empty_string: "No items found.".into(),
+            items: vec![],
+            default_order: OrderMethod::default(),
+        }
+    }
+
+    pub fn items(mut self, items: Vec<PaletteItem>) -> Self {
+        self.items = items;
+        self
+    }
+
+    pub fn placeholder(mut self, input_placeholder: impl Into<SharedString>) -> Self {
+        self.input_placeholder = input_placeholder.into();
+        self
+    }
+
+    pub fn empty_string(mut self, empty_string: impl Into<SharedString>) -> Self {
+        self.empty_string = empty_string.into();
+        self
+    }
+
+    // TODO: Hook up sort order
+    pub fn default_order(mut self, default_order: OrderMethod) -> Self {
+        self.default_order = default_order;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .id(self.id.clone())
+            .w_96()
+            .rounded_lg()
+            .bg(theme.elevated_surface)
+            .border()
+            .border_color(theme.border)
+            .child(
+                v_stack()
+                    .gap_px()
+                    .child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child(
+                        Label::new(self.input_placeholder.clone()).color(LabelColor::Placeholder),
+                    )))
+                    .child(div().h_px().w_full().bg(theme.filled_element))
+                    .child(
+                        v_stack()
+                            .id("items")
+                            .py_0p5()
+                            .px_1()
+                            .grow()
+                            .max_h_96()
+                            .overflow_y_scroll()
+                            .children(
+                                vec![if self.items.is_empty() {
+                                    Some(
+                                        h_stack().justify_between().px_2().py_1().child(
+                                            Label::new(self.empty_string.clone())
+                                                .color(LabelColor::Muted),
+                                        ),
+                                    )
+                                } else {
+                                    None
+                                }]
+                                .into_iter()
+                                .flatten(),
+                            )
+                            .children(self.items.into_iter().enumerate().map(|(index, item)| {
+                                h_stack()
+                                    .id(index)
+                                    .justify_between()
+                                    .px_2()
+                                    .py_0p5()
+                                    .rounded_lg()
+                                    .hover(|style| style.bg(theme.ghost_element_hover))
+                                    .active(|style| style.bg(theme.ghost_element_active))
+                                    .child(item)
+                            })),
+                    ),
+            )
+    }
+}
+
+#[derive(Component)]
+pub struct PaletteItem {
+    pub label: SharedString,
+    pub sublabel: Option<SharedString>,
+    pub keybinding: Option<Keybinding>,
+}
+
+impl PaletteItem {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            sublabel: None,
+            keybinding: None,
+        }
+    }
+
+    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
+        self.label = label.into();
+        self
+    }
+
+    pub fn sublabel(mut self, sublabel: impl Into<Option<SharedString>>) -> Self {
+        self.sublabel = sublabel.into();
+        self
+    }
+
+    pub fn keybinding<K>(mut self, keybinding: K) -> Self
+    where
+        K: Into<Option<Keybinding>>,
+    {
+        self.keybinding = keybinding.into();
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div()
+            .flex()
+            .flex_row()
+            .grow()
+            .justify_between()
+            .child(
+                v_stack()
+                    .child(Label::new(self.label.clone()))
+                    .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))),
+            )
+            .children(self.keybinding)
+    }
+}
+
+use gpui2::ElementId;
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use gpui2::{Div, Render};
+
+    use crate::{ModifierKeys, Story};
+
+    use super::*;
+
+    pub struct PaletteStory;
+
+    impl Render for PaletteStory {
+        type Element = Div<Self>;
+
+        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("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(),
+                                )),
+                            ]),
+                    )
+            }
+        }
+    }
+}

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

@@ -0,0 +1,154 @@
+use gpui2::{AbsoluteLength, AnyElement};
+use smallvec::SmallVec;
+
+use crate::prelude::*;
+use crate::settings::user_settings;
+use crate::v_stack;
+
+#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum PanelAllowedSides {
+    LeftOnly,
+    RightOnly,
+    BottomOnly,
+    #[default]
+    LeftAndRight,
+    All,
+}
+
+impl PanelAllowedSides {
+    /// Return a `HashSet` that contains the allowable `PanelSide`s.
+    pub fn allowed_sides(&self) -> HashSet<PanelSide> {
+        match self {
+            Self::LeftOnly => HashSet::from_iter([PanelSide::Left]),
+            Self::RightOnly => HashSet::from_iter([PanelSide::Right]),
+            Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]),
+            Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]),
+            Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]),
+        }
+    }
+}
+
+#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum PanelSide {
+    #[default]
+    Left,
+    Right,
+    Bottom,
+}
+
+use std::collections::HashSet;
+
+#[derive(Component)]
+pub struct Panel<V: 'static> {
+    id: ElementId,
+    current_side: PanelSide,
+    /// Defaults to PanelAllowedSides::LeftAndRight
+    allowed_sides: PanelAllowedSides,
+    initial_width: AbsoluteLength,
+    width: Option<AbsoluteLength>,
+    children: SmallVec<[AnyElement<V>; 2]>,
+}
+
+impl<V: 'static> Panel<V> {
+    pub fn new(id: impl Into<ElementId>, cx: &mut WindowContext) -> Self {
+        let settings = user_settings(cx);
+
+        Self {
+            id: id.into(),
+            current_side: PanelSide::default(),
+            allowed_sides: PanelAllowedSides::default(),
+            initial_width: *settings.default_panel_size,
+            width: None,
+            children: SmallVec::new(),
+        }
+    }
+
+    pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self {
+        self.initial_width = initial_width;
+        self
+    }
+
+    pub fn width(mut self, width: AbsoluteLength) -> Self {
+        self.width = Some(width);
+        self
+    }
+
+    pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self {
+        self.allowed_sides = allowed_sides;
+        self
+    }
+
+    pub fn side(mut self, side: PanelSide) -> Self {
+        let allowed_sides = self.allowed_sides.allowed_sides();
+
+        if allowed_sides.contains(&side) {
+            self.current_side = side;
+        } else {
+            panic!(
+                "The panel side {:?} was not added as allowed before it was set.",
+                side
+            );
+        }
+        self
+    }
+
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let current_size = self.width.unwrap_or(self.initial_width);
+
+        v_stack()
+            .id(self.id.clone())
+            .flex_initial()
+            .when(
+                self.current_side == PanelSide::Left || self.current_side == PanelSide::Right,
+                |this| this.h_full().w(current_size),
+            )
+            .when(self.current_side == PanelSide::Left, |this| this.border_r())
+            .when(self.current_side == PanelSide::Right, |this| {
+                this.border_l()
+            })
+            .when(self.current_side == PanelSide::Bottom, |this| {
+                this.border_b().w_full().h(current_size)
+            })
+            .bg(theme.surface)
+            .border_color(theme.border)
+            .children(self.children)
+    }
+}
+
+impl<V: 'static> ParentElement<V> for Panel<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::{Label, Story};
+    use gpui2::{Div, Render};
+
+    pub struct PanelStory;
+
+    impl Render for PanelStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, Panel<Self>>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(
+                    Panel::new("panel", cx).child(
+                        div()
+                            .id("panel-contents")
+                            .overflow_y_scroll()
+                            .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))),
+                    ),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,130 @@
+use gpui2::{hsla, red, AnyElement, ElementId, ExternalPaths, Hsla, Length, Size, View};
+use smallvec::SmallVec;
+
+use crate::prelude::*;
+
+#[derive(Default, PartialEq)]
+pub enum SplitDirection {
+    #[default]
+    Horizontal,
+    Vertical,
+}
+
+#[derive(Component)]
+pub struct Pane<V: 'static> {
+    id: ElementId,
+    size: Size<Length>,
+    fill: Hsla,
+    children: SmallVec<[AnyElement<V>; 2]>,
+}
+
+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
+
+        Self {
+            id: id.into(),
+            size,
+            fill: hsla(0.3, 0.3, 0.3, 1.),
+            children: SmallVec::new(),
+        }
+    }
+
+    pub fn fill(mut self, fill: Hsla) -> Self {
+        self.fill = fill;
+        self
+    }
+
+    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div()
+            .id(self.id.clone())
+            .flex()
+            .flex_initial()
+            .bg(self.fill)
+            .w(self.size.width)
+            .h(self.size.height)
+            .relative()
+            .child(div().z_index(0).size_full().children(self.children))
+            .child(
+                div()
+                    .z_index(1)
+                    .id("drag-target")
+                    .drag_over::<ExternalPaths>(|d| d.bg(red()))
+                    .on_drop(|_, files: View<ExternalPaths>, cx| {
+                        dbg!("dropped files!", files.read(cx));
+                    })
+                    .absolute()
+                    .inset_0(),
+            )
+    }
+}
+
+impl<V: 'static> ParentElement<V> for Pane<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
+}
+
+#[derive(Component)]
+pub struct PaneGroup<V: 'static> {
+    groups: Vec<PaneGroup<V>>,
+    panes: Vec<Pane<V>>,
+    split_direction: SplitDirection,
+}
+
+impl<V: 'static> PaneGroup<V> {
+    pub fn new_groups(groups: Vec<PaneGroup<V>>, split_direction: SplitDirection) -> Self {
+        Self {
+            groups,
+            panes: Vec::new(),
+            split_direction,
+        }
+    }
+
+    pub fn new_panes(panes: Vec<Pane<V>>, split_direction: SplitDirection) -> Self {
+        Self {
+            groups: Vec::new(),
+            panes,
+            split_direction,
+        }
+    }
+
+    fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        if !self.panes.is_empty() {
+            let el = div()
+                .flex()
+                .flex_1()
+                .gap_px()
+                .w_full()
+                .h_full()
+                .children(self.panes.into_iter().map(|pane| pane.render(view, cx)));
+
+            if self.split_direction == SplitDirection::Horizontal {
+                return el;
+            } else {
+                return el.flex_col();
+            }
+        }
+
+        if !self.groups.is_empty() {
+            let el = div()
+                .flex()
+                .flex_1()
+                .gap_px()
+                .w_full()
+                .h_full()
+                .bg(theme.editor)
+                .children(self.groups.into_iter().map(|group| group.render(view, cx)));
+
+            if self.split_direction == SplitDirection::Horizontal {
+                return el;
+            } else {
+                return el.flex_col();
+            }
+        }
+
+        unreachable!()
+    }
+}

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

@@ -0,0 +1,63 @@
+use crate::prelude::*;
+use crate::{Avatar, Facepile, PlayerWithCallStatus};
+
+#[derive(Component)]
+pub struct PlayerStack {
+    player_with_call_status: PlayerWithCallStatus,
+}
+
+impl PlayerStack {
+    pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
+        Self {
+            player_with_call_status,
+        }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+        let player = self.player_with_call_status.get_player();
+        self.player_with_call_status.get_call_status();
+
+        let followers = self
+            .player_with_call_status
+            .get_call_status()
+            .followers
+            .as_ref()
+            .map(|followers| followers.clone());
+
+        // if we have no followers return a slightly different element
+        // if mic_status == muted add a red ring to avatar
+
+        div()
+            .h_full()
+            .flex()
+            .flex_col()
+            .gap_px()
+            .justify_center()
+            .child(
+                div()
+                    .flex()
+                    .justify_center()
+                    .w_full()
+                    .child(div().w_4().h_0p5().rounded_sm().bg(player.cursor_color(cx))),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .justify_center()
+                    .h_6()
+                    .pl_1()
+                    .rounded_lg()
+                    .bg(if followers.is_none() {
+                        theme.transparent
+                    } else {
+                        player.selection_color(cx)
+                    })
+                    .child(Avatar::new(player.avatar_src().to_string()))
+                    .children(followers.map(|followers| {
+                        div().neg_ml_2().child(Facepile::new(followers.into_iter()))
+                    })),
+            )
+    }
+}

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

@@ -0,0 +1,79 @@
+use crate::prelude::*;
+use crate::{
+    static_project_panel_project_items, static_project_panel_single_items, Input, List, ListHeader,
+};
+
+#[derive(Component)]
+pub struct ProjectPanel {
+    id: ElementId,
+}
+
+impl ProjectPanel {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self { id: id.into() }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        div()
+            .id(self.id.clone())
+            .flex()
+            .flex_col()
+            .w_full()
+            .h_full()
+            .bg(theme.surface)
+            .child(
+                div()
+                    .id("project-panel-contents")
+                    .w_full()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll()
+                    .child(
+                        List::new(static_project_panel_single_items())
+                            .header(ListHeader::new("FILES").toggle(ToggleState::Toggled))
+                            .empty_message("No files in directory")
+                            .toggle(ToggleState::Toggled),
+                    )
+                    .child(
+                        List::new(static_project_panel_project_items())
+                            .header(ListHeader::new("PROJECT").toggle(ToggleState::Toggled))
+                            .empty_message("No folders in directory")
+                            .toggle(ToggleState::Toggled),
+                    ),
+            )
+            .child(
+                Input::new("Find something...")
+                    .value("buffe".to_string())
+                    .state(InteractionState::Focused),
+            )
+    }
+}
+
+use gpui2::ElementId;
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::{Panel, Story};
+    use gpui2::{Div, Render};
+
+    pub struct ProjectPanelStory;
+
+    impl Render for ProjectPanelStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, ProjectPanel>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(
+                    Panel::new("project-panel-outer", cx)
+                        .child(ProjectPanel::new("project-panel-inner")),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,53 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Component)]
+pub struct RecentProjects {
+    id: ElementId,
+}
+
+impl RecentProjects {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self { id: id.into() }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div().id(self.id.clone()).child(
+            Palette::new("palette")
+                .items(vec![
+                    PaletteItem::new("zed").sublabel(SharedString::from("~/projects/zed")),
+                    PaletteItem::new("saga").sublabel(SharedString::from("~/projects/saga")),
+                    PaletteItem::new("journal").sublabel(SharedString::from("~/journal")),
+                    PaletteItem::new("dotfiles").sublabel(SharedString::from("~/dotfiles")),
+                    PaletteItem::new("zed.dev").sublabel(SharedString::from("~/projects/zed.dev")),
+                    PaletteItem::new("laminar").sublabel(SharedString::from("~/projects/laminar")),
+                ])
+                .placeholder("Recent Projects...")
+                .empty_string("No matches")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+
+    pub struct RecentProjectsStory;
+
+    impl Render for RecentProjectsStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, RecentProjects>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(RecentProjects::new("recent-projects"))
+        }
+    }
+}

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

@@ -0,0 +1,205 @@
+use std::sync::Arc;
+
+use crate::prelude::*;
+use crate::{Button, Icon, IconButton, IconColor, ToolDivider, Workspace};
+
+#[derive(Default, PartialEq)]
+pub enum Tool {
+    #[default]
+    ProjectPanel,
+    CollaborationPanel,
+    Terminal,
+    Assistant,
+    Feedback,
+    Diagnostics,
+}
+
+struct ToolGroup {
+    active_index: Option<usize>,
+    tools: Vec<Tool>,
+}
+
+impl Default for ToolGroup {
+    fn default() -> Self {
+        ToolGroup {
+            active_index: None,
+            tools: vec![],
+        }
+    }
+}
+
+#[derive(Component)]
+#[component(view_type = "Workspace")]
+pub struct StatusBar {
+    left_tools: Option<ToolGroup>,
+    right_tools: Option<ToolGroup>,
+    bottom_tools: Option<ToolGroup>,
+}
+
+impl StatusBar {
+    pub fn new() -> Self {
+        Self {
+            left_tools: None,
+            right_tools: None,
+            bottom_tools: None,
+        }
+    }
+
+    pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.left_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.right_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.bottom_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    fn render(
+        self,
+        view: &mut Workspace,
+        cx: &mut ViewContext<Workspace>,
+    ) -> impl Component<Workspace> {
+        let theme = theme(cx);
+
+        div()
+            .py_0p5()
+            .px_1()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .bg(theme.status_bar)
+            .child(self.left_tools(view, cx))
+            .child(self.right_tools(view, cx))
+    }
+
+    fn left_tools(
+        &self,
+        workspace: &mut Workspace,
+        cx: &WindowContext,
+    ) -> impl Component<Workspace> {
+        div()
+            .flex()
+            .items_center()
+            .gap_1()
+            .child(
+                IconButton::<Workspace>::new("project_panel", Icon::FileTree)
+                    .when(workspace.is_project_panel_open(), |this| {
+                        this.color(IconColor::Accent)
+                    })
+                    .on_click(|workspace, cx| {
+                        workspace.toggle_project_panel(cx);
+                    }),
+            )
+            .child(
+                IconButton::<Workspace>::new("collab_panel", Icon::Hash)
+                    .when(workspace.is_collab_panel_open(), |this| {
+                        this.color(IconColor::Accent)
+                    })
+                    .on_click(|workspace, cx| {
+                        workspace.toggle_collab_panel();
+                    }),
+            )
+            .child(ToolDivider::new())
+            .child(IconButton::new("diagnostics", Icon::XCircle))
+    }
+
+    fn right_tools(
+        &self,
+        workspace: &mut Workspace,
+        cx: &WindowContext,
+    ) -> impl Component<Workspace> {
+        div()
+            .flex()
+            .items_center()
+            .gap_2()
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(Button::new("116:25"))
+                    .child(
+                        Button::<Workspace>::new("Rust").on_click(Arc::new(|workspace, cx| {
+                            workspace.toggle_language_selector(cx);
+                        })),
+                    ),
+            )
+            .child(ToolDivider::new())
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(
+                        IconButton::new("copilot", Icon::Copilot)
+                            .on_click(|_, _| println!("Copilot clicked.")),
+                    )
+                    .child(
+                        IconButton::new("envelope", Icon::Envelope)
+                            .on_click(|_, _| println!("Send Feedback clicked.")),
+                    ),
+            )
+            .child(ToolDivider::new())
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(
+                        IconButton::<Workspace>::new("terminal", Icon::Terminal)
+                            .when(workspace.is_terminal_open(), |this| {
+                                this.color(IconColor::Accent)
+                            })
+                            .on_click(|workspace, cx| {
+                                workspace.toggle_terminal(cx);
+                            }),
+                    )
+                    .child(
+                        IconButton::<Workspace>::new("chat_panel", Icon::MessageBubbles)
+                            .when(workspace.is_chat_panel_open(), |this| {
+                                this.color(IconColor::Accent)
+                            })
+                            .on_click(|workspace, cx| {
+                                workspace.toggle_chat_panel(cx);
+                            }),
+                    )
+                    .child(
+                        IconButton::<Workspace>::new("assistant_panel", Icon::Ai)
+                            .when(workspace.is_assistant_panel_open(), |this| {
+                                this.color(IconColor::Accent)
+                            })
+                            .on_click(|workspace, cx| {
+                                workspace.toggle_assistant_panel(cx);
+                            }),
+                    ),
+            )
+    }
+}

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

@@ -0,0 +1,275 @@
+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 {
+    id: ElementId,
+    title: String,
+    icon: Option<Icon>,
+    current: bool,
+    dirty: bool,
+    fs_status: FileSystemStatus,
+    git_status: GitStatus,
+    diagnostic_status: DiagnosticStatus,
+    close_side: IconSide,
+}
+
+#[derive(Clone, Debug)]
+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 {
+            id: id.into(),
+            title: "untitled".to_string(),
+            icon: None,
+            current: false,
+            dirty: false,
+            fs_status: FileSystemStatus::None,
+            git_status: GitStatus::None,
+            diagnostic_status: DiagnosticStatus::None,
+            close_side: IconSide::Right,
+        }
+    }
+
+    pub fn current(mut self, current: bool) -> Self {
+        self.current = current;
+        self
+    }
+
+    pub fn title(mut self, title: String) -> Self {
+        self.title = title;
+        self
+    }
+
+    pub fn icon<I>(mut self, icon: I) -> Self
+    where
+        I: Into<Option<Icon>>,
+    {
+        self.icon = icon.into();
+        self
+    }
+
+    pub fn dirty(mut self, dirty: bool) -> Self {
+        self.dirty = dirty;
+        self
+    }
+
+    pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
+        self.fs_status = fs_status;
+        self
+    }
+
+    pub fn git_status(mut self, git_status: GitStatus) -> Self {
+        self.git_status = git_status;
+        self
+    }
+
+    pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
+        self.diagnostic_status = diagnostic_status;
+        self
+    }
+
+    pub fn close_side(mut self, close_side: IconSide) -> Self {
+        self.close_side = close_side;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+        let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
+        let is_deleted = self.fs_status == FileSystemStatus::Deleted;
+
+        let label = match (self.git_status, is_deleted) {
+            (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
+                .color(LabelColor::Hidden)
+                .set_strikethrough(true),
+            (GitStatus::None, false) => Label::new(self.title.clone()),
+            (GitStatus::Created, false) => {
+                Label::new(self.title.clone()).color(LabelColor::Created)
+            }
+            (GitStatus::Modified, false) => {
+                Label::new(self.title.clone()).color(LabelColor::Modified)
+            }
+            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
+            (GitStatus::Conflict, false) => Label::new(self.title.clone()),
+        };
+
+        let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
+
+        let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
+            true => (
+                theme.ghost_element,
+                theme.ghost_element_hover,
+                theme.ghost_element_active,
+            ),
+            false => (
+                theme.filled_element,
+                theme.filled_element_hover,
+                theme.filled_element_active,
+            ),
+        };
+
+        let drag_state = TabDragState {
+            title: self.title.clone(),
+        };
+
+        div()
+            .id(self.id.clone())
+            .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
+            .drag_over::<TabDragState>(|d| d.bg(black()))
+            .on_drop(|_view, state: View<TabDragState>, cx| {
+                dbg!(state.read(cx));
+            })
+            .px_2()
+            .py_0p5()
+            .flex()
+            .items_center()
+            .justify_center()
+            .bg(tab_bg)
+            .hover(|h| h.bg(tab_hover_bg))
+            .active(|a| a.bg(tab_active_bg))
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .children(has_fs_conflict.then(|| {
+                        IconElement::new(Icon::ExclamationTriangle)
+                            .size(crate::IconSize::Small)
+                            .color(IconColor::Warning)
+                    }))
+                    .children(self.icon.map(IconElement::new))
+                    .children(if self.close_side == IconSide::Left {
+                        Some(close_icon())
+                    } else {
+                        None
+                    })
+                    .child(label)
+                    .children(if self.close_side == IconSide::Right {
+                        Some(close_icon())
+                    } else {
+                        None
+                    }),
+            )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::{h_stack, v_stack, Icon, Story};
+    use strum::IntoEnumIterator;
+
+    pub struct TabStory;
+
+    impl Render for TabStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            let git_statuses = GitStatus::iter();
+            let fs_statuses = FileSystemStatus::iter();
+
+            Story::container(cx)
+                .child(Story::title_for::<_, Tab>(cx))
+                .child(
+                    h_stack().child(
+                        v_stack()
+                            .gap_2()
+                            .child(Story::label(cx, "Default"))
+                            .child(Tab::new("default")),
+                    ),
+                )
+                .child(
+                    h_stack().child(
+                        v_stack().gap_2().child(Story::label(cx, "Current")).child(
+                            h_stack()
+                                .gap_4()
+                                .child(
+                                    Tab::new("current")
+                                        .title("Current".to_string())
+                                        .current(true),
+                                )
+                                .child(
+                                    Tab::new("not_current")
+                                        .title("Not Current".to_string())
+                                        .current(false),
+                                ),
+                        ),
+                    ),
+                )
+                .child(
+                    h_stack().child(
+                        v_stack()
+                            .gap_2()
+                            .child(Story::label(cx, "Titled"))
+                            .child(Tab::new("titled").title("label".to_string())),
+                    ),
+                )
+                .child(
+                    h_stack().child(
+                        v_stack()
+                            .gap_2()
+                            .child(Story::label(cx, "With Icon"))
+                            .child(
+                                Tab::new("with_icon")
+                                    .title("label".to_string())
+                                    .icon(Some(Icon::Envelope)),
+                            ),
+                    ),
+                )
+                .child(
+                    h_stack().child(
+                        v_stack()
+                            .gap_2()
+                            .child(Story::label(cx, "Close Side"))
+                            .child(
+                                h_stack()
+                                    .gap_4()
+                                    .child(
+                                        Tab::new("left")
+                                            .title("Left".to_string())
+                                            .close_side(IconSide::Left),
+                                    )
+                                    .child(Tab::new("right").title("Right".to_string())),
+                            ),
+                    ),
+                )
+                .child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "Git Status"))
+                        .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
+                            Tab::new("git_status")
+                                .title(git_status.to_string())
+                                .git_status(git_status)
+                        }))),
+                )
+                .child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "File System Status"))
+                        .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
+                            Tab::new("file_system_status")
+                                .title(fs_status.to_string())
+                                .fs_status(fs_status)
+                        }))),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,144 @@
+use crate::prelude::*;
+use crate::{Icon, IconButton, Tab};
+
+#[derive(Component)]
+pub struct TabBar {
+    id: ElementId,
+    /// Backwards, Forwards
+    can_navigate: (bool, bool),
+    tabs: Vec<Tab>,
+}
+
+impl TabBar {
+    pub fn new(id: impl Into<ElementId>, tabs: Vec<Tab>) -> Self {
+        Self {
+            id: id.into(),
+            can_navigate: (false, false),
+            tabs,
+        }
+    }
+
+    pub fn can_navigate(mut self, can_navigate: (bool, bool)) -> Self {
+        self.can_navigate = can_navigate;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let (can_navigate_back, can_navigate_forward) = self.can_navigate;
+
+        div()
+            .id(self.id.clone())
+            .w_full()
+            .flex()
+            .bg(theme.tab_bar)
+            // Left Side
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .flex_none()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(
+                                IconButton::new("arrow_left", Icon::ArrowLeft)
+                                    .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
+                            )
+                            .child(
+                                IconButton::new("arrow_right", Icon::ArrowRight).state(
+                                    InteractionState::Enabled.if_enabled(can_navigate_forward),
+                                ),
+                            ),
+                    ),
+            )
+            .child(
+                div().w_0().flex_1().h_full().child(
+                    div()
+                        .id("tabs")
+                        .flex()
+                        .overflow_x_scroll()
+                        .children(self.tabs.clone()),
+                ),
+            )
+            // Right Side
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .flex_none()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(IconButton::new("plus", Icon::Plus))
+                            .child(IconButton::new("split", Icon::Split)),
+                    ),
+            )
+    }
+}
+
+use gpui2::ElementId;
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+
+    pub struct TabBarStory;
+
+    impl Render for TabBarStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, TabBar>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(TabBar::new(
+                    "tab-bar",
+                    vec![
+                        Tab::new(1)
+                            .title("Cargo.toml".to_string())
+                            .current(false)
+                            .git_status(GitStatus::Modified),
+                        Tab::new(2)
+                            .title("Channels Panel".to_string())
+                            .current(false),
+                        Tab::new(3)
+                            .title("channels_panel.rs".to_string())
+                            .current(true)
+                            .git_status(GitStatus::Modified),
+                        Tab::new(4)
+                            .title("workspace.rs".to_string())
+                            .current(false)
+                            .git_status(GitStatus::Modified),
+                        Tab::new(5)
+                            .title("icon_button.rs".to_string())
+                            .current(false),
+                        Tab::new(6)
+                            .title("storybook.rs".to_string())
+                            .current(false)
+                            .git_status(GitStatus::Created),
+                        Tab::new(7).title("theme.rs".to_string()).current(false),
+                        Tab::new(8)
+                            .title("theme_registry.rs".to_string())
+                            .current(false),
+                        Tab::new(9)
+                            .title("styleable_helpers.rs".to_string())
+                            .current(false),
+                    ],
+                ))
+        }
+    }
+}

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

@@ -0,0 +1,101 @@
+use gpui2::{relative, rems, Size};
+
+use crate::prelude::*;
+use crate::{Icon, IconButton, Pane, Tab};
+
+#[derive(Component)]
+pub struct Terminal;
+
+impl Terminal {
+    pub fn new() -> Self {
+        Self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let can_navigate_back = true;
+        let can_navigate_forward = false;
+
+        div()
+            .flex()
+            .flex_col()
+            .w_full()
+            .child(
+                // Terminal Tabs.
+                div()
+                    .w_full()
+                    .flex()
+                    .bg(theme.surface)
+                    .child(
+                        div().px_1().flex().flex_none().gap_2().child(
+                            div()
+                                .flex()
+                                .items_center()
+                                .gap_px()
+                                .child(
+                                    IconButton::new("arrow_left", Icon::ArrowLeft).state(
+                                        InteractionState::Enabled.if_enabled(can_navigate_back),
+                                    ),
+                                )
+                                .child(IconButton::new("arrow_right", Icon::ArrowRight).state(
+                                    InteractionState::Enabled.if_enabled(can_navigate_forward),
+                                )),
+                        ),
+                    )
+                    .child(
+                        div().w_0().flex_1().h_full().child(
+                            div()
+                                .flex()
+                                .child(
+                                    Tab::new(1)
+                                        .title("zed — fish".to_string())
+                                        .icon(Icon::Terminal)
+                                        .close_side(IconSide::Right)
+                                        .current(true),
+                                )
+                                .child(
+                                    Tab::new(2)
+                                        .title("zed — fish".to_string())
+                                        .icon(Icon::Terminal)
+                                        .close_side(IconSide::Right)
+                                        .current(false),
+                                ),
+                        ),
+                    ),
+            )
+            // Terminal Pane.
+            .child(
+                Pane::new(
+                    "terminal",
+                    Size {
+                        width: relative(1.).into(),
+                        height: rems(36.).into(),
+                    },
+                )
+                .child(crate::static_data::terminal_buffer(&theme)),
+            )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+    pub struct TerminalStory;
+
+    impl Render for TerminalStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, Terminal>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(Terminal::new())
+        }
+    }
+}

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

@@ -0,0 +1,60 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Component)]
+pub struct ThemeSelector {
+    id: ElementId,
+}
+
+impl ThemeSelector {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self { id: id.into() }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div().child(
+            Palette::new(self.id.clone())
+                .items(vec![
+                    PaletteItem::new("One Dark"),
+                    PaletteItem::new("Rosé Pine"),
+                    PaletteItem::new("Rosé Pine Moon"),
+                    PaletteItem::new("Sandcastle"),
+                    PaletteItem::new("Solarized Dark"),
+                    PaletteItem::new("Summercamp"),
+                    PaletteItem::new("Atelier Cave Light"),
+                    PaletteItem::new("Atelier Dune Light"),
+                    PaletteItem::new("Atelier Estuary Light"),
+                    PaletteItem::new("Atelier Forest Light"),
+                    PaletteItem::new("Atelier Heath Light"),
+                ])
+                .placeholder("Select Theme...")
+                .empty_string("No matches")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use gpui2::{Div, Render};
+
+    use crate::Story;
+
+    use super::*;
+
+    pub struct ThemeSelectorStory;
+
+    impl Render for ThemeSelectorStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, ThemeSelector>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(ThemeSelector::new("theme-selector"))
+        }
+    }
+}

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

@@ -0,0 +1,215 @@
+use std::sync::atomic::AtomicBool;
+use std::sync::Arc;
+
+use gpui2::{Div, Render, View, VisualContext};
+
+use crate::prelude::*;
+use crate::settings::user_settings;
+use crate::{
+    Avatar, Button, Icon, IconButton, IconColor, MicStatus, PlayerStack, PlayerWithCallStatus,
+    ScreenShareStatus, ToolDivider, TrafficLights,
+};
+
+#[derive(Clone)]
+pub struct Livestream {
+    pub players: Vec<PlayerWithCallStatus>,
+    pub channel: Option<String>, // projects
+                                 // windows
+}
+
+#[derive(Clone)]
+pub struct TitleBar {
+    /// If the window is active from the OS's perspective.
+    is_active: Arc<AtomicBool>,
+    livestream: Option<Livestream>,
+    mic_status: MicStatus,
+    is_deafened: bool,
+    screen_share_status: ScreenShareStatus,
+}
+
+impl TitleBar {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        let is_active = Arc::new(AtomicBool::new(true));
+        let active = is_active.clone();
+
+        // cx.observe_window_activation(move |_, is_active, cx| {
+        //     active.store(is_active, std::sync::atomic::Ordering::SeqCst);
+        //     cx.notify();
+        // })
+        // .detach();
+
+        Self {
+            is_active,
+            livestream: None,
+            mic_status: MicStatus::Unmuted,
+            is_deafened: false,
+            screen_share_status: ScreenShareStatus::NotShared,
+        }
+    }
+
+    pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
+        self.livestream = livestream;
+        self
+    }
+
+    pub fn is_mic_muted(&self) -> bool {
+        self.mic_status == MicStatus::Muted
+    }
+
+    pub fn toggle_mic_status(&mut self, cx: &mut ViewContext<Self>) {
+        self.mic_status = self.mic_status.inverse();
+
+        // Undeafen yourself when unmuting the mic while deafened.
+        if self.is_deafened && self.mic_status == MicStatus::Unmuted {
+            self.is_deafened = false;
+        }
+
+        cx.notify();
+    }
+
+    pub fn toggle_deafened(&mut self, cx: &mut ViewContext<Self>) {
+        self.is_deafened = !self.is_deafened;
+        self.mic_status = MicStatus::Muted;
+
+        cx.notify()
+    }
+
+    pub fn toggle_screen_share_status(&mut self, cx: &mut ViewContext<Self>) {
+        self.screen_share_status = self.screen_share_status.inverse();
+
+        cx.notify();
+    }
+
+    pub fn view(cx: &mut WindowContext, livestream: Option<Livestream>) -> View<Self> {
+        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>) -> Div<Self> {
+        let theme = theme(cx);
+        let settings = user_settings(cx);
+
+        // let has_focus = cx.window_is_active();
+        let has_focus = true;
+
+        let player_list = if let Some(livestream) = &self.livestream {
+            livestream.players.clone().into_iter()
+        } else {
+            vec![].into_iter()
+        };
+
+        div()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .bg(theme.background)
+            .py_1()
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .h_full()
+                    .gap_4()
+                    .px_2()
+                    .child(TrafficLights::new().window_has_focus(has_focus))
+                    // === Project Info === //
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .when(*settings.titlebar.show_project_owner, |this| {
+                                this.child(Button::new("iamnbutler"))
+                            })
+                            .child(Button::new("zed"))
+                            .child(Button::new("nate/gpui2-ui-components")),
+                    )
+                    .children(player_list.map(|p| PlayerStack::new(p)))
+                    .child(IconButton::new("plus", Icon::Plus)),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(IconButton::new("folder_x", Icon::FolderX))
+                            .child(IconButton::new("exit", Icon::Exit)),
+                    )
+                    .child(ToolDivider::new())
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(
+                                IconButton::<TitleBar>::new("toggle_mic_status", Icon::Mic)
+                                    .when(self.is_mic_muted(), |this| this.color(IconColor::Error))
+                                    .on_click(|title_bar, cx| title_bar.toggle_mic_status(cx)),
+                            )
+                            .child(
+                                IconButton::<TitleBar>::new("toggle_deafened", Icon::AudioOn)
+                                    .when(self.is_deafened, |this| this.color(IconColor::Error))
+                                    .on_click(|title_bar, cx| title_bar.toggle_deafened(cx)),
+                            )
+                            .child(
+                                IconButton::<TitleBar>::new("toggle_screen_share", Icon::Screen)
+                                    .when(
+                                        self.screen_share_status == ScreenShareStatus::Shared,
+                                        |this| this.color(IconColor::Accent),
+                                    )
+                                    .on_click(|title_bar, cx| {
+                                        title_bar.toggle_screen_share_status(cx)
+                                    }),
+                            ),
+                    )
+                    .child(
+                        div().px_2().flex().items_center().child(
+                            Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                .shape(Shape::RoundedRectangle),
+                        ),
+                    ),
+            )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+
+    pub struct TitleBarStory {
+        title_bar: View<TitleBar>,
+    }
+
+    impl TitleBarStory {
+        pub fn view(cx: &mut WindowContext) -> View<Self> {
+            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>) -> Div<Self> {
+            Story::container(cx)
+                .child(Story::title_for::<_, TitleBar>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(self.title_bar.clone())
+        }
+    }
+}

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

@@ -0,0 +1,93 @@
+use gpui2::AnyElement;
+use smallvec::SmallVec;
+
+use crate::prelude::*;
+
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ToastOrigin {
+    #[default]
+    Bottom,
+    BottomRight,
+}
+
+/// Don't use toast directly:
+///
+/// - For messages with a required action, use a `NotificationToast`.
+/// - For messages that convey information, use a `StatusToast`.
+///
+/// A toast is a small, temporary window that appears to show a message to the user
+/// or indicate a required action.
+///
+/// Toasts should not persist on the screen for more than a few seconds unless
+/// they are actively showing the a process in progress.
+///
+/// Only one toast may be visible at a time.
+#[derive(Component)]
+pub struct Toast<V: 'static> {
+    origin: ToastOrigin,
+    children: SmallVec<[AnyElement<V>; 2]>,
+}
+
+impl<V: 'static> Toast<V> {
+    pub fn new(origin: ToastOrigin) -> Self {
+        Self {
+            origin,
+            children: SmallVec::new(),
+        }
+    }
+
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let mut div = div();
+
+        if self.origin == ToastOrigin::Bottom {
+            div = div.right_1_2();
+        } else {
+            div = div.right_2();
+        }
+
+        div.z_index(5)
+            .absolute()
+            .bottom_9()
+            .flex()
+            .py_1()
+            .px_1p5()
+            .rounded_lg()
+            .shadow_md()
+            .overflow_hidden()
+            .bg(theme.elevated_surface)
+            .children(self.children)
+    }
+}
+
+impl<V: 'static> ParentElement<V> for Toast<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use gpui2::{Div, Render};
+
+    use crate::{Label, Story};
+
+    use super::*;
+
+    pub struct ToastStory;
+
+    impl Render for ToastStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(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 🔗

@@ -0,0 +1,130 @@
+use gpui2::AnyElement;
+use smallvec::SmallVec;
+
+use crate::prelude::*;
+
+#[derive(Clone)]
+pub struct ToolbarItem {}
+
+#[derive(Component)]
+pub struct Toolbar<V: 'static> {
+    left_items: SmallVec<[AnyElement<V>; 2]>,
+    right_items: SmallVec<[AnyElement<V>; 2]>,
+}
+
+impl<V: 'static> Toolbar<V> {
+    pub fn new() -> Self {
+        Self {
+            left_items: SmallVec::new(),
+            right_items: SmallVec::new(),
+        }
+    }
+
+    pub fn left_item(mut self, child: impl Component<V>) -> Self
+    where
+        Self: Sized,
+    {
+        self.left_items.push(child.render());
+        self
+    }
+
+    pub fn left_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
+    where
+        Self: Sized,
+    {
+        self.left_items
+            .extend(iter.into_iter().map(|item| item.render()));
+        self
+    }
+
+    pub fn right_item(mut self, child: impl Component<V>) -> Self
+    where
+        Self: Sized,
+    {
+        self.right_items.push(child.render());
+        self
+    }
+
+    pub fn right_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
+    where
+        Self: Sized,
+    {
+        self.right_items
+            .extend(iter.into_iter().map(|item| item.render()));
+        self
+    }
+
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        div()
+            .bg(theme.toolbar)
+            .p_2()
+            .flex()
+            .justify_between()
+            .child(div().flex().children(self.left_items))
+            .child(div().flex().children(self.right_items))
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use std::path::PathBuf;
+    use std::str::FromStr;
+
+    use gpui2::{Div, Render};
+
+    use crate::{Breadcrumb, HighlightedText, Icon, IconButton, Story, Symbol};
+
+    use super::*;
+
+    pub struct ToolbarStory;
+
+    impl Render for ToolbarStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            let theme = theme(cx);
+
+            Story::container(cx)
+                .child(Story::title_for::<_, Toolbar<Self>>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(
+                    Toolbar::new()
+                        .left_item(Breadcrumb::new(
+                            PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
+                            vec![
+                                Symbol(vec![
+                                    HighlightedText {
+                                        text: "impl ".to_string(),
+                                        color: theme.syntax.color("keyword"),
+                                    },
+                                    HighlightedText {
+                                        text: "ToolbarStory".to_string(),
+                                        color: theme.syntax.color("function"),
+                                    },
+                                ]),
+                                Symbol(vec![
+                                    HighlightedText {
+                                        text: "fn ".to_string(),
+                                        color: theme.syntax.color("keyword"),
+                                    },
+                                    HighlightedText {
+                                        text: "render".to_string(),
+                                        color: theme.syntax.color("function"),
+                                    },
+                                ]),
+                            ],
+                        ))
+                        .right_items(vec![
+                            IconButton::new("toggle_inlay_hints", Icon::InlayHint),
+                            IconButton::new("buffer_search", Icon::MagnifyingGlass),
+                            IconButton::new("inline_assist", Icon::MagicWand),
+                        ]),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,100 @@
+use crate::prelude::*;
+
+#[derive(Clone, Copy)]
+enum TrafficLightColor {
+    Red,
+    Yellow,
+    Green,
+}
+
+#[derive(Component)]
+struct TrafficLight {
+    color: TrafficLightColor,
+    window_has_focus: bool,
+}
+
+impl TrafficLight {
+    fn new(color: TrafficLightColor, window_has_focus: bool) -> Self {
+        Self {
+            color,
+            window_has_focus,
+        }
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let fill = match (self.window_has_focus, self.color) {
+            (true, TrafficLightColor::Red) => theme.mac_os_traffic_light_red,
+            (true, TrafficLightColor::Yellow) => theme.mac_os_traffic_light_yellow,
+            (true, TrafficLightColor::Green) => theme.mac_os_traffic_light_green,
+            (false, _) => theme.filled_element,
+        };
+
+        div().w_3().h_3().rounded_full().bg(fill)
+    }
+}
+
+#[derive(Component)]
+pub struct TrafficLights {
+    window_has_focus: bool,
+}
+
+impl TrafficLights {
+    pub fn new() -> Self {
+        Self {
+            window_has_focus: true,
+        }
+    }
+
+    pub fn window_has_focus(mut self, window_has_focus: bool) -> Self {
+        self.window_has_focus = window_has_focus;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div()
+            .flex()
+            .items_center()
+            .gap_2()
+            .child(TrafficLight::new(
+                TrafficLightColor::Red,
+                self.window_has_focus,
+            ))
+            .child(TrafficLight::new(
+                TrafficLightColor::Yellow,
+                self.window_has_focus,
+            ))
+            .child(TrafficLight::new(
+                TrafficLightColor::Green,
+                self.window_has_focus,
+            ))
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use gpui2::{Div, Render};
+
+    use crate::Story;
+
+    use super::*;
+
+    pub struct TrafficLightsStory;
+
+    impl Render for TrafficLightsStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, TrafficLights>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(TrafficLights::new())
+                .child(Story::label(cx, "Unfocused"))
+                .child(TrafficLights::new().window_has_focus(false))
+        }
+    }
+}

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

@@ -0,0 +1,380 @@
+use std::sync::Arc;
+
+use chrono::DateTime;
+use gpui2::{px, relative, rems, Div, Render, Size, View, VisualContext};
+
+use crate::{prelude::*, NotificationsPanel};
+use crate::{
+    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,
+};
+
+#[derive(Clone)]
+pub struct Gpui2UiDebug {
+    pub in_livestream: bool,
+    pub enable_user_settings: bool,
+    pub show_toast: bool,
+}
+
+impl Default for Gpui2UiDebug {
+    fn default() -> Self {
+        Self {
+            in_livestream: false,
+            enable_user_settings: false,
+            show_toast: false,
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct Workspace {
+    title_bar: View<TitleBar>,
+    editor_1: View<EditorPane>,
+    show_project_panel: bool,
+    show_collab_panel: bool,
+    show_chat_panel: bool,
+    show_assistant_panel: bool,
+    show_notifications_panel: bool,
+    show_terminal: bool,
+    show_debug: bool,
+    show_language_selector: bool,
+    debug: Gpui2UiDebug,
+}
+
+impl Workspace {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        Self {
+            title_bar: TitleBar::view(cx, None),
+            editor_1: EditorPane::view(cx),
+            show_project_panel: true,
+            show_collab_panel: false,
+            show_chat_panel: false,
+            show_assistant_panel: false,
+            show_terminal: true,
+            show_language_selector: false,
+            show_debug: false,
+            show_notifications_panel: true,
+            debug: Gpui2UiDebug::default(),
+        }
+    }
+
+    pub fn is_project_panel_open(&self) -> bool {
+        self.show_project_panel
+    }
+
+    pub fn toggle_project_panel(&mut self, cx: &mut ViewContext<Self>) {
+        self.show_project_panel = !self.show_project_panel;
+
+        self.show_collab_panel = false;
+
+        cx.notify();
+    }
+
+    pub fn is_collab_panel_open(&self) -> bool {
+        self.show_collab_panel
+    }
+
+    pub fn toggle_collab_panel(&mut self) {
+        self.show_collab_panel = !self.show_collab_panel;
+
+        self.show_project_panel = false;
+    }
+
+    pub fn is_terminal_open(&self) -> bool {
+        self.show_terminal
+    }
+
+    pub fn toggle_terminal(&mut self, cx: &mut ViewContext<Self>) {
+        self.show_terminal = !self.show_terminal;
+
+        cx.notify();
+    }
+
+    pub fn is_chat_panel_open(&self) -> bool {
+        self.show_chat_panel
+    }
+
+    pub fn toggle_chat_panel(&mut self, cx: &mut ViewContext<Self>) {
+        self.show_chat_panel = !self.show_chat_panel;
+
+        self.show_assistant_panel = false;
+        self.show_notifications_panel = false;
+
+        cx.notify();
+    }
+
+    pub fn is_notifications_panel_open(&self) -> bool {
+        self.show_notifications_panel
+    }
+
+    pub fn toggle_notifications_panel(&mut self, cx: &mut ViewContext<Self>) {
+        self.show_notifications_panel = !self.show_notifications_panel;
+
+        self.show_chat_panel = false;
+        self.show_assistant_panel = false;
+
+        cx.notify();
+    }
+
+    pub fn is_assistant_panel_open(&self) -> bool {
+        self.show_assistant_panel
+    }
+
+    pub fn toggle_assistant_panel(&mut self, cx: &mut ViewContext<Self>) {
+        self.show_assistant_panel = !self.show_assistant_panel;
+
+        self.show_chat_panel = false;
+        self.show_notifications_panel = false;
+
+        cx.notify();
+    }
+
+    pub fn is_language_selector_open(&self) -> bool {
+        self.show_language_selector
+    }
+
+    pub fn toggle_language_selector(&mut self, cx: &mut ViewContext<Self>) {
+        self.show_language_selector = !self.show_language_selector;
+
+        cx.notify();
+    }
+
+    pub fn toggle_debug(&mut self, cx: &mut ViewContext<Self>) {
+        self.show_debug = !self.show_debug;
+
+        cx.notify();
+    }
+
+    pub fn debug_toggle_user_settings(&mut self, cx: &mut ViewContext<Self>) {
+        self.debug.enable_user_settings = !self.debug.enable_user_settings;
+
+        cx.notify();
+    }
+
+    pub fn debug_toggle_livestream(&mut self, cx: &mut ViewContext<Self>) {
+        self.debug.in_livestream = !self.debug.in_livestream;
+
+        self.title_bar = TitleBar::view(
+            cx,
+            Some(static_livestream()).filter(|_| self.debug.in_livestream),
+        );
+
+        cx.notify();
+    }
+
+    pub fn debug_toggle_toast(&mut self, cx: &mut ViewContext<Self>) {
+        self.debug.show_toast = !self.debug.show_toast;
+
+        cx.notify();
+    }
+
+    pub fn view(cx: &mut WindowContext) -> View<Self> {
+        cx.build_view(|cx| Self::new(cx))
+    }
+}
+
+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.
+        // Need to talk with Nathan/Antonio about this.
+        {
+            let settings = user_settings_mut(cx);
+
+            if self.debug.enable_user_settings {
+                settings.list_indent_depth = SettingValue::UserDefined(rems(0.5).into());
+                settings.ui_scale = SettingValue::UserDefined(1.25);
+            } else {
+                *settings = FakeSettings::default();
+            }
+        }
+
+        let root_group = PaneGroup::new_panes(
+            vec![Pane::new(
+                "pane-0",
+                Size {
+                    width: relative(1.).into(),
+                    height: relative(1.).into(),
+                },
+            )
+            .child(self.editor_1.clone())],
+            SplitDirection::Horizontal,
+        );
+
+        div()
+            .relative()
+            .size_full()
+            .flex()
+            .flex_col()
+            .font("Zed Sans")
+            .gap_0()
+            .justify_start()
+            .items_start()
+            .text_color(theme.text)
+            .bg(theme.background)
+            .child(self.title_bar.clone())
+            .child(
+                div()
+                    .flex_1()
+                    .w_full()
+                    .flex()
+                    .flex_row()
+                    .overflow_hidden()
+                    .border_t()
+                    .border_b()
+                    .border_color(theme.border)
+                    .children(
+                        Some(
+                            Panel::new("project-panel-outer", cx)
+                                .side(PanelSide::Left)
+                                .child(ProjectPanel::new("project-panel-inner")),
+                        )
+                        .filter(|_| self.is_project_panel_open()),
+                    )
+                    .children(
+                        Some(
+                            Panel::new("collab-panel-outer", cx)
+                                .child(CollabPanel::new("collab-panel-inner"))
+                                .side(PanelSide::Left),
+                        )
+                        .filter(|_| self.is_collab_panel_open()),
+                    )
+                    // .child(NotificationToast::new(
+                    //     "maxbrunsfeld has requested to add you as a contact.".into(),
+                    // ))
+                    .child(
+                        v_stack()
+                            .flex_1()
+                            .h_full()
+                            .child(div().flex().flex_1().child(root_group))
+                            .children(
+                                Some(
+                                    Panel::new("terminal-panel", cx)
+                                        .child(Terminal::new())
+                                        .allowed_sides(PanelAllowedSides::BottomOnly)
+                                        .side(PanelSide::Bottom),
+                                )
+                                .filter(|_| self.is_terminal_open()),
+                            ),
+                    )
+                    .children(
+                        Some(
+                            Panel::new("chat-panel-outer", cx)
+                                .side(PanelSide::Right)
+                                .child(ChatPanel::new("chat-panel-inner").messages(vec![
+                                    ChatMessage::new(
+                                        "osiewicz".to_string(),
+                                        "is this thing on?".to_string(),
+                                        DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
+                                            .unwrap()
+                                            .naive_local(),
+                                    ),
+                                    ChatMessage::new(
+                                        "maxdeviant".to_string(),
+                                        "Reading you loud and clear!".to_string(),
+                                        DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
+                                            .unwrap()
+                                            .naive_local(),
+                                    ),
+                                ])),
+                        )
+                        .filter(|_| self.is_chat_panel_open()),
+                    )
+                    .children(
+                        Some(
+                            Panel::new("notifications-panel-outer", cx)
+                                .side(PanelSide::Right)
+                                .child(NotificationsPanel::new("notifications-panel-inner")),
+                        )
+                        .filter(|_| self.is_notifications_panel_open()),
+                    )
+                    .children(
+                        Some(
+                            Panel::new("assistant-panel-outer", cx)
+                                .child(AssistantPanel::new("assistant-panel-inner")),
+                        )
+                        .filter(|_| self.is_assistant_panel_open()),
+                    ),
+            )
+            .child(StatusBar::new())
+            .when(self.debug.show_toast, |this| {
+                this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
+            })
+            .children(
+                Some(
+                    div()
+                        .absolute()
+                        .top(px(50.))
+                        .left(px(640.))
+                        .z_index(8)
+                        .child(LanguageSelector::new("language-selector")),
+                )
+                .filter(|_| self.is_language_selector_open()),
+            )
+            .z_index(8)
+            // Debug
+            .child(
+                v_stack()
+                    .z_index(9)
+                    .absolute()
+                    .bottom_10()
+                    .left_1_4()
+                    .w_40()
+                    .gap_2()
+                    .when(self.show_debug, |this| {
+                        this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
+                            Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
+                        ))
+                        .child(
+                            Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
+                                |workspace, cx| workspace.debug_toggle_toast(cx),
+                            )),
+                        )
+                        .child(
+                            Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
+                                |workspace, cx| workspace.debug_toggle_livestream(cx),
+                            )),
+                        )
+                    })
+                    .child(
+                        Button::<Workspace>::new("Toggle Debug")
+                            .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
+                    ),
+            )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use gpui2::VisualContext;
+
+    pub struct WorkspaceStory {
+        workspace: View<Workspace>,
+    }
+
+    impl WorkspaceStory {
+        pub fn view(cx: &mut WindowContext) -> View<Self> {
+            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.rs 🔗

@@ -0,0 +1,19 @@
+mod avatar;
+mod button;
+mod details;
+mod icon;
+mod input;
+mod label;
+mod player;
+mod stack;
+mod tool_divider;
+
+pub use avatar::*;
+pub use button::*;
+pub use details::*;
+pub use icon::*;
+pub use input::*;
+pub use label::*;
+pub use player::*;
+pub use stack::*;
+pub use tool_divider::*;

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

@@ -0,0 +1,69 @@
+use gpui2::img;
+
+use crate::prelude::*;
+
+#[derive(Component)]
+pub struct Avatar {
+    src: SharedString,
+    shape: Shape,
+}
+
+impl Avatar {
+    pub fn new(src: impl Into<SharedString>) -> Self {
+        Self {
+            src: src.into(),
+            shape: Shape::Circle,
+        }
+    }
+
+    pub fn shape(mut self, shape: Shape) -> Self {
+        self.shape = shape;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let mut img = img();
+
+        if self.shape == Shape::Circle {
+            img = img.rounded_full();
+        } else {
+            img = img.rounded_md();
+        }
+
+        img.uri(self.src.clone())
+            .size_4()
+            .bg(theme.image_fallback_background)
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+
+    pub struct AvatarStory;
+
+    impl Render for AvatarStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, Avatar>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(Avatar::new(
+                    "https://avatars.githubusercontent.com/u/1714999?v=4",
+                ))
+                .child(Story::label(cx, "Rounded rectangle"))
+                .child(
+                    Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
+                        .shape(Shape::RoundedRectangle),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,383 @@
+use std::sync::Arc;
+
+use gpui2::{div, DefiniteLength, Hsla, MouseButton, WindowContext};
+
+use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor};
+use crate::{prelude::*, LineHeightStyle};
+
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum IconPosition {
+    #[default]
+    Left,
+    Right,
+}
+
+#[derive(Default, Copy, Clone, PartialEq)]
+pub enum ButtonVariant {
+    #[default]
+    Ghost,
+    Filled,
+}
+
+impl ButtonVariant {
+    pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
+        let theme = theme(cx);
+
+        match self {
+            ButtonVariant::Ghost => theme.ghost_element,
+            ButtonVariant::Filled => theme.filled_element,
+        }
+    }
+
+    pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
+        let theme = theme(cx);
+
+        match self {
+            ButtonVariant::Ghost => theme.ghost_element_hover,
+            ButtonVariant::Filled => theme.filled_element_hover,
+        }
+    }
+
+    pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
+        let theme = theme(cx);
+
+        match self {
+            ButtonVariant::Ghost => theme.ghost_element_active,
+            ButtonVariant::Filled => theme.filled_element_active,
+        }
+    }
+}
+
+pub type ClickHandler<S> = Arc<dyn Fn(&mut S, &mut ViewContext<S>) + Send + Sync>;
+
+struct ButtonHandlers<V: 'static> {
+    click: Option<ClickHandler<V>>,
+}
+
+unsafe impl<S> Send for ButtonHandlers<S> {}
+unsafe impl<S> Sync for ButtonHandlers<S> {}
+
+impl<V: 'static> Default for ButtonHandlers<V> {
+    fn default() -> Self {
+        Self { click: None }
+    }
+}
+
+#[derive(Component)]
+pub struct Button<V: 'static> {
+    disabled: bool,
+    handlers: ButtonHandlers<V>,
+    icon: Option<Icon>,
+    icon_position: Option<IconPosition>,
+    label: SharedString,
+    variant: ButtonVariant,
+    width: Option<DefiniteLength>,
+}
+
+impl<V: 'static> Button<V> {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            disabled: false,
+            handlers: ButtonHandlers::default(),
+            icon: None,
+            icon_position: None,
+            label: label.into(),
+            variant: Default::default(),
+            width: Default::default(),
+        }
+    }
+
+    pub fn ghost(label: impl Into<SharedString>) -> Self {
+        Self::new(label).variant(ButtonVariant::Ghost)
+    }
+
+    pub fn variant(mut self, variant: ButtonVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    pub fn icon(mut self, icon: Icon) -> Self {
+        self.icon = Some(icon);
+        self
+    }
+
+    pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
+        if self.icon.is_none() {
+            panic!("An icon must be present if an icon_position is provided.");
+        }
+        self.icon_position = Some(icon_position);
+        self
+    }
+
+    pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
+        self.width = width;
+        self
+    }
+
+    pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
+        self.handlers.click = Some(handler);
+        self
+    }
+
+    pub fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
+        self
+    }
+
+    fn label_color(&self) -> LabelColor {
+        if self.disabled {
+            LabelColor::Disabled
+        } else {
+            Default::default()
+        }
+    }
+
+    fn icon_color(&self) -> IconColor {
+        if self.disabled {
+            IconColor::Disabled
+        } else {
+            Default::default()
+        }
+    }
+
+    fn render_label(&self) -> Label {
+        Label::new(self.label.clone())
+            .color(self.label_color())
+            .line_height_style(LineHeightStyle::UILabel)
+    }
+
+    fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
+        self.icon.map(|i| IconElement::new(i).color(icon_color))
+    }
+
+    pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let icon_color = self.icon_color();
+
+        let mut button = h_stack()
+            .relative()
+            .id(SharedString::from(format!("{}", self.label)))
+            .p_1()
+            .text_size(ui_size(cx, 1.))
+            .rounded_md()
+            .bg(self.variant.bg_color(cx))
+            .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
+            .active(|style| style.bg(self.variant.bg_color_active(cx)));
+
+        match (self.icon, self.icon_position) {
+            (Some(_), Some(IconPosition::Left)) => {
+                button = button
+                    .gap_1()
+                    .child(self.render_label())
+                    .children(self.render_icon(icon_color))
+            }
+            (Some(_), Some(IconPosition::Right)) => {
+                button = button
+                    .gap_1()
+                    .children(self.render_icon(icon_color))
+                    .child(self.render_label())
+            }
+            (_, _) => button = button.child(self.render_label()),
+        }
+
+        if let Some(width) = self.width {
+            button = button.w(width).justify_center();
+        }
+
+        if let Some(click_handler) = self.handlers.click.clone() {
+            button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
+                click_handler(state, cx);
+            });
+        }
+
+        button
+    }
+}
+
+#[derive(Component)]
+pub struct ButtonGroup<V: 'static> {
+    buttons: Vec<Button<V>>,
+}
+
+impl<V: 'static> ButtonGroup<V> {
+    pub fn new(buttons: Vec<Button<V>>) -> Self {
+        Self { buttons }
+    }
+
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let mut el = h_stack().text_size(ui_size(cx, 1.));
+
+        for button in self.buttons {
+            el = el.child(button.render(_view, cx));
+        }
+
+        el
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::{h_stack, v_stack, LabelColor, Story};
+    use gpui2::{rems, Div, Render};
+    use strum::IntoEnumIterator;
+
+    pub struct ButtonStory;
+
+    impl Render for ButtonStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            let states = InteractionState::iter();
+
+            Story::container(cx)
+                .child(Story::title_for::<_, Button<Self>>(cx))
+                .child(
+                    div()
+                        .flex()
+                        .gap_8()
+                        .child(
+                            div()
+                                .child(Story::label(cx, "Ghost (Default)"))
+                                .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                    v_stack()
+                                        .gap_1()
+                                        .child(
+                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                        )
+                                        .child(
+                                            Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
+                                        )
+                                })))
+                                .child(Story::label(cx, "Ghost – Left Icon"))
+                                .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                    v_stack()
+                                        .gap_1()
+                                        .child(
+                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                        )
+                                        .child(
+                                            Button::new("Label")
+                                                .variant(ButtonVariant::Ghost)
+                                                .icon(Icon::Plus)
+                                                .icon_position(IconPosition::Left), // .state(state),
+                                        )
+                                })))
+                                .child(Story::label(cx, "Ghost – Right Icon"))
+                                .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                    v_stack()
+                                        .gap_1()
+                                        .child(
+                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                        )
+                                        .child(
+                                            Button::new("Label")
+                                                .variant(ButtonVariant::Ghost)
+                                                .icon(Icon::Plus)
+                                                .icon_position(IconPosition::Right), // .state(state),
+                                        )
+                                }))),
+                        )
+                        .child(
+                            div()
+                                .child(Story::label(cx, "Filled"))
+                                .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                    v_stack()
+                                        .gap_1()
+                                        .child(
+                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                        )
+                                        .child(
+                                            Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
+                                        )
+                                })))
+                                .child(Story::label(cx, "Filled – Left Button"))
+                                .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                    v_stack()
+                                        .gap_1()
+                                        .child(
+                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                        )
+                                        .child(
+                                            Button::new("Label")
+                                                .variant(ButtonVariant::Filled)
+                                                .icon(Icon::Plus)
+                                                .icon_position(IconPosition::Left), // .state(state),
+                                        )
+                                })))
+                                .child(Story::label(cx, "Filled – Right Button"))
+                                .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                    v_stack()
+                                        .gap_1()
+                                        .child(
+                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                        )
+                                        .child(
+                                            Button::new("Label")
+                                                .variant(ButtonVariant::Filled)
+                                                .icon(Icon::Plus)
+                                                .icon_position(IconPosition::Right), // .state(state),
+                                        )
+                                }))),
+                        )
+                        .child(
+                            div()
+                                .child(Story::label(cx, "Fixed With"))
+                                .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                    v_stack()
+                                        .gap_1()
+                                        .child(
+                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                        )
+                                        .child(
+                                            Button::new("Label")
+                                                .variant(ButtonVariant::Filled)
+                                                // .state(state)
+                                                .width(Some(rems(6.).into())),
+                                        )
+                                })))
+                                .child(Story::label(cx, "Fixed With – Left Icon"))
+                                .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                    v_stack()
+                                        .gap_1()
+                                        .child(
+                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                        )
+                                        .child(
+                                            Button::new("Label")
+                                                .variant(ButtonVariant::Filled)
+                                                // .state(state)
+                                                .icon(Icon::Plus)
+                                                .icon_position(IconPosition::Left)
+                                                .width(Some(rems(6.).into())),
+                                        )
+                                })))
+                                .child(Story::label(cx, "Fixed With – Right Icon"))
+                                .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                    v_stack()
+                                        .gap_1()
+                                        .child(
+                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                        )
+                                        .child(
+                                            Button::new("Label")
+                                                .variant(ButtonVariant::Filled)
+                                                // .state(state)
+                                                .icon(Icon::Plus)
+                                                .icon_position(IconPosition::Right)
+                                                .width(Some(rems(6.).into())),
+                                        )
+                                }))),
+                        ),
+                )
+                .child(Story::label(cx, "Button with `on_click`"))
+                .child(
+                    Button::new("Label")
+                        .variant(ButtonVariant::Ghost)
+                        .on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,79 @@
+use crate::{prelude::*, v_stack, ButtonGroup};
+
+#[derive(Component)]
+pub struct Details<V: 'static> {
+    text: &'static str,
+    meta: Option<&'static str>,
+    actions: Option<ButtonGroup<V>>,
+}
+
+impl<V: 'static> Details<V> {
+    pub fn new(text: &'static str) -> Self {
+        Self {
+            text,
+            meta: None,
+            actions: None,
+        }
+    }
+
+    pub fn meta_text(mut self, meta: &'static str) -> Self {
+        self.meta = Some(meta);
+        self
+    }
+
+    pub fn actions(mut self, actions: ButtonGroup<V>) -> Self {
+        self.actions = Some(actions);
+        self
+    }
+
+    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .p_1()
+            .gap_0p5()
+            .text_xs()
+            .text_color(theme.text)
+            .size_full()
+            .child(self.text)
+            .children(self.meta.map(|m| m))
+            .children(self.actions.map(|a| a))
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::{Button, Story};
+    use gpui2::{Div, Render};
+
+    pub struct DetailsStory;
+
+    impl Render for DetailsStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(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"))
+                .child(
+                    Details::new("The quick brown fox jumps over the lazy dog")
+                        .meta_text("Sphinx of black quartz, judge my vow."),
+                )
+                .child(Story::label(cx, "With meta and actions"))
+                .child(
+                    Details::new("The quick brown fox jumps over the lazy dog")
+                        .meta_text("Sphinx of black quartz, judge my vow.")
+                        .actions(ButtonGroup::new(vec![
+                            Button::new("Decline"),
+                            Button::new("Accept").variant(crate::ButtonVariant::Filled),
+                        ])),
+                )
+        }
+    }
+}

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

@@ -0,0 +1,215 @@
+use gpui2::{svg, Hsla};
+use strum::EnumIter;
+
+use crate::prelude::*;
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum IconSize {
+    Small,
+    #[default]
+    Medium,
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum IconColor {
+    #[default]
+    Default,
+    Muted,
+    Disabled,
+    Placeholder,
+    Accent,
+    Error,
+    Warning,
+    Success,
+    Info,
+}
+
+impl IconColor {
+    pub fn color(self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        match self {
+            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(),
+        }
+    }
+}
+
+#[derive(Debug, Default, PartialEq, Copy, Clone, EnumIter)]
+pub enum Icon {
+    Ai,
+    ArrowLeft,
+    ArrowRight,
+    ArrowUpRight,
+    AudioOff,
+    AudioOn,
+    Bolt,
+    ChevronDown,
+    ChevronLeft,
+    ChevronRight,
+    ChevronUp,
+    Close,
+    Exit,
+    ExclamationTriangle,
+    File,
+    FileGeneric,
+    FileDoc,
+    FileGit,
+    FileLock,
+    FileRust,
+    FileToml,
+    FileTree,
+    Folder,
+    FolderOpen,
+    FolderX,
+    #[default]
+    Hash,
+    InlayHint,
+    MagicWand,
+    MagnifyingGlass,
+    Maximize,
+    Menu,
+    MessageBubbles,
+    Mic,
+    MicMute,
+    Plus,
+    Quote,
+    Replace,
+    ReplaceAll,
+    Screen,
+    SelectAll,
+    Split,
+    SplitMessage,
+    Terminal,
+    XCircle,
+    Copilot,
+    Envelope,
+}
+
+impl Icon {
+    pub fn path(self) -> &'static str {
+        match self {
+            Icon::Ai => "icons/ai.svg",
+            Icon::ArrowLeft => "icons/arrow_left.svg",
+            Icon::ArrowRight => "icons/arrow_right.svg",
+            Icon::ArrowUpRight => "icons/arrow_up_right.svg",
+            Icon::AudioOff => "icons/speaker-off.svg",
+            Icon::AudioOn => "icons/speaker-loud.svg",
+            Icon::Bolt => "icons/bolt.svg",
+            Icon::ChevronDown => "icons/chevron_down.svg",
+            Icon::ChevronLeft => "icons/chevron_left.svg",
+            Icon::ChevronRight => "icons/chevron_right.svg",
+            Icon::ChevronUp => "icons/chevron_up.svg",
+            Icon::Close => "icons/x.svg",
+            Icon::Exit => "icons/exit.svg",
+            Icon::ExclamationTriangle => "icons/warning.svg",
+            Icon::File => "icons/file.svg",
+            Icon::FileGeneric => "icons/file_icons/file.svg",
+            Icon::FileDoc => "icons/file_icons/book.svg",
+            Icon::FileGit => "icons/file_icons/git.svg",
+            Icon::FileLock => "icons/file_icons/lock.svg",
+            Icon::FileRust => "icons/file_icons/rust.svg",
+            Icon::FileToml => "icons/file_icons/toml.svg",
+            Icon::FileTree => "icons/project.svg",
+            Icon::Folder => "icons/file_icons/folder.svg",
+            Icon::FolderOpen => "icons/file_icons/folder_open.svg",
+            Icon::FolderX => "icons/stop_sharing.svg",
+            Icon::Hash => "icons/hash.svg",
+            Icon::InlayHint => "icons/inlay_hint.svg",
+            Icon::MagicWand => "icons/magic-wand.svg",
+            Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
+            Icon::Maximize => "icons/maximize.svg",
+            Icon::Menu => "icons/menu.svg",
+            Icon::MessageBubbles => "icons/conversations.svg",
+            Icon::Mic => "icons/mic.svg",
+            Icon::MicMute => "icons/mic-mute.svg",
+            Icon::Plus => "icons/plus.svg",
+            Icon::Quote => "icons/quote.svg",
+            Icon::Replace => "icons/replace.svg",
+            Icon::ReplaceAll => "icons/replace_all.svg",
+            Icon::Screen => "icons/desktop.svg",
+            Icon::SelectAll => "icons/select-all.svg",
+            Icon::Split => "icons/split.svg",
+            Icon::SplitMessage => "icons/split_message.svg",
+            Icon::Terminal => "icons/terminal.svg",
+            Icon::XCircle => "icons/error.svg",
+            Icon::Copilot => "icons/copilot.svg",
+            Icon::Envelope => "icons/feedback.svg",
+        }
+    }
+}
+
+#[derive(Component)]
+pub struct IconElement {
+    icon: Icon,
+    color: IconColor,
+    size: IconSize,
+}
+
+impl IconElement {
+    pub fn new(icon: Icon) -> Self {
+        Self {
+            icon,
+            color: IconColor::default(),
+            size: IconSize::default(),
+        }
+    }
+
+    pub fn color(mut self, color: IconColor) -> Self {
+        self.color = color;
+        self
+    }
+
+    pub fn size(mut self, size: IconSize) -> Self {
+        self.size = size;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let fill = self.color.color(cx);
+        let svg_size = match self.size {
+            IconSize::Small => ui_size(cx, 12. / 14.),
+            IconSize::Medium => ui_size(cx, 15. / 14.),
+        };
+
+        svg()
+            .size(svg_size)
+            .flex_none()
+            .path(self.icon.path())
+            .text_color(fill)
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use gpui2::{Div, Render};
+    use strum::IntoEnumIterator;
+
+    use crate::Story;
+
+    use super::*;
+
+    pub struct IconStory;
+
+    impl Render for IconStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            let icons = Icon::iter();
+
+            Story::container(cx)
+                .child(Story::title_for::<_, IconElement>(cx))
+                .child(Story::label(cx, "All Icons"))
+                .child(div().flex().gap_3().children(icons.map(IconElement::new)))
+        }
+    }
+}

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

@@ -0,0 +1,131 @@
+use crate::prelude::*;
+use crate::Label;
+use crate::LabelColor;
+
+#[derive(Default, PartialEq)]
+pub enum InputVariant {
+    #[default]
+    Ghost,
+    Filled,
+}
+
+#[derive(Component)]
+pub struct Input {
+    placeholder: SharedString,
+    value: String,
+    state: InteractionState,
+    variant: InputVariant,
+    disabled: bool,
+    is_active: bool,
+}
+
+impl Input {
+    pub fn new(placeholder: impl Into<SharedString>) -> Self {
+        Self {
+            placeholder: placeholder.into(),
+            value: "".to_string(),
+            state: InteractionState::default(),
+            variant: InputVariant::default(),
+            disabled: false,
+            is_active: false,
+        }
+    }
+
+    pub fn value(mut self, value: String) -> Self {
+        self.value = value;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    pub fn variant(mut self, variant: InputVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    pub fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
+        self
+    }
+
+    pub fn is_active(mut self, is_active: bool) -> Self {
+        self.is_active = is_active;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
+            InputVariant::Ghost => (
+                theme.ghost_element,
+                theme.ghost_element_hover,
+                theme.ghost_element_active,
+            ),
+            InputVariant::Filled => (
+                theme.filled_element,
+                theme.filled_element_hover,
+                theme.filled_element_active,
+            ),
+        };
+
+        let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
+            LabelColor::Disabled
+        } else {
+            LabelColor::Placeholder
+        });
+
+        let label = Label::new(self.value.clone()).color(if self.disabled {
+            LabelColor::Disabled
+        } else {
+            LabelColor::Default
+        });
+
+        div()
+            .id("input")
+            .h_7()
+            .w_full()
+            .px_2()
+            .border()
+            .border_color(theme.transparent)
+            .bg(input_bg)
+            .hover(|style| style.bg(input_hover_bg))
+            .active(|style| style.bg(input_active_bg))
+            .flex()
+            .items_center()
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .text_sm()
+                    .when(self.value.is_empty(), |this| this.child(placeholder_label))
+                    .when(!self.value.is_empty(), |this| this.child(label)),
+            )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+
+    pub struct InputStory;
+
+    impl Render for InputStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, Input>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(div().flex().child(Input::new("Search")))
+        }
+    }
+}

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

@@ -0,0 +1,221 @@
+use gpui2::{relative, Hsla, WindowContext};
+use smallvec::SmallVec;
+
+use crate::prelude::*;
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum LabelColor {
+    #[default]
+    Default,
+    Muted,
+    Created,
+    Modified,
+    Deleted,
+    Disabled,
+    Hidden,
+    Placeholder,
+    Accent,
+}
+
+impl LabelColor {
+    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+
+        match self {
+            Self::Default => theme.text,
+            Self::Muted => theme.text_muted,
+            Self::Created => gpui2::red(),
+            Self::Modified => gpui2::red(),
+            Self::Deleted => gpui2::red(),
+            Self::Disabled => theme.text_disabled,
+            Self::Hidden => gpui2::red(),
+            Self::Placeholder => theme.text_placeholder,
+            Self::Accent => gpui2::red(),
+        }
+    }
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum LineHeightStyle {
+    #[default]
+    TextLabel,
+    /// Sets the line height to 1
+    UILabel,
+}
+
+#[derive(Component)]
+pub struct Label {
+    label: SharedString,
+    line_height_style: LineHeightStyle,
+    color: LabelColor,
+    strikethrough: bool,
+}
+
+impl Label {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            line_height_style: LineHeightStyle::default(),
+            color: LabelColor::Default,
+            strikethrough: false,
+        }
+    }
+
+    pub fn color(mut self, color: LabelColor) -> Self {
+        self.color = color;
+        self
+    }
+
+    pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
+        self.line_height_style = line_height_style;
+        self
+    }
+
+    pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
+        self.strikethrough = strikethrough;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        div()
+            .when(self.strikethrough, |this| {
+                this.relative().child(
+                    div()
+                        .absolute()
+                        .top_px()
+                        .my_auto()
+                        .w_full()
+                        .h_px()
+                        .bg(LabelColor::Hidden.hsla(cx)),
+                )
+            })
+            .text_size(ui_size(cx, 1.))
+            .when(self.line_height_style == LineHeightStyle::UILabel, |this| {
+                this.line_height(relative(1.))
+            })
+            .text_color(self.color.hsla(cx))
+            .child(self.label.clone())
+    }
+}
+
+#[derive(Component)]
+pub struct HighlightedLabel {
+    label: SharedString,
+    color: LabelColor,
+    highlight_indices: Vec<usize>,
+    strikethrough: bool,
+}
+
+impl HighlightedLabel {
+    pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
+        Self {
+            label: label.into(),
+            color: LabelColor::Default,
+            highlight_indices,
+            strikethrough: false,
+        }
+    }
+
+    pub fn color(mut self, color: LabelColor) -> Self {
+        self.color = color;
+        self
+    }
+
+    pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
+        self.strikethrough = strikethrough;
+        self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        let highlight_color = theme.text_accent;
+
+        let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
+
+        let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
+
+        for (char_ix, char) in self.label.char_indices() {
+            let mut color = self.color.hsla(cx);
+
+            if let Some(highlight_ix) = highlight_indices.peek() {
+                if char_ix == *highlight_ix {
+                    color = highlight_color;
+
+                    highlight_indices.next();
+                }
+            }
+
+            let last_run = runs.last_mut();
+
+            let start_new_run = if let Some(last_run) = last_run {
+                if color == last_run.color {
+                    last_run.text.push(char);
+                    false
+                } else {
+                    true
+                }
+            } else {
+                true
+            };
+
+            if start_new_run {
+                runs.push(Run {
+                    text: char.to_string(),
+                    color,
+                });
+            }
+        }
+
+        div()
+            .flex()
+            .when(self.strikethrough, |this| {
+                this.relative().child(
+                    div()
+                        .absolute()
+                        .top_px()
+                        .my_auto()
+                        .w_full()
+                        .h_px()
+                        .bg(LabelColor::Hidden.hsla(cx)),
+                )
+            })
+            .children(
+                runs.into_iter()
+                    .map(|run| div().text_color(run.color).child(run.text)),
+            )
+    }
+}
+
+/// A run of text that receives the same style.
+struct Run {
+    pub text: String,
+    pub color: Hsla,
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::Story;
+    use gpui2::{Div, Render};
+
+    pub struct LabelStory;
+
+    impl Render for LabelStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx)
+                .child(Story::title_for::<_, Label>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(Label::new("Hello, world!"))
+                .child(Story::label(cx, "Highlighted"))
+                .child(HighlightedLabel::new(
+                    "Hello, world!",
+                    vec![0, 1, 2, 7, 8, 12],
+                ))
+        }
+    }
+}

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

@@ -0,0 +1,158 @@
+use gpui2::{Hsla, ViewContext};
+
+use crate::theme;
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum PlayerStatus {
+    #[default]
+    Offline,
+    Online,
+    InCall,
+    Away,
+    DoNotDisturb,
+    Invisible,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum MicStatus {
+    Muted,
+    #[default]
+    Unmuted,
+}
+
+impl MicStatus {
+    pub fn inverse(&self) -> Self {
+        match self {
+            Self::Muted => Self::Unmuted,
+            Self::Unmuted => Self::Muted,
+        }
+    }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum VideoStatus {
+    On,
+    #[default]
+    Off,
+}
+
+impl VideoStatus {
+    pub fn inverse(&self) -> Self {
+        match self {
+            Self::On => Self::Off,
+            Self::Off => Self::On,
+        }
+    }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum ScreenShareStatus {
+    Shared,
+    #[default]
+    NotShared,
+}
+
+impl ScreenShareStatus {
+    pub fn inverse(&self) -> Self {
+        match self {
+            Self::Shared => Self::NotShared,
+            Self::NotShared => Self::Shared,
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct PlayerCallStatus {
+    pub mic_status: MicStatus,
+    /// Indicates if the player is currently speaking
+    /// And the intensity of the volume coming through
+    ///
+    /// 0.0 - 1.0
+    pub voice_activity: f32,
+    pub video_status: VideoStatus,
+    pub screen_share_status: ScreenShareStatus,
+    pub in_current_project: bool,
+    pub disconnected: bool,
+    pub following: Option<Vec<Player>>,
+    pub followers: Option<Vec<Player>>,
+}
+
+impl PlayerCallStatus {
+    pub fn new() -> Self {
+        Self {
+            mic_status: MicStatus::default(),
+            voice_activity: 0.,
+            video_status: VideoStatus::default(),
+            screen_share_status: ScreenShareStatus::default(),
+            in_current_project: true,
+            disconnected: false,
+            following: None,
+            followers: None,
+        }
+    }
+}
+
+#[derive(PartialEq, Clone)]
+pub struct Player {
+    index: usize,
+    avatar_src: String,
+    username: String,
+    status: PlayerStatus,
+}
+
+#[derive(Clone)]
+pub struct PlayerWithCallStatus {
+    player: Player,
+    call_status: PlayerCallStatus,
+}
+
+impl PlayerWithCallStatus {
+    pub fn new(player: Player, call_status: PlayerCallStatus) -> Self {
+        Self {
+            player,
+            call_status,
+        }
+    }
+
+    pub fn get_player(&self) -> &Player {
+        &self.player
+    }
+
+    pub fn get_call_status(&self) -> &PlayerCallStatus {
+        &self.call_status
+    }
+}
+
+impl Player {
+    pub fn new(index: usize, avatar_src: String, username: String) -> Self {
+        Self {
+            index,
+            avatar_src,
+            username,
+            status: Default::default(),
+        }
+    }
+
+    pub fn set_status(mut self, status: PlayerStatus) -> Self {
+        self.status = status;
+        self
+    }
+
+    pub fn cursor_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
+        let theme = theme(cx);
+        theme.players[self.index].cursor
+    }
+
+    pub fn selection_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
+        let theme = theme(cx);
+        theme.players[self.index].selection
+    }
+
+    pub fn avatar_src(&self) -> &str {
+        &self.avatar_src
+    }
+
+    pub fn index(&self) -> usize {
+        self.index
+    }
+}

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

@@ -0,0 +1,31 @@
+use gpui2::{div, Div};
+
+use crate::prelude::*;
+
+pub trait Stack: Styled + Sized {
+    /// Horizontally stacks elements.
+    fn h_stack(self) -> Self {
+        self.flex().flex_row().items_center()
+    }
+
+    /// Vertically stacks elements.
+    fn v_stack(self) -> Self {
+        self.flex().flex_col()
+    }
+}
+
+impl<V: 'static> Stack for Div<V> {}
+
+/// Horizontally stacks elements.
+///
+/// Sets `flex()`, `flex_row()`, `items_center()`
+pub fn h_stack<V: 'static>() -> Div<V> {
+    div().h_stack()
+}
+
+/// Vertically stacks elements.
+///
+/// Sets `flex()`, `flex_col()`
+pub fn v_stack<V: 'static>() -> Div<V> {
+    div().v_stack()
+}

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

@@ -0,0 +1,16 @@
+use crate::prelude::*;
+
+#[derive(Component)]
+pub struct ToolDivider;
+
+impl ToolDivider {
+    pub fn new() -> Self {
+        Self
+    }
+
+    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+        let theme = theme(cx);
+
+        div().w_px().h_3().bg(theme.border)
+    }
+}

crates/ui2/src/elevation.md 🔗

@@ -0,0 +1,85 @@
+TODO: Originally sourced from Material Design 3, Rewrite to be more Zed specific
+
+# Elevation
+
+Zed applies elevation to all surfaces and components, which are categorized into levels.
+
+Elevation accomplishes the following:
+- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars.
+- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards.
+- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces.
+
+Elevations are the initial elevation values assigned to components by default.
+
+Components may transition to a higher elevation in some cases, like user interations.
+
+On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest.
+
+## Understanding Elevation
+
+Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations.
+
+Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview)
+
+## Elevation
+
+1. App Background (e.x.: Workspace, system window)
+1. UI Surface (e.x.: Title Bar, Panel, Tab Bar)
+1. Elevated Surface (e.x.: Palette, Notification, Floating Window)
+1. Wash
+1. Modal Surfaces (e.x.: Modal)
+1. Dragged Element (This is a special case, see Layer section below)
+
+### App Background
+
+The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app.
+
+### UI Surface
+
+The UI Surface, located above the app background, is the standard level for all elements
+
+Example Elements: Title Bar, Panel, Tab Bar, Editor
+
+### Elevated Surface
+
+Non-Modal Elevated Surfaces appear above the UI surface layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc.
+
+Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels
+
+You could imagine a variant of the assistant that floats in a window above the editor on this elevation, or a floating terminal window that becomes less opaque when not focused.
+
+### Wash
+
+Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design.
+
+### Modal Surfaces
+
+Modal Surfaces are used for elements that should appear above all other UI elements and are located above the wash layer. This is the maximum elevation at which UI elements can be rendered
+
+Elements rendered at this layer have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal.
+
+If the element does not have this behavior, it should be rendered at the Elevated Surface layer.
+
+## Layer
+Each elevation that can contain elements has its own set of layers that are nested within the elevations.
+
+1. TBD (Z -1 layer)
+1. Element (Text, button, surface, etc)
+1. Elevated Element (Popover, Context Menu, Tooltip)
+999. Dragged Element -> Highest Elevation
+
+Dragged elements jump to the highest elevation the app can render. An active drag should _always_ be the most foreground element in the app at any time.
+
+🚧 Work in Progress 🚧
+
+## Element
+Each elevation that can contain elements has it's own set of layers:
+
+1. Effects
+1. Background
+1. Tint
+1. Highlight
+1. Content
+1. Overlay
+
+🚧 Work in Progress 🚧

crates/ui2/src/elevation.rs 🔗

@@ -0,0 +1,70 @@
+#[doc = include_str!("elevation.md")]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Elevation {
+    ElevationIndex(ElevationIndex),
+    LayerIndex(LayerIndex),
+    ElementIndex(ElementIndex),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ElevationIndex {
+    AppBackground,
+    UISurface,
+    ElevatedSurface,
+    Wash,
+    ModalSurfaces,
+    DraggedElement,
+}
+
+impl ElevationIndex {
+    pub fn usize(&self) -> usize {
+        match *self {
+            ElevationIndex::AppBackground => 0,
+            ElevationIndex::UISurface => 100,
+            ElevationIndex::ElevatedSurface => 200,
+            ElevationIndex::Wash => 300,
+            ElevationIndex::ModalSurfaces => 400,
+            ElevationIndex::DraggedElement => 900,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum LayerIndex {
+    BehindElement,
+    Element,
+    ElevatedElement,
+}
+
+impl LayerIndex {
+    pub fn usize(&self) -> usize {
+        match *self {
+            LayerIndex::BehindElement => 0,
+            LayerIndex::Element => 100,
+            LayerIndex::ElevatedElement => 200,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ElementIndex {
+    Effect,
+    Background,
+    Tint,
+    Highlight,
+    Content,
+    Overlay,
+}
+
+impl ElementIndex {
+    pub fn usize(&self) -> usize {
+        match *self {
+            ElementIndex::Effect => 0,
+            ElementIndex::Background => 100,
+            ElementIndex::Tint => 200,
+            ElementIndex::Highlight => 300,
+            ElementIndex::Content => 400,
+            ElementIndex::Overlay => 500,
+        }
+    }
+}

crates/ui2/src/lib.rs 🔗

@@ -0,0 +1,44 @@
+//! # UI – Zed UI Primitives & Components
+//!
+//! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI.
+//!
+//! ## Work in Progress
+//!
+//! This crate is still a work in progress. The initial primitives and components are built for getting all the UI on the screen,
+//! much of the state and functionality is mocked or hard codeded, and performance has not been a focus.
+//!
+//! Expect some inconsistencies from component to component as we work out the best way to build these components.
+//!
+//! ## Design Philosophy
+//!
+//! Work in Progress!
+//!
+
+// TODO: Fix warnings instead of supressing.
+#![allow(dead_code, unused_variables)]
+
+mod components;
+mod elements;
+mod elevation;
+pub mod prelude;
+pub mod settings;
+mod static_data;
+
+pub use components::*;
+pub use elements::*;
+pub use prelude::*;
+pub use static_data::*;
+
+// This needs to be fully qualified with `crate::` otherwise we get a panic
+// at:
+//   thread '<unnamed>' panicked at crates/gpui2/src/platform/mac/platform.rs:66:81:
+//   called `Option::unwrap()` on a `None` value
+//
+// AFAICT this is something to do with conflicting names between crates and modules that
+// interfaces with declaring the `ClassDecl`.
+pub use crate::settings::*;
+
+#[cfg(feature = "stories")]
+mod story;
+#[cfg(feature = "stories")]
+pub use story::*;

crates/ui2/src/prelude.rs 🔗

@@ -0,0 +1,232 @@
+pub use gpui2::{
+    div, Component, Element, ElementId, ParentElement, SharedString, StatefulInteractive,
+    StatelessInteractive, Styled, ViewContext, WindowContext,
+};
+
+pub use crate::elevation::*;
+use crate::settings::user_settings;
+pub use crate::ButtonVariant;
+pub use theme2::theme;
+
+use gpui2::{rems, Hsla, Rems};
+use strum::EnumIter;
+
+pub fn ui_size(cx: &mut WindowContext, size: f32) -> Rems {
+    const UI_SCALE_RATIO: f32 = 0.875;
+
+    let settings = user_settings(cx);
+
+    rems(*settings.ui_scale * UI_SCALE_RATIO * size)
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum FileSystemStatus {
+    #[default]
+    None,
+    Conflict,
+    Deleted,
+}
+
+impl std::fmt::Display for FileSystemStatus {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Self::None => "None",
+                Self::Conflict => "Conflict",
+                Self::Deleted => "Deleted",
+            }
+        )
+    }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum GitStatus {
+    #[default]
+    None,
+    Created,
+    Modified,
+    Deleted,
+    Conflict,
+    Renamed,
+}
+
+impl GitStatus {
+    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+
+        match self {
+            Self::None => theme.transparent,
+            Self::Created => theme.git_created,
+            Self::Modified => theme.git_modified,
+            Self::Deleted => theme.git_deleted,
+            Self::Conflict => theme.git_conflict,
+            Self::Renamed => theme.git_renamed,
+        }
+    }
+}
+
+impl std::fmt::Display for GitStatus {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Self::None => "None",
+                Self::Created => "Created",
+                Self::Modified => "Modified",
+                Self::Deleted => "Deleted",
+                Self::Conflict => "Conflict",
+                Self::Renamed => "Renamed",
+            }
+        )
+    }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum DiagnosticStatus {
+    #[default]
+    None,
+    Error,
+    Warning,
+    Info,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum IconSide {
+    #[default]
+    Left,
+    Right,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum OrderMethod {
+    #[default]
+    Ascending,
+    Descending,
+    MostRecent,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum Shape {
+    #[default]
+    Circle,
+    RoundedRectangle,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum DisclosureControlVisibility {
+    #[default]
+    OnHover,
+    Always,
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum DisclosureControlStyle {
+    /// Shows the disclosure control only when hovered where possible.
+    ///
+    /// More compact, but not available everywhere.
+    ChevronOnHover,
+    /// Shows an icon where possible, otherwise shows a chevron.
+    ///
+    /// For example, in a file tree a folder or file icon is shown
+    /// instead of a chevron
+    Icon,
+    /// Always shows a chevron.
+    Chevron,
+    /// Completely hides the disclosure control where possible.
+    None,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter)]
+pub enum OverflowStyle {
+    Hidden,
+    Wrap,
+}
+
+#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
+pub enum InteractionState {
+    #[default]
+    Enabled,
+    Hovered,
+    Active,
+    Focused,
+    Disabled,
+}
+
+impl InteractionState {
+    pub fn if_enabled(&self, enabled: bool) -> Self {
+        if enabled {
+            *self
+        } else {
+            InteractionState::Disabled
+        }
+    }
+}
+
+#[derive(Default, PartialEq)]
+pub enum SelectedState {
+    #[default]
+    Unselected,
+    PartiallySelected,
+    Selected,
+}
+
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
+pub enum Toggleable {
+    Toggleable(ToggleState),
+    #[default]
+    NotToggleable,
+}
+
+impl Toggleable {
+    pub fn is_toggled(&self) -> bool {
+        match self {
+            Self::Toggleable(ToggleState::Toggled) => true,
+            _ => false,
+        }
+    }
+}
+
+impl From<ToggleState> for Toggleable {
+    fn from(state: ToggleState) -> Self {
+        Self::Toggleable(state)
+    }
+}
+
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
+pub enum ToggleState {
+    /// The "on" state of a toggleable element.
+    ///
+    /// Example:
+    ///     - A collasable list that is currently expanded
+    ///     - A toggle button that is currently on.
+    Toggled,
+    /// The "off" state of a toggleable element.
+    ///
+    /// Example:
+    ///     - A collasable list that is currently collapsed
+    ///     - A toggle button that is currently off.
+    #[default]
+    NotToggled,
+}
+
+impl From<Toggleable> for ToggleState {
+    fn from(toggleable: Toggleable) -> Self {
+        match toggleable {
+            Toggleable::Toggleable(state) => state,
+            Toggleable::NotToggleable => ToggleState::NotToggled,
+        }
+    }
+}
+
+impl From<bool> for ToggleState {
+    fn from(toggled: bool) -> Self {
+        if toggled {
+            ToggleState::Toggled
+        } else {
+            ToggleState::NotToggled
+        }
+    }
+}

crates/ui2/src/settings.rs 🔗

@@ -0,0 +1,76 @@
+use std::ops::Deref;
+
+use gpui2::{rems, AbsoluteLength, AppContext, WindowContext};
+
+use crate::prelude::*;
+
+pub fn init(cx: &mut AppContext) {
+    cx.set_global(FakeSettings::default());
+}
+
+/// Returns the user settings.
+pub fn user_settings(cx: &WindowContext) -> FakeSettings {
+    cx.global::<FakeSettings>().clone()
+}
+
+pub fn user_settings_mut<'cx>(cx: &'cx mut WindowContext) -> &'cx mut FakeSettings {
+    cx.global_mut::<FakeSettings>()
+}
+
+#[derive(Clone)]
+pub enum SettingValue<T> {
+    UserDefined(T),
+    Default(T),
+}
+
+impl<T> Deref for SettingValue<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        match self {
+            Self::UserDefined(value) => value,
+            Self::Default(value) => value,
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct TitlebarSettings {
+    pub show_project_owner: SettingValue<bool>,
+    pub show_git_status: SettingValue<bool>,
+    pub show_git_controls: SettingValue<bool>,
+}
+
+impl Default for TitlebarSettings {
+    fn default() -> Self {
+        Self {
+            show_project_owner: SettingValue::Default(true),
+            show_git_status: SettingValue::Default(true),
+            show_git_controls: SettingValue::Default(true),
+        }
+    }
+}
+
+// These should be merged into settings
+#[derive(Clone)]
+pub struct FakeSettings {
+    pub default_panel_size: SettingValue<AbsoluteLength>,
+    pub list_disclosure_style: SettingValue<DisclosureControlStyle>,
+    pub list_indent_depth: SettingValue<AbsoluteLength>,
+    pub titlebar: TitlebarSettings,
+    pub ui_scale: SettingValue<f32>,
+}
+
+impl Default for FakeSettings {
+    fn default() -> Self {
+        Self {
+            titlebar: TitlebarSettings::default(),
+            list_disclosure_style: SettingValue::Default(DisclosureControlStyle::ChevronOnHover),
+            list_indent_depth: SettingValue::Default(rems(0.3).into()),
+            default_panel_size: SettingValue::Default(rems(16.).into()),
+            ui_scale: SettingValue::Default(1.),
+        }
+    }
+}
+
+impl FakeSettings {}

crates/ui2/src/static_data.rs 🔗

@@ -0,0 +1,1019 @@
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use gpui2::ViewContext;
+use rand::Rng;
+use theme2::Theme;
+
+use crate::{
+    theme, Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
+    HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem,
+    Livestream, MicStatus, ModifierKeys, PaletteItem, Player, PlayerCallStatus,
+    PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
+};
+use crate::{HighlightedText, ListDetailsEntry};
+
+pub fn static_tabs_example() -> Vec<Tab> {
+    vec![
+        Tab::new("wip.rs")
+            .title("wip.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .fs_status(FileSystemStatus::Deleted),
+        Tab::new("Cargo.toml")
+            .title("Cargo.toml".to_string())
+            .icon(Icon::FileToml)
+            .current(false)
+            .git_status(GitStatus::Modified),
+        Tab::new("Channels Panel")
+            .title("Channels Panel".to_string())
+            .icon(Icon::Hash)
+            .current(false),
+        Tab::new("channels_panel.rs")
+            .title("channels_panel.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(true)
+            .git_status(GitStatus::Modified),
+        Tab::new("workspace.rs")
+            .title("workspace.rs".to_string())
+            .current(false)
+            .icon(Icon::FileRust)
+            .git_status(GitStatus::Modified),
+        Tab::new("icon_button.rs")
+            .title("icon_button.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new("storybook.rs")
+            .title("storybook.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .git_status(GitStatus::Created),
+        Tab::new("theme.rs")
+            .title("theme.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new("theme_registry.rs")
+            .title("theme_registry.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new("styleable_helpers.rs")
+            .title("styleable_helpers.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+    ]
+}
+
+pub fn static_tabs_1() -> Vec<Tab> {
+    vec![
+        Tab::new("project_panel.rs")
+            .title("project_panel.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .fs_status(FileSystemStatus::Deleted),
+        Tab::new("tab_bar.rs")
+            .title("tab_bar.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .git_status(GitStatus::Modified),
+        Tab::new("workspace.rs")
+            .title("workspace.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new("tab.rs")
+            .title("tab.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(true)
+            .git_status(GitStatus::Modified),
+    ]
+}
+
+pub fn static_tabs_2() -> Vec<Tab> {
+    vec![
+        Tab::new("tab_bar.rs")
+            .title("tab_bar.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .fs_status(FileSystemStatus::Deleted),
+        Tab::new("static_data.rs")
+            .title("static_data.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(true)
+            .git_status(GitStatus::Modified),
+    ]
+}
+
+pub fn static_tabs_3() -> Vec<Tab> {
+    vec![Tab::new("static_tabs_3")
+        .git_status(GitStatus::Created)
+        .current(true)]
+}
+
+pub fn static_players() -> Vec<Player> {
+    vec![
+        Player::new(
+            0,
+            "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            "nathansobo".into(),
+        ),
+        Player::new(
+            1,
+            "https://avatars.githubusercontent.com/u/326587?v=4".into(),
+            "maxbrunsfeld".into(),
+        ),
+        Player::new(
+            2,
+            "https://avatars.githubusercontent.com/u/482957?v=4".into(),
+            "as-cii".into(),
+        ),
+        Player::new(
+            3,
+            "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            "iamnbutler".into(),
+        ),
+        Player::new(
+            4,
+            "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            "maxdeviant".into(),
+        ),
+    ]
+}
+
+#[derive(Debug)]
+pub struct PlayerData {
+    pub url: String,
+    pub name: String,
+}
+
+pub fn static_player_data() -> Vec<PlayerData> {
+    vec![
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            name: "iamnbutler".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/326587?v=4".into(),
+            name: "maxbrunsfeld".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/482957?v=4".into(),
+            name: "as-cii".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1789?v=4".into(),
+            name: "nathansobo".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            name: "ForLoveOfCats".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(),
+            name: "SomeoneToIgnore".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(),
+            name: "JosephTLyons".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(),
+            name: "osiewicz".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(),
+            name: "KCaverly".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            name: "maxdeviant".into(),
+        },
+    ]
+}
+
+pub fn create_static_players(player_data: Vec<PlayerData>) -> Vec<Player> {
+    let mut players = Vec::new();
+    for data in player_data {
+        players.push(Player::new(players.len(), data.url, data.name));
+    }
+    players
+}
+
+pub fn static_player_1(data: &Vec<PlayerData>) -> Player {
+    Player::new(1, data[0].url.clone(), data[0].name.clone())
+}
+
+pub fn static_player_2(data: &Vec<PlayerData>) -> Player {
+    Player::new(2, data[1].url.clone(), data[1].name.clone())
+}
+
+pub fn static_player_3(data: &Vec<PlayerData>) -> Player {
+    Player::new(3, data[2].url.clone(), data[2].name.clone())
+}
+
+pub fn static_player_4(data: &Vec<PlayerData>) -> Player {
+    Player::new(4, data[3].url.clone(), data[3].name.clone())
+}
+
+pub fn static_player_5(data: &Vec<PlayerData>) -> Player {
+    Player::new(5, data[4].url.clone(), data[4].name.clone())
+}
+
+pub fn static_player_6(data: &Vec<PlayerData>) -> Player {
+    Player::new(6, data[5].url.clone(), data[5].name.clone())
+}
+
+pub fn static_player_7(data: &Vec<PlayerData>) -> Player {
+    Player::new(7, data[6].url.clone(), data[6].name.clone())
+}
+
+pub fn static_player_8(data: &Vec<PlayerData>) -> Player {
+    Player::new(8, data[7].url.clone(), data[7].name.clone())
+}
+
+pub fn static_player_9(data: &Vec<PlayerData>) -> Player {
+    Player::new(9, data[8].url.clone(), data[8].name.clone())
+}
+
+pub fn static_player_10(data: &Vec<PlayerData>) -> Player {
+    Player::new(10, data[9].url.clone(), data[9].name.clone())
+}
+
+pub fn static_livestream() -> Livestream {
+    Livestream {
+        players: random_players_with_call_status(7),
+        channel: Some("gpui2-ui".to_string()),
+    }
+}
+
+pub fn populate_player_call_status(
+    player: Player,
+    followers: Option<Vec<Player>>,
+) -> PlayerCallStatus {
+    let mut rng = rand::thread_rng();
+    let in_current_project: bool = rng.gen();
+    let disconnected: bool = rng.gen();
+    let voice_activity: f32 = rng.gen();
+    let mic_status = if rng.gen_bool(0.5) {
+        MicStatus::Muted
+    } else {
+        MicStatus::Unmuted
+    };
+    let video_status = if rng.gen_bool(0.5) {
+        VideoStatus::On
+    } else {
+        VideoStatus::Off
+    };
+    let screen_share_status = if rng.gen_bool(0.5) {
+        ScreenShareStatus::Shared
+    } else {
+        ScreenShareStatus::NotShared
+    };
+    PlayerCallStatus {
+        mic_status,
+        voice_activity,
+        video_status,
+        screen_share_status,
+        in_current_project,
+        disconnected,
+        following: None,
+        followers,
+    }
+}
+
+pub fn random_players_with_call_status(number_of_players: usize) -> Vec<PlayerWithCallStatus> {
+    let players = create_static_players(static_player_data());
+    let mut player_status = vec![];
+    for i in 0..number_of_players {
+        let followers = if i == 0 {
+            Some(vec![
+                players[1].clone(),
+                players[3].clone(),
+                players[5].clone(),
+                players[6].clone(),
+            ])
+        } else if i == 1 {
+            Some(vec![players[2].clone(), players[6].clone()])
+        } else {
+            None
+        };
+        let call_status = populate_player_call_status(players[i].clone(), followers);
+        player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status));
+    }
+    player_status
+}
+
+pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
+    let players = static_players();
+    let mut player_0_status = PlayerCallStatus::new();
+    let player_1_status = PlayerCallStatus::new();
+    let player_2_status = PlayerCallStatus::new();
+    let mut player_3_status = PlayerCallStatus::new();
+    let mut player_4_status = PlayerCallStatus::new();
+
+    player_0_status.screen_share_status = ScreenShareStatus::Shared;
+    player_0_status.followers = Some(vec![players[1].clone(), players[3].clone()]);
+
+    player_3_status.voice_activity = 0.5;
+    player_4_status.mic_status = MicStatus::Muted;
+    player_4_status.in_current_project = false;
+
+    vec![
+        PlayerWithCallStatus::new(players[0].clone(), player_0_status),
+        PlayerWithCallStatus::new(players[1].clone(), player_1_status),
+        PlayerWithCallStatus::new(players[2].clone(), player_2_status),
+        PlayerWithCallStatus::new(players[3].clone(), player_3_status),
+        PlayerWithCallStatus::new(players[4].clone(), player_4_status),
+    ]
+}
+
+pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
+    vec![
+        ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
+            .meta("4 people in stream."),
+        ListDetailsEntry::new("nathansobo accepted your contact request."),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_read_notification_items<V: 'static>() -> Vec<ListItem<V>> {
+    vec![
+        ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
+            Button::new("Decline"),
+            Button::new("Accept").variant(crate::ButtonVariant::Filled),
+        ]),
+        ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
+            .seen(true)
+            .meta("This stream has ended."),
+        ListDetailsEntry::new("as-cii accepted your contact request."),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_project_panel_project_items<V: 'static>() -> Vec<ListItem<V>> {
+    vec![
+        ListEntry::new(Label::new("zed"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(0)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new(".cargo"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".config"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".git").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".cargo"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".idea").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("assets"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("crates"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(1)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("activity_indicator"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("ai"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("audio"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("auto_update"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("breadcrumbs"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("call"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("sqlez").color(LabelColor::Modified))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2)
+            .toggle(ToggleState::NotToggled),
+        ListEntry::new(Label::new("gpui2"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(2)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("src"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(3)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("derive_element.rs"))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(4),
+        ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(1)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("docs").color(LabelColor::Default))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("src").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(3)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("ui").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(4)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("component").color(LabelColor::Created))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(5)
+            .toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("tab.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("target").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".dockerignore"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Cargo.lock"))
+            .left_icon(Icon::FileLock.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Cargo.toml"))
+            .left_icon(Icon::FileToml.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Dockerfile"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Procfile"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("README.md"))
+            .left_icon(Icon::FileDoc.into())
+            .indent_level(1),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_project_panel_single_items<V: 'static>() -> Vec<ListItem<V>> {
+    vec![
+        ListEntry::new(Label::new("todo.md"))
+            .left_icon(Icon::FileDoc.into())
+            .indent_level(0),
+        ListEntry::new(Label::new("README.md"))
+            .left_icon(Icon::FileDoc.into())
+            .indent_level(0),
+        ListEntry::new(Label::new("config.json"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(0),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_collab_panel_current_call<V: 'static>() -> Vec<ListItem<V>> {
+    vec![
+        ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
+        ListEntry::new(Label::new("nathansobo"))
+            .left_avatar("http://github.com/nathansobo.png?s=50"),
+        ListEntry::new(Label::new("maxbrunsfeld"))
+            .left_avatar("http://github.com/maxbrunsfeld.png?s=50"),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_collab_panel_channels<V: 'static>() -> Vec<ListItem<V>> {
+    vec![
+        ListEntry::new(Label::new("zed"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(0),
+        ListEntry::new(Label::new("community"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(1),
+        ListEntry::new(Label::new("dashboards"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("feedback"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("teams-in-channels-alpha"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("current-projects"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(1),
+        ListEntry::new(Label::new("codegen"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("gpui2"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("livestreaming"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("open-source"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("replace"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("semantic-index"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("vim"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("web-tech"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn example_editor_actions() -> Vec<PaletteItem> {
+    vec![
+        PaletteItem::new("New File").keybinding(Keybinding::new(
+            "N".to_string(),
+            ModifierKeys::new().command(true),
+        )),
+        PaletteItem::new("Open File").keybinding(Keybinding::new(
+            "O".to_string(),
+            ModifierKeys::new().command(true),
+        )),
+        PaletteItem::new("Save File").keybinding(Keybinding::new(
+            "S".to_string(),
+            ModifierKeys::new().command(true),
+        )),
+        PaletteItem::new("Cut").keybinding(Keybinding::new(
+            "X".to_string(),
+            ModifierKeys::new().command(true),
+        )),
+        PaletteItem::new("Copy").keybinding(Keybinding::new(
+            "C".to_string(),
+            ModifierKeys::new().command(true),
+        )),
+        PaletteItem::new("Paste").keybinding(Keybinding::new(
+            "V".to_string(),
+            ModifierKeys::new().command(true),
+        )),
+        PaletteItem::new("Undo").keybinding(Keybinding::new(
+            "Z".to_string(),
+            ModifierKeys::new().command(true),
+        )),
+        PaletteItem::new("Redo").keybinding(Keybinding::new(
+            "Z".to_string(),
+            ModifierKeys::new().command(true).shift(true),
+        )),
+        PaletteItem::new("Find").keybinding(Keybinding::new(
+            "F".to_string(),
+            ModifierKeys::new().command(true),
+        )),
+        PaletteItem::new("Replace").keybinding(Keybinding::new(
+            "R".to_string(),
+            ModifierKeys::new().command(true),
+        )),
+        PaletteItem::new("Jump to Line"),
+        PaletteItem::new("Select All"),
+        PaletteItem::new("Deselect All"),
+        PaletteItem::new("Switch Document"),
+        PaletteItem::new("Insert Line Below"),
+        PaletteItem::new("Insert Line Above"),
+        PaletteItem::new("Move Line Up"),
+        PaletteItem::new("Move Line Down"),
+        PaletteItem::new("Toggle Comment"),
+        PaletteItem::new("Delete Line"),
+    ]
+}
+
+pub fn empty_editor_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
+    EditorPane::new(
+        cx,
+        static_tabs_example(),
+        PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+        vec![],
+        empty_buffer_example(),
+    )
+}
+
+pub fn empty_buffer_example() -> Buffer {
+    Buffer::new("empty-buffer").set_rows(Some(BufferRows::default()))
+}
+
+pub fn hello_world_rust_editor_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
+    let theme = theme(cx);
+
+    EditorPane::new(
+        cx,
+        static_tabs_example(),
+        PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+        vec![Symbol(vec![
+            HighlightedText {
+                text: "fn ".to_string(),
+                color: theme.syntax.color("keyword"),
+            },
+            HighlightedText {
+                text: "main".to_string(),
+                color: theme.syntax.color("function"),
+            },
+        ])],
+        hello_world_rust_buffer_example(&theme),
+    )
+}
+
+pub fn hello_world_rust_buffer_example(theme: &Theme) -> Buffer {
+    Buffer::new("hello-world-rust-buffer")
+        .set_title("hello_world.rs".to_string())
+        .set_path("src/hello_world.rs".to_string())
+        .set_language("rust".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: true,
+            rows: hello_world_rust_buffer_rows(theme),
+        }))
+}
+
+pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
+    let show_line_number = true;
+
+    vec![
+        BufferRow {
+            line_number: 1,
+            code_action: false,
+            current: true,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "fn ".to_string(),
+                        color: theme.syntax.color("keyword"),
+                    },
+                    HighlightedText {
+                        text: "main".to_string(),
+                        color: theme.syntax.color("function"),
+                    },
+                    HighlightedText {
+                        text: "() {".to_string(),
+                        color: theme.text,
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 2,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "    // Statements here are executed when the compiled binary is called."
+                        .to_string(),
+                    color: theme.syntax.color("comment"),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 3,
+            code_action: false,
+            current: false,
+            line: None,
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 4,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "    // Print text to the console.".to_string(),
+                    color: theme.syntax.color("comment"),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 5,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "    println!(".to_string(),
+                        color: theme.text,
+                    },
+                    HighlightedText {
+                        text: "\"Hello, world!\"".to_string(),
+                        color: theme.syntax.color("string"),
+                    },
+                    HighlightedText {
+                        text: ");".to_string(),
+                        color: theme.text,
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 6,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "}".to_string(),
+                    color: theme.text,
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+    ]
+}
+
+pub fn hello_world_rust_editor_with_status_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
+    let theme = theme(cx);
+
+    EditorPane::new(
+        cx,
+        static_tabs_example(),
+        PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+        vec![Symbol(vec![
+            HighlightedText {
+                text: "fn ".to_string(),
+                color: theme.syntax.color("keyword"),
+            },
+            HighlightedText {
+                text: "main".to_string(),
+                color: theme.syntax.color("function"),
+            },
+        ])],
+        hello_world_rust_buffer_with_status_example(&theme),
+    )
+}
+
+pub fn hello_world_rust_buffer_with_status_example(theme: &Theme) -> Buffer {
+    Buffer::new("hello-world-rust-buffer-with-status")
+        .set_title("hello_world.rs".to_string())
+        .set_path("src/hello_world.rs".to_string())
+        .set_language("rust".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: true,
+            rows: hello_world_rust_with_status_buffer_rows(theme),
+        }))
+}
+
+pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
+    let show_line_number = true;
+
+    vec![
+        BufferRow {
+            line_number: 1,
+            code_action: false,
+            current: true,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "fn ".to_string(),
+                        color: theme.syntax.color("keyword"),
+                    },
+                    HighlightedText {
+                        text: "main".to_string(),
+                        color: theme.syntax.color("function"),
+                    },
+                    HighlightedText {
+                        text: "() {".to_string(),
+                        color: theme.text,
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 2,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "// Statements here are executed when the compiled binary is called."
+                        .to_string(),
+                    color: theme.syntax.color("comment"),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::Modified,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 3,
+            code_action: false,
+            current: false,
+            line: None,
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 4,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "    // Print text to the console.".to_string(),
+                    color: theme.syntax.color("comment"),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 5,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "    println!(".to_string(),
+                        color: theme.text,
+                    },
+                    HighlightedText {
+                        text: "\"Hello, world!\"".to_string(),
+                        color: theme.syntax.color("string"),
+                    },
+                    HighlightedText {
+                        text: ");".to_string(),
+                        color: theme.text,
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 6,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "}".to_string(),
+                    color: theme.text,
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 7,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "".to_string(),
+                    color: theme.text,
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::Created,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 8,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "// Marshall and Nate were here".to_string(),
+                    color: theme.syntax.color("comment"),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::Created,
+            show_line_number,
+        },
+    ]
+}
+
+pub fn terminal_buffer(theme: &Theme) -> Buffer {
+    Buffer::new("terminal")
+        .set_title("zed — fish".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: false,
+            rows: terminal_buffer_rows(theme),
+        }))
+}
+
+pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
+    let show_line_number = false;
+
+    vec![
+        BufferRow {
+            line_number: 1,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "maxdeviant ".to_string(),
+                        color: theme.syntax.color("keyword"),
+                    },
+                    HighlightedText {
+                        text: "in ".to_string(),
+                        color: theme.text,
+                    },
+                    HighlightedText {
+                        text: "profaned-capital ".to_string(),
+                        color: theme.syntax.color("function"),
+                    },
+                    HighlightedText {
+                        text: "in ".to_string(),
+                        color: theme.text,
+                    },
+                    HighlightedText {
+                        text: "~/p/zed ".to_string(),
+                        color: theme.syntax.color("function"),
+                    },
+                    HighlightedText {
+                        text: "on ".to_string(),
+                        color: theme.text,
+                    },
+                    HighlightedText {
+                        text: " gpui2-ui ".to_string(),
+                        color: theme.syntax.color("keyword"),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 2,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "λ ".to_string(),
+                    color: theme.syntax.color("string"),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+    ]
+}

crates/ui2/src/story.rs 🔗

@@ -0,0 +1,44 @@
+use gpui2::Div;
+
+use crate::prelude::*;
+
+pub struct Story {}
+
+impl Story {
+    pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
+        let theme = theme(cx);
+
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .pt_2()
+            .px_4()
+            .font("Zed Mono")
+            .bg(theme.background)
+    }
+
+    pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Component<V> {
+        let theme = theme(cx);
+
+        div()
+            .text_xl()
+            .text_color(theme.text)
+            .child(title.to_owned())
+    }
+
+    pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Component<V> {
+        Self::title(cx, std::any::type_name::<T>())
+    }
+
+    pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Component<V> {
+        let theme = theme(cx);
+
+        div()
+            .mt_4()
+            .mb_2()
+            .text_xs()
+            .text_color(theme.text)
+            .child(label.to_owned())
+    }
+}

crates/util/src/arc_cow.rs 🔗

@@ -1,12 +1,24 @@
-use std::sync::Arc;
+use std::{
+    borrow::Cow,
+    fmt::{self, Debug},
+    hash::{Hash, Hasher},
+    sync::Arc,
+};
 
-#[derive(PartialEq, Eq)]
 pub enum ArcCow<'a, T: ?Sized> {
     Borrowed(&'a T),
     Owned(Arc<T>),
 }
 
-use std::hash::{Hash, Hasher};
+impl<'a, T: ?Sized + PartialEq> PartialEq for ArcCow<'a, T> {
+    fn eq(&self, other: &Self) -> bool {
+        let a = self.as_ref();
+        let b = other.as_ref();
+        a == b
+    }
+}
+
+impl<'a, T: ?Sized + Eq> Eq for ArcCow<'a, T> {}
 
 impl<'a, T: ?Sized + Hash> Hash for ArcCow<'a, T> {
     fn hash<H: Hasher>(&self, state: &mut H) {
@@ -44,6 +56,27 @@ impl From<String> for ArcCow<'_, str> {
     }
 }
 
+impl<'a> From<Cow<'a, str>> for ArcCow<'a, str> {
+    fn from(value: Cow<'a, str>) -> Self {
+        match value {
+            Cow::Borrowed(borrowed) => Self::Borrowed(borrowed),
+            Cow::Owned(owned) => Self::Owned(owned.into()),
+        }
+    }
+}
+
+impl<T> From<Vec<T>> for ArcCow<'_, [T]> {
+    fn from(vec: Vec<T>) -> Self {
+        ArcCow::Owned(Arc::from(vec))
+    }
+}
+
+impl<'a> From<&'a str> for ArcCow<'a, [u8]> {
+    fn from(s: &'a str) -> Self {
+        ArcCow::Borrowed(s.as_bytes())
+    }
+}
+
 impl<'a, T: ?Sized + ToOwned> std::borrow::Borrow<T> for ArcCow<'a, T> {
     fn borrow(&self) -> &T {
         match self {
@@ -72,3 +105,12 @@ impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
         }
     }
 }
+
+impl<'a, T: ?Sized + Debug> Debug for ArcCow<'a, T> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ArcCow::Borrowed(borrowed) => Debug::fmt(borrowed, f),
+            ArcCow::Owned(owned) => Debug::fmt(&**owned, f),
+        }
+    }
+}

crates/util/src/util.rs 🔗

@@ -13,6 +13,7 @@ use std::{
     ops::{AddAssign, Range, RangeInclusive},
     panic::Location,
     pin::Pin,
+    sync::atomic::AtomicU32,
     task::{Context, Poll},
 };
 
@@ -410,6 +411,20 @@ impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
     }
 }
 
+static GPUI_LOADED: AtomicU32 = AtomicU32::new(0);
+
+pub fn gpui2_loaded() {
+    if GPUI_LOADED.fetch_add(2, std::sync::atomic::Ordering::SeqCst) != 0 {
+        panic!("=========\nYou are loading both GPUI1 and GPUI2 in the same build!\nFix Your Dependencies with cargo tree!\n=========")
+    }
+}
+
+pub fn gpui1_loaded() {
+    if GPUI_LOADED.fetch_add(1, std::sync::atomic::Ordering::SeqCst) != 0 {
+        panic!("=========\nYou are loading both GPUI1 and GPUI2 in the same build!\nFix Your Dependencies with cargo tree!\n=========")
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

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 🔗

@@ -7,26 +7,26 @@ 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,
     ]
 );
 

crates/zed2/Cargo.toml 🔗

@@ -0,0 +1,182 @@
+[package]
+description = "The fast, collaborative code editor."
+edition = "2021"
+name = "zed2"
+version = "0.109.0"
+publish = false
+
+[lib]
+name = "zed2"
+path = "src/zed2.rs"
+doctest = false
+
+[[bin]]
+name = "Zed"
+path = "src/main.rs"
+
+[dependencies]
+ai2 = { path = "../ai2"}
+# audio = { path = "../audio" }
+# activity_indicator = { path = "../activity_indicator" }
+# auto_update = { path = "../auto_update" }
+# breadcrumbs = { path = "../breadcrumbs" }
+call2 = { path = "../call2" }
+# channel = { path = "../channel" }
+cli = { path = "../cli" }
+# collab_ui = { path = "../collab_ui" }
+collections = { path = "../collections" }
+# command_palette = { path = "../command_palette" }
+# component_test = { path = "../component_test" }
+# context_menu = { path = "../context_menu" }
+client2 = { path = "../client2" }
+# clock = { path = "../clock" }
+copilot2 = { path = "../copilot2" }
+# copilot_button = { path = "../copilot_button" }
+# diagnostics = { path = "../diagnostics" }
+db2 = { path = "../db2" }
+# editor = { path = "../editor" }
+# feedback = { path = "../feedback" }
+# file_finder = { path = "../file_finder" }
+# search = { path = "../search" }
+fs2 = { path = "../fs2" }
+fsevent = { path = "../fsevent" }
+fuzzy = { path = "../fuzzy" }
+# go_to_line = { path = "../go_to_line" }
+gpui2 = { path = "../gpui2" }
+install_cli = { path = "../install_cli" }
+journal2 = { path = "../journal2" }
+language2 = { path = "../language2" }
+# language_selector = { path = "../language_selector" }
+lsp2 = { path = "../lsp2" }
+language_tools = { path = "../language_tools" }
+node_runtime = { path = "../node_runtime" }
+# assistant = { path = "../assistant" }
+# outline = { path = "../outline" }
+# plugin_runtime = { path = "../plugin_runtime",optional = true }
+project2 = { path = "../project2" }
+# project_panel = { path = "../project_panel" }
+# project_symbols = { path = "../project_symbols" }
+# quick_action_bar = { path = "../quick_action_bar" }
+# recent_projects = { path = "../recent_projects" }
+rpc2 = { path = "../rpc2" }
+settings2 = { path = "../settings2" }
+feature_flags2 = { path = "../feature_flags2" }
+sum_tree = { path = "../sum_tree" }
+shellexpand = "2.1.0"
+text = { path = "../text" }
+# terminal_view = { path = "../terminal_view" }
+theme2 = { path = "../theme2" }
+# theme_selector = { path = "../theme_selector" }
+util = { path = "../util" }
+# semantic_index = { path = "../semantic_index" }
+# vim = { path = "../vim" }
+# workspace = { path = "../workspace" }
+# welcome = { path = "../welcome" }
+# zed-actions = {path = "../zed-actions"}
+anyhow.workspace = true
+async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
+async-tar = "0.4.2"
+async-recursion = "0.3"
+async-trait.workspace = true
+backtrace = "0.3"
+chrono = "0.4"
+ctor = "0.1.20"
+env_logger.workspace = true
+futures.workspace = true
+ignore = "0.4"
+image = "0.23"
+indexmap = "1.6.2"
+isahc.workspace = true
+lazy_static.workspace = true
+libc = "0.2"
+log.workspace = true
+num_cpus = "1.13.0"
+parking_lot.workspace = true
+postage.workspace = true
+rand.workspace = true
+regex.workspace = true
+rsa = "0.4"
+rust-embed.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+schemars.workspace = true
+simplelog = "0.9"
+smallvec.workspace = true
+smol.workspace = true
+tempdir.workspace = true
+thiserror.workspace = true
+tiny_http = "0.8"
+toml.workspace = true
+tree-sitter.workspace = true
+tree-sitter-bash.workspace = true
+tree-sitter-c.workspace = true
+tree-sitter-cpp.workspace = true
+tree-sitter-css.workspace = true
+tree-sitter-elixir.workspace = true
+tree-sitter-elm.workspace = true
+tree-sitter-embedded-template.workspace = true
+tree-sitter-glsl.workspace = true
+tree-sitter-go.workspace = true
+tree-sitter-heex.workspace = true
+tree-sitter-json.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-markdown.workspace = true
+tree-sitter-python.workspace = true
+tree-sitter-toml.workspace = true
+tree-sitter-typescript.workspace = true
+tree-sitter-ruby.workspace = true
+tree-sitter-html.workspace = true
+tree-sitter-php.workspace = true
+tree-sitter-scheme.workspace = true
+tree-sitter-svelte.workspace = true
+tree-sitter-racket.workspace = true
+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]
+call2 = { path = "../call2", features = ["test-support"] }
+# client = { path = "../client", features = ["test-support"] }
+# editor = { path = "../editor", features = ["test-support"] }
+# gpui = { path = "../gpui", features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+language2 = { path = "../language2", features = ["test-support"] }
+# lsp = { path = "../lsp", features = ["test-support"] }
+project2 = { path = "../project2", features = ["test-support"] }
+# rpc = { path = "../rpc", features = ["test-support"] }
+# settings = { path = "../settings", features = ["test-support"] }
+# text = { path = "../text", features = ["test-support"] }
+# util = { path = "../util", features = ["test-support"] }
+# workspace = { path = "../workspace", features = ["test-support"] }
+unindent.workspace = true
+
+[package.metadata.bundle-dev]
+icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
+identifier = "dev.zed.Zed-Dev"
+name = "Zed Dev"
+osx_minimum_system_version = "10.15.7"
+osx_info_plist_exts = ["resources/info/*"]
+osx_url_schemes = ["zed-dev"]
+
+[package.metadata.bundle-preview]
+icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
+identifier = "dev.zed.Zed-Preview"
+name = "Zed Preview"
+osx_minimum_system_version = "10.15.7"
+osx_info_plist_exts = ["resources/info/*"]
+osx_url_schemes = ["zed-preview"]
+
+[package.metadata.bundle-stable]
+icon = ["resources/app-icon@2x.png", "resources/app-icon.png"]
+identifier = "dev.zed.Zed"
+name = "Zed"
+osx_minimum_system_version = "10.15.7"
+osx_info_plist_exts = ["resources/info/*"]
+osx_url_schemes = ["zed"]

crates/zed2/build.rs 🔗

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

crates/zed2/resources/info/DocumentTypes.plist 🔗

@@ -0,0 +1,62 @@
+<key>CFBundleDocumentTypes</key>
+<array>
+    <dict>
+        <key>CFBundleTypeIconFile</key>
+        <string>Document</string>
+        <key>CFBundleTypeRole</key>
+        <string>Editor</string>
+        <key>LSHandlerRank</key>
+        <string>Alternate</string>
+        <key>LSItemContentTypes</key>
+        <array>
+            <string>public.text</string>
+            <string>public.plain-text</string>
+            <string>public.utf8-plain-text</string>
+        </array>
+    </dict>
+    <dict>
+        <key>CFBundleTypeIconFile</key>
+        <string>Document</string>
+        <key>CFBundleTypeName</key>
+        <string>Zed Text Document</string>
+        <key>CFBundleTypeRole</key>
+        <string>Editor</string>
+        <key>CFBundleTypeOSTypes</key>
+        <array>
+            <string>****</string>
+        </array>
+        <key>LSHandlerRank</key>
+        <string>Default</string>
+        <key>CFBundleTypeExtensions</key>
+        <array>
+            <string>Gemfile</string>
+            <string>c</string>
+            <string>c++</string>
+            <string>cc</string>
+            <string>cpp</string>
+            <string>css</string>
+            <string>erb</string>
+            <string>ex</string>
+            <string>exs</string>
+            <string>go</string>
+            <string>h</string>
+            <string>h++</string>
+            <string>hh</string>
+            <string>hpp</string>
+            <string>html</string>
+            <string>js</string>
+            <string>json</string>
+            <string>jsx</string>
+            <string>md</string>
+            <string>py</string>
+            <string>rb</string>
+            <string>rkt</string>
+            <string>rs</string>
+            <string>scm</string>
+            <string>toml</string>
+            <string>ts</string>
+            <string>tsx</string>
+            <string>txt</string>
+        </array>
+    </dict>
+</array>

crates/zed2/resources/info/Permissions.plist 🔗

@@ -0,0 +1,24 @@
+<key>NSSystemAdministrationUsageDescription</key>
+<string>The operation being performed by a program in Zed requires elevated permission.</string>
+<key>NSAppleEventsUsageDescription</key>
+<string>An application in Zed wants to use AppleScript.</string>
+<key>NSBluetoothAlwaysUsageDescription</key>
+<string>An application in Zed wants to use Bluetooth.</string>
+<key>NSCalendarsUsageDescription</key>
+<string>An application in Zed wants to use Calendar data.</string>
+<key>NSCameraUsageDescription</key>
+<string>An application in Zed wants to use the camera.</string>
+<key>NSContactsUsageDescription</key>
+<string>An application in Zed wants to use your contacts.</string>
+<key>NSLocationAlwaysUsageDescription</key>
+<string>An application in Zed wants to use your location information, even in the background.</string>
+<key>NSLocationUsageDescription</key>
+<string>An application in Zed wants to use your location information.</string>
+<key>NSLocationWhenInUseUsageDescription</key>
+<string>An application in Zed wants to use your location information while active.</string>
+<key>NSMicrophoneUsageDescription</key>
+<string>An application in Zed wants to use your microphone.</string>
+<key>NSSpeechRecognitionUsageDescription</key>
+<string>An application in Zed wants to use speech recognition.</string>
+<key>NSRemindersUsageDescription</key>
+<string>An application in Zed wants to use your reminders.</string>

crates/zed2/resources/zed.entitlements 🔗

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.automation.apple-events</key>
+	<true/>
+	<key>com.apple.security.cs.allow-jit</key>
+	<true/>
+	<key>com.apple.security.device.audio-input</key>
+	<true/>
+	<key>com.apple.security.device.camera</key>
+	<true/>
+	<key>com.apple.security.personal-information.addressbook</key>
+	<true/>
+	<key>com.apple.security.personal-information.calendars</key>
+	<true/>
+	<key>com.apple.security.personal-information.location</key>
+	<true/>
+	<key>com.apple.security.personal-information.photos-library</key>
+	<true/>
+	<!-- <key>com.apple.security.cs.disable-library-validation</key>
+	<true/> -->
+</dict>
+</plist>

crates/zed2/src/assets.rs 🔗

@@ -0,0 +1,33 @@
+use anyhow::anyhow;
+use gpui2::{AssetSource, Result, SharedString};
+use rust_embed::RustEmbed;
+
+#[derive(RustEmbed)]
+#[folder = "../../assets"]
+#[include = "fonts/**/*"]
+#[include = "icons/**/*"]
+#[include = "themes/**/*"]
+#[include = "sounds/**/*"]
+#[include = "*.md"]
+#[exclude = "*.DS_Store"]
+pub struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
+        Self::get(path)
+            .map(|f| f.data)
+            .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
+    }
+
+    fn list(&self, path: &str) -> Result<Vec<SharedString>> {
+        Ok(Self::iter()
+            .filter_map(|p| {
+                if p.starts_with(path) {
+                    Some(p.into())
+                } else {
+                    None
+                }
+            })
+            .collect())
+    }
+}

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 🔗

@@ -0,0 +1,923 @@
+// Allow binary to be called Zed for a nice application menu when running executable directly
+#![allow(non_snake_case)]
+
+use crate::open_listener::{OpenListener, OpenRequest};
+use anyhow::{anyhow, Context as _, Result};
+use backtrace::Backtrace;
+use cli::{
+    ipc::{self, IpcSender},
+    CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
+};
+use client2::UserStore;
+use db2::kvp::KEY_VALUE_STORE;
+use fs2::RealFs;
+use futures::{channel::mpsc, SinkExt, StreamExt};
+use gpui2::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
+use isahc::{prelude::Configurable, Request};
+use language2::LanguageRegistry;
+use log::LevelFilter;
+
+use node_runtime::RealNodeRuntime;
+use parking_lot::Mutex;
+use serde::{Deserialize, Serialize};
+use settings2::{
+    default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore,
+};
+use simplelog::ConfigBuilder;
+use smol::process::Command;
+use std::{
+    env,
+    ffi::OsStr,
+    fs::OpenOptions,
+    io::{IsTerminal, Write},
+    panic,
+    path::Path,
+    sync::{
+        atomic::{AtomicU32, Ordering},
+        Arc,
+    },
+    thread,
+    time::{SystemTime, UNIX_EPOCH},
+};
+use util::{
+    channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
+    http::{self, HttpClient},
+    paths, ResultExt,
+};
+use uuid::Uuid;
+use zed2::languages;
+use zed2::{ensure_only_instance, AppState, Assets, IsOnlyInstance};
+
+mod open_listener;
+
+fn main() {
+    let http = http::client();
+    init_paths();
+    init_logger();
+
+    if ensure_only_instance() != IsOnlyInstance::Yes {
+        return;
+    }
+
+    log::info!("========== starting zed ==========");
+    let app = App::production(Arc::new(Assets));
+
+    let installation_id = app.executor().block(installation_id()).ok();
+    let session_id = Uuid::new_v4().to_string();
+    init_panic_hook(&app, installation_id.clone(), session_id.clone());
+
+    let fs = Arc::new(RealFs);
+    let user_settings_file_rx =
+        watch_config_file(&app.executor(), fs.clone(), paths::SETTINGS.clone());
+    let _user_keymap_file_rx =
+        watch_config_file(&app.executor(), fs.clone(), paths::KEYMAP.clone());
+
+    let login_shell_env_loaded = if stdout_is_a_pty() {
+        Task::ready(())
+    } else {
+        app.executor().spawn(async {
+            load_login_shell_environment().await.log_err();
+        })
+    };
+
+    let (listener, mut open_rx) = OpenListener::new();
+    let listener = Arc::new(listener);
+    let open_listener = listener.clone();
+    app.on_open_urls(move |urls, _| open_listener.open_urls(urls));
+    app.on_reopen(move |_cx| {
+        // todo!("workspace")
+        // if cx.has_global::<Weak<AppState>>() {
+        // if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
+        // workspace::open_new(&app_state, cx, |workspace, cx| {
+        //     Editor::new_file(workspace, &Default::default(), cx)
+        // })
+        // .detach();
+        // }
+        // }
+    });
+
+    app.run(move |cx| {
+        cx.set_global(*RELEASE_CHANNEL);
+        load_embedded_fonts(cx);
+
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+        handle_settings_file_changes(user_settings_file_rx, cx);
+        // handle_keymap_file_changes(user_keymap_file_rx, cx);
+
+        let client = client2::Client::new(http.clone(), cx);
+        let mut languages = LanguageRegistry::new(login_shell_env_loaded);
+        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);
+        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());
+
+        theme2::init(cx);
+        // context_menu::init(cx);
+        project2::Project::init(&client, cx);
+        client2::init(&client, cx);
+        // command_palette::init(cx);
+        language2::init(cx);
+        // editor::init(cx);
+        // go_to_line::init(cx);
+        // file_finder::init(cx);
+        // outline::init(cx);
+        // project_symbols::init(cx);
+        // project_panel::init(Assets, cx);
+        // channel::init(&client, user_store.clone(), cx);
+        // diagnostics::init(cx);
+        // search::init(cx);
+        // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
+        // vim::init(cx);
+        // terminal_view::init(cx);
+        copilot2::init(
+            copilot_language_server_id,
+            http.clone(),
+            node_runtime.clone(),
+            cx,
+        );
+        // assistant::init(cx);
+        // component_test::init(cx);
+
+        // cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
+        // cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
+        //     .detach();
+        // watch_file_types(fs.clone(), cx);
+
+        // languages.set_theme(theme::current(cx).clone());
+        // cx.observe_global::<SettingsStore, _>({
+        //     let languages = languages.clone();
+        //     move |cx| languages.set_theme(theme::current(cx).clone())
+        // })
+        // .detach();
+
+        // client.telemetry().start(installation_id, session_id, cx);
+
+        // todo!("app_state")
+        let app_state = Arc::new(AppState { client, user_store });
+        // let app_state = Arc::new(AppState {
+        //     languages,
+        //     client: client.clone(),
+        //     user_store,
+        //     fs,
+        //     build_window_options,
+        //     initialize_workspace,
+        //     background_actions,
+        //     workspace_store,
+        //     node_runtime,
+        // });
+        // cx.set_global(Arc::downgrade(&app_state));
+
+        // audio::init(Assets, cx);
+        // auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
+
+        // todo!("workspace")
+        // workspace::init(app_state.clone(), cx);
+        // recent_projects::init(cx);
+
+        // journal2::init(app_state.clone(), cx);
+        // language_selector::init(cx);
+        // theme_selector::init(cx);
+        // activity_indicator::init(cx);
+        // language_tools::init(cx);
+        call2::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+        // collab_ui::init(&app_state, cx);
+        // feedback::init(cx);
+        // welcome::init(cx);
+        // zed::init(&app_state, cx);
+
+        // cx.set_menus(menus::menus());
+
+        if stdout_is_a_pty() {
+            cx.activate(true);
+            let urls = collect_url_args();
+            if !urls.is_empty() {
+                listener.open_urls(urls)
+            }
+        } else {
+            upload_previous_panics(http.clone(), cx);
+
+            // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead
+            // of an *app, hence gets no specific callbacks run. Emulate them here, if needed.
+            if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
+                && !listener.triggered.load(Ordering::Acquire)
+            {
+                listener.open_urls(collect_url_args())
+            }
+        }
+
+        let mut _triggered_authentication = false;
+
+        match open_rx.try_next() {
+            Ok(Some(OpenRequest::Paths { paths: _ })) => {
+                // todo!("workspace")
+                // cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
+                //     .detach();
+            }
+            Ok(Some(OpenRequest::CliConnection { connection })) => {
+                let app_state = app_state.clone();
+                cx.spawn(move |cx| handle_cli_connection(connection, app_state, cx))
+                    .detach();
+            }
+            Ok(Some(OpenRequest::JoinChannel { 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;
+                //     cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
+                //         .await
+                // })
+                // .detach_and_log_err(cx)
+            }
+            Ok(None) | Err(_) => cx
+                .spawn({
+                    let app_state = app_state.clone();
+                    |cx| async move { restore_or_create_workspace(&app_state, cx).await }
+                })
+                .detach(),
+        }
+
+        let app_state = app_state.clone();
+        cx.spawn(|cx| {
+            async move {
+                while let Some(request) = open_rx.next().await {
+                    match request {
+                        OpenRequest::Paths { paths: _ } => {
+                            // todo!("workspace")
+                            // cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
+                            //     .detach();
+                        }
+                        OpenRequest::CliConnection { connection } => {
+                            let app_state = app_state.clone();
+                            cx.spawn(move |cx| {
+                                handle_cli_connection(connection, app_state.clone(), cx)
+                            })
+                            .detach();
+                        }
+                        OpenRequest::JoinChannel { channel_id: _ } => {
+                            // cx
+                            // .update(|cx| {
+                            //     workspace::join_channel(channel_id, app_state.clone(), None, cx)
+                            // })
+                            // .detach()
+                        }
+                    }
+                }
+            }
+        })
+        .detach();
+
+        // if !triggered_authentication {
+        //     cx.spawn(|cx| async move { authenticate(client, &cx).await })
+        //         .detach_and_log_err(cx);
+        // }
+    });
+}
+
+// async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
+//     if stdout_is_a_pty() {
+//         if client::IMPERSONATE_LOGIN.is_some() {
+//             client.authenticate_and_connect(false, &cx).await?;
+//         }
+//     } else if client.has_keychain_credentials(&cx) {
+//         client.authenticate_and_connect(true, &cx).await?;
+//     }
+//     Ok::<_, anyhow::Error>(())
+// }
+
+async fn installation_id() -> Result<String> {
+    let legacy_key_name = "device_id";
+
+    if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) {
+        Ok(installation_id)
+    } else {
+        let installation_id = Uuid::new_v4().to_string();
+
+        KEY_VALUE_STORE
+            .write_kvp(legacy_key_name.to_string(), installation_id.clone())
+            .await?;
+
+        Ok(installation_id)
+    }
+}
+
+async fn restore_or_create_workspace(_app_state: &Arc<AppState>, mut _cx: AsyncAppContext) {
+    todo!("workspace")
+    // if let Some(location) = workspace::last_opened_workspace_paths().await {
+    //     cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))
+    //         .await
+    //         .log_err();
+    // } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
+    //     cx.update(|cx| show_welcome_experience(app_state, cx));
+    // } else {
+    //     cx.update(|cx| {
+    //         workspace::open_new(app_state, cx, |workspace, cx| {
+    //             Editor::new_file(workspace, &Default::default(), cx)
+    //         })
+    //         .detach();
+    //     });
+    // }
+}
+
+fn init_paths() {
+    std::fs::create_dir_all(&*util::paths::CONFIG_DIR).expect("could not create config path");
+    std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path");
+    std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path");
+    std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path");
+}
+
+fn init_logger() {
+    if stdout_is_a_pty() {
+        env_logger::init();
+    } else {
+        let level = LevelFilter::Info;
+
+        // Prevent log file from becoming too large.
+        const KIB: u64 = 1024;
+        const MIB: u64 = 1024 * KIB;
+        const MAX_LOG_BYTES: u64 = MIB;
+        if std::fs::metadata(&*paths::LOG).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES)
+        {
+            let _ = std::fs::rename(&*paths::LOG, &*paths::OLD_LOG);
+        }
+
+        let log_file = OpenOptions::new()
+            .create(true)
+            .append(true)
+            .open(&*paths::LOG)
+            .expect("could not open logfile");
+
+        let config = ConfigBuilder::new()
+            .set_time_format_str("%Y-%m-%dT%T") //All timestamps are UTC
+            .build();
+
+        simplelog::WriteLogger::init(level, config, log_file).expect("could not initialize logger");
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+struct LocationData {
+    file: String,
+    line: u32,
+}
+
+#[derive(Serialize, Deserialize)]
+struct Panic {
+    thread: String,
+    payload: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    location_data: Option<LocationData>,
+    backtrace: Vec<String>,
+    app_version: String,
+    release_channel: String,
+    os_name: String,
+    os_version: Option<String>,
+    architecture: String,
+    panicked_on: u128,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    installation_id: Option<String>,
+    session_id: String,
+}
+
+#[derive(Serialize)]
+struct PanicRequest {
+    panic: Panic,
+    token: String,
+}
+
+static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
+
+fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: String) {
+    let is_pty = stdout_is_a_pty();
+    let app_metadata = app.metadata();
+
+    panic::set_hook(Box::new(move |info| {
+        let prior_panic_count = PANIC_COUNT.fetch_add(1, Ordering::SeqCst);
+        if prior_panic_count > 0 {
+            // Give the panic-ing thread time to write the panic file
+            loop {
+                std::thread::yield_now();
+            }
+        }
+
+        let thread = thread::current();
+        let thread_name = thread.name().unwrap_or("<unnamed>");
+
+        let payload = info
+            .payload()
+            .downcast_ref::<&str>()
+            .map(|s| s.to_string())
+            .or_else(|| info.payload().downcast_ref::<String>().map(|s| s.clone()))
+            .unwrap_or_else(|| "Box<Any>".to_string());
+
+        if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
+            let location = info.location().unwrap();
+            let backtrace = Backtrace::new();
+            eprintln!(
+                "Thread {:?} panicked with {:?} at {}:{}:{}\n{:?}",
+                thread_name,
+                payload,
+                location.file(),
+                location.line(),
+                location.column(),
+                backtrace,
+            );
+            std::process::exit(-1);
+        }
+
+        let app_version = client2::ZED_APP_VERSION
+            .or(app_metadata.app_version)
+            .map_or("dev".to_string(), |v| v.to_string());
+
+        let backtrace = Backtrace::new();
+        let mut backtrace = backtrace
+            .frames()
+            .iter()
+            .filter_map(|frame| Some(format!("{:#}", frame.symbols().first()?.name()?)))
+            .collect::<Vec<_>>();
+
+        // Strip out leading stack frames for rust panic-handling.
+        if let Some(ix) = backtrace
+            .iter()
+            .position(|name| name == "rust_begin_unwind")
+        {
+            backtrace.drain(0..=ix);
+        }
+
+        let panic_data = Panic {
+            thread: thread_name.into(),
+            payload: payload.into(),
+            location_data: info.location().map(|location| LocationData {
+                file: location.file().into(),
+                line: location.line(),
+            }),
+            app_version: app_version.clone(),
+            release_channel: RELEASE_CHANNEL.display_name().into(),
+            os_name: app_metadata.os_name.into(),
+            os_version: app_metadata
+                .os_version
+                .as_ref()
+                .map(SemanticVersion::to_string),
+            architecture: env::consts::ARCH.into(),
+            panicked_on: SystemTime::now()
+                .duration_since(UNIX_EPOCH)
+                .unwrap()
+                .as_millis(),
+            backtrace,
+            installation_id: installation_id.clone(),
+            session_id: session_id.clone(),
+        };
+
+        if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
+            log::error!("{}", panic_data_json);
+        }
+
+        if !is_pty {
+            if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
+                let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
+                let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
+                let panic_file = std::fs::OpenOptions::new()
+                    .append(true)
+                    .create(true)
+                    .open(&panic_file_path)
+                    .log_err();
+                if let Some(mut panic_file) = panic_file {
+                    writeln!(&mut panic_file, "{}", panic_data_json).log_err();
+                    panic_file.flush().log_err();
+                }
+            }
+        }
+
+        std::process::abort();
+    }));
+}
+
+fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
+    let telemetry_settings = *client2::TelemetrySettings::get_global(cx);
+
+    cx.executor()
+        .spawn(async move {
+            let panic_report_url = format!("{}/api/panic", &*client2::ZED_SERVER_URL);
+            let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?;
+            while let Some(child) = children.next().await {
+                let child = child?;
+                let child_path = child.path();
+
+                if child_path.extension() != Some(OsStr::new("panic")) {
+                    continue;
+                }
+                let filename = if let Some(filename) = child_path.file_name() {
+                    filename.to_string_lossy()
+                } else {
+                    continue;
+                };
+
+                if !filename.starts_with("zed") {
+                    continue;
+                }
+
+                if telemetry_settings.diagnostics {
+                    let panic_file_content = smol::fs::read_to_string(&child_path)
+                        .await
+                        .context("error reading panic file")?;
+
+                    let panic = serde_json::from_str(&panic_file_content)
+                        .ok()
+                        .or_else(|| {
+                            panic_file_content
+                                .lines()
+                                .next()
+                                .and_then(|line| serde_json::from_str(line).ok())
+                        })
+                        .unwrap_or_else(|| {
+                            log::error!(
+                                "failed to deserialize panic file {:?}",
+                                panic_file_content
+                            );
+                            None
+                        });
+
+                    if let Some(panic) = panic {
+                        let body = serde_json::to_string(&PanicRequest {
+                            panic,
+                            token: client2::ZED_SECRET_CLIENT_TOKEN.into(),
+                        })
+                        .unwrap();
+
+                        let request = Request::post(&panic_report_url)
+                            .redirect_policy(isahc::config::RedirectPolicy::Follow)
+                            .header("Content-Type", "application/json")
+                            .body(body.into())?;
+                        let response = http.send(request).await.context("error sending panic")?;
+                        if !response.status().is_success() {
+                            log::error!("Error uploading panic to server: {}", response.status());
+                        }
+                    }
+                }
+
+                // We've done what we can, delete the file
+                std::fs::remove_file(child_path)
+                    .context("error removing panic")
+                    .log_err();
+            }
+            Ok::<_, anyhow::Error>(())
+        })
+        .detach_and_log_err(cx);
+}
+
+async fn load_login_shell_environment() -> Result<()> {
+    let marker = "ZED_LOGIN_SHELL_START";
+    let shell = env::var("SHELL").context(
+        "SHELL environment variable is not assigned so we can't source login environment variables",
+    )?;
+    let output = Command::new(&shell)
+        .args(["-lic", &format!("echo {marker} && /usr/bin/env -0")])
+        .output()
+        .await
+        .context("failed to spawn login shell to source login environment variables")?;
+    if !output.status.success() {
+        Err(anyhow!("login shell exited with error"))?;
+    }
+
+    let stdout = String::from_utf8_lossy(&output.stdout);
+
+    if let Some(env_output_start) = stdout.find(marker) {
+        let env_output = &stdout[env_output_start + marker.len()..];
+        for line in env_output.split_terminator('\0') {
+            if let Some(separator_index) = line.find('=') {
+                let key = &line[..separator_index];
+                let value = &line[separator_index + 1..];
+                env::set_var(key, value);
+            }
+        }
+        log::info!(
+            "set environment variables from shell:{}, path:{}",
+            shell,
+            env::var("PATH").unwrap_or_default(),
+        );
+    }
+
+    Ok(())
+}
+
+fn stdout_is_a_pty() -> bool {
+    std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal()
+}
+
+fn collect_url_args() -> Vec<String> {
+    env::args()
+        .skip(1)
+        .filter_map(|arg| match std::fs::canonicalize(Path::new(&arg)) {
+            Ok(path) => Some(format!("file://{}", path.to_string_lossy())),
+            Err(error) => {
+                if let Some(_) = parse_zed_link(&arg) {
+                    Some(arg)
+                } else {
+                    log::error!("error parsing path argument: {}", error);
+                    None
+                }
+            }
+        })
+        .collect()
+}
+
+fn load_embedded_fonts(cx: &AppContext) {
+    let asset_source = cx.asset_source();
+    let font_paths = asset_source.list("fonts").unwrap();
+    let embedded_fonts = Mutex::new(Vec::new());
+    let executor = cx.executor();
+
+    executor.block(executor.scoped(|scope| {
+        for font_path in &font_paths {
+            if !font_path.ends_with(".ttf") {
+                continue;
+            }
+
+            scope.spawn(async {
+                let font_bytes = asset_source.load(font_path).unwrap().to_vec();
+                embedded_fonts.lock().push(Arc::from(font_bytes));
+            });
+        }
+    }));
+
+    cx.text_system()
+        .add_fonts(&embedded_fonts.into_inner())
+        .unwrap();
+}
+
+// #[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))
+//         .await;
+//     while (events.next().await).is_some() {
+//         let output = Command::new("npm")
+//             .current_dir("styles")
+//             .args(["run", "build"])
+//             .output()
+//             .await
+//             .log_err()?;
+//         if output.status.success() {
+//             cx.update(|cx| theme_selector::reload(cx))
+//         } else {
+//             eprintln!(
+//                 "build script failed {}",
+//                 String::from_utf8_lossy(&output.stderr)
+//             );
+//         }
+//     }
+//     Some(())
+// }
+
+// #[cfg(debug_assertions)]
+// async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
+//     let mut events = fs
+//         .watch(
+//             "crates/zed/src/languages".as_ref(),
+//             Duration::from_millis(100),
+//         )
+//         .await;
+//     while (events.next().await).is_some() {
+//         languages.reload();
+//     }
+//     Some(())
+// }
+
+// #[cfg(debug_assertions)]
+// fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+//     cx.spawn(|mut cx| async move {
+//         let mut events = fs
+//             .watch(
+//                 "assets/icons/file_icons/file_types.json".as_ref(),
+//                 Duration::from_millis(100),
+//             )
+//             .await;
+//         while (events.next().await).is_some() {
+//             cx.update(|cx| {
+//                 cx.update_global(|file_types, _| {
+//                     *file_types = project_panel::file_associations::FileAssociations::new(Assets);
+//                 });
+//             })
+//         }
+//     })
+//     .detach()
+// }
+
+// #[cfg(not(debug_assertions))]
+// async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
+//     None
+// }
+
+// #[cfg(not(debug_assertions))]
+// async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
+//     None
+// }
+
+// #[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();
+
+                // todo!("workspace")
+                // 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),
+//         ("Open command palette", &command_palette::Toggle),
+//         ("Open recent projects", &recent_projects::OpenRecent),
+//         ("Change your settings", &zed_actions::OpenSettings),
+//     ]
+// }

crates/zed2/src/only_instance.rs 🔗

@@ -0,0 +1,104 @@
+use std::{
+    io::{Read, Write},
+    net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream},
+    thread,
+    time::Duration,
+};
+
+use util::channel::ReleaseChannel;
+
+const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
+const CONNECT_TIMEOUT: Duration = Duration::from_millis(10);
+const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35);
+const SEND_TIMEOUT: Duration = Duration::from_millis(20);
+
+fn address() -> SocketAddr {
+    let port = match *util::channel::RELEASE_CHANNEL {
+        ReleaseChannel::Dev => 43737,
+        ReleaseChannel::Preview => 43738,
+        ReleaseChannel::Stable => 43739,
+    };
+
+    SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port))
+}
+
+fn instance_handshake() -> &'static str {
+    match *util::channel::RELEASE_CHANNEL {
+        ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
+        ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
+        ReleaseChannel::Stable => "Zed Editor Stable Instance Running",
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum IsOnlyInstance {
+    Yes,
+    No,
+}
+
+pub fn ensure_only_instance() -> IsOnlyInstance {
+    // todo!("zed_stateless")
+    // if *db::ZED_STATELESS {
+    //     return IsOnlyInstance::Yes;
+    // }
+
+    if check_got_handshake() {
+        return IsOnlyInstance::No;
+    }
+
+    let listener = match TcpListener::bind(address()) {
+        Ok(listener) => listener,
+
+        Err(err) => {
+            log::warn!("Error binding to single instance port: {err}");
+            if check_got_handshake() {
+                return IsOnlyInstance::No;
+            }
+
+            // Avoid failing to start when some other application by chance already has
+            // a claim on the port. This is sub-par as any other instance that gets launched
+            // will be unable to communicate with this instance and will duplicate
+            log::warn!("Backup handshake request failed, continuing without handshake");
+            return IsOnlyInstance::Yes;
+        }
+    };
+
+    thread::spawn(move || {
+        for stream in listener.incoming() {
+            let mut stream = match stream {
+                Ok(stream) => stream,
+                Err(_) => return,
+            };
+
+            _ = stream.set_nodelay(true);
+            _ = stream.set_read_timeout(Some(SEND_TIMEOUT));
+            _ = stream.write_all(instance_handshake().as_bytes());
+        }
+    });
+
+    IsOnlyInstance::Yes
+}
+
+fn check_got_handshake() -> bool {
+    match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) {
+        Ok(mut stream) => {
+            let mut buf = vec![0u8; instance_handshake().len()];
+
+            stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap();
+            if let Err(err) = stream.read_exact(&mut buf) {
+                log::warn!("Connected to single instance port but failed to read: {err}");
+                return false;
+            }
+
+            if buf == instance_handshake().as_bytes() {
+                log::info!("Got instance handshake");
+                return true;
+            }
+
+            log::warn!("Got wrong instance handshake value");
+            false
+        }
+
+        Err(_) => false,
+    }
+}

crates/zed2/src/open_listener.rs 🔗

@@ -0,0 +1,98 @@
+use anyhow::anyhow;
+use cli::{ipc::IpcSender, CliRequest, CliResponse};
+use futures::channel::mpsc;
+use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
+use std::ffi::OsStr;
+use std::os::unix::prelude::OsStrExt;
+use std::sync::atomic::Ordering;
+use std::{path::PathBuf, sync::atomic::AtomicBool};
+use util::channel::parse_zed_link;
+use util::ResultExt;
+
+use crate::connect_to_cli;
+
+pub enum OpenRequest {
+    Paths {
+        paths: Vec<PathBuf>,
+    },
+    CliConnection {
+        connection: (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
+    },
+    JoinChannel {
+        channel_id: u64,
+    },
+}
+
+pub struct OpenListener {
+    tx: UnboundedSender<OpenRequest>,
+    pub triggered: AtomicBool,
+}
+
+impl OpenListener {
+    pub fn new() -> (Self, UnboundedReceiver<OpenRequest>) {
+        let (tx, rx) = mpsc::unbounded();
+        (
+            OpenListener {
+                tx,
+                triggered: AtomicBool::new(false),
+            },
+            rx,
+        )
+    }
+
+    pub fn open_urls(&self, urls: Vec<String>) {
+        self.triggered.store(true, Ordering::Release);
+        let request = if let Some(server_name) =
+            urls.first().and_then(|url| url.strip_prefix("zed-cli://"))
+        {
+            self.handle_cli_connection(server_name)
+        } else if let Some(request_path) = urls.first().and_then(|url| parse_zed_link(url)) {
+            self.handle_zed_url_scheme(request_path)
+        } else {
+            self.handle_file_urls(urls)
+        };
+
+        if let Some(request) = request {
+            self.tx
+                .unbounded_send(request)
+                .map_err(|_| anyhow!("no listener for open requests"))
+                .log_err();
+        }
+    }
+
+    fn handle_cli_connection(&self, server_name: &str) -> Option<OpenRequest> {
+        if let Some(connection) = connect_to_cli(server_name).log_err() {
+            return Some(OpenRequest::CliConnection { connection });
+        }
+
+        None
+    }
+
+    fn handle_zed_url_scheme(&self, request_path: &str) -> Option<OpenRequest> {
+        let mut parts = request_path.split("/");
+        if parts.next() == Some("channel") {
+            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 });
+                    }
+                }
+            }
+        }
+        log::error!("invalid zed url: {}", request_path);
+        None
+    }
+
+    fn handle_file_urls(&self, urls: Vec<String>) -> Option<OpenRequest> {
+        let paths: Vec<_> = urls
+            .iter()
+            .flat_map(|url| url.strip_prefix("file://"))
+            .map(|url| {
+                let decoded = urlencoding::decode_binary(url.as_bytes());
+                PathBuf::from(OsStr::from_bytes(decoded.as_ref()))
+            })
+            .collect();
+
+        Some(OpenRequest::Paths { paths })
+    }
+}

crates/zed2/src/zed2.rs 🔗

@@ -0,0 +1,208 @@
+mod assets;
+pub mod languages;
+mod only_instance;
+mod open_listener;
+
+pub use assets::*;
+use client2::{Client, UserStore};
+use gpui2::{AsyncAppContext, Model};
+pub use only_instance::*;
+pub use open_listener::*;
+
+use anyhow::{Context, Result};
+use cli::{
+    ipc::{self, IpcSender},
+    CliRequest, CliResponse, IpcHandshake,
+};
+use futures::{channel::mpsc, SinkExt, StreamExt};
+use std::{sync::Arc, thread};
+
+pub 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 struct AppState {
+    pub client: Arc<Client>,
+    pub user_store: Model<UserStore>,
+}
+
+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() {
+                // todo!()
+                // 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;
+                // todo!("workspace")
+                // 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();
+            }
+        }
+    }
+}

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}%")

theme.txt 🔗

@@ -0,0 +1,254 @@
+    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
+     Running `target/debug/storybook`
+[crates/ui2/src/components/workspace.rs:182] color = [crates/ui2/src/color.rs:162] "ThemeColor debug" = "ThemeColor debug"
+ThemeColor {
+    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(0x464b57ff).into()",
+    border_variant: "rgba(0x464b57ff).into()",
+    border_focused: "rgba(0x293b5bff).into()",
+    border_transparent: "rgba(0x00000000).into()",
+    elevated_surface: "rgba(0x3b414dff).into()",
+    surface: "rgba(0x2f343eff).into()",
+    background: "rgba(0x3b414dff).into()",
+    filled_element: "rgba(0x3b414dff).into()",
+    filled_element_hover: "rgba(0xffffff1e).into()",
+    filled_element_active: "rgba(0xffffff28).into()",
+    filled_element_selected: "rgba(0x18243dff).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(0x18243dff).into()",
+    ghost_element_disabled: "rgba(0x00000000).into()",
+    text: "rgba(0xc8ccd4ff).into()",
+    text_muted: "rgba(0x838994ff).into()",
+    text_placeholder: "rgba(0xd07277ff).into()",
+    text_disabled: "rgba(0x555a63ff).into()",
+    text_accent: "rgba(0x74ade8ff).into()",
+    icon_muted: "rgba(0x838994ff).into()",
+    syntax: SyntaxColor {
+        comment: "rgba(0x5d636fff).into()",
+        string: "rgba(0xa1c181ff).into()",
+        function: "rgba(0x73ade9ff).into()",
+        keyword: "rgba(0xb477cfff).into()",
+    },
+    status_bar: "rgba(0x3b414dff).into()",
+    title_bar: "rgba(0x3b414dff).into()",
+    toolbar: "rgba(0x282c33ff).into()",
+    tab_bar: "rgba(0x2f343eff).into()",
+    editor_subheader: "rgba(0x2f343eff).into()",
+    editor_active_line: "rgba(0x2f343eff).into()",
+    terminal: "rgba(0x282c33ff).into()",
+    image_fallback_background: "rgba(0x3b414dff).into()",
+    git_created: "rgba(0xa1c181ff).into()",
+    git_modified: "rgba(0x74ade8ff).into()",
+    git_deleted: "rgba(0xd07277ff).into()",
+    git_conflict: "rgba(0xdec184ff).into()",
+    git_ignored: "rgba(0x555a63ff).into()",
+    git_renamed: "rgba(0xdec184ff).into()",
+    player: [
+        PlayerThemeColors {
+            cursor: "rgba(0x74ade8ff).into()",
+            selection: "rgba(0x74ade83d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xa1c181ff).into()",
+            selection: "rgba(0xa1c1813d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xbe5046ff).into()",
+            selection: "rgba(0xbe50463d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xbf956aff).into()",
+            selection: "rgba(0xbf956a3d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xb477cfff).into()",
+            selection: "rgba(0xb477cf3d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0x6eb4bfff).into()",
+            selection: "rgba(0x6eb4bf3d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xd07277ff).into()",
+            selection: "rgba(0xd072773d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xdec184ff).into()",
+            selection: "rgba(0xdec1843d).into()",
+        },
+    ],
+}
+[crates/ui2/src/components/workspace.rs:182] color = [crates/ui2/src/color.rs:162] "ThemeColor debug" = "ThemeColor debug"
+ThemeColor {
+    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(0x464b57ff).into()",
+    border_variant: "rgba(0x464b57ff).into()",
+    border_focused: "rgba(0x293b5bff).into()",
+    border_transparent: "rgba(0x00000000).into()",
+    elevated_surface: "rgba(0x3b414dff).into()",
+    surface: "rgba(0x2f343eff).into()",
+    background: "rgba(0x3b414dff).into()",
+    filled_element: "rgba(0x3b414dff).into()",
+    filled_element_hover: "rgba(0xffffff1e).into()",
+    filled_element_active: "rgba(0xffffff28).into()",
+    filled_element_selected: "rgba(0x18243dff).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(0x18243dff).into()",
+    ghost_element_disabled: "rgba(0x00000000).into()",
+    text: "rgba(0xc8ccd4ff).into()",
+    text_muted: "rgba(0x838994ff).into()",
+    text_placeholder: "rgba(0xd07277ff).into()",
+    text_disabled: "rgba(0x555a63ff).into()",
+    text_accent: "rgba(0x74ade8ff).into()",
+    icon_muted: "rgba(0x838994ff).into()",
+    syntax: SyntaxColor {
+        comment: "rgba(0x5d636fff).into()",
+        string: "rgba(0xa1c181ff).into()",
+        function: "rgba(0x73ade9ff).into()",
+        keyword: "rgba(0xb477cfff).into()",
+    },
+    status_bar: "rgba(0x3b414dff).into()",
+    title_bar: "rgba(0x3b414dff).into()",
+    toolbar: "rgba(0x282c33ff).into()",
+    tab_bar: "rgba(0x2f343eff).into()",
+    editor_subheader: "rgba(0x2f343eff).into()",
+    editor_active_line: "rgba(0x2f343eff).into()",
+    terminal: "rgba(0x282c33ff).into()",
+    image_fallback_background: "rgba(0x3b414dff).into()",
+    git_created: "rgba(0xa1c181ff).into()",
+    git_modified: "rgba(0x74ade8ff).into()",
+    git_deleted: "rgba(0xd07277ff).into()",
+    git_conflict: "rgba(0xdec184ff).into()",
+    git_ignored: "rgba(0x555a63ff).into()",
+    git_renamed: "rgba(0xdec184ff).into()",
+    player: [
+        PlayerThemeColors {
+            cursor: "rgba(0x74ade8ff).into()",
+            selection: "rgba(0x74ade83d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xa1c181ff).into()",
+            selection: "rgba(0xa1c1813d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xbe5046ff).into()",
+            selection: "rgba(0xbe50463d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xbf956aff).into()",
+            selection: "rgba(0xbf956a3d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xb477cfff).into()",
+            selection: "rgba(0xb477cf3d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0x6eb4bfff).into()",
+            selection: "rgba(0x6eb4bf3d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xd07277ff).into()",
+            selection: "rgba(0xd072773d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xdec184ff).into()",
+            selection: "rgba(0xdec1843d).into()",
+        },
+    ],
+}
+[crates/ui2/src/components/workspace.rs:182] color = [crates/ui2/src/color.rs:162] "ThemeColor debug" = "ThemeColor debug"
+ThemeColor {
+    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(0x464b57ff).into()",
+    border_variant: "rgba(0x464b57ff).into()",
+    border_focused: "rgba(0x293b5bff).into()",
+    border_transparent: "rgba(0x00000000).into()",
+    elevated_surface: "rgba(0x3b414dff).into()",
+    surface: "rgba(0x2f343eff).into()",
+    background: "rgba(0x3b414dff).into()",
+    filled_element: "rgba(0x3b414dff).into()",
+    filled_element_hover: "rgba(0xffffff1e).into()",
+    filled_element_active: "rgba(0xffffff28).into()",
+    filled_element_selected: "rgba(0x18243dff).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(0x18243dff).into()",
+    ghost_element_disabled: "rgba(0x00000000).into()",
+    text: "rgba(0xc8ccd4ff).into()",
+    text_muted: "rgba(0x838994ff).into()",
+    text_placeholder: "rgba(0xd07277ff).into()",
+    text_disabled: "rgba(0x555a63ff).into()",
+    text_accent: "rgba(0x74ade8ff).into()",
+    icon_muted: "rgba(0x838994ff).into()",
+    syntax: SyntaxColor {
+        comment: "rgba(0x5d636fff).into()",
+        string: "rgba(0xa1c181ff).into()",
+        function: "rgba(0x73ade9ff).into()",
+        keyword: "rgba(0xb477cfff).into()",
+    },
+    status_bar: "rgba(0x3b414dff).into()",
+    title_bar: "rgba(0x3b414dff).into()",
+    toolbar: "rgba(0x282c33ff).into()",
+    tab_bar: "rgba(0x2f343eff).into()",
+    editor_subheader: "rgba(0x2f343eff).into()",
+    editor_active_line: "rgba(0x2f343eff).into()",
+    terminal: "rgba(0x282c33ff).into()",
+    image_fallback_background: "rgba(0x3b414dff).into()",
+    git_created: "rgba(0xa1c181ff).into()",
+    git_modified: "rgba(0x74ade8ff).into()",
+    git_deleted: "rgba(0xd07277ff).into()",
+    git_conflict: "rgba(0xdec184ff).into()",
+    git_ignored: "rgba(0x555a63ff).into()",
+    git_renamed: "rgba(0xdec184ff).into()",
+    player: [
+        PlayerThemeColors {
+            cursor: "rgba(0x74ade8ff).into()",
+            selection: "rgba(0x74ade83d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xa1c181ff).into()",
+            selection: "rgba(0xa1c1813d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xbe5046ff).into()",
+            selection: "rgba(0xbe50463d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xbf956aff).into()",
+            selection: "rgba(0xbf956a3d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xb477cfff).into()",
+            selection: "rgba(0xb477cf3d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0x6eb4bfff).into()",
+            selection: "rgba(0x6eb4bf3d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xd07277ff).into()",
+            selection: "rgba(0xd072773d).into()",
+        },
+        PlayerThemeColors {
+            cursor: "rgba(0xdec184ff).into()",
+            selection: "rgba(0xdec1843d).into()",
+        },
+    ],
+}